博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零开始开发JVM语言(十四)Evaluator,REPL 与 Script
阅读量:6573 次
发布时间:2019-06-24

本文共 2837 字,大约阅读时间需要 9 分钟。

hot3.png

目录戳

上一篇实际上已经把编译器完成了。后续仅仅是周边工作,会轻松很多。这时候首先要搞定的是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是需要编译的。这就需要把运行时变量传入编译的文本中。这听起来很玄幻,是事实也是不可能做到的。能做到的只是控制一些参数,比如构造函数中,把对象通过参数传进去,这个很好实现。

除了参数,还有三个地方需要做下特殊处理。

  1. 方法。有时候可能为了方便重复工作,会定义一个方法。这个方法应当被记录,并在后续eval时都把它加上。
  2. import。这个没啥可说,必须记录。
  3. 跑repl时定义的类。这个只需要保证类加载器能够成功取出这个类即可,不需要过多处理。不要额外通过AST再定义一个类,会出现ClassCast或者类似原因的错误

Script

有了上面的基础工作,脚本的实现也十分简单。首先通过Evaluator做出一个/些类,然后加载这个/些类并反射出要执行的方法体。然后执行一下即可。

不过有些细节,我觉得可以参考下。

参数

我觉得脚本应该设计为能读取到控制台参数,所以给一个args:[]String变量表示外部传入的参数。做成方法参数即可。

require 与 返回值

像nodejs,会有一个requiremodule.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哦~

链接在这里~

转载于:https://my.oschina.net/wkgcass/blog/738829

你可能感兴趣的文章
Android 自定义GridView网格布局
查看>>
关于在帧中继fr环境下的NAT网络地址转换的实验
查看>>
2015-郭辉-项目采购管理+文档配置管理
查看>>
基于 jQuery & CSS3 实现智能提示输入框光标位置
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
ThreadLocal分析
查看>>
mysql优化:连接数
查看>>
github的使用(git shell )
查看>>
如何优化js代码(1)——字符串的拼接
查看>>
PHP 时间操作 / 跳转问题
查看>>
Windows 2012 R2 FSMO角色相关小记录
查看>>
2017年6月12日笔记
查看>>
(小蚂蚁站长吧)网站优化做好这八步你就是seo第一
查看>>
使用流的方式往页面前台输出图片
查看>>
java核心技术反射
查看>>
我的友情链接
查看>>
Maven创建新的依赖项目
查看>>
2015年10月26日作业
查看>>
LAMP,安装脚本
查看>>