Skip to main content

语义分析

类型系统

类型系统是一门语言所有的类型的集合,操作这些类型的规则,以及类型之间怎么相互作用

  • 根据领域的需求,设计自己的类型系统的特征
  • 在编译器中支持类型检查、类型推导和类型转换

设计类型系统

类型:是针对一组数值,以及在这组数值之上的一组操作

定规矩:可以检查施加在数据上的操作是否合法,通过类型检查降低计算出错的概率

取舍和权衡

  • 类型都是对象/支持非对象化的基础数据类型
  • 字符串是原生数据类型/是一个普通的类
  • 是静态类型语言/是动态类型语言
    • 静态类型语言(全部或者几乎全部的类型检查是在编译期进行的)
      • 程序错误较少(检查类型是否匹配,以及进行缺省的类型转换)
      • 性能更高(不需要在运行时再去做类型检查和转换)
    • 动态类型语言(类型的检查是在运行期进行的)
      • 语言太严格
      • 编程效率低(要一遍遍编译)
    • 强类型
    • 弱类型
  • 是否符合这门语言想解决的问题

类型检查、类型推导和类型转换

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 语句