本章节对应仓库
3.表达式解析 Github
巴科斯范式(BNF)
在本章之前,需要先科普一个知识点:BNF
如果已经有此类知识的读者可以跳过该段内容。
BNF
什么是BNF
BNF是一种形式化的语法表示方法,其有几个特点:
- 所描述的语法是上下文无关的
- 在双引号中的字(
"word")代表着这些字符本身。而double_quote用来代表双引号。 - 在双引号外的字(有可能有下划线)代表着语法部分。
- 尖括号(
< >)内包含的为必选项。 - 方括号(
[ ])内包含的为可选项。 - 大括号(
{ })内包含的为可重复0至无数次的项。 - 竖线(
|)表示在其左右两边任选一项,相当于"OR"的意思。 ::=是“被定义为”的意思- 终结符:语言中的基本元素,无法再分解
- 非终结符:可以再分解的元素
语法定义
描述简单的数字相加时,语法规则为:
add ::= <number> <"+"> <number>
这种表达式可以解析 1 + 2,解析后的结果为1 "+" 2
但这显然是不够用的,如果需要连续的数字相加时,如 1 + 2 + 3 时,上面的表达式便有了局限性。
因此需要对其进行改造,不过这并不难,BNF支持递归语法。
改造后的加法:
add ::= <add> <"+"> <number> | <number>
在解析 1 + 2 + 3 时,会解析为:(1 "+" 2) "+" 3
此时,简单的加法已经没有了难度。所以可以对其进行进一步改造,使其支持减法与括号
- 减法支持
expr0 ::= <expr0> <"+"|"-"> <number> | <number>
由于减法与加法的优先级相同,可以把加减法合并到一个表达式当中。
- 括号支持 相较减法,支持括号显然在难度上更大一些。由于括号会改变运算优先级,因此我们把带括号的表达式单独定义为一个非终结符。
primary ::= <"("> <expr0> <")"> | <number>
expr0 ::= <expr0> <"+"|"-"> <primary> | <primary>
如解析1 + (3 - 2 + 5)时,会按照如下方式进行解析:
expr0--primary--1
|
"+"
|
primary--"("
|
expr0--primary--3
| |
")" "-"
|
expr0--primary--2
|
"+"
|
primary--5
- 右结合的操作符
上面列举的加法是左结合的运算符,所谓左结合是指解析时按照从左往右进行结合,如1 + 2 + 3,如果
+号是左结合,那么最后得到的表达式是(1 + 2) + 3
而如果是右结合运算符,例如使用^符号表示乘方时,2 ^ 3 为2的3次方,2 ^ 3 ^ 2 为 2的(3的2次方)次方(即2的9次方),解析出来的表达式则是2 ^ (3 ^ 2)。
要表达右结合运算符时,便要对语法定义进行一定的变换。上述例子中,左结合的加法定义为:
primary ::= <"("> <expr0> <")"> | <number>
expr0 ::= <expr0> <"+"> <primary> | <primary>
则右结合的乘方定义为
primary ::= <"("> <expr0> <")"> | <number>
expr0 ::= <primary> <"^"> <expr0> | <primary>
SwordScript的算术表达式定义
在开始写语法解析代码前,我们需要先写出算术表达式的定义。
按照优先级,可以将操作符的优先级表格列出:
| 操作符 | 名称 | 优先级 | 类型 | 结合性 |
|---|---|---|---|---|
| . | 成员获取 | 1 | 成员获取 | 左结合 |
| [ ] | 数组取值 | 1 | 访问器调用 | 左结合 |
| ( ) | 函数调用 | 1 | 函数调用 | 左结合 |
| not | 取否 | 2 | 单目运算符 | 右结合 |
| - | 取负 | 2 | 单目运算符 | 右结合 |
| 乘方 | 3 | 双目运算符 | 右结合 | |
| * | 乘 | 4 | 双目运算符 | 左结合 |
| / | 除 | 4 | 双目运算符 | 左结合 |
| % | 取余 | 4 | 双目运算符 | 左结合 |
| + | 加 | 5 | 双目运算符 | 左结合 |
| - | 减 | 5 | 双目运算符 | 左结合 |
| 大于 | 6 | 双目运算符 | 左结合 | |
| >= | 大于等于 | 6 | 双目运算符 | 左结合 |
| < | 小于 | 6 | 双目运算符 | 左结合 |
| <= | 小于等于 | 6 | 双目运算符 | 左结合 |
| == | 等于 | 7 | 双目运算符 | 左结合 |
| != | 不等于 | 7 | 双目运算符 | 左结合 |
| and | 逻辑与 | 8 | 双目运算符 | 左结合 |
| or | 逻辑或 | 9 | 双目运算符 | 左结合 |
在本章中,暂不考虑成员、数组与函数调用
将以上操作符化作BNF如下:
primary ::= <"("> <expr> <")"> | <literal|identifier>
expr2 ::= <"not"|"-"> <primary> | <primary>
expr3 ::= <expr2> <"^"> <expr3> | <expr2>
expr4 ::= <expr4> <"*"|"/"|"%"> <expr3> | <expr3>
expr5 ::= <expr5> <"+"|"-"> <expr4> | <expr4>
expr6 ::= <expr6> <">"|">="|"<"|"<="> <expr5> | <expr5>
expr7 ::= <expr7> <"=="|"!="> <expr6> | <expr6>
expr8 ::= <expr8> <"and"> <expr7> | <expr7>
expr9 ::= <expr9> <"or"> <expr8> | <expr8>
expr ::= <expr9>
将BNF转换为解析器
有了以上定义后,便可以开始写解析器了。
新建一个类,命名为ScriptParser
ScriptParser.cs
using Sprache;
namespace SwordScript;
public static class ScriptParser
{
}
在定义表达式之前,我们需要定义终结符。
在上面的BNF中,终结符有各种符号以及literal与identifier
literal是字面量,指写在代码里的数值。因此可以得到定义:
public static readonly Parser<ASTLiteral> Literal =
(from nullValue in Lexer.Null select new ASTNullLiteral())
.Or<ASTLiteral>(from booleanValue in Lexer.Boolean select new ASTBooleanLiteral(booleanValue))
.Or(from doubleValue in Lexer.DoubleFloat select new ASTDoubleFloatLiteral(doubleValue))
.Or(from longValue in Lexer.LongInteger select new ASTLongIntegerLiteral(longValue))
.Or(from stringValue in Lexer.String select new ASTStringLiteral(stringValue));
同理可得identifier
public static readonly Parser<ASTIdentifier> Identifier =
from id in Lexer.Identifier select new ASTIdentifier(id);
primary
而在定义primary前,我们需要先定义一个指代表达式的非终结符expr
public static Parser<ASTNode> Expr;
expr的定义会在全部表达式定义完毕后定义。
primary的定义:
public static readonly Parser<ASTNode> Primary = (
from left in Parse.Char('(').SuperToken()
from expr in Parse.Ref(() => Expr)
from right in Parse.Char(')').SuperToken()
select expr)
.Or(Literal)
.Or(Identifier)
.SuperToken();
单元测试
在Tests工程新建类ExprTest
namespace Tests;
public class ExprTest
{
}
添加如下内容:
[Test]
public void PrimaryTest()
{
Assert.AreEqual("123", ScriptParser.Primary.Parse(" 123 ").ToString());
Assert.AreEqual("0.5", ScriptParser.Primary.Parse(" 0.5 ").ToString());
Assert.AreEqual("abc", ScriptParser.Primary.Parse(" abc ").ToString());
Assert.AreEqual("true", ScriptParser.Primary.Parse(" true ").ToString());
Assert.AreEqual("null", ScriptParser.Primary.Parse(" null ").ToString());
}
运行单元测试,全部通过
expr2 - 取非、取反
在定义expr2之前,首先需要定义对应的语法树节点。
新建类ASTUnaryExpr
namespace SwordScript;
public abstract class ASTUnaryExpr : ASTList
{
public ASTUnaryExpr(ASTNode expr) : base(new ASTNode[] { expr })
{
}
public ASTNode Expr => this[0];
}
该类将作为所有一元表达式的基类
新建类ASTUnaryExprNegative表示取负
public class ASTUnaryExprNegative : ASTUnaryExpr
{
public ASTUnaryExprNegative(ASTNode expr) : base(expr)
{
}
public override string ToString()
{
return $"-{Expr}";
}
}
新建类ASTUnaryExprNot表示取反
public class ASTUnaryExprNot : ASTUnaryExpr
{
public ASTUnaryExprNot(ASTNode expr) : base(expr)
{
}
public override string ToString()
{
return $"not {Expr}";
}
}
随后在Lexer类中定义如下静态方法:
/// <summary>
/// 返回下个字符不为Unicode字符的字符符号解析器
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
public static Parser<string> LetterSymbol(string symbol)
{
return (from symbolStr in Parse.String(symbol).Text()
from nextChar in Parse.Regex(@"[0-9_\p{L}]").Preview()
where nextChar.IsEmpty
select symbolStr).SuperToken();
}
/// <summary>
/// 返回普通的标点符号解析器
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
public static Parser<string> PunctuationSymbol(string symbol)
{
return (from symbolStr in Parse.String(symbol).Text() select symbolStr).SuperToken();
}
LetterSymbol用于解析如 and or not 这类由字母组成的符号,防止与标识符黏连在一起时也被识别
PunctuationSymbol用于解析普通的标点符号
添加负号与取非的词法解析:
public static readonly Parser<string> Negate = PunctuationSymbol("-");
public static readonly Parser<string> Not = LetterSymbol("not");
在ScriptParser类中定义expr2
public static readonly Parser<ASTNode> Expr2 =(
from symbol in Lexer.Negate.Or(Lexer.Not)
from expr in Primary
select (ASTNode)(symbol == "-" ? new ASTUnaryExprNegative(expr) : new ASTUnaryExprNot(expr)))
.Or(Primary)
.SuperToken();
单元测试
添加单元测试:
[Test]
public void Expr2Test()
{
Assert.AreEqual("123", ScriptParser.Expr2.Parse(" 123 ").ToString());
Assert.AreEqual("-123", ScriptParser.Expr2.Parse(" -123 ").ToString());
Assert.AreEqual("not false", ScriptParser.Expr2.Parse(" not false ").ToString());
Assert.AreEqual("nottrue", ScriptParser.Expr2.Parse(" nottrue ").ToString());
}
运行测试,全部通过
expr3 - 乘方
先定义乘方对应类:
ASTBinaryExprPower
public class ASTBinaryExprPower : ASTBinaryExpr
{
public ASTBinaryExprPower(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} ^ {Right})";
}
}
定义Expr3
public static readonly Parser<ASTNode> Expr3 =(
from left in Expr2
from symbol in Lexer.Power
from right in Expr3
select new ASTBinaryExprPower(left, right))
.Or(Expr2)
.SuperToken();
单元测试
[Test]
public void Expr3Test()
{
Assert.AreEqual("(1 ^ 4)", ScriptParser.Expr3.Parse(" 1 ^ 4 ").ToString());
Assert.AreEqual("(5 ^ 0.7)", ScriptParser.Expr3.Parse(" 5 ^ .7 ").ToString());
Assert.AreEqual("(0.1 ^ 0.1)", ScriptParser.Expr3.Parse(" 0.1 ^ .1 ").ToString());
Assert.AreEqual("(2 ^ (2 ^ 2))", ScriptParser.Expr3.Parse(" 2 ^ 2 ^ 2 ").ToString());
}
由于目前Expr还没有定义,所以暂时无法测试嵌套公式
运行测试,全部通过
expr4 - 乘、除、取余
与前面的表达式不同,从expr4开始,在文法上是无法直接输入解析器的。
左递归
先来看看expr4的文法:
expr4 ::= <expr4> <"*"|"/"|"%"> <expr3> | <expr3>
可以看到,expr4构成了一个左递归,有左递归的文法,在解析时可能会陷入无限左展开,如:
A ::= Aa | a
A = Aa
A = Aaa
A = Aaaa
A = Aaaaa.......
因此,在将BNF转换为语法解析前,需要先进行左递归消除。
消除左递归
首先需要分析,左递归的语句可以如何拆分。 网上常用的拆分方法如下:
expr4 ::= <expr3> <expr4'>
expr4' ::= <"*"|"/"|"%"> <expr3> <expr4'> | ε
注:ε代表空语法
而从方便语法解析的角度分析,其实也可以将expr4'视为一个重复N次的语法项
于是有了以下拆解:
expr4 ::= <expr3> {<"*"|"/"|"%"> <expr3>}
由于我们后面的表达式几乎都是左递归的文法,因此可以将左递归的表达式进行封装,封装后如下:
public delegate ASTNode CreateNode(ASTNode left,string op,ASTNode right);
public static Parser<ASTNode> LeftOperator(Parser<ASTNode> leftExpr, Parser<string> symbol,
CreateNode apply)
{
ASTNode CreateNode(ASTNode left, IEnumerable<Tuple<string,ASTNode>> rights)
{
foreach(var right in rights)
{
left = apply(left, right.Item1, right.Item2);
}
return left;
}
Parser<Tuple<string,ASTNode>> innerOperatorExpr = (
from getSymbol in symbol
from right in leftExpr
select new Tuple<string, ASTNode>(getSymbol, right)
).SuperToken();
Parser<ASTNode> operatorExpr = (
from left in leftExpr
from rights in innerOperatorExpr.Many()
select CreateNode(left, rights)
).SuperToken();
return operatorExpr;
}
之后的表达式只需要输入子项、符号与构造语法树的委托,即可直接构造并拆分左递归表达式文法。
Expr4定义
定义对应类:
public class ASTBinaryExprMultiply : ASTBinaryExpr
{
public ASTBinaryExprMultiply(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} * {Right})";
}
}
public class ASTBinaryExprDivide : ASTBinaryExpr
{
public ASTBinaryExprDivide(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} / {Right})";
}
}
public class ASTBinaryExprModulo : ASTBinaryExpr
{
public ASTBinaryExprModulo(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} % {Right})";
}
}
在Lexer类中定义操作符
public static readonly Parser<string> Multiply = PunctuationSymbol("*");
public static readonly Parser<string> Divide = PunctuationSymbol("/");
public static readonly Parser<string> Modulo = PunctuationSymbol("%");
定义expr4
public static readonly Parser<ASTNode> Expr4 = LeftOperator(Expr3, Lexer.Multiply.Or(Lexer.Divide).Or(Lexer.Modulo),
(left, op, right) =>
{
switch (op)
{
case "*":
return new ASTBinaryExprMultiply(left, right);
case "/":
return new ASTBinaryExprDivide(left, right);
case "%":
return new ASTBinaryExprModulo(left, right);
default:
throw new ArgumentException($"Unknown operator '{op}'");
}
});
单元测试
[Test]
public void Expr4Test()
{
Assert.AreEqual("(1 * 2)", ScriptParser.Expr4.Parse(" 1 * 2 ").ToString());
Assert.AreEqual("(1 / 2)", ScriptParser.Expr4.Parse(" 1 / 2 ").ToString());
Assert.AreEqual("(1 % 2)", ScriptParser.Expr4.Parse(" 1 % 2 ").ToString());
Assert.AreEqual("((((1 * 2) / 3) % 4) * 5)", ScriptParser.Expr4.Parse(" 1 * 2 / 3 % 4 * 5 ").ToString());
}
运行测试,全部通过
Expr5 - 加、减
定义对应类:
public class ASTBinaryExprPlus : ASTBinaryExpr
{
public ASTBinaryExprPlus(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} + {Right})";
}
}
public class ASTBinaryExprMinus : ASTBinaryExpr
{
public ASTBinaryExprMinus(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} - {Right})";
}
}
定义操作符:
public static readonly Parser<string> Plus = PunctuationSymbol("+");
public static readonly Parser<string> Minus = PunctuationSymbol("-");
定义expr5
public static readonly Parser<ASTNode> Expr5 = LeftOperator(Expr4, Lexer.Plus.Or(Lexer.Minus),
(left, op, right) =>
{
switch (op)
{
case "+":
return new ASTBinaryExprPlus(left, right);
case "-":
return new ASTBinaryExprMinus(left, right);
default:
throw new ArgumentException($"Unknown operator '{op}'");
}
});
单元测试
[Test]
public void Expr5Test()
{
Assert.AreEqual("(1 + 2)", ScriptParser.Expr5.Parse(" 1 + 2 ").ToString());
Assert.AreEqual("(1 - 2)", ScriptParser.Expr5.Parse(" 1 - 2 ").ToString());
Assert.AreEqual("(((1 + 2) - 3) + 4)", ScriptParser.Expr5.Parse(" 1 + 2 - 3 + 4 ").ToString());
}
运行单元测试,全部通过
Expr6 大于、大于等于、小于、小于等于
定义对应类:
public class ASTBinaryExprGreaterThan : ASTBinaryExpr
{
public ASTBinaryExprGreaterThan(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} > {Right})";
}
}
public class ASTBinaryExprGreaterThanOrEqual : ASTBinaryExpr
{
public ASTBinaryExprGreaterThanOrEqual(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} >= {Right})";
}
}
public class ASTBinaryExprLessThan : ASTBinaryExpr
{
public ASTBinaryExprLessThan(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} < {Right})";
}
}
public class ASTBinaryExprLessThanOrEqual : ASTBinaryExpr
{
public ASTBinaryExprLessThanOrEqual(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} <= {Right})";
}
}
定义操作符
public static readonly Parser<string> GreaterThan = PunctuationSymbol(">");
public static readonly Parser<string> GreaterThanOrEqual = PunctuationSymbol(">=");
public static readonly Parser<string> LessThan = PunctuationSymbol("<");
public static readonly Parser<string> LessThanOrEqual = PunctuationSymbol("<=");
定义Expr6
public static readonly Parser<ASTNode> Expr6 = LeftOperator(Expr5,
Lexer.GreaterThanOrEqual.Or(Lexer.GreaterThan)
.Or(Lexer.LessThanOrEqual).Or(Lexer.LessThan),
(left, op, right) =>
{
switch (op)
{
case ">":
return new ASTBinaryExprGreaterThan(left, right);
case ">=":
return new ASTBinaryExprGreaterThanOrEqual(left, right);
case "<":
return new ASTBinaryExprLessThan(left, right);
case "<=":
return new ASTBinaryExprLessThanOrEqual(left, right);
default:
throw new ArgumentException($"Unknown operator '{op}'");
}
});
需要注意的是,大于等于需要放在大于前,小于等于需要放在小于前,否则会因为先匹配了大于或小于符号,导致大于等于和小于等于无法匹配。
单元测试
[Test]
public void Expr6Test()
{
Assert.AreEqual("(1 < 2)", ScriptParser.Expr6.Parse(" 1 < 2 ").ToString());
Assert.AreEqual("(1 > 2)", ScriptParser.Expr6.Parse(" 1 > 2 ").ToString());
Assert.AreEqual("(1 <= 2)", ScriptParser.Expr6.Parse(" 1 <= 2 ").ToString());
Assert.AreEqual("(1 >= 2)", ScriptParser.Expr6.Parse(" 1 >= 2 ").ToString());
Assert.AreEqual("(((1 < 2) < 3) < 4)", ScriptParser.Expr6.Parse(" 1 < 2 < 3 < 4 ").ToString());
}
运行测试,全部通过
Expr7 等于、不等于
定义对应类:
public class ASTBinaryExprEqual : ASTBinaryExpr
{
public ASTBinaryExprEqual(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} == {Right})";
}
}
public class ASTBinaryExprNotEqual : ASTBinaryExpr
{
public ASTBinaryExprNotEqual(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} != {Right})";
}
}
定义操作符:
public static readonly Parser<string> Equal = PunctuationSymbol("==");
public static readonly Parser<string> NotEqual = PunctuationSymbol("!=");
定义expr7
public static readonly Parser<ASTNode> Expr7 = LeftOperator(Expr6, Lexer.Equal.Or(Lexer.NotEqual),
(left, op, right) =>
{
switch (op)
{
case "==":
return new ASTBinaryExprEqual(left, right);
case "!=":
return new ASTBinaryExprNotEqual(left, right);
default:
throw new ArgumentException($"Unknown operator '{op}'");
}
});
单元测试
[Test]
public void Expr7Test()
{
Assert.AreEqual("(1 == 2)", ScriptParser.Expr7.Parse(" 1 == 2 ").ToString());
Assert.AreEqual("(1 != 2)", ScriptParser.Expr7.Parse(" 1 != 2 ").ToString());
Assert.AreEqual("(((1 == 2) == 3) == 4)", ScriptParser.Expr7.Parse(" 1 == 2 == 3 == 4 ").ToString());
}
运行单元测试,全部通过
Expr8 逻辑与
定义对应类:
public class ASTBinaryExprAnd : ASTBinaryExpr
{
public ASTBinaryExprAnd(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} and {Right})";
}
}
定义操作符:
public static readonly Parser<string> And = LetterSymbol("and");
定义expr8
public static readonly Parser<ASTNode> Expr8 = LeftOperator(Expr7, Lexer.And,
(left, op, right) => new ASTBinaryExprAnd(left, right));
单元测试
[Test]
public void Expr8Test()
{
Assert.AreEqual("(a and b)", ScriptParser.Expr8.Parse(" a and b ").ToString());
Assert.AreEqual("((a and b) and c)", ScriptParser.Expr8.Parse(" a and b and c ").ToString());
}
运行单元测试,全部通过
Expr9 逻辑或
定义对应类:
public class ASTBinaryExprOr : ASTBinaryExpr
{
public ASTBinaryExprOr(ASTNode left, ASTNode right) : base(left, right)
{
}
public override string ToString()
{
return $"({Left} or {Right})";
}
}
定义操作符:
public static readonly Parser<string> Or = LetterSymbol("or");
定义Expr9
public static readonly Parser<ASTNode> Expr9 = LeftOperator(Expr8, Lexer.Or,
(left, op, right) => new ASTBinaryExprOr(left, right));
单元测试
[Test]
public void Expr9Test()
{
Assert.AreEqual("(a or b)", ScriptParser.Expr9.Parse(" a or b ").ToString());
Assert.AreEqual("((a or b) or c)", ScriptParser.Expr9.Parse(" a or b or c ").ToString());
}
运行单元测试,全部通过
表达式闭环
在定义完所有表达式后,回到Expr上,将其补充为如下定义:
public static readonly Parser<ASTNode> Expr = Parse.Ref(() => Expr9);
此时,表达式解析已经完成了一个闭环,所有的表达式相互嵌套应当没有了问题。
单元测试
[Test]
public void Expression()
{
Assert.AreEqual("((1 + 2) * (3 + 4))", ScriptParser.Expr.Parse(" (1 + 2) * (3 + 4) ").ToString());
Assert.AreEqual("(a >= ((3 * (b ^ 5)) - 8))", ScriptParser.Expr.Parse(" a >= 3 * b ^ 5 - 8 ").ToString());
Assert.AreEqual("((a and (b1 == b2)) or (((9 * 9) == c) and d))", ScriptParser.Expr.Parse(" a and b1 == b2 or 9*9 == c and d ").ToString());
Assert.AreEqual("((a and (b1 == (b2 or ((9 * 9) == c)))) and d)", ScriptParser.Expr.Parse(" a and b1 == (b2 or 9*9 == c) and d ").ToString());
}
在测试中,各级表达式嵌套及优先级匹配都运行通过
结语
本章内容较多,主要涉及了完整的算术表达式解析。在下章中,我们将以表达式解析出的抽象语法树为基础,进行表达式的计算。