一起打造编译器:语义分析理论与实践(三)

34 阅读3分钟

类型检查

类型检查是验证源代码中操作和语句是否遵循语言的类型系统的过程。

AST中表达式的类型检查通常包括两个步骤:

  1. 推断表达式的类型
  2. 检查其类型是否与我们期望的类型匹配

类型检查可能失败的原因有两种:

  1. 我们无法推断表达式的类型。例如因为表达式中有未知的标识符。
  2. 表达式的类型与期望的类型不匹配。

示例

  1. 数组访问:x[i]
    • 推断x和i的类型
    • 检查x是否为数组,i是否为整数
  2. 算术表达式:3 * a
    • 推断3和a的类型
    • 检查乘法是否支持这些类型
  3. 函数调用:f(3, x)
    • 推断3和x的类型
    • 检查这些类型是否与f的参数类型匹配
  4. 赋值:x[3] = a
    • 推断x[3]和a的类型
    • 检查左侧和右侧的类型是否匹配

强类型与弱类型语言

并非所有语言都进行类型检查。

两种广义的编程语言类别:

强类型: 所有变量都有声明的类型,并且所有类型错误都可以被编译器或运行时系统发现。例如,Java和Python

弱类型: “一切皆可”,运行时系统尽可能执行程序。例如,在Perl中,"20a" + 10得到30。而在C中,(time *)将int变量解释为指针。

这里不是错误,真的c在某种程度可以是弱类型语言,但我们一般认为它是强类型语言。

**无类型:**汇编语言,一切都是字节。

实现

在语义分析期间,如何表示类型?

abstract class Type { String name; }
class IntType extends Type { }
class StringType extends Type { }
class ArrayType extends Type {
Type elementType;
		int size;
}
class RecordType extends Type {
		Map<String,Type> fields;
}

因此,对于声明int x [10],符号表中的条目将如下所示。

用户定义的类型

在许多语言中,程序员可以定义新类型,例如

struct MyNewType{
	int x;
};

我们还必须在语义分析期间保留新类型的table,类似于变量和函数的symbol table

这意味着我们还必须对类型声明执行某种类型的检查:

  1. 使用类型声明中的类型是否存在?
  2. 没有重复的类型名称?
  3. 记录中没有重复的字段名称?

还记得两个步骤吗?

  1. 推断表达式的类型
  2. 检查类型是否匹配我们期望的类型
var leftType = getTypeOfExpression(s.leftSide);
var rightType = getTypeOfExpression(s.rightSide);
if(!leftType.equals(rightType))
	throw new TypeErrorException();

对于像int、double等基本类型,它能够很好地工作:

double x; x = 123.2;

但是不要忘了隐式转换。

隐式转化

许多编程语言定义了自动类型转换,以使程序员的生活更加轻松。

在Java中:double x = 123;它们不是相同的类型,但仍然可以工作,因为Java中有隐式类型转换。

类型兼容性

除了类型等价性之外,许多编程语言还具有类型兼容性的概念。

我们已经看到了Java中整数向双精度浮点数的隐式类型转换。

还有子类型:

例如

class B extends A{/*....*/}
A x = new B();

Recap

经过这三篇的文章,我们掌握了基础的语义分析知识,已经可以对Tiny语言进行语义分析了。但是考虑到我的时间,我不能很快写出一个实例,感兴趣的同学可以先试试。Tiny仓库