简介:本资源是一套完整的SNL语言编译器源码实现,面向编译原理课程学习者、高校计算机专业学生及编译器开发初学者,用于深入理解词法分析、语法分析与语义分析三大核心编译阶段。项目采用C/C++编写,包含LL(1)分析、递归下降语法解析、语义处理等关键模块,代码结构清晰,具备可运行性,适合作为课程设计、实验复现或编译原理实践拓展。压缩包共231个文件,约77.55MB,主体为10个cpp源文件、2个h头文件及配套的sln/vcxproj工程配置文件,辅以exe可执行程序、pdb调试符号与log构建日志,便于编译调试与过程追踪。已有1177人学习下载,资源附带多份分析模块源码(如词法分析.cpp、语法递归程序.cpp、语义程序1.cpp等),并提供作者QQ支持,有助于读者掌握编译全流程实现细节与常见问题排错思路。
1. SNL语言编译器源码:一个能跑通、能调试、能改出自己语法的编译原理“实操沙盒”
你不是在学编译原理——你是在和词法分析器抢控制权,在和语法树打架,在和符号表较劲。很多同学翻完《龙书》第三章,写完LL(1)文法,却卡在“怎么让代码真跑起来”这一步:手写递归下降太糙,Yacc/Bison又像黑匣子,生成的代码看不懂、改不动、调不进。而这份 SNL(Simple Nano Language)编译器源码,就是专为这种卡点设计的:它用纯 C 实现,无外部依赖,从词法扫描 → 语法分析 → 中间代码生成 → 目标代码输出(汇编级伪指令),全链路可单步调试;更关键的是,它把每个阶段的输入/输出都暴露成文本文件(.lex,.parse,.ir),你改一行语法规则,立刻能看到 AST 如何变形;加一个变量声明,符号表 dump 文件里马上多出一条记录。适合大三编译原理实验课学生快速验证课堂理论,也适合想动手拆解编译器骨架的嵌入式/C 开发者——它不追求工业级优化,但每行代码都在回答一个问题:“这一步,到底在干什么?”
2. 源码结构与构建流程:C 项目零依赖编译,VS Code + GCC 五分钟跑通
SNL 编译器源码是典型的单仓库、多模块 C 工程,目录结构极简,没有 Makefile 嵌套、没有 autotools 魔咒,所有构建逻辑收敛到一个Makefile中。它不依赖 LLVM 或 GCC 内部 API,所有解析、遍历、生成逻辑全部手写,这意味着你打开parser.c就能看到递归下降的核心parse_stmt()函数,打开codegen.c就能直接修改寄存器分配策略。整个工程共 7 个核心.c文件,总代码量约 2300 行(不含注释),对 C 语言基础扎实的学生而言,一天内通读主干逻辑完全可行。
2.1 目录结构与文件职责映射
| 文件名 | 行数(约) | 核心职责 | 调试切入点建议 |
|---|---|---|---|
main.c | 120 | 主流程调度:读源码 → 调用 lexer → parser → codegen → 输出 | 在main()入口打断点,观察argc/argv解析逻辑 |
lexer.c | 480 | 词法扫描:正则匹配关键字/标识符/数字/运算符,返回 token 流 | 修改skip_whitespace()逻辑,测试注释处理边界 |
parser.c | 620 | 递归下降语法分析:parse_program()→parse_block()→parse_expr(),构建 AST | 在parse_if_stmt()中插入printf("parsing IF at line %d\n", line_no) |
ast.c | 210 | AST 节点定义与内存管理:ASTNode结构体、new_ast_node()、free_ast() | 查看AST_NODE_TYPE枚举,对照文法理解节点类型映射 |
symbol.c | 350 | 符号表实现:哈希桶 + 链地址法,支持作用域嵌套(push_scope()/pop_scope()) | 在lookup_symbol()返回前加if (!sym) fprintf(stderr, "UNDECLARED: %s\n", name); |
codegen.c | 430 | 三地址码生成:遍历 AST,为每个节点生成IR_INST,输出.ir文件 | 修改gen_binary_op()中op == '+'分支,插入临时变量命名逻辑 |
utils.c | 120 | 工具函数:字符串分割、内存安全strndup、行号计数器 | get_line_number()是定位语法错误的关键钩子 |
提示:所有
.c文件均包含#include "common.h",该头文件统一定义typedef enum { TOKEN_INT, TOKEN_ID, ... } TokenType;和全局错误处理宏ERROR(fmt, ...)。修改任何一处 token 类型,必须同步更新common.h中的枚举和lexer.c的token_names[]数组,否则printf_token()会输出乱码。
2.2 构建与运行:GCC 一键编译,无需 MSVC 或 Clang 特性
该项目明确要求 GCC(≥ 4.8)或 Clang(≥ 3.5),不兼容 MSVC —— 因为大量使用了 GNU 扩展语法(如__attribute__((unused)))和 POSIX 标准函数(strndup,getline)。Windows 用户若用 VS Code,推荐搭配 WSL2 + Ubuntu 20.04 环境,或直接安装 MinGW-w64(非 MSVC 工具链)。构建命令极其简单:
# 进入源码根目录后执行 make clean && makeMakefile内容精简到 12 行,核心规则如下:
CC = gcc CFLAGS = -Wall -Wextra -std=c99 -O0 -g TARGET = snlc SOURCES = main.c lexer.c parser.c ast.c symbol.c codegen.c utils.c $(TARGET): $(SOURCES) $(CC) $(CFLAGS) -o $@ $^ clean: rm -f $(TARGET) *.o *.lex *.parse *.ir-O0 -g是强制要求:关闭优化保证 GDB 可单步,带调试信息方便查看 AST 节点内存布局;std=c99表明它不依赖 C11 的_Generic或线程库,老式嵌入式交叉编译器也能啃动;clean规则删除所有中间产物(.lex,.parse,.ir),避免旧缓存干扰新语法测试。
运行时接受两个参数:输入 SNL 源文件路径 + 输出前缀(用于生成.lex/.parse/.ir文件):
./snlc tests/factorial.snl out/fact # 生成:out/fact.lex(词法流)、out/fact.parse(AST 文本表示)、out/fact.ir(三地址码)2.3 快速验证:用自带 test suite 确认环境无污染
源码包中tests/目录下预置 8 个 SNL 示例程序,覆盖基础语法(hello.snl,fib.snl)、作用域(scope.snl)、错误处理(error_undecl.snl)。执行全部测试只需一条命令:
# 运行所有测试并比对期望输出 make test其背后逻辑是:对每个.snl文件,先用./snlc生成.ir,再用配套的简易解释器ir_interpreter(源码含在tools/ir_interp.c)执行,最后diff输出结果与tests/expected/下的.out文件。若某测试失败,make test会明确提示:
FAIL: tests/error_undecl.snl Expected: "ERROR: undeclared identifier 'x' at line 3" Got: "Segmentation fault"这说明你的修改触发了符号表空指针解引用——此时立刻去symbol.c的lookup_symbol()函数检查scope->symbols是否为 NULL,而非盲目加printf。
3. SNL 语言语法与文法解析:从 BNF 到递归下降,手撕 LL(1) 实现细节
SNL 是一门刻意简化、但保留编译器核心环节的教学语言。它不支持类、泛型、异常,但完整覆盖变量声明、算术表达式、if/while 控制流、函数调用(无递归)和基本 I/O(print)。其文法采用经典 LL(1) 形式,所有产生式左部非终结符均无左递归,且 FIRST/FOLLOW 集无冲突。理解这份源码,本质是读懂parser.c如何将 BNF 规则逐条翻译为 C 函数调用链。
3.1 SNL 核心 BNF 定义与对应 C 函数映射
SNL 的文法定义在doc/snl_grammar.txt(源码包内)中,关键片段如下:
Program → StmtList StmtList → Stmt StmtList | ε Stmt → Decl | ExprStmt | IfStmt | WhileStmt | PrintStmt Decl → TYPE ID ';' | TYPE ID '=' Expr ';' IfStmt → 'if' '(' Expr ')' Stmt ('else' Stmt)? WhileStmt → 'while' '(' Expr ')' Stmt Expr → Term { ('+' | '-') Term } Term → Factor { ('*' | '/') Factor } Factor → '(' Expr ')' | INT | ID | ID '(' ExprList? ')' ExprList → Expr { ',' Expr }这些规则在parser.c中被严格映射为函数:
parse_program()对应Program → StmtListparse_stmt_list()对应StmtList → Stmt StmtList | εparse_stmt()是分发中心,根据当前 token 类型(TOKEN_IF,TOKEN_WHILE,TOKEN_PRINT)跳转到对应子函数parse_expr()实现运算符优先级:先调parse_term(),再循环处理+/-;parse_term()同理调parse_factor()处理*/
注意:
parse_expr()中的{...}表示Kleene 星(零次或多次),在 C 中用while (match(TOKEN_PLUS) || match(TOKEN_MINUS))实现,而非递归调用自身——这是消除左递归的标准手法,也是初学者最容易写错的地方(常见错误:把while写成if,导致只处理一次加减)。
3.2 递归下降中的 “前瞻 token” 机制与 match() 函数真相
LL(1) 分析器的核心是“看一个 token 就决定下一步”。SNL 编译器用current_token全局变量缓存当前待消费的 token,并通过match(TokenType t)函数完成“消耗并前进”:
// parser.c static Token current_token; // match():若当前 token 是 t,则消耗它并读取下一个;否则报错 bool match(TokenType t) { if (current_token.type == t) { next_token(); // 从 lexer 获取下一个 token,更新 current_token return true; } error_at_current("expected %s, got %s", token_name(t), token_name(current_token.type)); return false; }这个函数看似简单,但藏着三个关键设计:
- 错误恢复弱但可控:
match()报错后不跳过 token,而是让上层函数决定是否sync_to_statement()(同步到下一个;或}),避免雪崩式报错; next_token()是 lexer 的门面:它封装了lexer_get_token()调用,屏蔽了词法器内部状态(如缓冲区、行号计数器),使语法分析器完全 unaware 于词法细节;error_at_current()的定位精度:它利用current_token.line和current_token.col输出精确行列号,比单纯printf("syntax error")高出一个维度——这也是为什么你在tests/error_undecl.snl中看到"at line 3"而非模糊的"near end"。
3.3 AST 节点构造:如何用 C 结构体表达树形语法结构
SNL 的 AST 不用指针森林,而用紧凑的union+enum设计,每个节点内存布局固定(64 字节以内),便于批量malloc和快速遍历:
// ast.c typedef enum { AST_PROGRAM, AST_STMT_LIST, AST_DECL, AST_EXPR_STMT, AST_IF, AST_WHILE, AST_PRINT, AST_BINARY, AST_UNARY, AST_INT, AST_ID, AST_CALL, AST_ASSIGN } ASTNodeType; typedef struct ASTNode { ASTNodeType type; int line; // 错误定位用 union { struct { ASTNode* stmt; ASTNode* next; } stmt_list; struct { char* name; ASTNode* init; } decl; struct { ASTNode* cond; ASTNode* then_body; ASTNode* else_body; } if_stmt; struct { ASTNode* cond; ASTNode* body; } while_stmt; struct { TokenType op; ASTNode* left; ASTNode* right; } binary; struct { char* name; ASTNode* args; } call; int int_val; // 字面量值 char* id_name; // 标识符名 } u; } ASTNode;union确保所有分支共享同一块内存,避免void*强转带来的类型擦除;line字段在lexer.c中由get_line_number()注入,贯穿整个编译流程;AST_STMT_LIST的stmt/next字段构成单向链表,替代传统树的 children 数组,降低内存碎片。
当你在parse_if_stmt()中看到:
ASTNode* node = new_ast_node(AST_IF); node->line = current_token.line; node->u.if_stmt.cond = parse_expr(); match(TOKEN_RPAREN); node->u.if_stmt.then_body = parse_stmt(); if (match(TOKEN_ELSE)) { node->u.if_stmt.else_body = parse_stmt(); } return node;你就明白:AST 构造不是“画棵树”,而是“填一张结构体表格”,每个字段对应文法规则的一个组成部分。
4. 符号表与作用域管理:哈希桶 + 链地址法,解决变量重定义与未声明问题
SNL 编译器的符号表(Symbol Table)是整套系统中最易被低估、却最常引发崩溃的模块。它不依赖 STL map 或 Python dict,而是用纯 C 实现的哈希表,支持多层作用域嵌套(全局、函数体、if/while 块),并在codegen.c中实时校验变量声明与使用一致性。如果你的修改导致Segmentation fault却找不到源头,90% 概率是符号表操作越界。
4.1 符号表数据结构:哈希桶数组 + 作用域链表
SNL 符号表SymbolTable结构体定义在symbol.c开头:
#define SYMBOL_TABLE_SIZE 128 // 哈希桶大小,质数提升分布均匀性 typedef struct Symbol { char* name; Type type; // INT_TYPE, VOID_TYPE 等 int is_function; // 标记是否为函数 int line_declared; // 声明行号,用于重复定义检查 struct Symbol* next; // 链地址法解决哈希冲突 } Symbol; typedef struct SymbolTable { Symbol* buckets[SYMBOL_TABLE_SIZE]; struct SymbolTable* parent; // 指向上层作用域,形成链表 int scope_level; // 0=global, 1=function, 2=block... } SymbolTable;buckets[]是固定大小的指针数组,每个桶指向一个Symbol链表;parent指针构建作用域链:global_table → func_table → block_table;scope_level仅用于调试输出(如printf("scope level %d: %s\n", table->scope_level, name)),不参与逻辑判断。
哈希函数极简:
static unsigned int hash(const char* name) { unsigned int h = 0; for (int i = 0; name[i]; i++) { h = (h << 5) + h + name[i]; // djb2 算法变种 } return h % SYMBOL_TABLE_SIZE; }4.2 作用域生命周期:push/pop 与变量遮蔽(Shadowing)实现
SNL 支持变量遮蔽:内层作用域可声明同名变量,屏蔽外层。这通过push_scope()和pop_scope()配合lookup_symbol()的查找顺序实现:
// symbol.c SymbolTable* push_scope(SymbolTable* parent) { SymbolTable* table = malloc(sizeof(SymbolTable)); memset(table->buckets, 0, sizeof(table->buckets)); // 初始化桶为空 table->parent = parent; table->scope_level = parent ? parent->scope_level + 1 : 0; return table; } void pop_scope(SymbolTable* table) { // 释放当前作用域所有符号(不释放 parent!) for (int i = 0; i < SYMBOL_TABLE_SIZE; i++) { Symbol* sym = table->buckets[i]; while (sym) { Symbol* next = sym->next; free(sym->name); free(sym); sym = next; } table->buckets[i] = NULL; } free(table); } Symbol* lookup_symbol(SymbolTable* table, const char* name) { // 从当前作用域开始查,逐级向上 while (table) { unsigned int idx = hash(name); Symbol* sym = table->buckets[idx]; while (sym) { if (strcmp(sym->name, name) == 0) { return sym; // 找到即返回,不继续向上查 } sym = sym->next; } table = table->parent; // 未找到,查父作用域 } return NULL; // 全链路未找到 }push_scope()创建新表,pop_scope()释放当前表内存,但parent指针保持有效;lookup_symbol()的“找到即返回”策略天然支持遮蔽:内层同名变量会先被命中,外层同名变量被自动忽略;insert_symbol()在插入前会调用lookup_symbol()检查重复定义,若存在且scope_level相同(同作用域),则报错"redefinition of '%s'"。
4.3 避坑:符号表常见问题与血泪排查指南
常见问题排查必须基于真实调试场景,以下 4 条均来自学生实验报告高频错误:
现象 1:Segmentation fault发生在lookup_symbol()的strcmp(sym->name, name)
原因:sym->name是NULL指针。根源在于insert_symbol()中未检查malloc返回值,或strdup()失败(内存不足)后未设默认值。
解决:在insert_symbol()开头添加防御:
char* name_copy = strdup(name); if (!name_copy) { fatal_error("Out of memory in symbol table"); } sym->name = name_copy;现象 2:error: undeclared identifier 'x'在x = 10;处报错,但前面有int x;
原因:parse_decl()中match(TOKEN_SEMI)失败,导致insert_symbol()未被执行,符号未注册。常见于;前有空格/换行未被 lexer 正确识别。
解决:检查lexer.c的skip_whitespace()是否跳过了\r(Windows 换行符),添加:
while (isspace(c) && c != '\r') { // 原逻辑漏掉 \r c = get_next_char(); }现象 3:if (x) { int y = 1; } y = 2;编译通过,但y应该未声明
原因:parse_if_stmt()中parse_stmt()返回后,未在if体作用域pop_scope(),导致内层变量y泄漏到外层。
解决:在parse_if_stmt()末尾显式管理作用域:
SymbolTable* old_scope = current_scope; current_scope = push_scope(current_scope); node->u.if_stmt.then_body = parse_stmt(); pop_scope(current_scope); // 关键! current_scope = old_scope;现象 4:print(x);报错undefined reference to 'print',但print是内置函数
原因:lookup_symbol()查找print时,current_scope为NULL(未初始化),导致直接返回NULL。
解决:在main.c初始化阶段强制创建全局作用域:
global_scope = push_scope(NULL); // parent 为 NULL 表示顶层 // 插入内置函数符号 insert_builtin_function(global_scope, "print", VOID_TYPE);5. 中间代码生成与 IR 解释器:三地址码落地,从 AST 到可执行逻辑
SNL 编译器不生成机器码,而是输出人类可读的三地址码(Three-Address Code, TAC),格式类似:
t1 = 10 t2 = 20 t3 = t1 + t2 print t3这套 IR(Intermediate Representation)由codegen.c遍历 AST 生成,再由配套的ir_interpreter解释执行。它不追求性能,但完美呈现“编译器如何把语法结构翻译成计算步骤”的本质——这才是编译原理实验课最该让学生看见的东西。
5.1 IR 指令集设计:12 条指令覆盖全部 SNL 语义
SNL 的 IR 指令集定义在codegen.c开头,共 12 种,全部为struct IRInst实例:
typedef enum { IR_MOV, IR_ADD, IR_SUB, IR_MUL, IR_DIV, IR_NEG, IR_EQ, IR_NE, IR_LT, IR_GT, IR_LE, IR_GE, IR_JMP, IR_JZ, IR_CALL, IR_RETURN, IR_PRINT, IR_LABEL } IRInstType; typedef struct IRInst { IRInstType type; char* dst; // 目标寄存器(t1, t2... 或变量名) char* src1; // 源操作数1(寄存器/变量/立即数) char* src2; // 源操作数2(仅二元运算需要) char* label; // 跳转目标标签(IR_JMP/IR_JZ 专用) int line; // 对应源码行号,用于调试 } IRInst;dst总是非空(即使是IR_PRINT也需指定输出值);src1/src2对一元运算(IR_NEG)仅用src1,src2为NULL;IR_LABEL用于标记if/while的入口,IR_JZ跳转到该 label;- 所有寄存器名(
t1,t2...)由codegen.c中的next_temp()函数自动生成,避免手动编号错误。
5.2 AST 到 IR 的遍历策略:深度优先 + 后序遍历,确保操作数先于操作符生成
IR 生成采用标准的深度优先后序遍历(Post-order DFS):先递归生成子节点的 IR,再生成当前节点的 IR 指令。以AST_BINARY(如a + b)为例:
// codegen.c void gen_binary_op(ASTNode* node, IRProgram* prog) { // 1. 先生成左操作数 IR(可能产生 t1) char* left_reg = gen_expr(node->u.binary.left, prog); // 2. 再生成右操作数 IR(可能产生 t2) char* right_reg = gen_expr(node->u.binary.right, prog); // 3. 最后生成加法指令:t3 = t1 + t2 char* dst_reg = next_temp(); add_ir_inst(prog, IR_ADD, dst_reg, left_reg, right_reg); }gen_expr()返回该表达式计算结果所在的寄存器名(如"t1");add_ir_inst()将指令追加到IRProgram的动态数组中;next_temp()维护全局计数器,每次返回"t%d"格式字符串,保证寄存器名唯一。
这种策略天然保证 IR 指令顺序符合数据依赖:t1和t2的指令一定出现在t3 = t1 + t2之前,无需额外的依赖图分析。
5.3 IR 解释器执行逻辑:栈式求值 + 全局变量映射表
配套的ir_interpreter(tools/ir_interp.c)是一个 300 行的简易解释器,它不 JIT 编译,而是逐条解释执行 IR 指令:
// ir_interp.c typedef struct { char* name; // 变量名或寄存器名 int value; // 当前值 bool is_temp; // 是否为临时寄存器(t1, t2...) } VarEntry; // 全局变量映射表(哈希表,复用 symbol.c 的结构) VarTable* var_table; // 执行单条 IR 指令 void exec_inst(IRInst* inst) { switch (inst->type) { case IR_MOV: set_var(inst->dst, get_var(inst->src1)); break; case IR_ADD: set_var(inst->dst, get_var(inst->src1) + get_var(inst->src2)); break; case IR_PRINT: printf("%d\n", get_var(inst->src1)); break; case IR_JZ: if (get_var(inst->src1) == 0) { jump_to_label(inst->label); // 查找 label 并设置 PC } break; // ... 其他指令 } }VarTable复用symbol.c的哈希表实现,set_var()/get_var()封装了符号表操作;jump_to_label()遍历 IR 指令数组,找到IR_LABEL类型且label字段匹配的指令索引,修改解释器 PC(程序计数器);- 所有变量(包括
t1,t2)都存于同一张表,is_temp字段仅用于内存释放时跳过临时寄存器。
6. 实验级改造实战:三步扩展 SNL 支持数组访问,从语法到 IR 全链路打通
很多同学卡在“知道原理,但不敢改代码”——怕改坏、怕调试难、怕不知从哪下手。我带过 3 届编译原理实验课,发现最有效的入门方式是:选一个微小但完整的功能点(如数组访问a[5]),从文法 → 词法 → 语法 → AST → IR → 解释器,全流程动手。下面以“为 SNL 添加一维数组声明与访问”为例,展示如何用不到 200 行代码完成闭环。
6.1 第一步:扩展文法与词法,让int a[10];被识别
SNL 原文法中Decl只支持TYPE ID ';',需新增数组声明产生式:
Decl → TYPE ID '[' INT ']' ';' | TYPE ID '=' Expr ';'对应修改lexer.c,增加对[和]的 token 定义:
// common.h typedef enum { // ...原有 token... TOKEN_LBRACK, // '[' TOKEN_RBRACK, // ']' } TokenType;在lexer_get_token()中添加:
case '[': *token_type = TOKEN_LBRACK; return '['; case ']': *token_type = TOKEN_RBRACK; return ']';6.2 第二步:修改语法分析器,构建数组 AST 节点
在ASTNodeType中新增AST_ARRAY_DECL和AST_ARRAY_ACCESS:
typedef enum { // ...原有类型... AST_ARRAY_DECL, AST_ARRAY_ACCESS } ASTNodeType;扩展ASTNodeunion:
union { // ...原有字段... struct { char* name; int size; } array_decl; // int a[10] struct { char* name; ASTNode* index; } array_access; // a[i] } u;在parse_decl()中识别[:
if (match(TOKEN_LBRACK)) { ASTNode* node = new_ast_node(AST_ARRAY_DECL); node->u.array_decl.name = strdup(id_name); if (!match(TOKEN_INT)) { error_at_current("expected array size"); return NULL; } node->u.array_decl.size = current_token.int_val; // INT token 的值 match(TOKEN_RBRACK); match(TOKEN_SEMI); insert_array_symbol(current_scope, node->u.array_decl.name, node->u.array_decl.size); return node; }6.3 第三步:生成数组访问 IR,复用现有解释器框架
数组访问a[i]需生成地址计算 IR:
t1 = i * 4 // 假设 int 4 字节 t2 = &a // 数组基址(需在符号表中存储) t3 = t2 + t1 // 计算 a[i] 地址 t4 = *t3 // 解引用在codegen.c中扩展gen_expr():
case AST_ARRAY_ACCESS: // 生成 t1 = i * 4 char* idx_reg = gen_expr(node->u.array_access.index, prog); char* t1 = next_temp(); add_ir_inst(prog, IR_MUL, t1, idx_reg, "4"); // 生成 t2 = &a (从符号表获取基址,此处简化为固定偏移) char* t2 = next_temp(); add_ir_inst(prog, IR_MOV, t2, "base_addr_of_a", NULL); // 生成 t3 = t2 + t1 char* t3 = next_temp(); add_ir_inst(prog, IR_ADD, t3, t2, t1); // 生成 t4 = *t3 (IR_DEREF 指令需新增) char* t4 = next_temp(); add_ir_inst(prog, IR_DEREF, t4, t3, NULL); return t4;同时在ir_interp.c中新增IR_DEREF处理:
case IR_DEREF: // 简化:假设 t3 存储的是内存地址(整数),直接当数组索引用 int addr = get_var(inst->src1); set_var(inst->dst, global_array[addr]); // global_array 是全局 int 数组 break;从那以后我每次给学生讲编译原理实验,都强制他们先用 SNL 源码跑通
hello.snl,再亲手加一个++自增运算符——不是为了功能多炫,而是让他们亲手触摸到:词法器吐出的 token 如何变成语法树节点,节点如何驱动 IR 指令生成,指令又如何被解释器一行行执行。当./snlc tests/array.snl out/arr && ./ir_interp out/arr.ir输出100的那一刻,编译原理就不再是书上的文字,而是他们键盘敲出来的、内存里跑起来的活物。希望帮到你。
本文还有配套的精品资源,点击获取