目录戳
上一篇实际上已经把编译器完成了。后续仅仅是周边工作,会轻松很多。这时候首先要搞定的是Evaluator
像javascript,php这类语言,都会提供一个eval函数,用来把字符串描述的表达式转换成程序并执行。Latte-lang作为一种灵活的语言,自然也少不了这项功能。 既然我们已经有了编译器,那么把字符串转换为程序就不是难事。
这里必须提一下,把语法分析和语义分析完全的独立开来,是有很大好处的。第一个好处是错误恢复,语法分析通常都是上下文无关的,这样错误恢复非常方便。但是语义分析是有关的,上文错误了后文的语义可能发生本质性的改变,所以很难恢复。这也造成了许多编译器干脆不做错误恢复的现象。
但是,如果语法分析和语义分析分离——比如语法上设计的好一点,让语法分析完全不需要知道语义就能解析——或者说语法分析时只需要附加很小很小的语义(一些上下文无关的语义)——这么做就可以在语法阶段完美的做到错误恢复。
另一个好处就是这里需要做的Evaluator了。通常eval时会把表达式和类型定义混在一起。这样当然没错,但是做编译器时候为了和java兼容并没有做这样的支持(也可以做支持,但是实现上会很丑陋)。所以,在这里需要做特殊处理。如果能够获得语义分析输出的AST,那么这项特殊处理会变得非常简单。
普通表达式
首先直接把表达式扔进 词法器 和 语法器,得到AST。比如1+2
就会得到类似于:
TwoVarOp( NumberLiteral("1") , "+" , NumberLiteral("2"))
的结构。
可以从AST得知,这是一个Expression,所以先包装成Return(...)的形式
然后,手动构造一个类和其中的方法,并把这个结构放进去:
ClassDef( "Eval", // 类名 []/* 修饰符 */, [] /* 注解 */, null /* 父类 */, [] /* 父接口 */, [ // 类内部内容 MethodDef( "method", // 方法名 Access(null, "Object"), // 返回类型 []/* 修饰符 */, [] /* 注解 */, [ // 方法体内容 Return( TwoVarOp( NumberLiteral("1") , "+" , NumberLiteral("2") ) ) ] ) ])
接着直接把这个构造的AST扔给语义分析和字节码生成,就能得到最终字节码了。
类型定义
如果遇到了类型定义,也没关系。因为语法分析不会管语义,所以实现上是不区分类型定义和表达式的,也就是说它们都可以出现,只要结束符之内不出现语法错误即可。
所以,跑出AST后,可以简单的遍历一下,看看哪些是类型定义,并将它们提取出来单独作为一个AST分支,并与生成的AST一起送入语义分析。
REPL
REPL就是 Read-Eval-Print-Loop。读,输出,循环很简单,最关键的是Eval的部分。不过又了上面的Evaluator之后,eval便不是难事。
在每一次需要eval时,都跑出AST,并放入指定的类中。不过有一点可能要注意下。上述实现中,对于表达式的处理,是放在方法中的。如果是repl,很可能需要记录曾经定义或者输入过的变量。所以放在方法中作为局部变量有点不太合适。这里应该作为字段来存储。
而Latte-lang设计非常好,它支持一个“类构造块”结构,类构造块中的变量自动成为字段。所以无须过多设计,直接把AST放入类下面即可。
但是,曾经的变量是运行时的,而eval是需要编译的。这就需要把运行时变量传入编译的文本中。这听起来很玄幻,是事实也是不可能做到的。能做到的只是控制一些参数,比如构造函数中,把对象通过参数传进去,这个很好实现。
除了参数,还有三个地方需要做下特殊处理。
- 方法。有时候可能为了方便重复工作,会定义一个方法。这个方法应当被记录,并在后续eval时都把它加上。
- import。这个没啥可说,必须记录。
- 跑repl时定义的类。这个只需要保证类加载器能够成功取出这个类即可,不需要过多处理。不要额外通过AST再定义一个类,会出现ClassCast或者类似原因的错误
Script
有了上面的基础工作,脚本的实现也十分简单。首先通过Evaluator做出一个/些类,然后加载这个/些类并反射出要执行的方法体。然后执行一下即可。
不过有些细节,我觉得可以参考下。
参数
我觉得脚本应该设计为能读取到控制台参数,所以给一个args:[]String变量表示外部传入的参数。做成方法参数即可。
require 与 返回值
像nodejs,会有一个require
与module.exports
,用起来效果很好。实现脚本的时候我觉得可以参考下。我的实现方式是这样:require放在编译器里做实现,这样不管是脚本还是普通程序都能使用require功能。而exports功能只在脚本有,实现上是一个return,也就是把方法返回值看作export的对象。
这么做有个好处,就是实现方便。因为方法本来就有返回值,这样做不需要额外处理
对于现在的latte-lang来说,有个坏处。现在latte-lang的lambda可以捕捉任何局部变量,但是不能修改外部的值,因为局部变量是做为值传递到lambda里面的,里面的所有操作都不会影响外面的值(就是说你可以在lambda里面赋值,但是没效果)。所以不能像node那样随意修改。
后续考虑加个指针功能,比如i : * int
,这时它的类型实际上是Container类型(比如说)。所有操作都在Container.item上完成。传递时也传递整个Container。这样就解决了上述问题。
(指针是第一步计划,第二步是自动判断哪些需要做成指针并自动加上。最近有点忙,估计一个月内能做出来吧。。感觉功能并不难,但是代码有点多,难免少考虑某些情况,所以用例可能得疯狂的加)
对这个项目有兴趣的可以下载下来编译一下然后跑一跑哦~我现在已经用它做过一个简单的网站了(用的vertx,写完发现风格跟nodejs+express很像)。有任何bug欢迎提issue哦~
链接在这里~