任务要求
用解释器接受用绘图语言编写的源程序,经过语法和语义分析之后,将源程序所规定的图形绘制出来。具体任务可以划分为3个主要阶段:词法分析、语法分析和语义分析。
解释器的具体实现要求采用以下两种不同的方案:
(1)利用某种程序设计语言(如C/C++、Java等)手工编写完整的解释器;
(2)利用编译器的自动生成工具LEX和YACC分别编写词法和语法分析程序。
两种方案的语义部分基本相同,主要区别在于词法和语法分析器的构造是手工完成还是借助于工具完成。
能够将编译原理课程中所学的基本理论和技术融会贯通,从编译器构造者的角度去思考和构造复杂系统;能够选择与使用恰当的计算机系统的应用环境与开发工具,综合运用所学的编译理论解决相关的实际问题。
功能展示
输入:
注:这是一行注释
批:这也是一行注释
注:先画一个圆
息赋【一百又五和负又五,一百一十差十】。
旋赋【彀商四】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
注:四周画椭圆
基赋【一百,零】。
息赋【一百五十,七十五】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
基赋【零,一百】。
息赋【七十五,一百五十】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
基赋【负一百,零】。
息赋【一百五十,七十五】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
基赋【零,负一百】。
息赋【七十五,一百五十】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
绘图结果:
实现思路
自行规定一种基于中文参数方程的高级绘图语言——“CCC语言”。这种语言是基于中文的规则与数学的运算进行绘图程序编程的。编写相关程序,将这个高级语言转换成Python代码。这里的Python代码可以视作一种实现绘图的机器语言。经过由高级语言的编译过程生成“机器语言”,最终就可以实现画图的目的。
本次任务中,选择Python中一个简单的绘图库——turtle——来作为目标语言。turtle库的基本操作是:在一个横轴为x、纵轴为y的坐标平面中,以原点(0,0)位置为开始点,根据一组函数指令的控制来移动,从而在它爬行的路径上绘制图形。
本次任务的一大难点是确定每次循环绘图下一个坐标的值,利用参数方程及三角函数公式成功推导出了这个结果。在报告的“4.3 基于参数方程的绘图语言生成”一节做了详细的数学推导与验证。
1 描述绘图语言定义
1.1 EBNF表示的词法规则与语法规则描述
程序 -> {语句 。}
语句 -> {原点设置语句|坐标缩放语句|画布旋转语句|循环绘图语句|注释批注语句}
原点设置语句 -> 基 赋 【 表达式 , 表达式 】
坐标缩放语句 -> 息 赋 【 表达式 , 表达式 】
画布旋转语句 -> 旋 赋 表达式
循环绘图语句 -> 走 变量 出 表达式 至 表达式 步 表达式 绘 【 表达式 , 表达式 】
注释批注语句 -> (注 | 批) 字符串
表达式 -> 项 {(和 | 差) 项}
项 -> 因子{(积 | 商) 因子}
因子 -> (和 | 差)因子 | 组件
组件 -> 原子 幂
原子 -> 常量 | 变量 | 函数【表达式】 | 【表达式】
1.2 语义规则的非形式化描述
语义规则被严格限制在以下五种类型中:
循环绘图(走-绘)
比例设置(息,默认(1, 1))
角度旋转(旋,默认0)
坐标平移(基,默认(0, 0))
注释(注或批)
另外,存在默认的配置与规则如下:
左上角为原点
x方向从左向右增长
y方向从上到下增长
1.3 绘图语言源程序示例
(1)正确的例子
注:先画一个圆
息赋【一百又五和负又五,一百一十差十】。
旋赋【彀商四】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
注:画一个椭圆
基赋【一百,零】。
息赋【一百五十,七十五】。
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】】。
(2)错误的例子
注:出现不合法单词
基赋【一百,100】。
注:表达式缺失
息赋【一百又五和负又五】。
注:括号不匹配
走甲出零至【二积彀】步【彀商五十】绘【余【甲】,正【甲】。
2 词法分析
2.1 主要的数据结构
定义了Token类,用于存放每个Token的类型,值,函数等属性。其定义如下:
class Token:
def __init__(self, t_type, t_value, t_function):
self.t_type = t_type
self.t_value = t_value
self.t_function = t_function
在本任务中,共定义了以下七类Token,分别是:
常量——数值字面量和标识符形式的常量名
变量——即参数名称
函数——供调用的内置函数
保留字——语句中固定含义的标识符
运算符——主要为四则运算符
分隔符——供函数调用或者改变运算次序
数字组成——汉字表示数字的组成部分
2.2 描述单词的整体状态图
中文编程相较于英文编程而言具有一大优势——言简意赅。体现到CCC语言中,就是可以使用单一的汉字规定绘图动作。这样就可以保证除了变量和数字之外,所有Token的编码是等长的。
描述单词的整体状态图如图 1 描述单词的整体状态图所示。
2.3 手工编写的词法分析程序的算法流程图
词法分析的过程可以分为两部分:读入与分析。读入由Scanner类完成,分析由Lexer类完成。
读入的算法流程图如图 2 读入的算法流程图所示。
分析的算法流程图如图 3 分析的算法流程图所示。
其中,将汉字表示的数字转换成float类型的算法流程图如图 4 数字类型转换算法流程图所示。
3 语法分析
3.1 主要的数据结构
定义了Parser类用于实现语法分析的操作。Parser类中包含一个Lexer类的字段,因为语法分析的工作需要在词法分析的基础上进行。
Parser类的定义如下:
class Parser:
def __init__(self):
self.scanner = lexer.Scanner(FILENAME)
self.dataflow = self.scanner.analyse()
self.pos = 0
self.length = len(self.dataflow)
在语法分析过程中,需要生成表达式的语法树。而Python中的ast库可以自动生成表达式的语法树,其相关代码如下:
def print_syntax_tree(self, s):
print("表达式入口")
s = s.replace('【', '(').replace('】', ')').replace(',', ',')
print(astunparse.dump(ast.parse(s, filename='<unknown>')))
print("表达式出口")
3.2 算法流程图
4 语义分析
4.1 主要的数据结构
在本项目中,语义分析部分实际上也承担了中间代码生成、代码优化的工作。在分析语义的同时,生成包含原点偏移量、坐标放缩量、旋转角度、循环绘图值这四组参数的信息。因此定义一个继承了Parser类的Checker类,经过一系列处理后生成携带以上信息的对象。
Checker类的定义如下:
class Checker(parser.Parser):
def __init__(self):
super().__init__()
self.s_origin = [0.0, 0.0]
self.s_scale = [1.0, 1.0]
self.s_rot = 0.0
self.s_for = [0.0, 0.0, 0.0]
self.s_draw = ['', '']
self.variable = ''
self.intermediate_code = []
因为最终的目标是将CCC语言转换成Python代码,因此还需要编写一个Compiler类。其通过文件写入的形式,读取Checker类中包含的四组参数值,生成一个可执行的绘图.py文件。
Compiler类的定义如下:
class Compiler:
def __init__(self):
self.checker = checker.Checker()
self.intermediate_code = self.checker.analyze()
4.2 算法流程图
4.3 基于参数方程的绘图语言生成
由于turtle库是一个描记轨迹的过程,因此程序只需要关心下一个点的坐标,然后调用turtle.goto(x, y)语句绘制轨迹即可。为了将前面的原点偏移、坐标轴放缩、旋转角度、循环作图的四组参数统一起来,引入参数方程的概念:
一般地,在平面直角坐标系中,如果曲线上任意一点的坐标x、y都是某个变数t的函数,并且对于t的每一个允许的取值,由方程组确定的点(x, y)都在这条曲线上,那么这个方程就叫做曲线的参数方程,联系变数x、y的变数t叫做参数。
已知目前接收到的变量有四组,分别为:origin(x, y)、scale(x, y)、rot、for(begin, end, step, tx, ty)。其中origin中的xy分别表示横轴、纵轴的偏移量;scale中的xy分别表示横轴、纵轴的放缩量;rot表示旋转的角度(弧度制);for中的begin、end、step分别代表循环作图的起始位置、终止位置和步长(iterator为t),而tx和ty则表示横纵坐标方向上关于自变量t的参数方程。
用lx、ly分别表示横纵坐标放缩后的单位长度,可得:
lx=tx*scale(x)
ly=ty*scale(y)
将放缩后的单位长度作为新的单位,计算旋转与偏移后的横纵坐标。由三角函数的和差公式化简可得:
x=lx*cosrot+ly*sinrot+origin(x)
y=ly*cosrot+lx*sinrot+origin(y)
即得到每次循环中,下一个目标点的横纵坐标(x, y)。然后调用turtle.goto(x, y)语句绘制轨迹即可绘制一条直线。如果步长足够小,那么就可以在视觉上形成平滑曲线。
5 完整代码
下载地址(免费,但拜托点个Star)
BFU-CS/CourseDesign/PrinciplesOfCompilation at main · CCCP-lus/BFU-CS (github.com)