MUA是ZJU编程语言原理(Principle of Programming Language)课程中用到的编程语言。本项目要做的工作是使用JAVA语言完成一个MUA语言的解释器,通过命令行与用户进行交互,解析并运行用户输入的代码。
本项目仅能够针对课程的测试点实现基本要求,但还有很多部分需要完善(例如在遇见具有语法错误的代码时,不能进行解析,而是整个程序会报错,函数的参数数量有限制,save和load的实际操作,etc.),以及有很多部分可以优化(例如各class的构造,其实有一些可以去掉),会在后续进行更新。
以下是对于该语言基本语法以及解析器实现方式的说明。
基本语法
首先要注意的是,MUA是一个解释型语言,但是和Python不同,MUA语言中,空格、tab、换行等不能标记一个语句的结束,或者分支情况。
基本数据类型value
数字number,字word,表list,布尔bool
- 数字的字面量以[0~9]或’-‘开头,不区分整数,浮点数
- 字的字面量以双引号
"
开头,不含空格,采用Unicode编码。在"
后的任何内容,直到空格(包括空格、tab和回车)为止的字符(不含空格),都是这个字的一部分,包括其中可能有的"
和[]
等符号 - 表的字面量量以方括号
[]
包含,其中的元素以空格分隔;元素可是任意类型;元素类型可不一致;- 表的第一个元素和
[
之间,以及最后一个元素和]
之间不需要由空格分隔 - 表中的字不需要
"
引导 - 这是一个有三层表的字面量的例子:
[a [b [c d] e]]
- 表的第一个元素和
- 布尔量只有两个值:
true
和false
- 数字和布尔量在计算时可以被看作是字的特殊形式,即在字面量和变量中的字,当其中的内容是数字或布尔量时,总是可以根据需要自动被转换成数字或布尔量
名字name
一个以字母开头,只含有字母和数字及下划线的字,可以用做名字,名字区分大小写。
函数名和变量名都算是name
word只有在作为name使用时需要加"
(例如在make
、save
、print
、isname
以及字表操作等等),其他时候(比如在list中,调用函数时,print里面的输出等等)不需要用"
。
基本操作operation
基本形式:操作名 参数
操作名是一个名字,与参数间以空格分隔。参数可以有多个,多个参数间以空格分隔。每个操作所需的参数数量是确定的,所以不需要括号或语句结束符号。有的操作有返回值,有的没有。
一个程序就是操作的序列。
基本操作有:
//
:注释,从//到行末均为注释make <name> <value>
: 将value绑定到name上,绑定后的名字位于命名空间。此文档中的基本操作的名字不能重新命名make “b “a –> thing “b == a, thing :b == value of a
thing <name>
:返回word所绑定的值:<name>
:与thing相同:a == thing “a
erase <name>
:清除word所绑定的值isname <word>
:返回word是否是一个名字,true/falseisname “a
print <value>
:输出valueprint “a –> a
print 1 –> 1
print true –> true
print :a –> 6 (如果前面make “a 6)
print [a b c] –> a b cread
:返回一个从标准输入读取的数字或字运算符operator
add
,sub
,mul
,div
,mod
:<operator> <number> <number>
eq
,gt
,lt
:<operator> <number|word> <number|word>
and
,or
:<operator> <bool> <bool>
not
:not <bool>
readlist
:返回一个从标准输入读取的一行,构成一个表,行中每个以空格分隔的部分是list的一个元素- 用
readlist
读入的只可能是单层的表
- 用
repeat <number> <list>
:运行list中的代码number次
函数定义和调用
定义
make <name> [<list1> <list2>]
,其中
- name为函数名
- list1为参数表
- list2为操作表
以下为函数定义的例子:
1 | make "prt [ |
调用
<functionName> <arglist>
,其中:
<functionName>
为make中定义的函数名,不需要双引号"
<arglist>
是参数表<arglist>
中的值和函数定义时的<list1>
中名字进行一一对应绑定
以下为函数调用的例子:prt "hello
本地变量
在函数中访问(读取)变量的值的时候,首先访问本地,如果本地不存在,则访问全局
在函数中做
make
时,永远只写本地:- 检查本函数内是否存在这个名字,如果存在,则对已有的变量赋值;
- 否,则在本地定义⼀个新的变量
函数相关的操作
output <value>
:设定value为返回给调用者的值,但是不停止执行stop
:停止执行export
:将本地make的值输出到全局- 如果全局没有这个变量,则增加⼀个全局变量
- 如果全局已经有了同名的变量,则替换全部变量的值
表达式计算
允许使用以下运算符对数字进行计算:+-*/%()
为了方便便识别,要求表达式的外⾯面必须有括号()
包围。
支持负数
(5+-2) –> 3.0
判断
if <bool> <list1> <list2>
:如果bool为真,则执行list1,否则执行list2。list均可以为空表isnumber <value>
:返回value是否是数字isword <value>
:返回value是否是字islist <value>
:返回value是否是表isbool <value>
:返回value是否是布尔量isempty <word|list>
: 返回word或list是否是空
数值计算
random <number>
:返回[0,number)的一个随机数sqrt <number>
:返回number的平方根int <number>
: floor the int
字表处理
word <word> <word|number|bool>
:将两个word合并为一个word,第二个值可以是word、number或boolsentence <value1> <value2>
:将value1和value2合并成一个表,两个值的元素并列,value1的在value2的前面返回结果为list
sentence “a “b -> a b
sentence [a] [b] -> a b
sentence “a [b] -> a b
sentence [a [b c] d] [x y] -> a [b c] d x ylist <value1> <value2>
:将两个值合并为一个表,如果值为表,则不打开这个表list “a “b -> a b
list [a] [b] -> [a] [b]join <list> <value>
:将value作为list的最后一个元素加入到list中(如果value是表,则整个value成为表的最后一个元素)join [a b] “c -> a b c
join [a b] [c] -> a b [c]
join [a b] first [c] -> a b cfirst <word|list>
:返回word的第一个字符,或list的第一个元素first “abc -> a (word)
first [a b c] -> a (word)
first [[a] b] -> a (list)
first “123 -> 1 (word)
islist first [] -> true
isempty first [] -> truelast <word|list>
:返回word的最后一个字符,list的最后一个元素butfirst <word|list>
:返回除第一个元素外剩下的表,或除第一个字符外剩下的字注意返回是list
islist butfirst [a b c] -> true
islist butfirst [a b] -> truebutlast <word|list>
:返回除最后一个元素外剩下的表,或除最后一个字符外剩下的字
其他操作
wait <number>
:等待number个mssave <word>
:保存当前命名空间在word文件中save “a.mua
load <word>
:从word文件中装载内容,加入当前命名空间erall
:清除当前命名空间的全部内容poall
:列出当前命名空间的全部名字
既有名字
系统提供了一些常⽤用的量量,或可以由其他操作实现但是常用的操作,作为固有的名字。这些名字是可以被删除(erase)的。
pi
:3.14159run <list>
:运行list中的代码
具体实现
整体结构
项目中创建的class介绍如下:
command
MUA语句中的每个单词都是一个command
,包含number
、bool
、list
、word
、operator
、expression
、file
几个类别,具有名字、类别、值(针对number、bool、list、expression,根据类别不同有不同类型的值)几个属性(其中list会被看成一个整体)。
例如语句 if eq thing “a 6 [print :a] [print 0]
name type value if operation / eq operation / thing operation / “a word / 6 number 6 (double) [print :a] list [print :a] (String) [print 0] list [print 0] (String)
command
的构造可以有三种方式,构造一个空的command、提供name和type(在构造时根据name判断并填充value)、提供name(构造时根据name判断并填充type和value),具体实现如下:
1 | command(){} |
其中analyzeType
是用于根据name判断command类型的方法,setCommandValue是根据name和type设置value的方法。这两个方法的实现如下:
1 | public void analyzeType(String name) { |
1 | public void setCommandValue(String type) { |
commandStack
输入的MUA语句会被作为一个commandStack
类存储起来,顾名思义,commandStack
是一个由很多command
组成的stack,通过Vector
来组织。例如if eq thing "a 6 [print :a] [print 0]
将会被分为七个command
,存储在一个commandStack
里。
commandStack
的构造包括空白构造、根据一个MUA语句进行构造两种方式,实现如下:
1 | commandStack(){} |
commandStack
的方法仿照Vector
,包含get(index)
、setElementAt(newCommand, index)
、insertElementAt(newCommand, index)
、removeElementAt(index)
、subStack(begin, end)
、subStack(begin)
,便于操作。
variable
变量。每个被make
过的word
都会被作为一个变量存储起来,包含number
、bool
、word
、list
几个类别,具有名字、类别、值(不同类别有不同类型的值)几个属性。每一种类别的变量都有一个单独的构造函数,此外还有拷贝构造,以及未赋值的变量构造。
1 | //empty variable |
有getValue()
、chageValue(newValue)
方法,可以用于获取和改变变量的值
varStack
varStack
即 variable
组成的 stack,存储当前命名空间下的所有变量,整个程序会有一个全局的varStack
,每个函数都有其对应的局部的varStack
,使用Vector
来管理。
含有isname
方法,用于判断当前变量空间是否含有对应名字的变量。
含有往varStack
中添加variable
的方法,有add(variable)
、add(name)
、add(name, value)
几种方式,要注意的是,如果在栈中有与新添加的变量同名的变量,则进行覆盖。
可以根据name来移除变量(remove(name)
)、获取变量的值(getValue(name)
)、查找对应变量(find(name)
)。
此外,包含仿照Vector
的方法,size()
removeElementAt(index)
。
function
函数。包含名字、变量列表、操作列表(分别用stack来存当前函数对应的variable
和commandStack
,每一句操作都是一个commandStack)
除了空白构造和拷贝构造之外,可以通过提供名字、变量列表、操作列表来构造,其中,变量列表和操作列表以String
输入,在构造时存储在对应stack中,通过Vector
来管理
funcStack
funcStack
即function
组成的 stack,存储所有的函数
exprMember
表达式中的每个成员,包含名字、类型(number/operator)、值(针对number)三个属性
expression
用stack存储一整句表达式,可以通过操作stack来进行运算。
要注意的是,-
可能有减法和负数两种含义,要在构造时进行判断,如果为减法,exprMember
的类型将是operator
,如果是负数,则与其后面的数字一起组成新的数值,类型是number
。
将一整句表达式拆开并存储为由exprMember
组成的stack的构造如下
1 | expression(String expr){ |
整体实现
整体的实现通过栈进行,分析commandStack
中各command
的属性,对其中的operator
进行对应操作,取出operator
以及对应的操作数,操作完之后把结果放回栈中。
预处理
为方便后续处理,在通过MUA语句构造commandStack
时,将所有的:
都替换成thing "
。
读入MUA语句
读入时跳过所有注释
读入时要注意的是判断是否读完一整句话,如果没有读完(list或者expression,或者一句话的操作数不足),需要继续读下一行,拼起来作为完整的语句。
运行
由于可能会出现多个语句在同一行的情况,因此只能从前往后运行。
判断当前operator的操作数是否被运算完全,即后面的对应数量的command
是否都不是operator
类型。如果都不是,说明一个语句已完全,则运行当前operator
,否则,递归判断遇到的下一个operator
。
函数运行
函数运行时,先按照变量列表,使用传递给函数的参数对函数变量空间的对应变量进行值改变,然后依次运行操作列表中的操作。通过依次调用funcStack
和function
中负责运行函数的方法进行运行。
需要注意的是:
如果在操纵列表中出现
stop
,直接返回。为了实现递归操作,在运行函数时使用拷贝构造函数,构造一个完全相同的新函数再运行,而非直接操作原来的函数。
实现如下:
1 | if(func.isFunc("\"" + oper_command.get(begin_index).getCommandName())) { |
funcStack
中的方法:
1 | public String runFunc(String funcName, commandStack funcVarStack) throws IOException { |
function
中的方法:
1 | public String run(commandStack funcVarStack) throws IOException { |
表达式运算
为方便起见,在表达式运算中与commandStack
中的处理相反,将所有thing "
都替换成:
来操作。
表达式运算的几个特殊情况:
- 处理内部含有
()
的表达式,通过递归进行 - 含有变量的表达式,检测到下一个字符是
+-*/%
中的一个,或者到达表达式结尾,则说明变量名结束。根据检测到的变量名,在变量空间中用对应的数值来替代 - 含有函数的表达式,检测函数名的方式与检测变量名类似。先运行对应函数,然后再将运算得到的值替代表达式中的函数名,运算表达式。
实现如下:
1 | public static String manageExpression(String expression, varStack vars) throws IOException { |
其中calculate()
是expression
类中的方法,原理是通过expression
中构造的栈进行运算,按顺序取operator
,同时取出它两边的number
作为operand进行运算,然后将新的值放回到栈中。由于表达式中只有+-*/%
几种运算符,就根据优先级顺序,先处理*/%
,再处理+-
。
基本操作
make <name> <value>
或make <name> [<list1> <list2>]
如果定义的是函数,创建新的函数并加入函数栈中。如果定义的是变量,创建新的变量并加入当前变量空间的变量栈中。
1 | case "make": |
thing <name>
从变量栈中取出对应变量的值并取代
1 | case "thing": |
erase <name>
从变量栈中删掉对应名字的变量
1 | case "erase": |
isname <word>
在变量栈和函数栈中都进行查找,如果当前word存在于变量栈或函数栈中,则为name
1 | case "isname": |
print <value>
输出,要注意不同格式的输出
1 | case "print": |
read
返回从标准输入读取的数字或字
1 | case "read": |
readlist
读入单层的表
这里好像没做对,后续更新
1 | case "readlist": |
repeat <number> <list>
将<list>
中的内容作为一个新的commandStack
,进行循环操作
1 | case "repeat": |
算数运算
包括add、sub、mul、div、mod,以add为例
1 | case "add": |
大小比较
包括eq、gt、lt,以eq为例
1 | case "eq": |
逻辑运算
包括and、or、not,以and为例
1 | case "and": |
if <bool> <list1> <list2>
根据条件是true
或者false
判断运行哪个<list>
,将对应<list>
中的操作作为一个新的commandStack
进行运行
1 | case "if": |
run <list>
同if
和repeat
中的做法,将<list>
视作新的commandStack
进行运行
1 | case "run": |
判断
包括isnumber
, isword
, islist
, isbool
, isempty
,即判断下一个command
的类别即可,以isnumber
为例
1 | case "isnumber": |
字表合并
包括word
, sentence
, list
, join
,将后面两个字/表合并为新的表,以word
为例
1 | case "word": |
字表提取
first
, butfirst
, last
, butlast
,提取出对应位置的字/表,按照要求进行处理之后放回栈。以first
和butfirst
为例
1 | case "first": |
1 | case "butfirst": |
save <word>
用了取巧的方式,只保存当前空间的变量和函数,而不是将所有代码都保存下来。可能要改。
将变量和函数变成对应make
格式的MUA语句进行输出和保存。
1 | case "save": |
load <word>
读取对应文件,依次运行其中的语句
1 | case "load": |
erall
清空变量栈和函数栈
1 | case "erall": |
poall
列出变量栈和函数栈的全部内容
1 | case "poall": |
完整代码请见github,欢迎star :)