自己動手實現Lua--實現TAILCALL指令


最近在看《自己動手實現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>

也沒有問題✌️


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM