语法: 程序的行文规则
语法是编程语言设计者规定的如何解释程序编写方式的一系列规则。
每种编程语言的语法规则都不太一样,并且不同规则之间存在冲突还会有特殊的写法,但是她们最终都会被转译为机器语言来执行。
表达式
我们以一个常见的算术表达式来说明不同语法规则下的书写以及设计者的思路。
FORTH: 后缀表达式
FORTH 语言开发与 1958 年,当时的程序员还在使用汇编甚至机器语言(C语言诞生于20世纪70年代)。FORTH 语言使用了成为栈的数值存储空间(现代编程语言的值类型同样是存储在栈空间),而模拟栈进入进出而诞生了 FORTH 的语法:
上图就是整个运算逻辑:
- 首先存在一个栈空间
- 遇到 1 放入到栈空间中
- 遇到 2 放入到栈空间中
- 遇到 + 则从栈空间中取出两个值( + 是双目运算符)
- 运算结果放入到栈中
对于计算顺序来说同样需要遵循栈入栈出的顺序来实现:
1 2 + 3 * # -> (1+2)*3
# 对应栈
2 + 3 *
1 1 3 3 9
1 2 3 * + # -> 1 + 2 * 3
# 这个优先级比较特殊,对应栈
3 *
2 2 6 +
1 1 1 1 7
后缀表达式是计算机理解的表达式,在底层像中缀表达式就需要修改为后缀表达式才能够计算。这在编写计算器应用时有体现。
主流: 中缀表达式
这类表达式的运算符都为操作数的中间,他们更接近人们的书写习惯。其中 C 语言就是标准的中缀表达式,而现在的主流语言或多或少都借鉴了 C 语言的语法因此大多数都是中缀表达式。
注意中缀表达式仅仅是为了实现更好的可读性,它内部的实现依然是基于栈的。我们以 Python 为例:
# dis 能够显示 VM 执行的命令
import dis
# 可以看到整个操作就是 Forth 中的 x y + z *
dis.dis(lambda x,y,z: (x+y)*z)
1 0 RESUME 0
2 LOAD_FAST 0 (x)
4 LOAD_FAST 1 (y)
6 BINARY_OP 0 (+)
10 LOAD_FAST 2 (z)
12 BINARY_OP 5 (*)
16 RETURN_VALUE
LISP: 前缀表达式
同样是 1958 年诞生的 LISP 语言则是选用了前缀表达式,它的语法规则中括号占用了很大的权重,所有的表达式都需要使用括号来标示完整的执行单元:
语法树和语法分析器
程序都是使用字符串表达的,他们最终会通过语法分析器读入、解析来建立语法树。
上面是 FORTH 和 LISP 中对 (1+2)*3
表达式的语法树,尽管他们的表达式语法差距很大但是整个语法树结构是一样的。Python 也是同样的道理,尽管它包含了更复杂的对象:
# ast 是 Python 的抽象语法树工具
import ast
ast.dump(ast.parse("(1 + 2) * 3"))
Module(
body=[Expr(
value=BinOp(
left=BinOp(
left=Constant(value=1),
op=Add(),
right=Constant(value=2)
),
op=Mult(),
right=Constant(value=3))
)
], type_ignores=[])
语法之间的竞争
语法分析器来解析源代码时会根据语法规则来构建语法树,如果一个编程语言存在大量的语法规则就会导致不同规则之间的竞争。这也会造成一些非常容易出错的语法细节。
例如 C++ 中为了引入模块功能,就添加了 vector<int>
这样的语法规则,它利用了不等号来构造表达式,这种方法在使用二重表达式时会存在 >> 即移位运算符。这就需要其他特殊的方式来避免语法之间的竞争:
因此语言的设计应当有所取舍,过多的语法规则就会导致更多语法竞争的引入。