最近在看《自己動手實現Lua—虛擬機器、編譯器和標準庫》。這是本挺不錯的書,通過學習此書能夠對Lua語言有比較深刻的理解,此外還可以對如何自己實現一門指令碼語言有直觀的認識。對於想學習Lua的同學,安利一下這本書。
廢話不多說,書中留了一個作業,讓讀者自己實現TAILCALL
指令,實現尾呼叫的優化。本文就算是交作業吧。
本部落格已經遷移至CatBro's Blog,那裡是我自己搭建的個人部落格,頁面效果比這邊更好,支援站內搜尋,評論回覆還支援郵件提醒,歡迎關注。這邊只會在有時間的時候不定期搬運一下。
尾呼叫
尾呼叫,被調函式可以重用主調函式的呼叫幀,可以有效緩解呼叫棧溢位。
不過尾呼叫的條件非常苛刻,必須是直接返回函式呼叫。下面的是一個尾呼叫的例子,TAILCALL
指令後面肯定緊跟著RETURN
指令,並且RETURN
指令的運算元A跟TAILCALL
相同,RETURN
指令的運算元B肯定是0。
$ luac -l -
return f(a)
^D
main <stdin:0,0> (5 instructions at 0x7fa2b9d00070)
0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions
1 [1] GETTABUP 0 0 -1 ; _ENV "f"
2 [1] GETTABUP 1 0 -2 ; _ENV "a"
3 [1] TAILCALL 0 2 0
4 [1] RETURN 0 0
5 [1] RETURN 0 1
下面幾個例子,都不是尾呼叫。
$ luac -l -
return (f(a))
^D
main <stdin:0,0> (5 instructions at 0x7fb5a34035e0)
0+ params, 2 slots, 1 upvalue, 0 locals, 2 constants, 0 functions
1 [1] GETTABUP 0 0 -1 ; _ENV "f"
2 [1] GETTABUP 1 0 -2 ; _ENV "a"
3 [1] CALL 0 2 2
4 [1] RETURN 0 2
5 [1] RETURN 0 1
$ luac -l -
return f(a)+1
^D
main <stdin:0,0> (6 instructions at 0x7f98c3d00070)
0+ params, 2 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1 [1] GETTABUP 0 0 -1 ; _ENV "f"
2 [1] GETTABUP 1 0 -2 ; _ENV "a"
3 [1] CALL 0 2 2
4 [1] ADD 0 0 -3 ; - 1
5 [1] RETURN 0 2
6 [1] RETURN 0 1
$ luac -l -
return {f(a)}
^D
main <stdin:0,0> (7 instructions at 0x7fb1b95006f0)
0+ params, 3 slots, 1 upvalue, 0 locals, 2 constants, 0 functions
1 [1] NEWTABLE 0 0 0
2 [1] GETTABUP 1 0 -1 ; _ENV "f"
3 [1] GETTABUP 2 0 -2 ; _ENV "a"
4 [1] CALL 1 2 0
5 [1] SETLIST 0 0 1 ; 1
6 [1] RETURN 0 2
7 [1] RETURN 0 1
$ luac -l -
return 1, f(a)
^D
main <stdin:0,0> (6 instructions at 0x7f8f9c500070)
0+ params, 3 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1 [1] LOADK 0 -1 ; 1
2 [1] GETTABUP 1 0 -2 ; _ENV "f"
3 [1] GETTABUP 2 0 -3 ; _ENV "a"
4 [1] CALL 1 2 0
5 [1] RETURN 0 0
6 [1] RETURN 0 1
CALL指令
我們先來看看普通的CALL
指令的操作流程:
// R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
func call(i Instruction, vm LuaVM) {
a, b, c := i.ABC()
a += 1
// println(":::"+ vm.StackToString())
nArgs := _pushFuncAndArgs(a, b, vm)
vm.Call(nArgs, c-1)
_popResults(a, c, vm)
}
首先將位於暫存器中的函式和引數推入棧頂,然後呼叫Call()
方法執行函式,最後將棧上的返回值出棧並放入指定暫存器中。
_pushFuncAndArgs()
和_popResults()
分別要處理運算元B和運算元C為0的特殊情況。運算元B為0的情況,部分引數已經在棧上了(由前面的CALL
指令或VARARG
指令留在棧上)。
func _pushFuncAndArgs(a, b int, vm LuaVM) (nArgs int) {
if b >= 1 {
vm.CheckStack(b)
for i := a; i < a+b; i++ {
vm.PushValue(i)
}
return b - 1
} else {
_fixStack(a, vm)
return vm.GetTop() - vm.RegisterCount() - 1
}
}
而運算元C為0的情況,則不需要將返回值從棧上彈出。直接將返回值留在棧上,供後面的指令使用。
func _popResults(a, c int, vm LuaVM) {
if c == 1 {
// no results
} else if c > 1 {
for i := a + c - 2; i >= a; i-- {
vm.Replace(i)
}
} else {
// leave results on stack
vm.CheckStack(1)
vm.PushInteger(int64(a))
}
}
然後我們來看下Call()
方法做了什麼。它首先根據索引找到要呼叫的函式的值,檢查它是否是閉包型別,如果不是直接報錯。然後通過callLuaClosure()
呼叫該函式。
// [-(nargs+1), +nresults, e]
// http://www.lua.org/manual/5.3/manual.html#lua_call
func (self *luaState) Call(nArgs, nResults int) {
val := self.stack.get(-(nArgs + 1))
if c, ok := val.(*closure); ok {
fmt.Printf("call %s<%d,%d>\n", c.proto.Source,
c.proto.LineDefined, c.proto.LastLineDefined)
self.callLuaClosure(nArgs, nResults, c)
} else {
panic("not function!")
}
}
callLuaClosure()
稍微有點複雜,來看下程式碼。首先從閉包的函式原型中獲取到各種資訊,如暫存器個數、固定引數個數、是否是vararg。接著建立一個新的呼叫幀,並將閉包與呼叫幀關聯。然後將函式和引數值全部從主調幀棧頂彈出,並將固定引數壓入被調幀棧頂,如果是vararg且引數個數大於固定引數個數,還要將vararg引數記錄下來。
到這新幀就準備就緒了,將新幀推入呼叫棧頂,然後呼叫runLuaClosure()
開始執行被調函式的指令。執行結束之後,被調幀的任務結束,將其彈出呼叫棧頂。
此時,返回值還留在被調幀的棧頂,需要移到主調幀棧頂。
func (self *luaState) callLuaClosure(nArgs, nResults int, c *closure) {
nRegs := int(c.proto.MaxStackSize)
nParams := int(c.proto.NumParams)
isVararg := c.proto.IsVararg == 1
// create new lua stack
newStack := newLuaStack(nRegs + 20)
newStack.closure = c
// pass args, pop func
funcAndArgs := self.stack.popN(nArgs + 1)
newStack.pushN(funcAndArgs[1:], nParams)
newStack.top = nRegs
if nArgs > nParams && isVararg {
newStack.varargs = funcAndArgs[nParams+1:]
}
// run closure
self.pushLuaStack(newStack)
self.runLuaClosure()
self.popLuaStack()
// return results, nResults == c - 1
if nResults != 0 {
results := newStack.popN(newStack.top - nRegs)
self.stack.check(len(results))
self.stack.pushN(results, nResults)
}
}
實現TAILCALL指令
知道了CALL
指令的流程,我們就可以著手實現TAILCALL
指令了。其運算元A和運算元B的含義跟CALL
指令完全一致,運算元C沒用,相當於固定為0。所以_pushFuncAndArgs()
和_popResults()
函式可以重用,主要是修改中間的呼叫流程。我們首先給LuaVM新增一個介面TailCall()
。
在api/lua_vm.go
檔案中新增如下程式碼:
type LuaVM interface {
...
TailCall(nArgs int) // add this
}
因為TAILCALL
指令後面緊跟著RETURN
指令,且RETURN
指令的運算元B為0,運算元A跟TAILCALL
指令一樣。所以_popResults()
之後,我們啥都不用幹,直接把返回值保留在棧上即可。
在vm/inst_call.go
檔案中修改tailCall()
如下:
// return R(A)(R(A+1), ... ,R(A+B-1))
func tailCall(i Instruction, vm LuaVM) {
a, b, _ := i.ABC()
a += 1
nArgs := _pushFuncAndArgs(a, b, vm)
vm.TailCall(nArgs)
_popResults(a, 0, vm)
// no need to _return() as ‘b’ of the following ‘RETURN’ is 0,
// ‘a’ of ‘RETURN’ is same ‘as’ a of ‘TAILCALL’
}
在state/api_call.go
檔案中新增TailCall()
程式碼如下
// [-(nargs+1), +nresults, e]
func (self *luaState) TailCall(nArgs int) {
val := self.stack.get(-(nArgs + 1))
if c, ok := val.(*closure); ok {
fmt.Printf("tailcall %s<%d,%d>\n", c.proto.Source,
c.proto.LineDefined, c.proto.LastLineDefined)
self.tailCallLuaClosure(nArgs, c)
} else {
panic("not function!")
}
}
然後在tailCallLuaClosure()
中實現主要邏輯。在state/api_call.go
檔案中新增tailCallLuaClosure()
程式碼如下。
首先同樣是從閉包中獲取函式原型的資訊。因為當前幀在TAILCALL
之後肯定跟著RETURN
,所以儲存引數之後可以直接清理掉當前幀的棧,然後直接給被調函式重用。將被調函式的資訊設定到當前幀,先檢查棧空間是否夠,然後將當前幀關聯到新的閉包,然後將固定引數推入棧頂,修改棧頂指標指向最後一個暫存器。如果是vararg且引數個數大於固定引數個數,還要將vararg引數記錄下來。
一切就緒之後,就可以呼叫runLuaClosure()
開始執行閉包的指令了。執行完畢後返回值保留在棧頂。
func (self *luaState) tailCallLuaClosure(nArgs int, c *closure) {
nRegs := int(c.proto.MaxStackSize)
nParams := int(c.proto.NumParams)
isVararg := c.proto.IsVararg == 1
// store args
args := self.stack.popN(nArgs)
// clean the stack
self.SetTop(0)
// check if stack space is enough
self.stack.check(nRegs + 20)
// substitue the closure to new one
self.stack.closure = c
// push fixed args
self.stack.pushN(args[1:], nParams)
self.stack.top = nRegs
// store varargs
if nArgs > nParams && isVararg {
self.stack.varargs = args[nParams+1:]
}
// run closure
self.runLuaClosure()
}
測試程式碼
我們重新編譯Go程式碼
cd $LUAGO/go
export GOPATH=$PWD/ch08
export GOBIN=$PWD/ch08/bin
go install luago
然後來編寫Lua指令碼,我們編寫了一個求和的函式
local function sum(n, s, fun)
if n == 0 then
return s
end
s = s + n
return fun(n-1, s, fun)
end
local function assert(v)
if not v then fail() end
end
local v1 = sum(0, 0, sum)
assert(v1 == 0)
local v2 = sum(1, 0, sum)
assert(v2 == 1)
local v3 = sum(3, 0, sum)
assert(v3 == 6)
local v4 = sum(10, 0, sum)
assert(v4 == 55)
local v5 = sum(10000, 0, sum)
assert(v5 == 50005000)
local v6 = sum(1000000, 0, sum)
assert(v6 == 500000500000)
先來看看sum()
函式會被編譯成什麼指令。
$ luac -l -
local function sum(n, s, fun)
if n == 0 then
return s
end
s = s + n
return fun(n-1, s, fun)
end
^D
main <stdin:0,0> (2 instructions at 0x7fe071d00070)
0+ params, 2 slots, 1 upvalue, 1 local, 0 constants, 1 function
1 [7] CLOSURE 0 0 ; 0x7fe071e00110
2 [7] RETURN 0 1
function <stdin:1,7> (11 instructions at 0x7fe071e00110)
3 params, 7 slots, 0 upvalues, 3 locals, 2 constants, 0 functions
1 [2] EQ 0 0 -1 ; - 0
2 [2] JMP 0 1 ; to 4
3 [3] RETURN 1 2
4 [5] ADD 1 1 0
5 [6] MOVE 3 2
6 [6] SUB 4 0 -2 ; - 1
7 [6] MOVE 5 1
8 [6] MOVE 6 2
9 [6] TAILCALL 3 4 0
10 [6] RETURN 3 0
11 [7] RETURN 0 1
可以看到的確是被編譯成了TAILCALL
,後面緊跟RETURN
指令,且RETURN
指令的運算元A與TAILCALL
相同,運算元B為0。
我們編譯Lua指令碼,然後用我們的虛擬機器進行執行。
luac ../lua/ch08/tailcall.lua
./ch08/bin/luago luac.out
call @../lua/ch08/tailcall.lua<0,0>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
panic: GETTABUP
哦哦,執行失敗了,殘念!
於是加日誌進行問題排查,把指令執行時的棧打出來
{% fold 點選展開 %}
call @../lua/ch08/tailcall.lua<0,0>
CLOSURE 0 0 [nil][nil][nil][nil][nil][nil][nil]
CLOSURE 1 1 [function][nil][nil][nil][nil][nil][nil]
MOVE 2 0 [function][function][nil][nil][nil][nil][nil]
LOADK 3 -1 [function][function][function][nil][nil][nil][nil]
LOADK 4 -1 [function][function][function][0][nil][nil][nil]
MOVE 5 0 [function][function][function][0][0][nil][nil]
CALL 2 4 2 [function][function][function][0][0][function][nil]
call @../lua/ch08/tailcall.lua<1,7>
EQ 0 0 -1 [0][0][function][nil][nil][nil][nil]
RETURN 1 2 [0][0][function][nil][nil][nil][nil]
RETURN after [0][0][function][nil][nil][nil][nil][0]
CALL after [function][function][0][0][0][function][nil]
MOVE 3 1 [function][function][0][0][0][function][nil]
EQ 1 2 -1 [function][function][0][function][0][function][nil]
JMP 0 1 [function][function][0][function][0][function][nil]
LOADBOOL 4 1 0 [function][function][0][function][0][function][nil]
CALL 3 2 1 [function][function][0][function][true][function][nil]
call @../lua/ch08/tailcall.lua<9,11>
TEST 0 1 [true][nil]
JMP 0 2 [true][nil]
RETURN 0 1 [true][nil]
RETURN after [true][nil]
CALL after [function][function][0][function][true][function][nil]
MOVE 3 0 [function][function][0][function][true][function][nil]
LOADK 4 -2 [function][function][0][function][true][function][nil]
LOADK 5 -1 [function][function][0][function][1][function][nil]
MOVE 6 0 [function][function][0][function][1][0][nil]
CALL 3 4 2 [function][function][0][function][1][0][function]
call @../lua/ch08/tailcall.lua<1,7>
EQ 0 0 -1 [1][0][function][nil][nil][nil][nil]
JMP 0 1 [1][0][function][nil][nil][nil][nil]
ADD 1 1 0 [1][0][function][nil][nil][nil][nil]
MOVE 3 2 [1][1][function][nil][nil][nil][nil]
SUB 4 0 -2 [1][1][function][function][nil][nil][nil]
MOVE 5 1 [1][1][function][function][0][nil][nil]
MOVE 6 2 [1][1][function][function][0][1][nil]
TAILCALL 3 4 0 [1][1][function][function][0][1][function]
RETURN 3 0 [1][function][nil][nil][nil][nil][nil]
RETURN after [1][function][nil][nil][nil][nil]
TAILCALL after [1][function][nil][nil][nil][nil][4]
RETURN 0 1 [1][function][nil][nil][nil][nil][4]
RETURN after [1][function][nil][nil][nil][nil][4]
CALL after [function][function][0][nil][1][0][function]
MOVE 4 1 [function][function][0][nil][1][0][function]
EQ 1 3 -2 [function][function][0][nil][function][0][function]
LOADBOOL 5 0 1 [function][function][0][nil][function][0][function]
CALL 4 2 1 [function][function][0][nil][function][false][function]
call @../lua/ch08/tailcall.lua<9,11>
TEST 0 1 [false][nil]
GETTABUP 1 0 -1 [false][nil]
panic: GETTABUP
{% endfold %}
我們發現在TAILCALL
開始執行之後立馬呼叫了RETURN
,問題就是出在這裡,我們雖然替換了當前幀的閉包為被調函式的閉包,但是忘了更新pc。於是修改tailCallLuaClosure()
初始化pc為0。
func (self *luaState) tailCallLuaClosure(nArgs int, c *closure) {
// substitue the closure to new one
self.stack.closure = c
self.stack.pc = 0 // add this
}
重新編譯之後測試
$ go install luago
$ ./ch08/bin/luago luac.out
... # 省略前面的日誌
call @../lua/ch08/tailcall.lua<1,7>
EQ 0 0 -1 [1][0][function][nil][nil][nil][nil]
JMP 0 1 [1][0][function][nil][nil][nil][nil]
ADD 1 1 0 [1][0][function][nil][nil][nil][nil]
MOVE 3 2 [1][1][function][nil][nil][nil][nil]
SUB 4 0 -2 [1][1][function][function][nil][nil][nil]
MOVE 5 1 [1][1][function][function][0][nil][nil]
MOVE 6 2 [1][1][function][function][0][1][nil]
TAILCALL 3 4 0 [1][1][function][function][0][1][function]
EQ 0 0 -1 [1][function][nil][nil][nil][nil][nil]
JMP 0 1 [1][function][nil][nil][nil][nil][nil]
ADD 1 1 0 [1][function][nil][nil][nil][nil][nil]
panic: arithmetic error!
哦哦,又執行失敗了♂️!檢視列印的棧資訊,TAILCALL
指令執行之前棧的情況是正常的,三個引數都已經推入棧頂[0][1][function]
,但是執行EQ
指令前棧中卻少了一個引數,只有[1][function]
。於是檢查程式碼,原來是陣列索引搞錯了,Go的起始索引是0,跟Lua搞混了♂️。。。
修改tailCallLuaClosure()
如下之後
func (self *luaState) tailCallLuaClosure(nArgs int, c *closure) {
nRegs := int(c.proto.MaxStackSize)
nParams := int(c.proto.NumParams)
isVararg := c.proto.IsVararg == 1
// store args
args := self.stack.popN(nArgs)
// clean the stack
self.SetTop(0)
// check if stack space is enough
self.stack.check(nRegs + 20)
// substitue the closure to new one
self.stack.closure = c
self.stack.pc = 0
// push fixed args
self.stack.pushN(args, nParams)
self.stack.top = nRegs
// store varargs
if nArgs > nParams && isVararg {
self.stack.varargs = args[nParams:]
}
// run closure
self.runLuaClosure()
}
重新編譯,然後繼續執行,程式進入了死迴圈。。。
{% fold 點選展開 %}
$ go install luago
$ ./ch08/bin/luago luac.out
... # 程式停不下來了。。。
call @../lua/ch08/tailcall.lua<1,7>
EQ 0 0 -1 [1][0][function][nil][nil][nil][nil]
JMP 0 1 [1][0][function][nil][nil][nil][nil]
ADD 1 1 0 [1][0][function][nil][nil][nil][nil]
MOVE 3 2 [1][1][function][nil][nil][nil][nil]
SUB 4 0 -2 [1][1][function][function][nil][nil][nil]
MOVE 5 1 [1][1][function][function][0][nil][nil]
MOVE 6 2 [1][1][function][function][0][1][nil]
TAILCALL 3 4 0 [1][1][function][function][0][1][function]
EQ 0 0 -1 [0][1][function][nil][nil][nil][nil]
RETURN 1 2 [0][1][function][nil][nil][nil][nil]
RETURN after [0][1][function][nil][nil][nil][nil][1]
TAILCALL after [0][1][function][nil][nil][nil][nil][1][4]
ADD 1 1 0 [0][1][function][nil][nil][nil][nil][1][4]
MOVE 3 2 [0][1][function][nil][nil][nil][nil][1][4]
SUB 4 0 -2 [0][1][function][function][nil][nil][nil][1][4]
MOVE 5 1 [0][1][function][function][-1][nil][nil][1][4]
MOVE 6 2 [0][1][function][function][-1][1][nil][1][4]
TAILCALL 3 4 0 [0][1][function][function][-1][1][function][1][4]
EQ 0 0 -1 [-1][1][function][nil][nil][nil][nil]
JMP 0 1 [-1][1][function][nil][nil][nil][nil]
ADD 1 1 0 [-1][1][function][nil][nil][nil][nil]
MOVE 3 2 [-1][0][function][nil][nil][nil][nil]
SUB 4 0 -2 [-1][0][function][function][nil][nil][nil]
MOVE 5 1 [-1][0][function][function][-2][nil][nil]
MOVE 6 2 [-1][0][function][function][-2][0][nil]
TAILCALL 3 4 0 [-1][0][function][function][-2][0][function]
EQ 0 0 -1 [-2][0][function][nil][nil][nil][nil]
JMP 0 1 [-2][0][function][nil][nil][nil][nil]
ADD 1 1 0 [-2][0][function][nil][nil][nil][nil]
MOVE 3 2 [-2][-2][function][nil][nil][nil][nil]
SUB 4 0 -2 [-2][-2][function][function][nil][nil][nil]
MOVE 5 1 [-2][-2][function][function][-3][nil][nil]
MOVE 6 2 [-2][-2][function][function][-3][-2][nil]
...
{% endfold %}
觀察到在TAILCALL
結束之後,又繼續執行RETURN
指令後面的ADD
指令了。
TAILCALL 3 4 0 [1][1][function][function][0][1][function]
EQ 0 0 -1 [0][1][function][nil][nil][nil][nil]
RETURN 1 2 [0][1][function][nil][nil][nil][nil]
RETURN after [0][1][function][nil][nil][nil][nil][1]
TAILCALL after [0][1][function][nil][nil][nil][nil][1][4]
ADD 1 1 0 [0][1][function][nil][nil][nil][nil][1][4]
正常在執行第3條指令RETURN
之後就應該返回上層了
function <../lua/ch08/tailcall.lua:1,7> (11 instructions at 0x7fd783c03070)
3 params, 7 slots, 0 upvalues, 3 locals, 2 constants, 0 functions
1 [2] EQ 0 0 -1 ; - 0
2 [2] JMP 0 1 ; to 4
3 [3] RETURN 1 2
4 [5] ADD 1 1 0
5 [6] MOVE 3 2
6 [6] SUB 4 0 -2 ; - 1
7 [6] MOVE 5 1
8 [6] MOVE 6 2
9 [6] TAILCALL 3 4 0
10 [6] RETURN 3 0
11 [7] RETURN 0 1
猛然想到,之前把RETURN
語句給略過了,雖然在返回值的處理上是沒有問題,但是外層幀依賴RETURN
指令來結束
func (self *luaState) runLuaClosure() {
...
if inst.Opcode() == vm.OP_RETURN {
break;
}
}
所以我們修改runLuaClosure()
,使TAILCALL
指令執行之後也結束。
func (self *luaState) runLuaClosure() {
...
if inst.Opcode() == vm.OP_RETURN || inst.Opcode() == vm.OP_TAILCALL {
break;
}
}
還有一個地方需要修改,因為我們省略了RETURN
指令,所以tailCall()
裡的_popResults()
也就不需要了,否則棧上會多出一個值。_popResults()
會推一個整數值(即a)到棧頂指示返回值起始的暫存器位置,相應的在RETURN
指令的時候會把這個整數值彈出。
// return R(A)(R(A+1), ... ,R(A+B-1))
func tailCall(i Instruction, vm LuaVM) {
a, b, _ := i.ABC()
a += 1
// todo: optimize tail call!
nArgs := _pushFuncAndArgs(a, b, vm)
vm.TailCall(nArgs)
// _popResults(a, 0, vm)
// no need to _return() as ‘b’ of the following ‘RETURN’ is 0,
// ‘a’ of ‘RETURN’ is same ‘as’ a of ‘TAILCALL’
}
重新編譯執行,這回終於大功告成了!
$ go install luago
$ ./ch08/bin/luago luac.out
call @../lua/ch08/tailcall.lua<0,0>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
call @../lua/ch08/tailcall.lua<1,7>
call @../lua/ch08/tailcall.lua<9,11>
我們再來試試可變引數的情況,修改Lua指令碼如下
local function sum(n, s, fun, ...)
if n == 0 then
return s
end
local args = {...}
if args[n] then
s = s + args[n]
end
return fun(n-1, s, fun, ...)
end
local function assert(v)
if not v then fail() end
end
local v1 = sum(0, 0, sum)
assert(v1 == 0)
local v2 = sum(1, 0, sum, 1)
assert(v2 == 1)
local v3 = sum(3, 0, sum, 1, 2, 3)
assert(v3 == 6)
local v4 = sum(3, 0, sum, 1, 2, 3, 4)
assert(v4 == 6)
local v3 = sum(3, 0, sum, 1, 2)
assert(v3 == 3)
編譯Lua指令碼,執行測試。
$ luac ../lua/ch08/tailcall2.lua
$ ./ch08/bin/luago luac.out
call @../lua/ch08/tailcall2.lua<0,0>
call @../lua/ch08/tailcall2.lua<1,10>
call @../lua/ch08/tailcall2.lua<12,14>
call @../lua/ch08/tailcall2.lua<1,10>
call @../lua/ch08/tailcall2.lua<12,14>
call @../lua/ch08/tailcall2.lua<1,10>
call @../lua/ch08/tailcall2.lua<12,14>
call @../lua/ch08/tailcall2.lua<1,10>
call @../lua/ch08/tailcall2.lua<12,14>
call @../lua/ch08/tailcall2.lua<1,10>
call @../lua/ch08/tailcall2.lua<12,14>
也沒有問題️