• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-08-20 11:01 Aet 隐藏边栏 |   抢沙发  2 
文章评分 2 次,平均分 5.0

什么是解释器/编译器

解释器 vs 编译器:有什么区别

  1. 很多人以为"编译器把代码变成机器码,解释器直接执行代码",这个说法不算错,但不够准确
    1. 更准确的区分方式是看代码的"意义"(semantics)是何时、如何被实现的:
  2. 编译器(Compiler):
    1. 把源代码翻译成另一种语言(通常是更底层的,比如机器码、字节码,甚至是另一种高级语言),翻译完之后,"执行"这件事交给别人(CPU、虚拟机)去做
    2. 编译器本身不负责运行你的程序,只负责翻译
  3. 解释器(Interpreter):
    1. 直接读取源代码(或者某种中间表示),一边理解一边执行,不产生一个独立的"翻译产物"交给别人运行

关键认知

  1. 这两者不是二选一,很多真实系统是混合体
  2. Java
    1. 先编译成字节码(.class 文件),再由 JVM 解释执行或 JIT 编译成机器码
    2. 编译 + 解释都用上了
  3. V8ChromeJS 引擎):
    1. 先解释执行,热点代码再 JIT 编译成机器码
    2. 同一段代码,执行路径会变

一段代码从文本到执行,中间发生了什么

  1. 不管是解释器还是编译器,前几个阶段几乎是通用的,可以想象成一条流水线

  1. 这条流水线里,词法分析和语法分析这两步,不管你最终是做解释器还是编译器,几乎是完全一样的

几个贯穿全程的核心概念

  1. Token(词法单元):
    1. 源代码里最小的、有意义的片段
    2. 比如 1 + 2 里,1+2 各是一个 Token
    3. 空格、注释这些通常在词法分析阶段就被丢弃或跳过
  2. AST(抽象语法树,Abstract Syntax Tree):
    1. Token 序列按语法规则组织成的树状结构,体现了代码的"层级关系"
    2. 比如 1 + 2 * 3AST 会体现出"乘法先算"这个优先级关系,而不是简单地从左到右排列
  3. 文法(Grammar):
    1. 描述一门语言"什么样的 Token 组合是合法的"的规则集合,是设计语法分析器之前必须先想清楚的东西
  4. 求值(Evaluation):
    1. 给一段代码/AST 一个具体的"运行结果"或"副作用"的过程,是解释器的核心动作

目标脚本语言

  1. 具备:
    1. 基本数据类型:数字、字符串、布尔值、nil(空值)
    2. 变量、赋值
    3. 算术/比较/逻辑运算
    4. 控制流:if/while/for
    5. 函数(含闭包)
    6. 简单的类(面向对象,可选深入)

示例

  1. token

  1. 光切开还不够,编译器还需要知道每一块是"什么性质"的东西
# 文本 类型 说明
1 var 关键字(Keyword 语言保留字,有特殊含义,不能被用户当变量名用
2 age 标识符(Identifier) 用户自定义的名字(变量名/函数名/类名等)
3 = 运算符(Operator,具体是赋值运算符)
4 18 数字字面量(Number Literal 一个具体的数值
5 + 运算符(Operator,具体是加法运算符)
6 2 数字字面量(Number Literal
7 ; 标点符号(Punctuation,具体是语句结束符)
  1. 问题一:agevar 都是字母开头,词法分析器怎么区分?
    1. 分两步走,先按同一套规则识别,再做一次"关键字表查询"
    2. 具体来说,词法分析器扫描到字母时,不会一开始就判断"这是关键字还是标识符",而是统一按照"标识符规则"(字母/下划线开头,后面跟字母/数字/下划线)先把整个单词扫出来,比如扫出 var 这三个字符
    3. 扫完之后,拿这个字符串去一张"关键字表"里查一下
    4. 如果查到了(var 在表里),就标记为关键字类型;如果没查到(age 不在表里),就标记为标识符类型

  1. 问题二:18 要不要处理小数点?
    1. 词法分析阶段需要考虑到,规则要设计得能兼容小数,即便这条语句里没用到

  1. 这就引出一个词法分析器设计上很实际的问题:
    1. 扫描一个数字时,扫描器要"往前多看一个字符"(这个动作叫 lookahead,向前看/预读),才能判断到底要不要把小数点也吞进来
    2. 比如扫描到 18 后面这个字符,如果是 . 并且 . 后面紧跟的还是数字,就继续吞进来组成 18.5
    3. 如果 . 后面不是数字,就不能把 . 也当作这个数字的一部分
  2. 这种"预读一个字符再决定"的技巧,是词法分析器实现中一个非常核心、会反复用到的手法

词法分析(Lexer/Scanner)实现

第一步:定义 Token 是什么

  1. 一个 Token 光有"文本内容"还不够,词法分析器至少要给每个 Token 附上这些信息

  1. 为什么要记录 line(行号)?
    1. 因为词法分析阶段发现的任何问题(比如遇到一个不认识的字符),都要能告诉用户"第几行出错了",这是编译器友好性的基本要求,从一开始就要设计进去,不要等报错系统写不出来了再回头加

第二步:Scanner 类的骨架

  1. 词法分析的核心逻辑其实就是一个循环:
    1. 不断地看当前字符是什么,决定要生成什么 Token,然后把"指针"往前移动

第三步:处理字符串字面量

  1. 注意这里一个容易被忽略的设计点:
    1. lexeme(原始文本,含引号)和 stringValue(真正的字符串内容,不含引号)我们分开存了两份
    2. 这是因为将来做错误提示时,你可能想显示用户写的原始文本
    3. 而解释器求值时,用的是去掉引号后的真实值
    4. 原始文本和"语义值"是两个不同的东西,要分开保存,这个设计思路后面在数字、布尔值上也会延续

第四步:处理数字字面量(预读小数点)

  1. 这里 peek() == '.' && isDigit(peekNext()) 这一行
    1. 必须同时满足"当前是小数点"和"小数点后面紧跟数字",才把小数点吞进来,否则像 18. 这种后面没有数字的情况就不会被错误地当成小数处理

第五步:处理标识符与关键字

语法分析(Parsing

目标

  1. 设计 AST(抽象语法树)节点
  2. 写一个递归下降解析器(Recursive Descent Parser),把 token 序列变成 AST
  3. 用一个 AstPrinter 把树打印出来验证

上下文无关文法(Context-Free Grammar

  1. 专门描述"表达式该怎么解析"
  2. 读作"由...组成",左边是一个语法规则的名字,右边是它的定义
  3. 表示"或者"(几选一)
  4. ( ... ) 只是分组,跟数学里的括号一个作用
  5. * 表示"前面这坨内容可以重复 0 次或多次"(跟正则表达式里的 * 一个意思)
  6. 全大写的 NUMBERSTRING 是终结符(terminal
    1. 对应Scanner 吐出来的具体 token,不能再往下展开了
  7. 小写的 equalitycomparison 这些是非终结符(non-terminal
    1. 还得继续展开成别的规则,直到展开成终结符为止

  1. 这套规则在干嘛:编码"优先级"
    1. 6 行规则其实是从低优先级到高优先级排的一条链:

  1. 为什么这么排?
    1. 因为递归下降解析器是"从外往里剥洋葱"的:
    2. 先假设整个表达式是最低优先级的运算(== !=),如果没有就往里剥一层看是不是 > < >= <=,再往里剥是不是 + -,再往里是 * /,最后剥到最里面才是一个具体的数字/字符串/括号表达式
    3. 这样"层层剥"天然就实现了优先级——*+ 先被"看到"(更靠里),所以会先结合
  2. 逐条对着看

  1. 用一个例子走一遍

  1. 解读

AST 节点定义

  1. 对应 AST 只需要 4 种节点:BinaryGroupingLiteralUnary

    1. 文法有 7 条规则,但 AST 只需要 4 种节点,因为好几条规则最终产生的树形状是一样的
  2. 关键区分:规则管"怎么解析",节点管"树长什么样"

  1. 为什么要故意这样设计
    1. 文法规则的"层数"是为了编码运算符优先级——层数越多,优先级判断越精细
    2. AST 只关心"这段代码最终该怎么求值/怎么执行"

Expr.h

AstPrinter.h

Parser.h

Token.h

main.cpp

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享