您的位置:首页 > 其它

从零开始开发JVM语言(十一)Lambda

2016-06-26 23:23 357 查看
摘要: 编译器 语义分析 Lambda

目录戳这里

这篇说说“内部方法”和“Lambda”如何解析。

先看看java是怎么实现lambda的:

public static void x() {
Runnable r = () -> {

};
}

生成如下字节码:

public static void x();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: invokedynamic #53,  0             // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_0
6: return

这段很好理解,使用
indy
指令获取一个Runnable对象,然后保存在0号位置上。

此外生成了一个private方法:
lambda$x$0


private static void lambda$x$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=0
0: iconst_1
1: istore_0
2: return

这个方法就是lambda表达式中的语句:把整数1存储在0位置。

还生成了一个
indy bootstrap method


0: #165 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#166 ()V
#167 invokestatic SimpleTest.lambda$x$0:()V
#166 ()V

有点长,不过仔细看下就是调用一个方法罢了。上一篇说过lambda bootstrap方法前三个参数是必须的,其它参数可以自己加。JDK加了三个
MethodType
invokestatic SimpleTest.lambda$x$0:()V
表示的是lambda对象指向的方法,在执行lambda时执行的就是
lambda$x$0
中的语句。

很简单吧?

生成的是指定类型对象,并且对象是由
indy
指令获取的,所以还要规定bootstrap方法之类的东西。而lambda中的过程在某个private方法中规定。

在这个例子中有两点没有体现

对于非static方法中的lambda并不会使用
this
,而是额外传入一个对象,用来表示当前对象。不知道为何这么设计。

如果有已定义的局部变量,那么生成的方法会把
用到的
局部变量作为参数。

所以说,要解析lambda就要生成一个新的方法,这个方法是private的。

Latte
中,“内部方法”会生成一个方法,并且会把所有已定义的变量作为方法参数的一部分。所以我在实现时先实现了内部方法才去解析
lambda


#内部方法
代码戳这里

所谓的“内部方法”是指在方法中再定义一个方法。例如

foo(a)
b=1
bar(c)
return a+b+c
d=2
return bar(3)+d

foo
中由定义了一个方法
bar
。内部方法可以访问外部的局部变量。这似乎有点闭包的意思?

我的语言
Latte
支持“内部方法”,但是不支持闭包。因为
Latte
的内部方法只能访问外部变量,它们传入方法时新开了引用,所以在内部方法内可以赋值,但是不影响外部。(好吧,其实是实现时偷懒了。后续版本再加入修改外部变量的特性吧。。)而且方法并非变量,不能像js之类的语言那样返回一个函数,所以和闭包没有关系,只是一个方法而已。

语义分析在遇到内部方法时,将检查是否有重名方法,参数名是否重复等。这些validation就不多说了。
在合法性检查结束后,编译器将在本类中创建一个新方法。这个方法的参数为(之前定义过的局部变量,内部方法定义的变量)

例如上述
foo bar
的例子,在
bar
之前定义了
a
,
b
两个局部变量(d在bar之后,所以并不会被捕捉(capture)),而
bar
自己有一个参数
c
,所以最终生成的方法大致是这样:

xxx(a,b,c)
return a+b+c

在调用时自然也需要将局部变量传入。所以调用看起来是
bar(3)
,实际上编译器会编译成
bar(a,b,3)


这么做的好处是:实现非常简单,几乎不用加入任何额外的东西。新建一个方法,把其中的Statement设置为lambda中的statement,然后跑一下语义分析(实现上基本上就是调用一个方法而已),结束。
而且如此实现,对字段的操作毫无影响。
static
方法没有
this
,非static方法的
this
也可以直接用(因为编译器才不管你是不是内部方法呢~)。所以我强烈推荐这种实现方式。

至于命名,的确需要做额外处理。因为生成的内部方法名称一般都会加个后缀,比如
$0
$1
这样的。于是需要做一个映射:内部方法名到实际方法的映射。

缺点也有。当然,相比java没啥缺点,毕竟java语法糖少,像内部类还得要
final
才能capture。

#Lambda
代码戳这里

有了“内部方法”后lambda解析就变得非常方便了。static方法可以直接按照java的实现:

创建一个内部方法

使用和java一模一样的方式调用
indy
bootstrap method
来获取对象

但是对于非static方法 或者 要实现的方法“是由抽象类定义的”(就是所谓的functional abstract class,定义在前文有写)。

java对于非static方法生成的lambda会多一个对象表示
this
,而不会直接用
this
,与我的“内部方法”生成的方法策略不符。而java与支持“lambda抽象类”,所以也不能直接用。

那么怎么办呢?其实也很简单。做到这一步了,几本整个编译器架构成型了。嫌麻烦的手动做一个AST来实现/继承函数式的类型,然后再交给语义分析器。不嫌麻烦的直接把语义分析的输出格式做出来。

前者有可能在加了特性后出现莫名其妙的问题,后者实现麻烦。我是用的后者,不过前者其实也是可取的,测试用例写好点就行了。

这个“手动”加的类应该有什么特性呢?

需要保持所有的已定义的局部变量。

要知道执行哪个方法。

用lambda中的语句实现那唯一一个没实现的方法。

我构建的类大概长这样

class LambdaClassName(
MethodHandle 要执行的方法引用
Object 在哪个对象上执行
List 局部变量
):要实现的类型
实现的方法(参数):返回类型 ; (当然,也有可能是void,那样就执行方法但不返回其值)
newList=LinkedList(local);
newList.add(0, o);
newList.add(x);
newList.add(y);
return methodHandle.invokeWithArguments(newList);

实现中逻辑如下:

创建一个内部方法

获取指向它的MethodHandle

构建List存放局部变量

创建上述的类

用MethodHandle,this,List构造这个类

最终获取到的就是lambda表达式的返回对象了。

#Runtime Lambda
Latte
可以不指定类型,所以默认lambda类型为
lt::lang::function::FunctionX
,所以可能需要在运行时cast到所需类型。

那么怎么做呢?

验证是否为函数式接口/抽象类上一篇题过,运行时的做法更简单,直接
getMethods()
,看abstract方法个数就行。
接口的cast比较简单,jdk提供了
Proxy

而类的实现并没有那么简单,毕竟工程上也得要cglib这种第三方库。

然而我们在运行时有编译器呀~我的做法是最原始的:拼接字符串。把源代码拼出来然后丢给编译器。代码戳这里

这篇就这些吧~下一篇说说“重载方法”执行哪个如何确定~

最后,希望看官能够关注我的编译器哦~Latte
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  编译器 语义分析