编译原理之学习 lua 3.1 (七) Closure 闭包支持
编译原理之学习 lua 3.1 (七) Closure 闭包支持
刘军兴 发表于4年前
编译原理之学习 lua 3.1 (七) Closure 闭包支持
  • 发表于 4年前
  • 阅读 798
  • 收藏 7
  • 点赞 0
  • 评论 0

腾讯云 学生专属云服务套餐 10元起购>>>   

lua 3.1 与其前一个版本 3.0 比, 有了很大的变化, 可参见历史文件 HISTORY.
 
我们关心的有:
1. 解析由 LR 的变成手写的 LL 递归下降解析器了, 文法变化,代码生成变化了;
2. 新概念: 闭包 (closure)
   NEW CONCEPT: anonymous functions with closures (via "upvalues").

首先研究解析器问题:
解析入口: TProtoFunc *luaY_parser(ZIO *z)
函数: next(LexState *): 读入下一个符号.
chunk(LexState *): 文法正式的入口.

下面用定界符 <% %> 表示代码块. 因 {} 被 LL 文法使用.
文法符号含义:
  -> 推导出
  { } 可重复 1-M 次, 相当于正则 ()+ 中多次的意思.
  [ ] 可选, 即可 0-1 次
  -- 后面加一些注释

program -> chunk <%代码: close_func() 完成一个整的 code chunk. %> ;
chunk -> statlist ret
statlist -> { stat [;] }  -- 语句列表是一个或多个语句, 中间可选分号分隔
  对应函数 statlist() 实现即写成:
    while (true) {
   stat() and if (END) break;  -- 读入语句结束则 退出循环.
   optional(';');
 }
stat -> IF ifpart END  -- if 语句
      | WHILE cond DO block END -- while 语句
   | DO block END -- do 语句块
   | REPEAT block UNTIL exp1
   | FUNCTION funcname body
   | LOCAL localnamelist decinit
   | stat -> func | ['%'] NAME assignment  -- 函数调用或赋值
      | error -- 不是上述情况则是错误.

回顾原 LL 文法中 func 调用和赋值的部分:
stat -> functioncall
stat -> varlist1 '=' exprlist1
functioncall -> funcvalue funcParams
funcvalue -> varexp | varexp ':' NAME
varlist1 -> var | varlist1 ',' var
var -> singlevar | varexp '[' expr1 ']' | varexp '.' NAME
singlevar -> NAME
varexp -> var

合并 var (LR格式):
  var -> NAME | var '[' expr1 ']' | var '.' NAME
转换为 LL 形式的:
  var -> NAME { '[' expr1 ']' | '.' NAME }
则 FOLLOWS() 集合中就是 NAME

varlist1 写成 LL 形式的:
  varlist1 -> var { ',' var }*
则 varlist1 的 FOLLOWS() 集合是 NAME.
(对产生式的各种变换, LL,LR 等还是让计算机科学家做好, 我们开心使用和学习就挺好了)

funcvalue -> var {':' NAME}
则 funvalue 的 FOLLOWS() 集合是 NAME, 和 varlist1 相同了, 故而
  在 var 读入后根据后一个终结符才能判定是 func, 还是 assign.
例子:  f(1,2,3)  -- 读入 NAME=f, 在之后遇到 '(' 才能判定是函数调用.
例子: x=4 或 x,y=5,6 -- 读入 NAME=x, 在之后遇到 '=', ',' 才能判定是
  赋值.
为此需要将非终结符 var 的综合属性传递给后面分析步骤用, 此一属性以结构
  vardesc 描述.

结构 vardesc:
  varkind k; -- var 类型描述.
  int info; -- 该 var 的索引, 根据 varkind 类型有不同含义.
 
关于 varkind 的原注释:
  must include a "exp" option because LL(1) cannot distinguish
  between variables, upvalues and function calls on first sight.
也即上面通过产生式说明的问题.


实现上函数 var_or_func(vardesc *v) 读取第一个 var, 此时尚分辨不出
后面将是 '(' 等导去函数调用, 还是 '=' 等导去赋值.
  var_or_func -> ['%'] NAME var_or_func_tail
程序逻辑如下:
  if (有 '%' 符号) -- 引用上一层函数的局部变量, 即闭包变量.
    push_upvalue() -- 产生代码, 压栈 upvalue. 研究闭包时详述
    vardesc.kind = VEXP, .info = 0  -- 表示已经获取了
  else -- 否则是一个变量的名字
    singlevar(vardesc, NAME, ...)
  var_or_func_tail() -- 产生式 var_or_func -> ... var_or_func_tail

函数 singlevar(vardesc, NAME, ...) 中:
  if (NAME 是一个局部变量)
    vardesc.kind = VLOCAL, .info = 局部变量索引 -- 是一个局部变量时
  else
    if (NAME 是上一级函数中的变量) 警告: 不能访问上级变量
 否之: vardesc.kind = VGLOBAL, .info = 全局变量索引.

函数 var_or_func_tail() 相当于下面产生式(可多次重复):
  var_or_func_tail ->* '.' NAME  -- 如 a.b, vardesc.kind = VDOT
    | '[' expr1 ']' -- 如 a['b'], 对应 vardesc.kind = VINDEXED
 | ':' NAME funcparams -- 如 obj:f(1,2), 对应 vardesc.kind = VEXP
 | (以 '(' or '{' 开头的) funcparams -- 如 f(1,2), f{a=1,b=2},
    对应 vardesc.kind = VEXP

故而函数 var_or_func_tail() 返回时, 会填写好 vardesc 的信息.
(这里 vardesc 结构兼具综合属性和继承属性的特点, 即即向下传递信息,
 又向上传递信息)

后续需要 var 的地方, 主要一是作为表达式左值, 如 varlist1 中, 一是作为
表达式右值, 如 expr, expr1 中. 当用作左值时, kind=VGLOBAL,VLOCAL,
VDOT,VINDEXED 可用作左值, 其表示不同的寻址方式, 见于函数 storevar();
当作用右值时, VEXP可以直接用, 其它几种需要加载该地址的值, 见于函数
lua_pushvar(). (这两个函数一个读一个写, 本来有相对应性, 可名字由于
历史原因吧, 不是很配对)

函数 lua_pushvar():
  if (var 是 VGLOBAL 类型的)  -- 即全局变量
    产生 PUSHGLOBAL i 指令, 其中 i 是此全局变量的索引, 即 vardesc.info 值
  else if (var 是 VLOCAL 类型的) -- 局部变量, 函数参数
    产生 PUSHLOCAL i 指令
  else if (var 是 VDOT 类型的) -- a.b 形式访问的
    产生 GETDOTTED 指令
  else if (var 是 VINDEXED 类型的) -- a['b'] 形式访问的
    产生 GETTABLE 指令
  else if (var 是 VEXP 类型的) -- 已经是值了
    close_exp() -- 和函数多返回值情况有关, 此处略.
  之后, 设置 vardesc.kind = VEXP, 因为已经将值加载到栈中了.

  当 var 是 upvalues 时候, 已经在函数 var_or_func() 中使用 push_upvalue()
  产生了 PUSHUPVALUE 指令.

这里与原有的 3.0, 2.1, 1.1 等早期版本对比, 区分了 a.b, a['b'] 的访问为
两种寻址方式(旧版本中都是用 PUSHINDEXED 来寻址), 新增了 PUSHUPVALUE 寻址
方式, 也即现在有 5 种寻址方式了.

同样 storevar():
  if (var 是 VGLOBAL 类型的)
    产生 STOREGLOBAL 指令
  else if (var 是 VLOCAL 类型的)
    产生 STORELOCAL 指令
  else if (var 是 VINDEXED 类型的)
    产生 SETTABLE 指令
  else
    报告错误.

这里解释下为什么 storevar() 没出现的其它 var 类型:
  1. VEXP: 是右值, 不能当做左值用, 所以不出现.
  2. VDOT: 在 storevar() 调用前(如在 assignment() 函数中), 转为
    VINDEXED 类型的, 所以没有 SETDOTTED 指令与 GETDOTTED 对应,
    是有点奇怪. 也许这种不匹配现象在后续版本有变化?
  3. upvalue 类型的: 在前面解析中已经转换为 VEXP 类型了.
    这里意味着 upvalue 类型的只是做右值, 不能当左值, 即不能被修改.


解析器改为使用 LL 文法的手写的递归下降解析器, 在文法上要改变, 在
代码生成上也要改变以适应, 尤其是表达式和 if 语句块. 有时间我们再
详细学习吧.

 

下面再看看新引入的闭包问题. 上面研究的时候已经遇到 upvalue 类型的
变量了, 我们可以合理推测 lua 3.1 中闭包"只是"将上一级函数的局部变量
"复制" 到被返回的子函数中, 来实现闭包的. 下面的例子验证这一点:
  function f(n)
    local upv = n
    print ('before create rf(), upv = ' .. upv)
    local rf = function()  -- 准备用来返回的闭包函数
      print ('inner %upv = ' .. %upv)
    end
    upv = upv + 100
    print ('after create rf(), upv = ' .. upv)
    return rf
  end

  rf = f(123) -- get the closure
  rf()        -- call the closure

这个例子执行的结果为:
  before create rf(), upv = 123
  after create rf(), upv = 223
  inner %upv = 123

结果是返回的闭包函数 c 在调用时 %upv 已经被复制到 c 的 upvalue[]
数组中了, 所以后面再改变也不会反应到闭包函数中了.

另外在内部函数 rf() 中, 只能用 %upv 语法使用外层变量, 否则会报告
语法错误. 再就是只能将 %upv 做右值用, 不能写 %upv = 789 当左值用.
这些都可以容易的验证出来.

下面研究如何实现闭包的, 为此函数 pushupvalue() 被下面仔细查看:
  pushupvalue(l, NAME) -- NAME 为此 upvalue 的名字, 如上例中的 "upv"
    判断略
    i = indexupvalue(NAME)
    产生代码 PUSHUPVALUE i

其中函数 indexupvalue(NAME):
  在当前正在编译的函数 FuncState *f 中查找 upvalues[] 数组, 找到
    则返回索引 i
  未找到, 则添加一个新的 upvalue 项到 f->upvalues[] 中.

在虚拟机 lvm.c 的函数 lua_execute() 中, 指令 PUSHUPVALUE 为:
  case PUSHUPVALUE:
    *top++ = closure->consts[upvalue 的索引];

即从 closure (表示当前正在执行的闭包函数), 的表格 consts[] 指定索引
位置中加载该 upvalue 的值. 现在剩下的最后一个问题, 该 upvalue 是什么
时候被复制到 consts[] 中的? 最合理的猜测是, 当该 closure 被创建的
时候被填入的, 否则我们上面那个例子, 在后面的改变也应该会影响该值,
事实是没有影响. 故而下面继续研究:

在产生式:
  stat -> FUNCTION funcname body  --- 用于定义函数的产生式
  body -> '(' parlist ')' chunk END <%代码块1%>
后一个 body 的产生式, 代码块1 处会调用 func_onstack() 函数, 有关
upvalue 值复制的秘密操作应该就藏在这里:

func_onstack(FuncState *func):
  for (i = 0; i < func->nupvalues; i++)
    lua_pushvar(ls, &func->upvalues[i]);
  产生代码 CLOSURE func->nupvalues

其中的 lua_pushvar() 部分产生将 upvalue 压栈的指令, 应该一定是上一级
函数的 VLOCAL 类型的变量(因此 lua 这里似乎不支持多层上级).

下面看虚拟机中 CLOSURE 指令:
  case CLOSURE:
    *top++ = consts[aux] -- 推测是得到闭包的函数原型地址
 luaV_closure(*pc++) -- 应是产生闭包函数, 根据要绑定的闭包值

函数 luaV_closure(nelems):
  Closure *c = new Closure() -- 新建一个闭包对象
  c->consts[0] = Func -- 函数原型
  c->consts[...] = 从栈上复制
  返回此创建的闭包.

总结: lua 3.1 实现了闭包, 但比较简单有限:
1. 闭包变量在创建闭包函数之后, 发生的改变不再影响到闭包函数了.
2. 闭包变量只能是上一级父函数的, 不能是更多层级的父函数(代码上看是这样,
   未实测验证)
3. 语法上要用 %upv 形式, 也许有清晰的优点, 也许有麻烦的缺点?
4. 从语义上说, 单词 upvalue 表示 "上一级的值", 似乎比较贴切.

实现的具体方法:
1. 分解函数对象为函数原型(FuncState) 和闭包函数(Closure) 两个对象,
  后续 lua 版本这个区分更明显, 引用也更清晰. lua 3.1 用 consts[0] 引用,
  即麻烦, 也不清晰.
2. 闭包函数 Closure 在创建的时候, 复制上一级父函数的被引用的局部变量到
  Closure 的 consts[] 表中, 这些变量被 "闭包" 到该 Closure 中, 后续仍
  可使用. 但这样的 "复制" 实现, 语义上应不完备. const 语义上表示这些值
  是常量了.
3. 新增了几条指令用于支持闭包: CLOSURE 指令用于创建闭包; PUSHUPVALUE
  用于读访问闭包变量, 没有写闭包变量的指令(其被当 const 对待).

标签: lua 源码学习
共有 人打赏支持
粉丝 55
博文 141
码字总数 220645
×
刘军兴
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: