目录戳这里java
上一篇完成了语义分析的第三步。这一篇将开始语义分析的第四步也是最后一步,指令的解析。git
这一步目标是将Statement转化为指令。github
JVM针对同一事件的指令经常分为5种:bootstrap
i,l,f,d,a
分别是 整型,长整型,浮点型,双精度型和引用类型。jvm
比方说,从局部变量表的第0个位置读取参数.net
iload_0 // int lload_0 // long fload_0 // float dload_0 // double aload_0 // ref
而像byte
,short
,char
,boolean
统一使用i指令
来完成读写。调试
JVM指令虽然相似汇编却比汇编高级不少。JVM指令的格式在第七篇中大体说了一下,不过最好仍是参考jvm specification详细了解。
这里说一说比汇编高级的地方。code
从字节码就能看出,JVM并不要求编译器在编译期进行链接。而是记录一个字符串,表示须要链接的类。好比父类就是用相似java/lang/Object
来描述的。指令集中一样,像“调用方法”,“设置、读取字段”这样的功能,所有都是字符串描述链接信息。对象
putfield #13 // some/pkg/Class.someField invokevirtual #25 // java/io/PrintStream.println(Ljava/lang/Object;)V
虽然实际结构与注释中的字符串有些差异,不过大体如此。具体的差别等写到“字节码生成”时候再说。blog
这里须要着重说一说InvokeDynamic,由于写jvm语言若是不是静态类型强类型,那么这个指令颇有用。
#InvokeDynamic
下文将InvokeDynamic简称indy
这是一个很不错的指令,它让弱类型或者动态类型语言可以很是优雅的编写字节码(有时候也不只仅是字节码)。
indy工做过程为:
BootstrapMethod
获取CallSite
CallSite
Java8只有在使用Lambda时才经过indy
完成,个人语言Latte-lang
只要在编译期找不到要调用的方法,就会使用indy
指令。
经过javap
反编译来查看,能够看出indy
长这样:
java:
Consumer<?> c = (o)->{ System.out.println(o); };
指令:
0: invokedynamic #2, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
在最下面还有一条BootstrapMethods
0: #32 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: #33 (Ljava/lang/Object;)V #34 invokestatic SimpleTest.lambda$main$0:(Ljava/lang/Object;)V #33 (Ljava/lang/Object;)V
从字节码获取调用者的类型,方法名,MethodType,以及一些其它信息 调用者的lookup是由jvm获取的,不可人为修改。 方法名,在这里就是accept
,在indy指令处指定 MethodType,在这里就是()Ljava/util/function/Consumer;
,它也是在indy指令处指定的。 这三项为BootstrapMethod的前三个参数,且必须指定
调用BootstrapMethod
获取CallSite
BootstrapMethod在BootstrapMethods
中描述,在indy中指定。 须要规定的是调用方式(invokestatic),调用的方法,以及除了上述三个必需参数外的额外参数 在调用时,这些参数将被包装成对象并调用指定的bootstrap方法以获取CallSite
使用操做数栈做为参数调用CallSite
操做数栈做为参数的个数为MethodType
括号中参数的个数。在这里是0个,也就是不须要参数。 调用CallSite
后的返回值类型为MethodType
括号后的类型,在这里就是Ljava/util/function/Consumer;
若是有返回值则将返回值写入操做数栈 将获取的Consumer
放入操做数栈。
若是有兴趣能够查看LambdaMetafactory
源码并加断点调试。可是毕竟java没有使用带参数的indy,因此建议在java中编译Latte-lang
代码并执行,在lt.lang.Dynamic
中下断点。
虽然indy
完成了字节码的duck type
,可是,因为java,jvm指令自己强类型的特性,这些弱类型最终仍是要经过反射之类的方式完成调用。 举个例子,在Latte中:
method(o) return o + 1
定义了一个method
方法,有一个o
参数。而这个参数的类型是java.lang.Object
类型(由于能够向方法内传入任何类型的值,因此这里只能是Object
类型)。
对于返回值的表达式o+1
,Latte
对于运算符的处理是方法绑定,+
绑定了_add_方法。这里就至关于_o.add(1)_。因为Object类型没有add方法,因此不能用传统的invokevirtual
之类进行调用,那么尝试使用indy
。在使用时,MethodType
应当包含o
和1
,也就是java/lang/Object
和I
。可是,即便这样,在第二步取得CallSite
时,依旧得不到具体类型,Object
类依旧不存在add
方法。只有等到第三步有了实际参数才可能获得类型。因此最终仍是须要反射才能完成调用。
#解析指令 因为语义分析前3个步骤,整个类型的体系已经创建了起来,因此全部指令均可以用对象化的形式进行描述。
例如“返回1”,在语法、语义、字节码分别有这样的表示:
AST: Return(NumberLiteral(1)) Ins: TReturn(IntValue(1), I) ByteCode: iconst_1 ireturn
咱们在语义分析中书写形式为Ins
这种。携带类型信息,包括指令信息,又有二维结构。 我定义的Ins
能够在这里看到。
使用对象定义指令有许多好处,它不但可以与字节码相近,还能与java相近。与字节码相近能够方便字节码的生成,与java相近可让咱们很轻松的构造出这些指令,减小错误。此外,在写exception-table
或者goto
时候也能够保证指令包括的部分是完整的语句而不会只有某些表达式。
下一篇说说一些不常规的语句(例如“内部方法”、“lambda”等)该怎么解析~
最后,但愿看官可以关注个人编译器哦~Latte