本章节对应库房
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());
}
在测验中,各级表达式嵌套及优先级匹配都运转经过
结语
本章内容较多,主要触及了完整的算术表达式解析。在下章中,咱们将以表达式解分出的抽象语法树为基础,进行表达式的计算。