解释
好久没写软工计的笔记了。开始讲 Python 后,因为对 Python 有点了解,就懒得记了。不过即使到后面有一些之前不了解的,亦或者到 Java 完全不了解,也没记。但现在又回到理论了,还是记点。
一些基本概念已经在计算系统基础中介绍过了。
解释器流程 Read-Eval-Print Loop (REPL):
- 读取(Read)用户的输入(并转化为表达式)
- 求值(Evaluate)表达式
- 打印(Print)结果
- 循环(Loop)上述过程直至结束
计算器语言(Calculator Language)
新创造一个语言 Calc,并为其实现一个解释器。
该过程叫 metalinguistic abstraction,即用一种语言描述另一种语言。
新创造的语言称为 domain-specific language(DSL),即特定领域语言。
1 | calc> add(3, 4) |
用一个语言去解释另一个语言(常见的解释及设计方案):
- C 解释 Python(CPython)
- Java 解释 Python(Jython)
- Python 解释 Python(Pypy)
下面用 Python 实现一个 Calc 解释器。
语法和语义(Syntax & Semantics)
Calc 表达式:
- 基本表达式(primitive expression):数字
- 调用表达式(call expression):操作符名字,加上括号,括号里是由括号隔开的操作数列表
操作符:
- add, +:所有实参的和
- sub, -:若只有一个实参,则返回其负值;若有多个实参,则从第一个实参开始减去后续所有实参
- mul, *:所有实参的积
- div, /:从第一个实参开始除以后续所有实参
设定为前缀(prefix)表示法,即操作符在操作数之前。
类似 Lisp 语言的表达式:
1 | (+ 3 4) |
Read-Eval-Print Loop
1 | def read_eval_print_loop(): |
- Read:
input
- Parse:
calc_parse
- Eval:
calc_eval
- Print:
print
Parse
但仅有原始的字符串是不够的,对于计算机而言,一个简单的结构化的表达式更加容易理解,因此我们要进一步对原始的字符串进行「解析」。
解析器(parser)接受字符串输入,返回一个表达式。
步骤:
- 词法分析(lexical analysis):将字符串分解为 token
- Identify the words: Divide input string into meaningful tokens, such as integer litterals, identifiers, key words.
- 识别单词:将输入字符串分解为有意义的 token,如整数字面量、标识符、关键字
- 语法分析(syntactic analysis):将 token 组合成表达式
- Derive the structure of sentence: Converts tokens into a more structured representation (parse tree or expression tree).
- 推导句子结构:将 token 转化为更有结构的表达(解析树或表达式树)
flowchart LR
1[text] -->|lexical analysis| 2[tokens] -->|syntactic analysis| 3[expression tree]
1 | def calc_parse(line): |
tokenize("add(2, mul(4, 6))")
返回
['add', '(', '2', ',', 'mul', '(', '4', ',', '6', ')', ')'] |
analyze
函数将 token 转化为表达式树
flowchart TB
add --> 2 & mul
mul --> 4 & 6
Eval
求值(evaluation)
给定一个解析后的表达式(表达式树),Evaluation 过程用来求出该表达式的值。
- 基本表达式:直接返回其值
- 调用表达式:递归地求出所有操作数的值,然后应用操作符
- 对每个操作数进行求值(evaluate)
- 将求得的值放到实参的列表上
- 将操作符应用到实参列表上
1 | def calc_eval(exp): |
1 | def calc_apply(operator, args): |
Python 子集语言 Py
- 环境
- 有了变量可以在「记忆」信息,这些变量可以在环境中可访问
- 函数有参数、函数体、并且知道当被调用时所在的环境
- 语句
- 语句比表达式更加复杂,表达了自身求值、改变环境等操作
REPL for Py:
1 | def read_eval_print_loop(interactive=True): |
py_read_and_parse
: Read in a statement and produce aStatement
object(读入一个语句并生成一个Statement
对象)evaluate
: Evaluate the statement we read, using the global environment(使用全局环境对读入的语句进行求值)print
: Print the result of the evaluation(打印求值结果)
环境(Environment)
作用是追踪变量名字和相应的值。使用字典来实现。
1 | class Environment: |
self.enclosing_env
: parent environment(父环境)self.bindings
: variable bindings(变量绑定)self.nonlocal_refs
: nonlocal references(非局部引用)
1 | class Environment: |
is_global
: Check if the environment is global(检查环境是否为全局环境)note_nonlocal_ref
: Record a nonlocal reference(记录一个非局部引用)
在环境中查找名字绑定的值:
1 | class Environment: |
__getitem__
特殊方法,可以直接用env[var]
来查找名字绑定的值
在环境中改变名字绑定的值:
1 | class Environment: |
__setitem__
特殊方法,可以直接用env[name] = value
来改变名字绑定的值
语句(Statement)
- 赋值语句(assignment statement):自动对表达式进行求值,并将值绑定到目标名字上
- 调用语句(call statement):对操作符和操作数分别求值,然后将执行 apply
- 控制语句(control statement):复合语句,包含布尔表达式,语句块,首先对布尔表达式进行求值,根据结果是否对语句块中的语句进行求值
赋值语句:
1 | class AssignStatement(Statement): |
__init__
:初始化赋值语句expr.evaluate
:在所给环境对赋值语句进行求值
调用语句:
1 | class CallStatement(Statement): |
__init__
:初始化调用语句operator.evaluate(env)
:在所给环境对操作符进行求值operand.evaluate(env)
:在所给环境对操作数进行求值operator.apply(arguments)
:对操作符和操作数进行应用
核心是对 Function
对象的 apply
方法的调用。
构造函数表达:
1 | class Function: |
Function
类:抽象函数类,定义了apply
方法PrimitiveFunction
类:原始函数类
内建基本函数:
1 | primitive_functions = [ |
用户定义函数:
1 | class CompoundFunction(Function): |
__init__
:初始化用户定义函数,包含参数(args)、函数体(body)、所在环境(env)call_env
:调用时创建一个帧for name, value
:将实参绑定到形参上for statement
:在当前帧中,对函数体每个语句进行求值except
:直到获取返回值
实现返回语句
1 | class StopFunction(Exception): |
控制语句:
1 | class ControlStatement(Statement): |
总结
我们实际上是用「Python 的程序语句」来表达对象语言(我们自己定义的语言)的语义。
这种表达语义的方式就是操作语义(Operational Semantics),即描述语言是如何执行的。
抽象解释(Abstract Interpretation)
具体语义的问题
程序的具体语义形式化了程序在所有可能环境中的所有可能执行。
例如
1 | unsigned int a = input(); |
代表
1 | a = 0 --> a = 1 |
对于一个具体的操作语义而言,可以将程序的执行看成是「状态」(即程序的变量构成的环境)的不断迁移的过程(如何迁移为具体语义定义)。
由于输入的无限可能,执行路径有无数可能(甚至某一条路径无限长,即不停机)。
然而对于这种具有无限的踪迹的语义来说,很多关于程序的不平凡的问题都没有答案。
- 比如全停机问题:程序在任何可能输入下会终止吗?
更一般地,我们关心一些有关程序的安全性的问题。
所谓的安全性就是:「没有坏事发生」,具体的安全性与具体的「坏事」相关
- 例如:不可终止、空指针解引用、数组越界等。
安全性要求执行中的任何有限步骤内都保持这个性质。
「测试」无法解决这个问题,因为测试是具体语义上的 sample 技术(可能漏掉一些可能非安全状态的输入)。
「有界模型检验」(bounded model checking)也无法解决这个问题,因为它只能检查有限长度的路径。
「抽象解释」:赋予语句抽象语义,利用抽象的域替代无限的输入,状态的迁移变成了抽象域的迁移,减少了计算空间。
抽象解释
例如判定程序中所有数字变量的正负性。
抽象域:
+
:正数-
:负数0
:零⊤
:都可能(未知)⊥
:非法(未定义)
抽象语义:
- 迁移函数:在抽象域上进行求值
+
++
=+
-
+-
=-
0
+0
=0
+
+-
=⊤
⊤
/0
=⊥
- ……
例如
1 | a = 5; a = + |