LUA_API lua_arith (2)

开篇

上一节分析了 lua_arith 的大部分代码,由于篇幅原因,留到本节将继续讲解剩余的部分:

luaO_arith(L, op, L->top - 2, L->top - 1, L->top - 2);
L->top--;  /* remove second operand */
lua_unlock(L);

解析

现在我们来看运算规则 luaO_arith

// lobject.c 123
void luaO_arith (lua_State *L, int op, const TValue *p1, const TValue *p2,
                 TValue *res) {
  switch (op) {
    case LUA_OPBAND: case LUA_OPBOR: case LUA_OPBXOR:
    case LUA_OPSHL: case LUA_OPSHR:
    case LUA_OPBNOT: {  /* operate only on integers */
      lua_Integer i1; lua_Integer i2;
      if (tointeger(p1, &i1) && tointeger(p2, &i2)) {
        setivalue(res, intarith(L, op, i1, i2));
        return;
      }
      else break;  /* go to the end */
    }
    case LUA_OPDIV: case LUA_OPPOW: {  /* operate only on floats */
      lua_Number n1; lua_Number n2;
      if (tonumber(p1, &n1) && tonumber(p2, &n2)) {
        setfltvalue(res, numarith(L, op, n1, n2));
        return;
      }
      else break;  /* go to the end */
    }
    default: {  /* other operations */
      lua_Number n1; lua_Number n2;
      if (ttisinteger(p1) && ttisinteger(p2)) {
        setivalue(res, intarith(L, op, ivalue(p1), ivalue(p2)));
        return;
      }
      else if (tonumber(p1, &n1) && tonumber(p2, &n2)) {
        setfltvalue(res, numarith(L, op, n1, n2));
        return;
      }
      else break;  /* go to the end */
    }
  }
  /* could not perform raw operation; try metamethod */
  lua_assert(L != NULL);  /* should not fail when folding (compile time) */
  luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD));
}

TValue

注意到参数 3, 4 是两个操作数,且它们都是 TValue * 类型,我们找到 TValue 的定义:

// lobject.h 100
/*
** Union of all Lua values
*/
typedef union Value {
  GCObject *gc;    /* collectable objects : 可回收的对象 */
  void *p;         /* light userdata : 轻量级 userdata */
  int b;           /* booleans : 布尔值*/
  lua_CFunction f; /* light C functions : 轻量级 C 函数 */
  lua_Integer i;   /* integer numbers : 整型数 */
  lua_Number n;    /* float numbers : 浮点数 */
} Value;


#define TValuefields  Value value_; int tt_


typedef struct lua_TValue {
  TValuefields;
} TValue;

TValue 是一个结构体别名,用它可以定义一个结构体,其中包含一个联合体 value_ 和一个整型数 tt_;而 value_ 包含了所有 Lua 的值类型:可回收的对象轻量级 userdata布尔值轻量级 C 函数整型数浮点数。也就是说,除了 GCObject,其他 5 个数据类型是不用 Lua 回收机制的。

我们来看 GCObject

// lobject.h 72
/*
** Common type for all collectable objects
*/
typedef struct GCObject GCObject;


/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader  GCObject *next; lu_byte tt; lu_byte marked


/*
** Common type has only the common header
*/
struct GCObject {
  CommonHeader;
};

知道 GCObject 首先是个结构体,其次它包含了三个域 :GCObject *next; lu_byte tt; lu_byte marked;这里 GCObject * 又被替换为结构体类型 struct GCObject *。再看 lu_byte

// llimits.h 35
/* chars used as small naturals (so that 'char' is reserved for characters) */
typedef unsigned char lu_byte;

可知,lu_byte 就是简单的类型别名,用一个 unsigned char 类型来表示。
综上对 GCObject 的分析,可得:所有的 GCObject 都用一个单向链表串了起来,每个 GCObject 对象都携带 tt, marked 两个标记(其中 tt 用来识别其类型,marked 域用于标记清除的工作,这些是后话了,现在我也不懂)

现在是不是可以理解 Lua 官方对值和类型的说明了:

Lua is a dynamically typed language. This means that variables do not have types; only values do. There are no type definitions in the language. All values carry their own type.

因为 TValue 携带了所有的 Lua 值类型,这就是变量没有类型,只有值才有类型的原因!

Operator Rules

解释完 TValue,我们来归纳一下 switch 之后的运算符规则:

  • LUA_OPBAND、 LUA_OPBOR、 LUA_OPBXOR、 LUA_OPSHL、 LUA_OPSHR、 LUA_OPBNOT 遵循 [1] 操作;
  • LUA_OPDIV、 LUA_OPPOW 遵循 [2] 操作;
  • LUA_OPADD、 LUA_OPSUB、 LUA_OPMUL、 LUA_OPMOD、 LUA_OPUNM 遵循 default [3] 操作。

下面以 [1] 作为切入口来解析具体的运算服规则,由于其他操作活类似或略有不同,因此不再多费唇舌。
注意到 [1] 中的元素都是单目运算符,其运算规则为:

{
    lua_Integer i1; lua_Integer i2;
    if (tointeger(p1, &i1) && tointeger(p2, &i2)) {
      setivalue(res, intarith(L, op, i1, i2));
      return;
    }
    else break;  /* go to the end */
}

按步骤来说:

  1. 声明两个 lua_Integer 类型数据 i1, i2
  2. 将两个操作数进行类型转换,并将结果分别存储到 i1, i2
  3. 如果转换成功,则执行运算后直接返回。
  4. 如果转换失败,则跳出 switch

接下来我们逐个分析几个要点:

  1. lua_Integer
  2. tointeger
  3. ntarith
  4. setivalue

1. lua_Integer

lua_Integer 定义在 lua.h 93 行:typedef LUA_INTEGER lua_Integer;。这里是为 LUA_INTEGER 定义了别名 lua_Integer。而 LUA_INTEGERluaconf.h 524 ~ 574 行之间有详细的说明,它指代的是整型数据类型。

2. tointeger

tointeger 定义在 lvm.h 43 行:

#define tointeger(o,i) \
    (ttisinteger(o) ? (*(i) = ivalue(o), 1) : luaV_tointeger(o,i,LUA_FLOORN2I))

再看 ttisintegerivalue 是这么定义的:

/* raw type tag of a TValue */
#define rttype(o) ((o)->tt_)

#define checktag(o,t)   (rttype(o) == (t))
#define ttisinteger(o)    checktag((o), LUA_TNUMINT)

#define check_exp(c,e)    (lua_assert(c), (e))
#define val_(o)   ((o)->value_)
#define ivalue(o) check_exp(ttisinteger(o), val_(o).i)

现在我们知道 oLua 原始值 TValue *o, rttype(o) 获取的是原始值的 tt_ 标记,而 tt_ 则是原始值类型标记,因此通过 checktag(o,t) 可以检查原始值类型标记 tt_ 是否与预期 t 符合,已达到检查值类型的目的。

接着看 ivalue(o):首先它通过 ttisinteger(o) 检查当前原始值的类型标记 tt_ 是不是整型作为断言的判断条件,其次通过 val_(o) 取出原始值的真实值 value_ 中的整型域作为断言的消息,最后通过 check_exp(c,e) 进行断言操作。

我们回过头看 ttisinteger,就可以拆成 tt + isinteger,意思是原始值的标记 tt_ 标记的是不是整型类型。那么 LUA_TNUMINT 指代的就是 Lua 中的整型类型了。来看看是不是这样:

#define LUA_TNUMBER   3

/* Variant tags for numbers */
#define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4))  /* float numbers */
#define LUA_TNUMINT (LUA_TNUMBER | (1 << 4))  /* integer numbers */

一看不得了,我们顺便知道了 LUA_TNUMBER 定义了 LUA_TNUMFLTLUA_TNUMINT,也就是 number 类型包含了整型和浮点型,这也跟官方的介绍不谋而合:

The type number represents both integer numbers and real (floating-point) numbers.

然而,有点经验的同学可能有疑问了:在 Lua 5.2 版本中,所有的 number 不都是浮点数吗?那这又是什么鬼?
一个可能是我们理解错了,一个可能就是在 Lua 5.3 版本中对其做出了修改。我们看版本变更日志是否有提及此事:

8.1 – Changes in the Language

  • The main difference between Lua 5.2 and Lua 5.3 is the introduction of an integer subtype for numbers. Although this change should not affect "normal" computations, some computations (mainly those that involve some kind of overflow) can give different results.
    You can fix these differences by forcing a number to be a float (in Lua 5.2 all numbers were float), in particular writing constants with an ending .0 or using x = x + 0.0 to convert a variable. (This recommendation is only for a quick fix for an occasional incompatibility; it is not a general guideline for good programming. For good programming, use floats where you need floats and integers where you need integers.)
  • The conversion of a float to a string now adds a .0 suffix to the result if it looks like an integer. (For instance, the float 2.0 will be printed as 2.0, not as 2.) You should always use an explicit format when you need a specific format for numbers.

简单翻译:

  • Lua 5.3 与 Lua 5.2 版本主要的差别就是 numbers 新增了子类型 integer。尽管这个改动并不影响“普通”的计算,但部分计算(主要是一些涉及溢出的计算)可能会与之前的版本的计算结果有所不同。
    一个修正的方案是将 number 强转为 float (在 Lua 5.2 中所有的 numbers 都是 float)。你可以在常熟后加上 .0 或者使用 x = x + 0.0 的方法来转换。(但这些建议是为了兼容而作的妥协,而并非良好的编程纲领,好的方式是:当用 float 则用 float,当用 integer 则用 integer,泾渭分明,分而治之。)
  • 当将 float 转换为 string 时,如果结果看上去像是 integer,会在结果最后加上 .0。(举个例子,浮点型 2.0 打印出来是 2.0,而不是 2。)

果不其然,我们的分析是正确的,大家可以用两个版本分别验证一下:

Lua 5.2.4
> a = 1 + 3.0
> print(a)
4
---------------
Lua 5.3.3
> a = 1 + 3.0
> print(a)
4.0

另提一点, 5.3 版本的解释器已经支持直接计算了:

Lua 5.2.4
> 1 + 3.0
stdin:1: unexpected symbol near '1'

Lua 5.3.3
> 1 + 3.0
4.0

现在剩下一个 luaV_tointeger 还在浆糊之中,我们再行探究:

// lvm.c 94
/*
** try to convert a value to an integer, rounding according to 'mode':
** mode == 0: accepts only integral values
** mode == 1: takes the floor of the number
** mode == 2: takes the ceil of the number
*/
// 注意:integral 是完整的、整体的意思,而非整数!
int luaV_tointeger (const TValue *obj, lua_Integer *p, int mode) {
  TValue v;
 again:
  if (ttisfloat(obj)) {
    lua_Number n = fltvalue(obj); // 取原始值 value_ 的 n 域
    lua_Number f = l_floor(n);    // 对 n 进行 floor 操作,赋值给 f
    // 如果 n != f,那么 obj 就是不可转换为类整数的浮点数 (如2.01)
    if (n != f) {  /* not an integral value? */
      // 而如果 mode = 0,即要求操作数可转换为类整数,那么就直接返回 0
      if (mode == 0) return 0;  /* fails if mode demands integral value */
      // 如果 mode > 1,即要求将操作数进行向上取整
      else if (mode > 1)  /* needs ceil? */
        f += 1;  /* convert floor to ceil (remember: n != f) */
    }
    // 然后执行 number 转换为 integer 操作
    return lua_numbertointeger(f, p);
  }
  else if (ttisinteger(obj)) {
    // 如果操作数本身就是整型数据,那么直接讲原始值的 value_ 的 整数域赋值给 *p 即可
    *p = ivalue(obj);
    return 1;
  }
  else if (cvt2num(obj) &&
            luaO_str2num(svalue(obj), &v) == vslen(obj) + 1) {
    // 如果操作数是字符串,且可完全转换为 number,则将操作数替换为转换后的值后,重头开始转换
    obj = &v;
    goto again;  /* convert result from 'luaO_str2num' to an integer */
  }
  // 转换失败
  return 0;  /* conversion failed */
}

/********* 补充定义 **********/

// fltvalue 
#define fltvalue(o) check_exp(ttisfloat(o), val_(o).n)

// l_floor 
#define l_floor(x)    (l_mathop(floor)(x))

// lua_numbertointeger 
#define lua_numbertointeger(n,p) \
  ((n) >= (LUA_NUMBER)(LUA_MININTEGER) && \
   (n) < -(LUA_NUMBER)(LUA_MININTEGER) && \
      (*(p) = (LUA_INTEGER)(n), 1))

// svalue 
// 从原始值中获取实际的字符串(字节数组)
/* get the actual string (array of bytes) from a Lua value */
#define svalue(o)       getstr(tsvalue(o))

// LUA_FLOORN2I 
// 默认 LUA_FLOORN2I = 0
/*
** You can define LUA_FLOORN2I if you want to convert floats to integers
** by flooring them (instead of raising an error if they are not
** integral values)
*/
#if !defined(LUA_FLOORN2I)
#define LUA_FLOORN2I    0
#endif

现在,我们重新理解 luaV_tointeger(o,i,LUA_FLOORN2I) 就很简单了,就是将原始值 o
转换为整型数据,然后赋值给 i,当然如果转换成功则返回 1,失败则返回 0
回过头再看 tointeger 的定义:

#define tointeger(o,i) \
    (ttisinteger(o) ? (*(i) = ivalue(o), 1) : luaV_tointeger(o,i,LUA_FLOORN2I))

现在可以轻松地解释清楚了:如果原始值 o 的当前类型标记是整型,则取它的整型数据部分,并返回 1;否则,尝试进行转换,转换成功则取转换结果并返回 1,失败则返回 0;其中 1 代表成功,0 代表失败。完美!

3. intarith

intarith 可以拆解为 int(eger)arith(metic),顾名思义:整数运算的意思。我们来看定义:

static lua_Integer intarith (lua_State *L, int op, lua_Integer v1,
                                                   lua_Integer v2) {
  switch (op) {
    case LUA_OPADD: return intop(+, v1, v2);
    case LUA_OPSUB:return intop(-, v1, v2);
    case LUA_OPMUL:return intop(*, v1, v2);
    case LUA_OPMOD: return luaV_mod(L, v1, v2);
    case LUA_OPIDIV: return luaV_div(L, v1, v2);
    case LUA_OPBAND: return intop(&, v1, v2);
    case LUA_OPBOR: return intop(|, v1, v2);
    case LUA_OPBXOR: return intop(^, v1, v2);
    case LUA_OPSHL: return luaV_shiftl(v1, v2);
    case LUA_OPSHR: return luaV_shiftl(v1, -v2);
    case LUA_OPUNM: return intop(-, 0, v1);
    case LUA_OPBNOT: return intop(^, ~l_castS2U(0), v1);
    default: lua_assert(0); return 0;
  }
}

/********** 补充定义 **********/

#define intop(op,v1,v2) l_castU2S(l_castS2U(v1) op l_castS2U(v2))

#if !defined(l_castU2S)
#define l_castU2S(i)  ((lua_Integer)(i))
#endif

#if !defined(l_castS2U)
#define l_castS2U(i)  ((lua_Unsigned)(i))
#endif

/* unsigned integer type */
typedef LUA_UNSIGNED lua_Unsigned;

#define LUA_UNSIGNED    unsigned LUAI_UACINT

#define LUAI_UACINT   LUA_INTEGER

intop(op, v1, v2),先将 v1, v2 转换为无符号整型,计算出结果之后,再将结果转换为有符号整型。
luaV_modluaV_divluaV_shiftl 则对应 取模、除法、左移 操作,这里就不展开了,读者自行探究。

4. setivalue

现在来看最后一个要点:setivalue(v1, v2)
我们知道 ivalue 是取原始值的整型部分数值,那么不妨把 setivalue 拆解为 setivalue,解释为将 v2 赋值给 v1 的整型部分。来看看是不是这样?

#define setivalue(obj,x) \
  { TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_TNUMINT); }

#define settt_(o,t) ((o)->tt_=(t))

我们看到 setivaluesettt_ 是宏定义,我们用 v1, v2 带入后展开:

{ TValue *io=(v1); val_(io).i=(v2); io->tt_=(LUA_TNUMINT); }

果然,除了最后将原始值的类型标记 tt_ 赋值为 LUA_TNUMINT,其他和我们所料不差。我们也需要谨记,当需要改变 Lua 值类型时,必须同时改变它的 tt_ 类型标记。

luaT_trybinTM

如果 switch 中的运算失败,继而跳出 switch,那么就需要执行 luaT_trybinTM

/* could not perform raw operation; try metamethod */
luaT_trybinTM(L, p1, p2, res, cast(TMS, (op - LUA_OPADD) + TM_ADD));

我们看注释说明:如果无法执行原始的运算,则进行元方法运算。
由于元方法涉及运算符的重载,这部分属于 Lua 元方法的内容,我们先标记一下,暂且跳过,等接触到元方法之后,回过头再来看这部分内容。

回到最初

// ...
luaO_arith(L, op, L->top - 2, L->top - 1, L->top - 2);
L->top--;  /* remove second operand */
lua_unlock(L);

当运算完成后,我们看到栈进行了一次自减操作,目的是移除位于栈中 L->top - 1 处的第二操作数,这个时候栈中 L->top - 2 索引处存储的则是运算结果了。

小结

  • TValue * 定义的结构体是 Lua 的原始值;
  • 原始值中携带两份数据,一个是 Value 联合体定义的 value_,它是携带真实值的正体;一个是类型标记 tt_
  • value_ 包含了所有 Lua 值类型的数据,分别是:可回收的对象 : gc轻量级 userdata : p布尔值 : b轻量级 C 函数 : f整型数 : i浮点数 : n
  • 当原始值数据变化时,类型标记 tt_ 也将对应改变,如: (o)->i=n_x,则对应地 (o)->tt_=LUA_TNUMINT

终了

  1. 看一次源码,复习好多 C 知识,虽然很累,但很充实。
  2. 栈的数据结构可以去复习一下。
  3. 阅读中发现了很多 Lua 内置实现的彩蛋,有一种探险的惊喜。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,783评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,396评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,834评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,036评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,035评论 5 362
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,242评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,727评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,376评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,415评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,463评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,140评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,734评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,809评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,028评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,521评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,119评论 2 341

推荐阅读更多精彩内容