语义分析
类型系统
类型系统是一门语言所有的类型的集合,操作这些类型的规则,以及类型之间怎么相互作用
- 根据领域的需求,设计自己的类型系统的特征
- 在编译器中支持类型检查、类型推导和类型转换
设计类型系统
类型:是针对一组数值,以及在这组数值之上的一组操作
定规矩:可以检查施加在数据上的操作是否合法,通过类型检查降低计算出错的概率
取舍和权衡
- 类型都是对象/支持非对象化的基础数据类型
- 字符串是原生数据类型/是一个普通的类
- 是静态类型语言/是动态类型语言
- 静态类型语言(全部或者几乎全部的类型检查是在编译期进行的)
- 程序错误较少(检查类型是否匹配,以及进行缺省的类型转换)
- 性能更高(不需要在运行时再去做类型检查和转换)
- 动态类型语言(类型的检查是在运行期进行的)
- 语言太严格
- 编程效率低(要一遍遍编译)
- 强类型
- 弱类型
- 静态类型语言(全部或者几乎全部的类型检查是在编译期进行的)
- 是否符合这门语言想解决的问题
类型检查、类型推导和类型转换
a = b + 10;
类型推导(Type Inference)
如果 b 是一个浮点型,b+10 的结果也是浮点型。如果 b 是字符串型,实际的结果是字符串的连接
case ScriptParser.ADD:
if (type1 == PrimitiveType.String || type2 == PrimitiveType.String){
type = PrimitiveType.String;
} else if (type1 instanceof PrimitiveType && type2 instanceof PrimitiveType){
// 类型“向上”对齐,比如一个int和一个float,取float
type = PrimitiveType.getUpperType(type1,type2);
} else {
console.log("operand should be PrimitiveType for additive operation");
}
break;
private Object add(Object obj1, Object obj2, Type targetType) {
Object rtn = null;
if (targetType == PrimitiveType.String) {
result = String.valueOf(obj1) +
String.valueOf(obj2);
} else if (targetType == PrimitiveType.Integer) {
result = ((Number)obj1).intValue() +
((Number)obj2).intValue();
} else if (targetType == PrimitiveType.Float) {
result = ((Number)obj1).floatValue()+
((Number)obj2).floatValue();
}
...
return result;
}
S 属性(Synthesized Attribute)
如果一种属性能够从下级节点推导出来,那么这种属性就叫做 S 属性,是通过下级节点和自身来确定的
I 属性(Inherited Attribute)
AST 中某个节点的属性是由上级节点、兄弟节点和它自身来决定的
int a;
variableDeclarators
: typeType variableDeclarator (',' variableDeclarator)*
;
variableDeclarator
: variableDeclaratorId ('=' variableInitializer)?
;
variableDeclaratorId
: IDENTIFIER ('[' ']')*
;
typeType
: (classOrInterfaceType| functionType | primitiveType) ('[' ']')*
;
// Go 语言两种声明变量的方式
var a int = 10 //第一种
a := 10 //第二种
类型检查(Type Checking)
当右边的值计算完,赋值给 a 的时候,要检查左右两边的类型是否匹配
- 赋值语句(检查赋值操作左边和右边的类型是否匹配)
- 变量声明语句(因为变量声明语句中也会有初始化部分,所以也需要类型匹配)
- 函数传参(调用函数的时候,传入的参数要符合形参的要求)
- 函数返回值(从函数中返回一个值的时候,要符合函数返回值的规定)
类型转换(Type Conversion)
如果 a 的类型是浮点型,而右边传过来的是整型,一般要进行缺省的类型转换
// MySQL 自动将'2'转换成了数字
select 1 + '2';
强制做类型转换,只有到运行期才能检查出错误
引用消解
在程序里使用变量、函数、类等符号时,我们需要知道它们指的是谁,要能对应到定义它们的地方
#include <stdio.h>
int a = 1;
void main()
{
a = 2;
int a = 3;
int b = a;
printf("in func: a=%d b=%d \n", a, b);
}
// output
in func: a=3 b=3
变量的引用消解
比对变量
函数的引用消解
比对函数名称,参数和返回值(可以叫函数原型,或者函数的类型)
需要返回值、 参数个数、每个参数的类型都能匹配
面向对象编程语言:当一个参数需要一个对象时,程序中提供其子类的一个实例也可以
class MyClass1{} //父类
class MyClass2 extends MyClass1{} //子类
MyClass1 obj1;
MyClass2 obj2;
function fun(MyClass1 obj){} //参数需要父类的实例
fun(obj2); //提供子类的实例
强类型编程语言:考虑某个实参是否能够被自动转换成形参所要求的类型(需要 double 类型的地方,传一个 int 也可以)
命名空间:
play.PlayScriptCompiler.Compile(); //Java语言
play::PlayScriptCompiler.Compile(); //C++语言
左值和右值
左值
a + 3;
a = 3;
第 3 行应该取出 a 的地址,或者说 a 的引用,然后用赋值操作把 3 这个值写到 a 的内存地址。这时,我们说取出来的是 a 的左值(L-value)
- 赋值表达式的左边
- 带有初始化的变量声明语句中的变量
- 当给函数形参赋值的时候
- 一元操作符: ++ 和 ––
- 其他需要改变变量内容的操作
判断表达式是否能生成合格的左值:
出现在赋值语句左边的,必须是能够获得左值的表达式
- 一个变量
- 一个类的属性
2+3
右值
就是我们通常所说的值,不是地址
属性计算
属性计算是做上下文分析,或者说语义分析的一种算法
- 它的变量定义是哪个(这就引用到定义该变量的 Symbol)
- 它的类型是什么
- 它的作用域是什么
- 这个节点求值时,是否该返回左值
- 能否正确地返回一个左值
- 它的值是什么
属性文法(Attribute Grammar)
正则文法,上下文无关文法
属性文法:在上下文无关文法的基础上做了一些增强,使之能够计算属性值
// 上下文无关文法表达加法和乘法运算的例子
add → add + mul
add → mul
mul → mul * primary
mul → primary
primary → "(" add ")"
primary → integer
// 对 value 属性进行计算的属性文法
add1 → add1 + mul [ add1.value = add2.value + mul.value ]
add → mul [ add.value = mul.value ]
mul1 → mul2 * primary [ mul1.value = mul2.value * primary.value ]
mul → primary [ mul.value = primary.value ]
primary → "(" add ")" [ primary.value = add.value ]
primary → integer [ primary.value = strToInt(integer.str) ]
特点:它会基于语法规则,增加一些与语义处理有关的规则
语义分析过程
类型和作用域解析
把自定义类、函数和和作用域的树都分析出来
可以使用在前,声明在后
类型的消解
把所有出现引用到类型的地方都消解掉
变量声明、函数参数声明、类的继承等等
引用的消解和 S 属性的类型的推导
对所有的变量、函数调用 ,都跟它的定义关联起来,并完成所有的类型计算
做类型检查
当赋值语句左右两边的类型不兼容的时候报错
做一些语义合法性的检查
- break 只能出现在循环语句中
- 某个函数声明了返回值一定要有 return 语句