在上一篇教程中,我们已经体验了 MPS 创建语言的流程的一半。 接下来,我们将进行 Code Generation ,把之前的『语言』生成的代码导出为 Java 代码,并运行它。
准备工作
- 阅读并跟随上一篇博客完成之前的工作。
- 学会用 Java 写 Hello World
本文主要内容
你在看上篇教程的时候肯定以为是分成上下的,结果没想到出了个『中』,你一定很绝望吧。
其实不是这样的,我只是想把最初的三个概念分开说而已, MPS 很简单的。
有哪些新概念
- 没有新引入的概念
开始吧
打开你的 MPS ,进入上次创建的工程。
打开左边的、昨天说的不会讲太多但是是今天的主菜的『Generator』, 并打开昨天我们自动生成的 map_PrintlnSet。
你可以看到,这是一个命名非常不规范的 Java 类——一个空的 Java 类。不过这是生成的代码,不用担心。
对了,忘记介绍概念了。
先复习一下
上篇教程讲了Concept 这个概念(这句话翻译成英语会很鬼畜,哈哈哈哈)——它:
- 是 AST 的节点
- 可以拥有别的节点(作为父节点)
- 可选地成为一个根节点
- 上一篇没有提到的是,它可以拥有别的 AST 节点的引用。可惜我们暂时不会用到这个特性
还有一些别的没有 cover 到的点,我将会在后期列出。
为什么不现在说完
一下子甩一堆概念是一种耍流氓。狠狠地黑一波现在绝大多数国产书。
看我的教程,你放心。我和那些半灌水不一样,我可是空灌水!
Generator
MPS 通过你的在破界神编辑器里编辑的 AST ,生成目标代码并编译。这也是为什么我之前说『java 文件不是源码而是目标文件』的原因。
而这里的Generator,就是你进行代码生成的模板。可以理解吧? MPS 按照这个模板来生成代码。 熟悉具有宏特性语言( i.e. Lisp 系列, Rust, Scala, etc.)的人应该可以很容易理解:
- MPS 的 AST 就是宏的参数
- MPS 的 Generator 就是宏的 body
- MPS 的 Generator 非常强大,不过这个教程里的例子暂时体现不出来
我们来编写最基本的部分,先写个 main 函数,里面写个输出语句。 请记得使用Live Template哦。
(上面的链接指向的博客是我还不知道 Live Template 可以定制的时候写的,所以没往深处讲,不过内容是正确的)
可以看到我已经写好了一个非常友善的输出语句。现在,我们要对它进行模板化处理——先选中这个输出语句。
注意
这是一个非常容易出错的地方,是绝大多数新人可能犯错的地方。在 Java 中,
System.out.println("")
是一个表达式,而
System.out.println("");
是一个语句。
区别(仅仅是这一个地方的区别,他是正确的,但是不具有普适性,读者也不需要了解普适性的区别,因为用不到):
概念 | 语法上 | 对应的语义上 |
---|---|---|
表达式 | 没分号 | 有『返回值』 |
语句 | 有分号 | 没有『返回值』,或者说返回 Unit |
然后我们这里是选中一个语句!对它使用Alt+Enter,选择LOOP 开头 clause 结尾的那个。
然后你看到了原本不可能出现在 Java 代码中的东西。这个东西原本是需要一个很长的过程创建的, MPS 提供了快速创建的方法。
然后我们选中那个字符串的内容,不包含双引号,使用Alt+Enter,选择 content 结尾的那个:
然后你又一次看到了那个美元符号+中括号组成的东西。
然后我们的工作就完成了!是不是很懵逼!什么都没懂!没关系!现在先完成语言,等会再解释。
再次注意
千万不要忘记编译!!!Ctrl+F9!!!
我在写教程的时候就忘了, Orz。
回到刚才的语言
回到昨天创建的那个 PrintlnSet ,我们在打开的编辑器里面右键,选择Preview Generated Text。
生成的代码是这样的:
package VerboseLang.sandbox;
/*Generated by MPS */
public class map_PrintlnSet {
public static void main(String[] args) {
System.out.println("Fuck you ZhiHu Editor!!!!");
System.out.println("My name is Van, I'm an artist.");
System.out.println("I'm a performance artist.");
}
}
但是你似乎并没有看到类似『Run』、『Execute』之类的字样,对吧?因为我们需要让它成为一个可执行的 AST ,才能运行它。
而不仅仅是需要有 main 函数。
我们对左边 logical view 的 VerboseLang 那个地方进行Alt+Enter,找到上篇博客提到过的『Dependency』,再添加这个叫
jetbrains.mps.execution.util
的 Dependency :
然后找到 PrintlnSet 的定义,让它实现一个接口——IMainClass:
注意, MPS 的接口和 Java 的接口也有一点不一样——
语言 | inheritance |
---|---|
Java | 无, Java8 以后可以使用 default |
MPS | 有 |
因此我们只要实现一个接口,也能获得它的功能。
实现了 IMainClass 的 AST 节点,在作根节点时,这颗 AST 就是可以运行的。
编译一下。
运行吧
还记得上次说的添加 JDK 这个 Dependency 吗?我有一处疏忽,别忘了给 Sandbox 也加上 JDK ,这样它才能调用 Java 类库:
右键你刚 Preview 了 Generated Text 的 PrintlnSet ,可以看到, Run 的按钮出现了!
点一下,然后就可以看到运行结果了!
好累呀,终于整出了可以运行的语言呀~
这 Generator 里面的东西到底是啥
前文说过, Generator 实际上就是宏,它是代码模板, MPS 根据你写的 AST ,对 Generator 里面的代码进行一次 map。
当它 map 到一个有 macro 的节点时(也就是刚才我们整进去的美元符号和方括号一家亲的东西), 会根据这个 macro 的要求,将你在 Sandbox 里面写的 AST 的信息填进去。
而原本写在美元符号+方括号一家亲里面的东西,就只是一个占位用的东西,多数情况下,多数内容都会被替换掉。
回顾一下刚才那个 AST ,我们对一个『语句』加上了一个循环的 macro ,也就是所它会对所有的循环内容进行遍历, 然后对里面的内容分别进行填充,每个循环内容产生一份这个东西,然后对里面的 macro 带入这个循环内容进行处理。
而我们对字符串加上的那个 macro ,就是将那个『循环内容』,也就是那个 Println ,把它的 content 填进去,原本的就没了。
比如我们这个例子, MPS 对很多个『Println』进行循环,那所有的 Println 就会被遍历,每个 Println 对应一个这个语句, 然后它们的 content 就会被依次填入字符串。
也就有了你看到的『Generated Text』的样子。
预告
不用你说我都知道,这语法:
println set {
clauses :
println {
content : Fuck you ZhiHu Editor!!!!
}
println {
content : My name is Van, I'm an artist.
}
println {
content : I'm a performance artist.
}
}
丑死了!!!!!!!!!!!!!!!!
下期讲怎么改变它的语法。
不用担心重构代码的问题,由于存储在 MPS 里的是 AST ,因此,你改变了语法之后,原本的代码也会随着语法的改变而改变。
这得益于 LOP 的伟大。晚安。