新特性提案
声明:
标注有“参考实现”的部分给出了一个可供参考的实现方案,不是实验要求。大家可自行决定如何实现。
具体细节上的要求有待实验各阶段进一步明确。本文档仅供参考
特性一:抽象类
支持用 abstract 关键字定义抽象类,及修饰其中的抽象成员方法。由于 Decaf 不支持对成员变量和静态方法的重写 (override),所以不允许出现抽象的成员变量和静态方法。
文法
新增关键字 'abstract'。
注意:抽象方法不能有函数体;语法上已经限制了成员变量和静态方法不能是抽象的。
类型检查
错误1:若一个类中含有抽象成员,或者它继承了一个抽象类但没有重写所有的抽象方法,那么该类必须声明为抽象类。
错例1-1:
错例1-2:
报错信息分别为:'Foo' is not abstract and does not override all abstract methods 和 'Baz' is not abstract and does not override all abstract methods
错误2:抽象类不能使用 'new' 进行实例化。
错例2:
报错信息:Cannot instantiate abstract class 'Foo'
注意:抽象类中可以定义任何普通的成员(成员变量、成员方法、静态方法),如:
抽象类中的静态方法可以像普通的静态方法一样调用,即:
或者
代码生成(参考实现)
由于抽象类仍然是一个类型,所以在运行时仍有可能会进行 instanceof 的判断,故对抽象类生成一个虚表也是有必要的,该虚表至少应该还有两项元信息:即父类的虚表指针(若存在)和该类的名称。
但是,由于抽象类不可实例化,因此抽象类中存在的所有成员方法都是不可能会被直接访问到的,因此抽象类的虚表无需记录任何成员方法的入口地址。这些成员方法的信息只存在于它的非抽象子类中。如:
对应的虚表为
但是你需要自己保证在一个继承链上,同样的方法名在虚表中的偏移量一致。
抽象类的静态方法与普通静态方法无区别,为它们各自生成一段函数即可。
特性二:本地类型推导
新增 'var' 关键字,支持编译器进行本地类型推导。所谓“本地”,是指仅推断一个表达式的类型,而不是推断一个函数的类型(“全局”推导)。
语法
新增 'var' 关键字,并支持作为一个简单表达式出现函数体内,或出现在 for 循环的初始化语句和更新语句的位置。
类型推导
针对如下例子:
应该推断出 x
的类型是整数,s
的类型是字符串。
此外,在实现了特性三的基础上,用 'var' 关键字可以方便的定义 Lambda 表达式,如:
本地类型推导要求推断出表达式最精确的类型,如:
这里 o
的类型应该是 class B
而不是 class A
。有关函数类型的推断请见特性三。
特性三:First-class functions
支持函数式语言中常见的 first-class functions,要求支持 lambda 表达式、将方法名直接当做函数使用、正确构造闭包环境等。
函数类型
文法
允许用户在程序中标注函数类型:
这里我们采用类似于 C 语言函数指针和 JVM 类型描述符的语法来标注函数类型,如 int(class A, string)
表示该函数返回值类型是 int
,它接受两个参数,类型分别是 class A
和 string
。
与实际类型的对应
类型字面量(语法元素)与实际类型(语义元素)的对应如下:
特别地,VoidType 只能作为函数的返回类型(包括lambda),不能作为任何变量的类型或者函数参数的类型。
子类型 (Subtyping) 检查
记 <: 是类型上的二元关系,它满足:
自反性:t <: t
传递性:If t1 <: t2 and t2 <: t3, then t1 <: t3
类继承:If c1 extends c2, then ClassType(c1) <: ClassType(c2)
函数:If t <: s and si <: ti for every i, then FunType([t1, t2, ..., tn], t) <: FunType([s1, s2, ..., sn], s)
Lambda 表达式
文法
新增 Lambda 表达式并扩展 call 的文法:
新增关键字 'fun' 和操作符 '=>'。操作符 '=>' 具有最低优先级。
注意这里我们要求 Lambda 表达式的每个参数必须标明类型,但是返回的表达式不标注类型。
类型推导与检查
对于 fun (t1 x1, t2 x2, ...) => y
,若推导出 y
的类型为 t
,那么整个 Lambda 表达式的类型为 FunType([t1, t2, ...], t)
。
若 y
是一个 BlockStmt
,则 y
的类型要么是 VoidType(当 BlockStmt 的所有执行路径不返回值时),要么是 BlockStmt 的所有执行路径返回值的最小“上界”。定义:称 t 是类型 t1, t2, ..., tn 的上界,若 t1 <: t, t2 <: t , ..., tn <: t。若该上界不存在,应该报错:Incompatible return types in blocked expression
类型检查规则同原有规则,如对于赋值语句和函数调用,应遵循子类型的相应规则。
捕获变量与闭包生成(参考实现)
针对每种类型的 Lambda 表达式,为了支持它像对象一样可以到处传递,我们可以把它真的实例化为一个对象,并规定 apply 方法为 Lambda 表达式调用时执行的函数。例如,任何 int(int)
类型的 Lambda 表达式都应该是如下类的某个子类的实例:
这样在调用该 Lambda 表达式 e
时,就相当于调用其 apply
方法:e(x) === e.apply(x)
对于 Lambda 表达式中引用的除参数外的其他变量,需要捕获其在当前作用域的值(IntType)或者引用(其他类型)。如:
func
需要捕获当前作用域中 x
的值, a
的引用以及 this
的引用。并将捕获的变量包装在闭包对应的类中:
那么 func
对应的对象为:
由于这段代码是自动生成的 (synthetic),我们可以突破成员变量 protected 的束缚。
若 Lambda 表达式函数体内对于某个本地变量进行了赋值,那么为了保证该本地变量下次读取时确实是更新了的值,我们必须捕获这个变量的引用。简单起见,我们规定不能对一个变量直接赋值(无论是传值还是引用),但如果传入 Lambda 表达式的是一个对象或数组的引用,则可以通过该引用修改类的成员或数组元素。
代码执行后 a.v
应为 10
。解开 Lambda 后:
提示:
为了避免为每一种类型的闭包都生成一个抽象类,可以考虑设计类似于 java.lang.Object 这样的超类,这样只用为不同参数个数的闭包生成单独的类。(通过类型擦除实现泛型)
除了用类来包装一个函数闭包外,采用函数指针的实现也是可行的
将方法名直接当做函数使用
需要返回函数或者接受函数作为参数时,可直接使用类型匹配的函数名,来替代实际的 Lambda 表达式,如:
由于 print
的类型就是 void(int)
,可以直接传给 foreach
作为参数。生成代码时,直接将 foreach
封装为相应的闭包对象即可:
Last updated
Was this helpful?