Lua4.0 语法分析

Lua 最初使用的是 Yacc 生成的语法分析器,后来改为手写的递归下降语法分析器(Recursive descent parser)。因为一般在语言发展的早期,语言的语法还没有稳定下来,变动可能会比较多,用工具可以快速的验证自己的想法。当语法稳定下来之后,一般都会采用手写的语法分析器。因为这样程序员是调试可控的,并且一般也能有更好的性能表现。递归下降的语法分析对编程语言的语法有要求。因 Lua 的语法比较简单,是 LL(1) 文法。所以,手工编写语法分析也就是情理之中的事了。

关于递归下降的语法分析,网上资料不少,比如维基百科:

https://en.wikipedia.org/wiki/Recursive_descent_parser

比如这篇:

http://www.cnblogs.com/Ninputer/archive/2011/06/21/2085527.html

里面都有很不错的示例。

具体内容就不贴出来了,感兴趣的同学可以自行阅读。

说到这儿,再往下说,就是具体的代码了。

对照 Lua 的 EBNF 语法(在 Lua 手册的最后面,有完整的语法表),一点点的套代码吧。

看一个经典的“hello world”程序吧。

程序的内容如下所示:

print("hello world! (using print)\n")

假设把它保存为 hello.lua 文件。

通过执行下面的命令,打印出它的字节码。

luac.exe -l hello.lua

下面是程序的输出:

main <0:@hello.lua> (4 instructions/16 bytes at 0087A0E8)

0 params, 2 stacks, 0 locals, 2 strings, 0 numbers, 0 functions, 2 lines

1  [1]     GETGLOBAL       0       ; print

2  [1]     PUSHSTRING      1       ; "hello world! (using print)\n"

3  [1]     CALL            0 0

4  [1]     END

程序输出的意思:

main 表示是顶层主程序,如果是函数定义的话,这里显示为 function

0:@hello.lua 程序从第 0 行开始,文件名为 hello.lua

后面的表示 4 条指令,共 16 个字节,位于内存 0087A0E8 处。

再下一行表示,有多少个参数,占用了多少个栈空间,局部变量个数,多少个字符串,数字,函数,及行数。

再下面就是字节码指令的具体内容(以方便阅读的形式表示出来的)。

每行的内容分别为 指令号,对应源代码的行号,指令,分号后面的为注释。

再来看看指令的具体内容:

GETGLOBAL   取得一个全局变量,由注释可以看到,取得的全局变量为 print

PUSHSTRING   字符串压栈

CALL        函数调用

END   结束

取得全局变量的时候,把取出来的全局变量放到什么地方?放到了数据栈上,就是下面字符串压栈的那个同样的栈。

在字节码运行的时候,Lua 虚拟机根据字节码指令指示,把相关的数据放到数据栈上,而后进行相应的操作。

因为虚拟机对数据栈的操作只会在栈顶进行,所以,我们说 Lua4.0 的虚拟机是栈式虚拟机。

看一看这个语法分析的函数调用流程:

从 luaY_parser 开始看。

Proto *luaY_parser (lua_State *L, ZIO *z) {
  struct LexState lexstate;
  struct FuncState funcstate;
  luaX_setinput(L, &lexstate, z, luaS_new(L, zname(z)));
  open_func(&lexstate, &funcstate);
  next(&lexstate);  /* read first token */
  chunk(&lexstate);
  //...
  return funcstate.f;
}

程序上来先设置好 LexState, FuncState

调用 next 取得第一个 token,就是 print 。

进入 chunk

static void chunk (LexState *ls) {
  /* chunk -> { stat [‘;‘] } */
  int islast = 0;
  while (!islast && !block_follow(ls->t.token)) {
    islast = stat(ls);
    optional(ls, ‘;‘);
    //...
  }
}

进入语句 stat

static int stat (LexState *ls) {
  int line = ls->linenumber;  /* may be needed for error messages */
  switch (ls->t.token) {
    // other cases
    case TK_NAME: case ‘%‘: {  /* stat -> namestat */
      namestat(ls);
      return 0;
    }
    // other cases
  }
}

里面是个大的 switch case 。

这里触发的是 TK_NAME:

进入 namestat

static void namestat (LexState *ls) {
  /* stat -> func | [‘%‘] NAME assignment */
  FuncState *fs = ls->fs;
  expdesc v;
  var_or_func(ls, &v);
  if (v.k == VEXP) {  /* stat -> func */
    check_condition(ls, luaK_lastisopen(fs), "syntax error");  /* an upvalue? */
    luaK_setcallreturns(fs, 0);  /* call statement uses no results */
  }
  else {  /* stat -> [‘%‘] NAME assignment */
    int left = assignment(ls, &v, 1);
    luaK_adjuststack(fs, left);  /* remove eventual garbage left on stack */
  }
}

进入 var_or_func,设置表达式描述符 expdesc

static void var_or_func (LexState *ls, expdesc *v) {
  /* var_or_func -> [‘%‘] NAME var_or_func_tail */
  if (optional(ls, ‘%‘)) {  /* upvalue? */
    pushupvalue(ls, str_checkname(ls));
    v->k = VEXP;
    v->u.l.t = v->u.l.f = NO_JUMP;
  }
  else  /* variable name */
    singlevar(ls, str_checkname(ls), v);
  var_or_func_tail(ls, v);
}

显然,这里不是 upvalue,只是一个简单的变量名。

进入 singlevar, 先取得一个 TString 的值:

static TString *str_checkname (LexState *ls) {
  TString *ts;
  check_condition(ls, (ls->t.token == TK_NAME), "<name> expected");
  ts = ls->t.seminfo.ts;
  next(ls);
  return ts;
}

取得一个 TString

static void singlevar (LexState *ls, TString *n, expdesc *var) {
  int level = search_local(ls, n, var);
  if (level >= 1)  /* neither local (0) nor global (-1)? */
    luaX_syntaxerror(ls, "cannot access a variable in outer scope", n->str);
  else if (level == -1)  /* global? */
    var->u.index = string_constant(ls->fs, n);
}

查看是否是局部变量,显然不是。这里 level 值为 -1。

取得 string 在全局字符串表中的下标:

static int string_constant (FuncState *fs, TString *s) {
  Proto *f = fs->f;
  int c = s->u.s.constindex;
  if (c >= f->nkstr || f->kstr[c] != s) {
    luaM_growvector(fs->L, f->kstr, f->nkstr, 1, TString *,
                    "constant table overflow", MAXARG_U);
    c = f->nkstr++;
    f->kstr[c] = s;
    s->u.s.constindex = c;  /* hint for next time */
  }
  return c;
}

回到 var_or_func, 进入 var_or_func_tail

static void var_or_func_tail (LexState *ls, expdesc *v) {
  for (;;) {
    switch (ls->t.token) {
      // other cases:
      case ‘(‘: case TK_STRING: case ‘{‘: {  /* var_or_func_tail -> funcargs */
        luaK_tostack(ls, v, 1);  /* `v‘ must be on stack */
        funcargs(ls, 0);
        v->k = VEXP;
        v->u.l.t = v->u.l.f = NO_JUMP;
        break;
      }
      default: return;  /* should be follow... */
    }
  }
}

因为当前的 token 为 ‘(‘,所以进入上面的 case。

进入 luaK_tostack(位于 lcode.c 文件中),生成字节码 GETGLOBAL 0

进入 funcargs,设置函数 print 的参数。

static void funcargs (LexState *ls, int slf) {
  FuncState *fs = ls->fs;
  int slevel = fs->stacklevel - slf - 1;  /* where is func in the stack */
  switch (ls->t.token) {
    case ‘(‘: {  /* funcargs -> ‘(‘ [ explist1 ] ‘)‘ */
      int line = ls->linenumber;
      int nargs = 0;
      next(ls);
      if (ls->t.token != ‘)‘)  /* arg list not empty? */
        nargs = explist1(ls);
      check_match(ls, ‘)‘, ‘(‘, line);
#ifdef LUA_COMPAT_ARGRET
      if (nargs > 0)  /* arg list is not empty? */
        luaK_setcallreturns(fs, 1);  /* last call returns only 1 value */
#else
      UNUSED(nargs);  /* to avoid warnings */
#endif
      break;
    }
    // other cases
  }
  fs->stacklevel = slevel;  /* call will remove function and arguments */
  luaK_code2(fs, OP_CALL, slevel, MULT_RET);
}

因为有参数,所以进入 explist1

static int explist1 (LexState *ls) {
  /* explist1 -> expr { ‘,‘ expr } */
  int n = 1;  /* at least one expression */
  expdesc v;
  expr(ls, &v);
  while (ls->t.token == ‘,‘) {
    luaK_tostack(ls, &v, 1);  /* gets only 1 value from previous expression */
    next(ls);  /* skip comma */
    expr(ls, &v);
    n++;
  }
  luaK_tostack(ls, &v, 0);  /* keep open number of values of last expression */
  return n;
}

先通过 expr 取得表达示描述信息。

进入 expr, subexpr, simpleexp

因为是一个简单的表达式,只有一个字符串 "hello world! (using print)\n",

static void simpleexp (LexState *ls, expdesc *v) {
  FuncState *fs = ls->fs;
  switch (ls->t.token) {
    // other cases
    case TK_STRING: {  /* simpleexp -> STRING */
      code_string(ls, ls->t.seminfo.ts);  /* must use `seminfo‘ before `next‘ */
      next(ls);
      break;
    }
    // other codes

code_string 设置字节码,PUSHSTRING 1

回到 funcargs。设置函数调用字节码:

luaK_code2(fs, OP_CALL, slevel, MULT_RET);

回到 namestat,设置返回字节码:END

回到 chunk,luaY_parser 语法分析结束。

语法分析过程再跟踪下去基本就是个体力活了,不再举例子了。

对照着 EBNF 及生成的字节码,一步步的跟踪就是了。

其它的语法分析的理论,参见编译原理。

或者是看开头部分的那些简要的描述,也可以了解了大概。

----------------------------------------

到目前为止的问题:

> TString 数据结构和 luaS_new

> 函数原型优化 luaU_optchunk

> 打印函数原型 luaU_printchunk

> dump 函数原型 luaU_dumpchunk

----------------------------------------

时间: 2024-08-25 18:25:25

Lua4.0 语法分析的相关文章

Lua5.0 语法分析

写着写着,又来到了这里. 这次是写还是不写,嗯,这是一个问题. 先说点题外话,没准也算是题内. 经过这段时间的代码阅读,分析,调试,感觉自己的代码控制力提高了一些. 当然了,不单是 Lua 相关的,也有其它的工作中的代码阅读. 相比之下,还是工作中阅读的代码量要更大一些,更快一些. 究其原因,可能是工作中的代码阅读的目的性要更强一些吧. 打个比方,工作中的代码阅读是为了解决某个很具体的问题. 带着问题去阅读代码,针对性更强一些. 也比较容易把关注点集中在相关的代码上,这个过程中不自觉地就忽略了其

Lua4.0 开篇

标题说是 4.0,其实这里分析的是 4.0.1.不过按照 Lua 的版本号规则,小号只做 bug fix .所以,下面的所说的 4.0 指的就是 release 4.0.1(在不引起混淆的情况下). 4.0 发布于 2000 年 11 月,4.0.1 发布于 2002.7,我们看的上一个版本 2.4 则是发布于 1996 年 5 月,怎么说这个版本也是二十一世纪的了. 4.0 算是比较新的版本了,因为它有在线版的代码和文档.在线文档在 http://www.lua.org/manual/,其实从

Lua4.0 参考手册(一)1-3

说明:这个文档是 doc 目录里的 manual.html 文件.原文版权归原作者所有,这篇翻译只是作为学习之用.如果翻译有不当之处,请参考原文.-------------------以下是正文-------------------编程语言 Lua4.0 的参考手册--------------------------------------1 简介--------------------------------------Lua 是一个扩展编程语言,支持通用编程功能与数据描述功能.作为一个强大

Lua4.0 ZIO缓冲区

来看一下 ZIO 缓冲. 词法分析读一个一个的字符就是从它读的. 或者 umdump 时也是从它读字符(一个 char 字节)的. 缓冲区隔离了下层数据源的不同,对上层提供一致的读取接口. 相关的代码文件是 lzio.h 和 lzio.c . 先看一下数据结构: #ifndef ZBSIZE #define ZBSIZE  256             /* buffer size */ #endif struct zio {    size_t n;                     

Lua4.0 编译入口

解决上一篇的问题,上代码了. C 语言程序的入口为 main 函数,Lua 编译器的入口为 luac.c 文件里的 main 函数. 先来看一下 main 函数: int main(int argc, const char* argv[]) {  Proto** P,*tf;  int i=doargs(argc,argv);  argc-=i; argv+=i;  if (argc<=0) usage("no input files given",NULL);  L=lua_o

Lua4.0 词法分析

在说语法分析之前,先说一下词法分析. 因为语法分析时会调用词法分析. 词法分析相关的文件为 llex.h,llex.c. 先来看一下词法分析的头文件. RESERVED 保留字枚举. Token 结构体,词法分析就是把输入源分析成一个个的 token. 这个词比较常见,不再翻译成中文,因为我也不知道它的准确的中文叫什么. LexState 结构体,词法分析状态机,记录当前词法分析的状态以及一些相关的环境. 下面的是几个词分析的方法,注意 lua 中的命名约定. lua*_ 开头的都是 lua 内

Lua4.0 代码

这个标题是 2014.11.13 号写的,今天总算是写入内容了. 离上次写代码分析时间有点长,都忘记自己之前是如何写的了. 不管这些历史包袱了,这次轻装上阵,想到哪就写到哪. 开始. 参照 4.0 的 INSTALL 文档内容,编译下.在 Linux 机器上比较简单,make 一下就好了. 在 Windows 上建工程的话,也算是比较容易.这部分之前有描述,这里就不再重复了. 简单的说一下这次的代码分析可能会写什么内容.(注意,这里用"可能"的原因的是,依目前的想法,最大程度上会这么写

Lua4.0 字符串相关

这节看一下字符串相关的: TString 数据结构如下所示,可以看到,TString 不单用于处理字符串,还可用于处理用户自定义数据. /* ** String headers for string table */ /* ** most `malloc' libraries allocate memory in blocks of 8 bytes. TSPACK ** tries to make sizeof(TString) a multiple of this granularity, t

Lua4.0 翻译小结

4.0 手册翻译完了,小结一下. 这一次从 2.4 跳到 4.0,手册绝大部分都得从头翻译.以前的 2.4 的只参考了一小部分,所以,翻译得进度比较慢.不过,好在现在已经翻译完了. 翻译过程中,有几节内容不太熟悉,翻译的可能不好.也有的是因为英语的意思没有太明白.不过,如果遇到不了解的,还是以英文为主. 另外,有一点需要解释一下.为什么一个完整的手册要分这么多篇?一个主要的原因是发博客时的字数限制.博客的字数限制是一万字.为了不破节,就自己这么分篇儿了.再者,觉得一篇太长的话, 每次看后面的内容