语法分析器初步学习——LISP语法分析

本文参考自vczh的《如何手写语法分析器》。

LISP的表达式是按照前缀的形式写的,比如(1+2)*(3+4)在LISP中会写成(*(+ 1 2)(+ 3 4)),1 + 2会写成(+ 1 2)。

LISP语言的语法如下形式:

1.Operator = “+” | “-” | “*” | “/”

2.Expression = <数字> | ”(”Expression”)” | “(”Operator Expression Expression”)”

我们根据以上两条语法规则来写代码:

// LISP语法分析器
#include <iostream>
#include <string>
using namespace std; // 检测是否是空白符
bool IsBlank(char ch)
{
return ch == ' ' || ch == '\t';
} // 检测Text是否是Stream的前缀
// 如果是前缀,则返回true,并将pos前移Text.size()个字符
// 如果不是前缀,则返回false
// 此函数一开始会过滤掉Stream开头的空格
bool IsPrefix(const string& Stream, int& pos, const string& Text)
{
int read = pos; // 过滤空白符
while (IsBlank(Stream[read]))
{
++read;
}
// 不能写为:
// while (IsBlank(Stream[read++]));
// 因为这样写会导致read至少加1 if (Stream.substr(read, Text.size()) == Text) // 如果是前缀
{
pos = read + Text.size();
return true;
}
else
{
return false;
}
} // 检测Stream开头是否是操作符+、-、*、/
// 是的话,函数返回实际的操作符,并将pos便宜到操作符之后
// 否则返回0
// 判断语法1:Operator = “+” | “-” | “*” | “/”
char IsOperator(const string& Stream, int& pos)
{
if (IsPrefix(Stream, pos, "+")
|| IsPrefix(Stream, pos, "-")
|| IsPrefix(Stream, pos, "*")
|| IsPrefix(Stream, pos, "/")) // 如果开头是操作符
{
return Stream[pos - ]; // 如果是的话,pos已经向前偏移了
}
else
{
return ;
}
} // 表达式结构体
struct Expression
{
int Result; // 返回表达式结果
string Error; // 返回错误信息,没错误则为空
int Start; // 错误发生的位置 Expression() : Result(), Start() {}
}; // 检测Stream开头是否是数字,如果是,则将pos便宜到数字之后
// 函数返回Expression
// 判断语法2中的第一部分:Expression = <数字>
Expression GetNumber(const string& Stream, int& pos)
{
Expression Result; bool GotNumber = false; int read = pos; // 过滤空白符
while (IsBlank(Stream[read]))
{
++read;
} while (true)
{
// 依次读入一个字符
char ch = Stream[read];
if (ch >= '' && ch <= '')
{
Result.Result = Result.Result * + ch - '';
GotNumber = true;
++read;
}
else
{
break;
}
} if (GotNumber)
{
pos = read;
}
else
{
Result.Error = "这里需要数字";
Result.Start = read;
} return Result;
} // 检测Stream开头是否是表达式
// 如果是,则将pos前移到表达式后
// 实现语法2:Expression = <数字> | “(”Expression“)” | “(”Operator Expression Expression“)”
Expression GetExpression(const string& Stream, int& pos)
{
int read = pos; // 检测开头是否是数字
// 语法2第一部分:Expression = <数字>
Expression Result = GetNumber(Stream, read); if (!Result.Error.empty()) // 如果开头不是数字
{
if (IsPrefix(Stream, read, "(")) // 检测是否"("开头
{
// 将Result的Error清空
Result.Error.clear(); char Operator = ;
if ((Operator = IsOperator(Stream, read)) != )
// 如果是操作符,语法2第三部分:Expression = “(”Operator Expression Expression“)”
{
// 获取左参数
// 递归调用
Expression left = GetExpression(Stream, read);
if (!left.Error.empty())
{
return left;
}
// 保持当前read
int rightRead = read;
// 获取右参数
// 递归调用
Expression right = GetExpression(Stream, read);
if (!right.Error.empty())
{
return right;
} // 根据操作符Operator进行计算
switch (Operator)
{
case '+':
Result.Result = left.Result + right.Result;
break; case '-':
Result.Result = left.Result - right.Result;
break; case '*':
Result.Result = left.Result * right.Result;
break; case '/':
if (right.Result == ) // 除数为0
{
Result.Error = "除数为0";
Result.Start = rightRead;
}
else
{
Result.Result = left.Result / right.Result;
}
break; default: // 这种情况不会发生,因为前提是Operator,所以只有+、-、*、/四种情况
Result.Error = "未知的操作符";
Result.Start = read;
return Result;
}
}
else
// 如果不是操作符
// 语法2的第二部分:Expression = “(”Expression“)”
{
// 获取表达式
Result = GetExpression(Stream, read);
// 如果获取失败,则直接返回
if (!Result.Error.empty())
{
return Result;
}
} // 检测是否有配套的")"
if (!IsPrefix(Stream, read, ")"))
{
Result.Error = "此处缺少右括号";
Result.Start = read;
}
}
} // 如果没有出错,则更新pos
if (Result.Error.empty())
{
pos = read;
} // 检测是否有配套")"时,如果不存在,可以直接将Result返回
// 这样在后面就不用检测是否出错了,因为前面凡是出错的情况
// 都返回了,这样就不用检测了,而直接更新pos: pos = read return Result;
} // 测试
int main()
{
while (true)
{
string Stream;
cout << "输入一个LISP表达式" << endl;
getline(cin, Stream); int pos = ; if (IsPrefix(Stream, pos, "exit"))
{
break;
} pos = ; Expression Result = GetExpression(Stream, pos);
if (!Result.Error.empty())
{
cout << "表达式错误" << endl;
cout << "位置:" << Result.Start << endl;
cout << "错误信息:" << Result.Error << endl;
}
else
{
cout << "结果:" << Result.Result << endl;
}
} return ;
}

下面对程序代码解释如下:

         数据结构

程序中用string型的字符串Stream来存储用户输入的表达式,int型的pos 作为当前扫描的位置。

Expression结构体用来标识语法二中的Expression,Expression结构体可以标识<数字> | ”(”Expression”)” | “(”Operator Expression Expression”)”三种形式的任意一种。Result元素用来记录正确表达式时的计算结果,Error用来当语法分析表达式时遇到的错误时,将错误信息记录下来。Start用来记录错误的位置,准确来讲是用来记录最后正确的下一个位置,因为错误字符串前的空白符会被忽略。

         函数

IsBlank:用来检测char字符是否为空白符。

IsPrefix:用来检测Text字符串是否是Stream从pos起头的前缀。

IsOperator:用来检测Stream从pos起头,是否是以操作符+、-、*、/开头的。

GetNumber:用来检测Stream从pos起头,是否是以数字开头的。

GetExpression:用来检测Stream从pos开头,是否以表达式开头。

语法分析器的关键在于写出正确的语法,然后根据语法导出代码。语法定义中存在递归定义,代码中相应地也会出现递归调用。代码应该一一对应的对应到语法定义。

         设计的巧妙之处

pos和read:pos用来标识扫描的位置,pos只用来记录正确分析的位置。在IsPrefix、GetNumber、GetExpression等函数中,都会在定义一个read,初始化为pos,用来作为实际的扫描游标,只有在正确分析后,read才会用来更新pos,否则read会返回给Result的Start,用来记录错误发生的位置,并且直接返回Result。用read来代替pos的内部操作,可以防止当分析失败时,还要讲pos还原的问题。

Expression结构体:Expression结构包含三个元素,Start和Error字段可以很地记录错误发生的位置以及错误信息。这样可以很好的处理发生错误的情况。

以上是最为简单的递归下降语法分析,更多的语法分析方面的知识有待进一步学习。

语法分析器初步学习——LISP语法分析的更多相关文章

  1. C# 语法分析器(二)LR(0) 语法分析

    系列导航 (一)语法分析介绍 (二)LR(0) 语法分析 (三)LALR 语法分析 (四)二义性文法 (五)错误恢复 (六)构造语法分析器 首先,需要介绍下 LALR 语法分析的基础:LR(0) 语法 ...

  2. SQLite Lemon 语法分析器学习与使用

    本文是浙江大学出版社的<LEMON语法分析生成器(LALR 1类型)源代码情景分析>学习笔记. 用到的Windows下的编译器介绍MinGW(http://www.mingw.org/): ...

  3. 开源语法分析器--ANTLR

      序言 有的时候,我还真是怀疑过上本科时候学的那些原理课究竟是不是在浪费时间.比方学完操作系统原理之后我们并不能自己动手实现一个操作系统:学完数据库原理我们也不能弄出个像样的DBMS出来:相同,学完 ...

  4. 初步学习python

    自计算机诞生以来,也伴随着计算机语言的诞生,现在,全世界的编程语言有600多种,但流行的编程语言也就20多种. Java和C一直占据着前两名.但是近年来伴随着人工智能的发展,Python发展迅猛,以其 ...

  5. LinQ的初步学习与总结

    嘿嘿,说起来ORM和LinQ,就感觉离我好遥远的,在学校是没有学习的,所以总感觉学习了LinQ就是大神,现在嘛,终于也体会一点,感觉LinQ只是初步学习,没有太难,当然以后使用在项目中就没有这样的简单 ...

  6. LR(1)语法分析器生成器(生成Action表和Goto表)java实现(二)

    本来这次想好好写一下博客的...结果耐心有限,又想着烂尾总比断更好些.于是还是把后续代码贴上.不过后续代码是继续贴在BNF容器里面的...可能会显得有些臃肿.但目前管不了那么多了.先贴上来吧hhh.说 ...

  7. LR(1)语法分析器生成器(生成Action表和Goto表)java实现(一)

    序言 : 在看过<自己实现编译器链接器>源码之后,最近在看<编译器设计>,但感觉伪代码还是有点太浮空.没有掌握的感觉,也因为内网几乎没有LR(1)语法分析器生成器的内容,于是我 ...

  8. mysql存储过程的初步学习及案例示例

    存储过程 几个月前小编开始初步接触学习存储过程,当然是跟着大神的视频学习的,在学习的过程中自己也记录了一下笔记,如今整理一下,接下来我将从概念,优缺点以及语法和实际应用几方面为大家详细讲解一下存储过程 ...

  9. 03.从0实现一个JVM语言系列之语法分析器-Parser-03月01日更新

    从0实现JVM语言之语法分析器-Parser 相较于之前有较大更新, 老朋友们可以复盘或者针对bug留言, 我会看到之后答复您! 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个 ...

随机推荐

  1. Keil 报错汇总

    main.c(6): warning:  #1-D: last line of file ends without a newline 解决:main.c 最后一行加回车就可以了. keil中文注释出 ...

  2. yii NAV x下拉

    $menuItems[] = [ 'label' => "<img src='/images/small.jpg'>", 'url' => ['/site/ ...

  3. (转载)jenkins 安装 SVN Publisher 后向 svn 提交代码报错: E170001: Authentication required for...

    问题描写叙述 安装并启动 jenkins 后,加入了 SVN Publisher 插件,然后在构建任务的“构建后操作”操作中加入了“Publish to Subversion repository”相 ...

  4. .html() 与.text() 获取值、取值 区别

    1.html代码<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <t ...

  5. shell脚本中比较两个小数的办法

    具体情况#man bc 然而对小数进行比较的相关方法有几个: 1. 自己的解决方法,判断小数点后最多有几位数(N),然后对将要比较的两个数值进行 乘与10的N次方 也就是将小数点去掉来进行比较(小数点 ...

  6. polyfill

    [polyfill] 在JavaScript的世界里,有两个词经常被提到,shim和polyfill.它们指的都是什么,又有什么区别? 一个shim是一个库,它将一个新的API引入到一个旧的环境中,而 ...

  7. NBU 还原LINUX ORACLE RAC数据库(MIDDB)

    MIDDB集群数据库恢复 目录 MIDDB集群数据库恢复... 1 1.安装 NBUcilent 1 2.修改hosts文件... 2 3.修改hosts文件... 2 4.使用bplis读取备份文件 ...

  8. BufferedReader .BufferedWriter执行文本复制

    /** * 需求:演示 BufferedReader 和 BufferedWriter 的使用,复制一个 java 文件 */ package cn.itcast.others.iostream; i ...

  9. 七:python 对象类型详解三:列表

    一:列表简介: 1,列表可以包含任何种类的对象:数字.字符串甚至集合对象类型.列表都是可变对象,它们都支持在原处修改的操作,可以通过指定的偏移量和分片.列表方法调用.删除语句等方法来实现.关键的作用有 ...

  10. VIM常见命令

    1.删除所有数据 进入到第1行按下d,然后输入G回车,此时内容被全部清除 2.替换 :s/vivian/sky/ 替换当前行第一个 vivian 为 sky:s/vivian/sky/g 替换当前行所 ...