前言
okey,前面我们实现了,内置的数据结构,函数,变量。所以的话,我们是时候来实现我们的默认方法了。例如Python当中的默认函数:print。这里我们非常庆幸,我们使用的是Python来实现这个,因为在底层,我们就直接使用Python的print实现就好了,哈哈。如果是使用C++ 来实现的话,虽然也是类似的,但是的话,里面的各种变量处理令人头更大。说到这个,又不得不人我想起了,先前在查看《30天手写操作系统》这本书,并进行实践的时候。用汇编来为C语言的输出函数提供lib,那可真让人难受(于是这个项目,我选择搁置,后来是打算做算法开放平台,里面涉及到的资源调度思想直接照搬,只是打算在负载均衡上面加上智能算法策略,通过PSO,去自适应处理。可惜实现难度太大,我根本无法在兼顾考研的同时来完成它,至少在秋招之前,不可能完成,并且比较深入,讨论起来比较麻烦)
okey,那么我们开始吧,到这里的话,我们的这个小语言应该就算是结束了,之后的话,我们就可以happy了,当然,当然,我们还需要去讨论它的语法,这一点非常重要,别忘了,我们这个项目一开始的目标:
适合小朋友使用的编程语言,并且支持中文编程
。所以在这里的话,我想得好好想想用上面样的方式来处理。不过这不是本节需要关系的内容。在这里我们,需要关系的是如何实现内置的函数,同时实现换行操作,换一句话说,就是支持文本编写。毕竟在终端编写代码属实有点费劲。
那么在这里的话,我们实际上要做的其实是比较完整的实现了。当然这里没有对象什么的操作,反正给小孩子用的。
先来看到我们的实现吧:
在这里写了一个文件:
换行实现(终端)
ok,在这里的话,我们来先实现换行,这个换行和我们先前可不一样,先前我们执行的时候 ,是在终端执行,然后的话,每一次执行的时候,其实都是构造新的语法树,然后执行解释,只不过,先前我们在使用全局变量,来进行维护变量和这个函数罢了。所以看起来好像具备上下文一样。当然这里的话,我们实现的只是终端换行。啥意思呢,就像这样:
那么这个时候你可能要问了,为啥在终端要这样处理,加上;但是在我们的这个文件里面就不用,原因的话很简单:(其实你要加上也可以,python就是这样的)
我们是一次性把所有的文本读入进去了,所有不需要。但是在终端不一样。
okey,废话不多说了,发车了。
词法解析
老规矩,先定义Token类型
在这里我们只是多了这个,然后的话,我们需要解析实现我们的词法。
这里的话,只有一个字符所以,直接这样处理即可。
语法解析
之后的话,自然就是我们的语法解析了,这个很重要。
那么在这里的话,我们来看到我们最终版本的一个语法:
statements : NEWLINE* statement (NEWLINE+ statement)* NEWLINE*
statement : KEYWORD:RETURN expr?
: KEYWORD:CONTINUE
: KEYWORD:BREAK
: expr
expr : KEYWORD:VAR IDENTIFIER EQ expr
: comp-expr ((KEYWORD:AND|KEYWORD:OR) comp-expr)*
comp-expr : NOT comp-expr
: arith-expr ((EE|LT|GT|LTE|GTE) arith-expr)*
arith-expr : term ((PLUS|MINUS) term)*
term : factor ((MUL|DIV) factor)*
factor : (PLUS|MINUS) factor
: power
power : call (POW factor)*
call : atom (LPAREN (expr (COMMA expr)*)? RPAREN)?
atom : INT|FLOAT|STRING|IDENTIFIER
: LPAREN expr RPAREN
: list-expr
: if-expr
: for-expr
: while-expr
: func-def
list-expr : LSQUARE (expr (COMMA expr)*)? RSQUARE
if-expr : KEYWORD:IF expr KEYWORD:THEN
(statement if-expr-b|if-expr-c?)
| (NEWLINE statements KEYWORD:END|if-expr-b|if-expr-c)
if-expr-b : KEYWORD:ELIF expr KEYWORD:THEN
(statement if-expr-b|if-expr-c?)
| (NEWLINE statements KEYWORD:END|if-expr-b|if-expr-c)
if-expr-c : KEYWORD:ELSE
statement
| (NEWLINE statements KEYWORD:END)
for-expr : KEYWORD:FOR IDENTIFIER EQ expr KEYWORD:TO expr
(KEYWORD:STEP expr)? KEYWORD:THEN
statement
| (NEWLINE statements KEYWORD:END)
while-expr : KEYWORD:WHILE expr KEYWORD:THEN
statement
| (NEWLINE statements KEYWORD:END)
func-def : KEYWORD:FUN IDENTIFIER?
LPAREN (IDENTIFIER (COMMA IDENTIFIER)*)? RPAREN
(ARROW expr)
| (NEWLINE statements KEYWORD:END)
statements
:表示一组语句,可以包含多个statement
,以及通过NEWLINE
分隔的多个语句。statement
:表示一个语句,可以是返回语句(以KEYWORD:RETURN
开头,后面可以跟一个可选的表达式)、继续语句(KEYWORD:CONTINUE
)、中断语句(KEYWORD:BREAK
)或者表达式。expr
:表示一个表达式,可以是变量赋值表达式(以KEYWORD:VAR
开头,后面跟标识符和EQ
,再后面是另一个表达式)、比较表达式(可以包含等于、小于、大于等比较运算符)或逻辑表达式(使用KEYWORD:AND
或KEYWORD:OR
)。comp-expr
:表示一个比较表达式,可以是取反操作符NOT
接着是一个比较表达式,或者是算术表达式跟上一个或多个比较操作符。arith-expr
:表示一个算术表达式,可以是项跟上一个或多个加号或减号。term
:表示一个项,可以是因子跟上一个或多个乘号或除号。factor
:表示一个因子,可以是正号或负号接着是另一个因子,或者是power
。power
:表示一个幂表达式,可以是call
跟上一个或多个幂操作符。call
:表示一个函数调用,可以是原子表达式(整数、浮点数、字符串或标识符)或者是原子表达式后面跟上括号和参数列表。atom
:表示一个原子表达式,可以是整数、浮点数、字符串、标识符、括号中的表达式、列表表达式、if表达式、for表达式、while表达式或函数定义。list-expr
:表示一个列表表达式,可以是一对方括号中的表达式列表。if-expr
:表示一个if条件语句,可以是在KEYWORD:IF
和KEYWORD:THEN
之间跟上一个表达式和一个或多个语句。还可以包含KEYWORD:ELIF
和KEYWORD:ELSE
分支。if-expr-b
:表示elif分支,可以是在KEYWORD:ELIF
和KEYWORD:THEN
之间跟上一个表达式和一个或多个语句。if-expr-c
:表示else分支,可以是KEYWORD:ELSE
跟上一个语句。for-expr
:表示一个for循环语句,可以是在KEYWORD:FOR
和KEYWORD:THEN
之间跟上一个标识符、一个表达式和一个或多个语句。还可以包含可选的KEYWORD:STEP
子句。while-expr
:表示一个while循环语句,可以是在KEYWORD:WHILE
和KEYWORD:THEN
之间跟上一个表达式和一个语句。func-def
:表示一个函数定义,可以是在KEYWORD:FUN
后面跟上一个可选的标识符、参数列表和一个可选的返回值表达式。okey,描述完毕这个之后的话,我们就可以干活了,这里的话我们多了这个statements
当然这个是一个抽象的概念
因为解析的时候,我们从这里进入:
这个时候它的概念和我们先前做基本的数学运算节点的时候是一样的。
可以这样理解,这个玩意是在我们的exper的基础上再进行抽象。
里面具体的内容的话,我们下面再说,因为我们还有关键字要实现,和这个关联。
解释器执行
啥也不用动,因为这个玩意是抽象的概念,我们的执行器,只是从入口根节点开始干活,你这个只是抽象的节点,只是为了划分子树的层级做的操作而已,换一句话说,这个玩意只是改变了AST树的结构而已。对于节点的类型没有改动。
return break continue 实现
之后的话,我们还有三个关键字的处理,那就是我们的return,还有break,以及continue。函数调用,没有返回值可还行,循环执行,没有停止可还行。所以的话,我们这里还需要有这个。
那么这个怎么做的,老规矩是要解析到token,但是这个的话,我们先前已经有了。然后的话,三个节点,显然是有具体含义的,所以的话,我们显然需要去定义好我们的解析节点
解析节点
class ReturnNode:
def __init__(self, node_to_return, pos_start, pos_end):
self.node_to_return = node_to_return
self.pos_start = pos_start
self.pos_end = pos_end
class ContinueNode:
def __init__(self, pos_start, pos_end):
self.pos_start = pos_start
self.pos_end = pos_end
class BreakNode:
def __init__(self, pos_start, pos_end):
self.pos_start = pos_start
self.pos_end = pos_end
这下我想应该不用给注释了吧,其实这个家伙就是一个标记。
解析实现
okey,接下来的话,我们要开始解析了。这个时候的话我们要明确的是,这个玩意出现的位置不是单独某一个里面的,什么意思,就比如我们先前的函数 -> 这个符号肯定是和我们的函数有关,但是的话,这个return你可以放在函数里面,也可以放在函数的循环里面。
然后的话,显然这个会出现在我们的statement里面,所以的话,我们在这里先看到我们的这个实现:
def statement(self):
res = ParseResult()
pos_start = self.current_tok.pos_start.copy()
if self.current_tok.matches(TT_KEYWORD, 'return'):
res.register_advancement()
self.advance()
expr = res.try_register(self.expr())
if not expr:
self.reverse(res.to_reverse_count)
return res.success(ReturnNode(expr, pos_start, self.current_tok.pos_start.copy()))
if self.current_tok.matches(TT_KEYWORD, 'continue'):
res.register_advancement()
self.advance()
return res.success(ContinueNode(pos_start, self.current_tok.pos_start.copy()))
if self.current_tok.matches(TT_KEYWORD, 'break'):
res.register_advancement()
self.advance()
return res.success(BreakNode(pos_start, self.current_tok.pos_start.copy()))
expr = res.register(self.expr())
if res.error:
return res.failure(InvalidSyntaxError(
self.current_tok.pos_start, self.current_tok.pos_end,
"Expected 'return', 'continue', 'break', 'var', 'if', 'for', 'while', 'fun', int, float, identifier, '+', '-', '(', '[' or 'not'"
))
return res.success(expr)
这个玩意的实现,就是把这三个类型的节点加上到语法树里面。
之后的话我们来看到这个函数:
不要看到这个函数挺长的没有注释,其实这个家伙就只是做了这个事情:
就是检查换行表识(下一个语句的表识)
解释器实现
之后的话,就是我们的解释器了,这里看到这个:
那么首先的话,在RTResult这个类里面,新加了这个方法:
def success_continue(self):
self.reset()
self.loop_should_continue = True
return self
def success_break(self):
self.reset()
self.loop_should_break = True
return self
def should_return(self):
return (
self.error or
self.func_return_value or
self.loop_should_continue or
self.loop_should_break
)
然后在执行的时候
把,我们的返回节点(要返回的值的节点的值拿到,然后返回)
然后continue和break的话,直接处理。
内置函数的实现
其实这个内置函数的话,和我们的用户自己定义的函数区别其实不大,只是内置的函数是直接调用Python的可以。
在这里面我们内置了这些函数:
BaseFunction
这里的话,把先前的这个(上一篇博文)的Function类进行了拆分。
检查参数是否合理等等基本的东西。
class BaseFunction(Value):
def __init__(self, name):
super().__init__()
self.name = name or ""
def generate_new_context(self):
new_context = Context(self.name, self.context, self.pos_start)
new_context.symbol_table = SymbolTable(new_context.parent.symbol_table)
return new_context
def check_args(self, arg_names, args):
res = RTResult()
if len(args) > len(arg_names):
return res.failure(RTError(
self.pos_start, self.pos_end,
f"{len(args) - len(arg_names)} too many args passed into {self}",
self.context
))
if len(args) < len(arg_names):
return res.failure(RTError(
self.pos_start, self.pos_end,
f"{len(arg_names) - len(args)} too few args passed into {self}",
self.context
))
return res.success(None)
def populate_args(self, arg_names, args, exec_ctx):
for i in range(len(args)):
arg_name = arg_names[i]
arg_value = args[i]
arg_value.set_context(exec_ctx)
exec_ctx.symbol_table.set(arg_name, arg_value)
def check_and_populate_args(self, arg_names, args, exec_ctx):
res = RTResult()
res.register(self.check_args(arg_names, args))
if res.should_return(): return res
self.populate_args(arg_names, args, exec_ctx)
return res.success(None)
Function
然后我们的这个Function继承这个家伙,这个的话其实就是用户自己定义的函数。
BuiltInFunction
之后的话,就是我们的内置函数了,为啥叫这个名字,其实我也不知道,不过在python里面C语言写的函数就是built函数。
在这里的话,主要说说两个内置函数的实现,因为其他的都是类似的。
append的实现
我们先来看到这个append的实现,我们先来看到这个:
这个的话,是我们的核心函数(其实这个是一个代理,对比我们先前写的Java框架的话)
这个注释说清楚了,我们就直接看到这个append的处理方法:
def execute_append(self, exec_ctx):
list_ = exec_ctx.symbol_table.get("list")
value = exec_ctx.symbol_table.get("value")
if not isinstance(list_, List):
return RTResult().failure(RTError(
self.pos_start, self.pos_end,
"First argument must be list",
exec_ctx
))
list_.elements.append(value)
return RTResult().success(Number.null)
这里的话,我们的参数在传递过来的时候,也就是exectue这个家伙拿过来的args,得到的是我们这里定义的类型,List类型和 value类型。然后封装到了args里面,这个是一个列表类型的:
因为在解析器里面执行函数节点的时候,会把这个参数进行封装为List类型的(这个是Python当中的)
然后在执行这段代码的时候,我们会把:
当前函数执行的上下文给到执行上下文当中。
具体的如何给到就是执行这个代码:
运行结果之后就是:
拿到了这个:
假设我们此时执行的代码是这样的:
var a=[1]
append(a,1)
由于这个是变量,这个变量的话此时也是在我们的全局的content里面的,然后Python当中这个是引用类型,所以直接调用appen(python)中的方法:
当然由于封装了一下,elements才是python当中的list.
run_file实现
这个家伙的话,就是我们的这个执行文件的函数实现了。
def execute_run(self, exec_ctx):
fn = exec_ctx.symbol_table.get("fn")
if not isinstance(fn, String):
return RTResult().failure(RTError(
self.pos_start, self.pos_end,
"Second argument must be string",
exec_ctx
))
fn = fn.value
try:
with open(fn, "r") as f:
script = f.read()
except Exception as e:
return RTResult().failure(RTError(
self.pos_start, self.pos_end,
f"Failed to load script "{fn}"n" + str(e),
exec_ctx
))
_, error = run(fn, script)
if error:
return RTResult().failure(RTError(
self.pos_start, self.pos_end,
f"Failed to finish executing script "{fn}"n" +
error.as_string(),
exec_ctx
))
return RTResult().success(Number.null)
这里面的实现就是直接,读取文本,然后交给解释器。里面的这个run方法其实就是:
总结
以上的话,就是我们完整的内容了,那么接下来的话,差不多还有两篇博文,这个系列就结束了。预告一下,一个是完整项目结构,然后是语法特性,然后打包得到执行软件。还有一个就是作为使用者如何使用。
这个语法的话,得斟酌斟酌,要支持中文,但是不能太拗口。