目錄戳這裏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