news 2026/10/12 16:46:19

SNL编译器源码:C语言实现的编译原理实操沙盒

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SNL编译器源码:C语言实现的编译原理实操沙盒

简介:本资源是一套完整的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.c120主流程调度:读源码 → 调用 lexer → parser → codegen → 输出在main()入口打断点,观察argc/argv解析逻辑
lexer.c480词法扫描:正则匹配关键字/标识符/数字/运算符,返回 token 流修改skip_whitespace()逻辑,测试注释处理边界
parser.c620递归下降语法分析:parse_program()→parse_block()→parse_expr(),构建 AST在parse_if_stmt()中插入printf("parsing IF at line %d\n", line_no)
ast.c210AST 节点定义与内存管理:ASTNode结构体、new_ast_node()、free_ast()查看AST_NODE_TYPE枚举,对照文法理解节点类型映射
symbol.c350符号表实现:哈希桶 + 链地址法,支持作用域嵌套(push_scope()/pop_scope())在lookup_symbol()返回前加if (!sym) fprintf(stderr, "UNDECLARED: %s\n", name);
codegen.c430三地址码生成:遍历 AST,为每个节点生成IR_INST,输出.ir文件修改gen_binary_op()中op == '+'分支,插入临时变量命名逻辑
utils.c120工具函数:字符串分割、内存安全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 && make

Makefile内容精简到 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 → StmtList
  • parse_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; }

这个函数看似简单,但藏着三个关键设计:

  1. 错误恢复弱但可控:match()报错后不跳过 token,而是让上层函数决定是否sync_to_statement()(同步到下一个;或}),避免雪崩式报错;
  2. next_token()是 lexer 的门面:它封装了lexer_get_token()调用,屏蔽了词法器内部状态(如缓冲区、行号计数器),使语法分析器完全 unaware 于词法细节;
  3. 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的那一刻,编译原理就不再是书上的文字,而是他们键盘敲出来的、内存里跑起来的活物。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 16:46:14

从PEID到DIE:查壳工具的进化与命令行批量识别实践

简介&#xff1a;Detect It Easy&#xff08;以下简称DIE&#xff09;是一款专业而强大的查壳与文件识别工具&#xff0c;在安全分析与逆向工程中用途广泛&#xff0c;尤其适合需要快速识别程序加壳类型、排查恶意样本或验证软件保护方案的场景。相比传统查壳工具PEID&#xff…

作者头像 李华
网站建设 2026/10/12 16:46:07

产品研发创新体系L1-L3流程规划:从战略到操作的三层架构

简介&#xff1a;这是一份面向企业研发总监、项目经理及流程规划岗位的PPT资料&#xff0c;围绕产品研发创新体系L1-L3流程规划展开&#xff0c;目的正是解决技术预研与产品开发之间流程割裂、责权不清的问题。内容以53页篇幅完整梳理技术研究管理链路&#xff0c;包括课题立项…

作者头像 李华
网站建设 2026/10/12 16:44:23

网络切片仿真资源包实战:从资源分配翻车到半静态预留优化

简介&#xff1a;网络切片仿真.rar 面向5G通信网络方向的研究生、科研人员与网络工程师&#xff0c;聚焦共享物理基础设施上多虚拟网络的定制化建模与性能评估。资源围绕网络切片概念、NFV与SDN虚拟化技术、SLA服务等级协议、资源分配、安全隔离、动态调整与自愈能力等核心知识…

作者头像 李华
网站建设 2026/10/12 16:44:02

代码评审实战指南:从流程设计到团队协作的完整落地

1. 从“代码评审”这件事说起“open-code-review”这个标题&#xff0c;乍一看像是一个开源项目的名字&#xff0c;实际上它指向的是一类非常具体的工作场景&#xff1a;把代码评审&#xff08;Code Review&#xff09;这件事&#xff0c;从“靠人盯”变成“有章法、可复用、能…

作者头像 李华