翻译
查看原帖
翻译
745184
normalpcer楼主2025/1/5 17:10

在这一系列问题中,你需要实现一个 Lua 语言(5.1 版本)的子集,称为 mini-lua(mua)。这是一个刘汝佳的实验语言,主要用于实现算法,而非真实世界的程序。

这是本系列的第二个问题,需要你实现表达式求值的功能。接下来的语法用扩展 BNF(巴科斯范式)描述,具体地,{x} 表示 x 重复一次或多次,[x] 表示 x 出现零次或一次,x|y 表示 xy 恰好出现一次(不可兼有)。

类型(Types)

mini-lua 是一个动态类型语言。这意味着变量没有类型之分,只有值拥有类型。这个语言中不能定义类型。所有的值都带有各自的类型。

  • Nil:nil 的类型,和其他的值都不同。它通常表示缺少有用的值,即缺省值。
  • Boolean:表示 truefalse 的类型。
  • Number:表示实数(双精度浮点数)。在内部实现中,数字由 IEEE-754 标准表示(C/C++/Java 语言的 double 类型)。整数也通过这种方式存储。这没有什么大问题,只要没有不精确的运算(如除法),IEEE-754 可以精确地表示整数。
  • String:表示若干个 ASCII 字符组成的字符串。
  • Function:表示函数。函数没有名称,有名称的是持有这个函数的变量。
  • Table:表示一个映射表,它可以由除了 nil 以外的任何值充当索引,而不仅是数字。 Table 是异构的(heterogeneous),它可以包含任何非 nil 的值。

TableFunction 都属于对象(Object):变量不实际拥有它们的值,而是包含一个引用。复制、传参和返回一个对象时,新的对象也会指向同一个值,而不涉及到任何复制。

致 Lua 程序员:Lua 还有另外两种基础类型:userdatathread,在 Mini-Lua 中不做支持。Mini-Lua 对 FP(函数式编程,Functional Programming)的支持有限,例如,Mini-Lua 不支持 lambda 表达式,函数也不能返回另一个函数或闭包(closure)。

变量(Variables)

变量的定义如下:

var -> NAME . NAME | '['expr']'

这里的 expr 表示任意合法的表达式,因为表可以包含任意类型的索引。注意,. 符号只是一个语法糖,让表达式看起来更像成员访问。例如,a.name 等价于 a["name"]

变量由 nil 充当默认值。如果给一个变量赋值为 nil,就相当于移除了这个变量。类似地,你可以给表中的元素赋值为 nil,从而移除这个元素。

致 Lua 程序员:Lua 提供了更多的语法糖,NAME : NAME args,我们忽略这一条。

简单表达式(Simple Expressions)

任何的表达式都由“简单表达式”组成。

simple_exp -> NUMBER | STRING | nil | true | false | '{' '}' | var | function_call

这里的花括号表示一张空的表。

致 Lua 程序员:Lua 还提供了更多方便的构造表的方法。然而,它们增加了语言的复杂度,所以我们不使用它们。此外,Mini-Lua 不支持 lambda 表达式。

函数调用(Function Calls)

上一节中提到的函数调用表达式定义如下:

function_call -> var '(' [expr{, expr}] ')'

致 Lua 程序员:Mini-Lua 中,调用函数的唯一方法是指定一个保有目标函数的变量。例如 (print)(1) 不会起作用,而 a = (f) 是允许的(见下文)。在 Lua 中,如果你使用一个字符串常量或者一张表调用函数,可以省略函数调用的括号。Mini-Lua 不支持这种语法。

表达式(Expressions)

我们使用运算符组织简单表达式,从而形成复杂的表达式(expr)。

expr -> simple_expr | expr binop expr | unop expr | '('expr')'

即可以使用二元运算符,前缀一元运算符以及圆括号。

下方列出了运算符的优先级(从低到高)。

  • or
  • and
  • < > <= >= == ~=
  • ..
  • + -
  • * / %
  • not # -(unary)
  • ^

需要特别注意以下几点:

  • 不等于使用 ~= 表示,而非 <>!=
  • %(取模)对于实数定义为 a - floor(a/b) * b
  • 仅有 ^(乘方)运算符是从右向左结合的。
  • notandor 始终返回 truefalse。只有 andor 存在短路机制。
  • 连接字符串使用 .. 而非 +
  • # 用于获取字符串或表的长度。表的长度被定义为最大的非负整数 N 使得 T[1], T[2] 直到 T[N] 皆非 nil

致 Lua 程序员:Lua 中字符串连接是右结合的。

但是对于这个问题,结合性不会影响结果。另外,andor 在 Lua 中不总是返回布尔值,但是为了简化问题,我们在 Mini-Lua 中限定其返回布尔值。最重要的变化可能是,字符串和数字之间不再有隐式转换。

库函数(Library Functions)

处于测试目的,你需要实现一些函数(见 URL)。

  • tonumber(e):尝试将参数转为数字。如果参数为数字或包含一个数字的字符串,返回这个数字;否则返回 nil。注意,这里没有 Lua 中的可选参数。

  • tostring(e):接受任意参数,并转换为合理格式的字符串。在 mini-lua 中,将函数和表分别转换为 "function""table"。如果参数是数字,相当于格式化字符串 %.14g。如果你使用的是其他编程语言,并且不了解格式字符串的确切语义,你可以选择自己的方式。我们在评测中不要求格式严格一致。

  • print(e):输出 tostring(e),跟随一个换行符。在 Mini-Lua 中,该函数只接受一个参数。

  • string.rep(s, n):返回一个字符串,由字符串 s 重复 n 次组成。

  • string.sub(s, i[, j]):返回字符串 ij 的子串,ij 可以为负。如果 j 未指定,假设它为 -1(即字符串长度)。

  • table.concat(tabel[, sep]):给定一个元素均为字符串的表,返回一个由这些字符串连接而成的字符串。如果 sep 未指定,默认为空串。

  • table.sort(table[, comp]):对表 table 进行原地排序,comp 表示元素之间的小于关系。如果 comp 未指定,则使用默认的比较函数。不要求排序算法稳定,即相同元素的相对顺序可以改变。

  • math.abs(x)math.floor(x)math.ceil(x)math.sqrt(x)math.exp(x)math.log(x)math.log10(x)math.pimath.rad(x)math.deg(x)math.acos(x)math.asin(x)math.atan(x)math.atan2(y, x)math.cos(x)math.sin(x)math.tan(x)math.min(x, y)math.max(x, y):对应功能的数学运算。三角函数均为弧度制。

输入格式

输入若干个 mini-lua 程序。每个程序包含若干行,要么是一个 print(expr) 语句,要么是一个赋值语句 var = expr。所有表达式遵循以上规则。一个空行绊脚石一个程序的结束(所有变量重置为 nil)。这些表达式将是正确的,并且能够计算出合理的值(例如,你不需要处理 NaN 或算术溢出,也不会被要求比较数字与字符串)。我们不会为变量和函数重新赋值。程序中将不包含注释。

输出

对于每个存在输出的行,打印该表达式的结果。在打印数字时,你可以打印任意多的小数位数,只要相对误差或绝对误差不超过 10910^{-9} 即可。

提示

如果你想从 Lua 官方编译器的源代码中学习,可以下载 5.1.4 版本,并查看 lparser.c 文件中的 subexpr 函数(你可以在其之前看到运算符优先级表)。

2025/1/5 17:10
加载中...