diff --git a/published/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md b/published/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md new file mode 100644 index 0000000000..88d42c5c98 --- /dev/null +++ b/published/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md @@ -0,0 +1,265 @@ +Ohm:用两百行 JavaScript 创造你自己的编程语言 +========== + +![](https://www.pubnub.com/wp-content/uploads/2016/08/ohm-banner.jpg) + +解析器是一种超级有用的软件库。从概念上简单的说,它们的实现很有挑战性,并且在计算机科学中经常被认为是黑魔法。在这个系列的博文中,我会向你们展示为什么你不需要成为哈利波特就能够精通解析器这种魔法。但是为了以防万一带上你的魔杖吧! + +我们将探索一种叫做 Ohm 的新的开源库,它使得搭建解析器很简单并且易于重用。在这个系列里,我们使用 Ohm 去识别数字,构建一个计算器等等。在这个系列的最后你将已经用不到 200 行的代码发明了一种完整的编程语言。这个强大的工具将让你能够做到一些你可能过去认为不可能的事情。 + +###为什么解析器很困难? + +解析器非常有用。在很多时候你可能需要一个解析器。或许有一种你需要处理的新的文件格式,但还没有人为它写了一个库;又或许你发现了一种古老格式的文件,但是已有的解析器不能在你的平台上构建。我已经看到这样的事发生无数次。 Code 在或者不在, Data 就在那里,不增不减。 + +从根本上来说,解析器很简单:只是把一个数据结构转化成另一个。所以你会不会觉得你要是邓布利多校长就好了? + +解析器历来是出奇地难写,所面临的挑战是绝大多数现有的工具都很老,并且需要一定的晦涩难懂的计算机科学知识。如果你在大学里上过编译器课程,那么课本里也许还有从上世纪七十年传下来的技术。幸运的是,解析器技术从那时候起已经提高了很多。 + +典型的,解析器是通过使用一种叫作[形式语法(formal grammar)][1]的特殊语法来定义你想要解析的东西来创造的,然后你需要把它放入像 [Bison][2] 和 [Yacc][3] 的工具中,这些工具能够产生一堆 C 代码,这些代码你需要修改或者链接到你实际写入的编程语言中。另外的选择是用你更喜欢的语言亲自动手写一个解析器,这很慢且很容易出错,在你能够真正使用它之前还有许多额外的工作。 + +想像一下,是否你关于你想要解析的东西的语法描述也是解析器?如果你能够只是直接运行这些语法,然后仅在你需要的地方增加一些挂钩(hook)呢?那就是 Ohm 所可以做到的事。 + +### Ohm 简介 + +[Ohm][4]是一种新的解析系统。它类似于你可能已经在课本里面看到过的语法,但是它更强大,使用起来更简单。通过 Ohm, 你能够使用一种灵活的语法在一个 .ohm 文件中来写你自己的格式定义,然后使用你的宿主语言把语义加入到里面。在这篇博文里,我们将用 JavaScript 作为宿主语言。 + +Ohm 建立于一个为创造更简单、更灵活的解析器的多年研究基础之上。VPRI 的 [STEPS program (pdf)][5] 使用 Ohm 的前身 [Ometa][6] 为许多特殊的任务创造了专门的语言(比如一个有 400 行代码的平行制图描绘器)。 + +Ohm 有许多有趣的特点和符号,但是相比于全部解释它们,我认为我们只需要深入其中并构建一些东西就行了。 + +###解析整数 + +让我们来解析一些数字。这看起来会很简单,只需在一个文本串中寻找毗邻的数字,但是让我们尝试去处理所有形式的数字:整数和浮点数、十六进制数和八进制数、科学计数、负数。解析数字很简单,正确解析却很难。 + +亲自构建这个代码将会很困难,会有很多问题,会伴随有许多特殊的情况,比如有时会相互矛盾。正则表达式或许可以做的这一点,但是它会非常丑陋而难以维护。让我们用 Ohm 来试试。 + +用 Ohm 构建的解析器涉及三个部分:语法(grammar)、语义(semantics)和测试(tests)。我通常挑选问题的一部分为它写测试,然后构建足够的语法和语义来使测试通过。然后我再挑选问题的另一部分,增加更多的测试、更新语法和语义,从而确保所有的测试能够继续通过。即使我们有了新的强大的工具,写解析器从概念上来说依旧很复杂。测试是用一种合理的方式来构建解析器的唯一方法。现在,让我们开始工作。 + +我们将从整数开始。一个整数由一系列相互毗邻的数字组成。让我们把下面的内容放入一个叫做 grammar.ohm 的文件中: + +``` +CoolNums { + // just a basic integer + Number = digit+ +} +``` + +这创造了一条匹配一个或多个数字(`digit`)叫作 `Number` 的单一规则。`+` 意味着一个或更多,就在正则表达式中一样。当有一个或更多的数字时,这个规则将会匹配它们,如果没有数字或者有一些不是数字的东西将不会匹配。“数字(`digit`)”的定义是从 0 到 9 其中的一个字符。`digit` 也是像 `Number` 一样的规则,但是它是 Ohm 的其中一条构建规则因此我们不需要去定义它。如果我们想的话可以推翻它,但在这时候这没有任何意义,毕竟我们不打算去发明一种新的数。 + +现在,我们可以读入这个语法并用 Ohm 库来运行它。 + +把它放入 test1.js: + +``` +var ohm = require('ohm-js'); +var fs = require('fs'); +var assert = require('assert'); +var grammar = ohm.grammar(fs.readFileSync('src/blog_numbers/syntax1.ohm').toString()); +``` + +`Ohm.grammar` 调用将读入该文件并解析成一个语法对象。现在我们可以增加一些语义。把下面内容增加到你的 JavaScript 文件中: + +``` +var sem = grammar.createSemantics().addOperation('toJS', { + Number: function(a) { + return parseInt(this.sourceString,10); + } +}); +``` + +这通过 `toJS` 操作创造了一个叫作 `sem` 的语法集。这些语义本质上是一些对应到语法中每个规则的函数。每个函数当与之相匹配的语法规则被解析时将会被调用。上面的 `Number` 函数将会在语法中的 `Number` 规则被解析时被调用。语法(grammar)定义了在语言中这些代码是什么,语义(semantics)定义了当这些代码被解析时应该做什么。 + +语义函数能够做我们想做的任何事,比如打印出故障信息、创建对象,或者在任何子节点上递归调用 `toJS`。此时我们仅仅想把匹配的文本转换成真正的 JavaScript 整数。 + +所有的语义函数有一个内含的 `this` 对象,带有一些有用的属性。其 `source` 属性代表了输入文本中和这个节点相匹配的部分。`this.sourceString` 是一个匹配输入的串,调用内置在 JavaScript 中的 `parseInt` 函数会把这个串转换成一个数。传给 `parseInt` 的 `10` 这个参数告诉 JavaScript 我们输入的是一个以 `10` 为基底(10 进制)的数。如果少了这个参数, JavaScript 也会假定以 10 为基底,但是我们把它包含在里面因为后面我们将支持以 16 为基底的数,所以使之明确比较好。 + +既然我们有一些语法,让我们来实际解析一些东西看一看我们的解析器是否能够工作。如何知道我们的解析器可以工作?通过测试,许多许多的测试,每一个可能的边缘情况都需要一个测试。 + +使用标准的断言 `assert` API,以下这个测试函数能够匹配一些输入并运用我们的语义把它转换成一个数,然后把这个数和我们期望的输入进行比较。 + +``` + function test(input, answer) { + var match = grammar.match(input); + if(match.failed()) return console.log("input failed to match " + input + match.message); + var result = sem(match).toJS(); + assert.deepEqual(result,answer); + console.log('success = ', result, answer); + } +``` + +就是如此。现在我们能够为各种不同的数写一堆测试。如果匹配失败我们的脚本将会抛出一个例外。否则就打印成功信息。让我们尝试一下,把下面这些内容加入到脚本中: + +``` + test("123",123); + test("999",999); + test("abc",999); +``` + +然后用 `node test1.js` 运行脚本。 + +你的输出应该是这样: + +``` +success = 123 123 +success = 999 999 +input failed to match abcLine 1, col 1: +> 1 | abc + ^ +Expected a digit +``` + +真酷。正如预期的那样,前两个成功了,第三个失败了。更好的是,Ohm 自动给了我们一个很棒的错误信息指出匹配失败。 + +###浮点数 + +我们的解析器工作了,但是它做的工作不是很有趣。让我们把它扩展成既能解析整数又能解析浮点数。改变 grammar.ohm 文件使它看起来像下面这样: + +``` +CoolNums { + // just a basic integer + Number = float | int + int = digit+ + float = digit+ "." digit+ +} +``` + +这把 `Number` 规则改变成指向一个浮点数(`float`)或者一个整数(`int`)。这个 `|` 代表着“或”。我们把这个读成“一个 `Number` 由一个浮点数或者一个整数构成。”然后整数(`int`)定义成 `digit+`,浮点数(`float`)定义成 `digit+` 后面跟着一个句号然后再跟着另一个 `digit+`。这意味着在句号前和句号后都至少要有一个数字。如果一个数中没有一个句号那么它就不是一个浮点数,因此就是一个整数。 + +现在,让我们再次看一下我们的语义功能。由于我们现在有了新的规则所以我们需要新的功能函数:一个作为整数的,一个作为浮点数的。 + +``` +var sem = grammar.createSemantics().addOperation('toJS', { + Number: function(a) { + return a.toJS(); + }, + int: function(a) { + console.log("doing int", this.sourceString); + return parseInt(this.sourceString,10); + }, + float: function(a,b,c) { + console.log("doing float", this.sourceString); + return parseFloat(this.sourceString); + } +}); +``` + +这里有两件事情需要注意。首先,整数(`int`)、浮点数(`float`)和数(`Number`)都有相匹配的语法规则和函数。然而,针对 `Number` 的功能不再有任何意义。它接收子节点 `a` 然后返回该子节点的 `toJS` 结果。换句话说,`Number` 规则简单的返回相匹配的子规则。由于这是在 Ohm 中任何规则的默认行为,因此实际上我们不用去考虑 `Number` 的作用,Ohm 会替我们做好这件事。 + +其次,整数(`int`)有一个参数 `a`,然而浮点数有三个:`a`、`b` 和 `c`。这是由于规则的实参数量(arity)决定的。[实参数量(arity)][7] 意味着一个规则里面有多少参数。如果我们回过头去看语法,浮点数(`float`)的规则是: + +``` + float = digit+ "." digit+ +``` + +浮点数规则通过三个部分来定义:第一个 `digit+`、`.`、以及第二个 `digit+`。这三个部分都会作为参数传递给浮点数的功能函数。因此浮点数必须有三个参数,否则 Ohm 库会给出一个错误。在这种情况下我们不用在意参数,因为我们仅仅直接攫取了输入串,但是我们仍然需要参数列在那里来避免编译器错误。后面我们将实际使用其中一些参数。 + +现在我们可以为新的浮点数支持添加更多的测试。 + +``` +test("123",123); +test("999",999); +//test("abc",999); +test('123.456',123.456); +test('0.123',0.123); +test('.123',0.123); +``` + +注意最后一个测试将会失败。一个浮点数必须以一个数开始,即使它就是个 0,`.123` 不是有效的,实际上真正的 JavaScript 语言也有相同的规则。 + +###十六进制数 + +现在我们已经有了整数和浮点数,但是还有一些其它的数的语法最好可以支持:十六进制数和科学计数。十六进制数是以 16 为基底的整数。十六进制数的数字能从 0 到 9 和从 A 到 F。十六进制数经常用在计算机科学中,当用二进制数据工作时,你可以仅仅使用两个数字表示 0 到 255 的数。 + +在绝大多数源自 C 的编程语言(包括 JavaScript),十六进制数通过在前面加上 `0x` 来向编译器表明后面跟的是一个十六进制数。为了让我们的解析器支持十六进制数,我们只需要添加另一条规则。 + +``` + Number = hex | float | int + int = digit+ + float = digit+ "." digit+ + hex = "0x" hexDigit+ + hexDigit := "0".."9" | "a".."f" | "A".."F" +``` + +我实际上已经增加了两条规则。十六进制数(`hex`)表明它是一个 `0x` 后面一个或多个十六进制数字(`hexDigits`)的串。一个十六进制数字(`hexDigit`)是从 0 到 9,或从 a 到 f,或 A 到 F(包括大写和小写的情况)的一个字符。我也修改了 `Number` 规则来识别十六进制数作为另外一种可能的情况。现在我们只需要另一条针对十六进制数的功能规则。 + +``` + hex: function(a,b) { + return parseInt(this.sourceString,16); + } +``` + +注意到,在这种情况下,我们把 `16` 作为基底传递给 `parseInt`,因为我们希望 JavaScript 知道这是一个十六进制数。 + +我略过了一些很重要需要注意的事。`hexDigit` 的规则像下面这样: + +``` + hexDigit := "0".."9" | "a".."f" | "A".."F" +``` + +注意我使用的是 `:=` 而不是 `=`。在 Ohm 中,`:=` 是当你需要推翻一条规则的时候使用。这表明 Ohm 已经有了一条针对 `hexDigit` 的默认规则,就像 `digit`、`space` 等一堆其他的东西。如果我使用了 `=`, Ohm 将会报告一个错误。这是一个检查,从而避免我无意识的推翻一个规则。由于新的 `hexDigit` 规则和 Ohm 的构建规则一样,所以我们可以把它注释掉,然后让 Ohm 自己来实现它。我留下这个规则只是因为这样我们可以看到它实际上是如何进行的。 + +现在,我们可以添加更多的测试然后看到十六进制数真的能工作: + +``` +test('0x456',0x456); +test('0xFF',255); +``` + +###科学计数 + +最后,让我们来支持科学计数。科学计数是针对非常大或非常小的数的,比如 `1.8×10^3`。在大多数编程语言中,科学计数法表示的数会写成这样:1.8e3 表示 18000,或者 1.8e-3 表示 .018。让我们增加另外一对规则来支持这个指数表示: + +``` + float = digit+ "." digit+ exp? + exp = "e" "-"? digit+ +``` + +上面在浮点数规则末尾增加了一个指数(`exp`)规则和一个 `?`。`?` 表示没有或有一个,所以指数(`exp`)是可选的,但是不能超过一个。增加指数(`exp`)规则也改变了浮点数规则的实参数量,所以我们需要为浮点数功能增加另一个参数,即使我们不使用它。 + +``` + float: function(a,b,c,d) { + console.log("doing float", this.sourceString); + return parseFloat(this.sourceString); + }, +``` + +现在我们的测试可以通过了: + +``` +test('4.8e10',4.8e10); +test('4.8e-10',4.8e-10); +``` + +###结论 + +Ohm 是构建解析器的一个很棒的工具,因为它易于上手,并且你可以递增的增加规则。Ohm 也还有其他我今天没有写到的很棒的特点,比如调试观察仪和子类化。 + +到目前为止,我们已经使用 Ohm 来把字符串翻译成 JavaScript 数,并且 Ohm 经常用于把一种表示方式转化成另外一种。然而,Ohm 还有更多的用途。通过放入不同的语义功能集,你可以使用 Ohm 来真正处理和计算东西。一个单独的语法可以被许多不同的语义使用,这是 Ohm 的魔法之一。 + +在这个系列的下一篇文章中,我将向你们展示如何像真正的计算机一样计算像 `(4.85 + 5 * (238 - 68)/2)` 这样的数学表达式,不仅仅是解析数。 + +额外的挑战:你能够扩展语法来支持八进制数吗?这些以 8 为基底的数能够只用 0 到 7 这几个数字来表示,前面加上一个数字 0 或者字母 `o`。看看针对下面这些测试情况是够正确。下次我将给出答案。 + +``` +test('0o77',7*8+7); +test('0o23',0o23); +``` + +-------------------------------------------------------------------------------- + +via: https://www.pubnub.com/blog/2016-08-30-javascript-parser-ohm-makes-creating-a-programming-language-easy/ + +作者:[Josh Marinacci][a] +译者:[ucasFL](https://github.com/ucasFL) +校对:[wxy](https://github.com/wxy) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]: https://www.pubnub.com/blog/author/josh/ +[1]: https://en.wikipedia.org/wiki/Formal_grammar +[2]: https://en.wikipedia.org/wiki/GNU_bison +[3]: https://en.wikipedia.org/wiki/Yacc +[4]: https://github.com/cdglabs/ohm +[5]: http://www.vpri.org/pdf/tr2012001_steps.pdf +[6]: http://tinlizzie.org/ometa/ +[7]: https://en.wikipedia.org/wiki/Arity \ No newline at end of file diff --git a/translated/tech/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md b/translated/tech/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md deleted file mode 100644 index 70dd8091a4..0000000000 --- a/translated/tech/20160830 Ohm JavaScript Parser that Create a Language in 200 Lines of Code.md +++ /dev/null @@ -1,192 +0,0 @@ -Ohm: 一种可以用两百行代码创造一种语言的 JavaScript 解释器 -解释器是一种非常有用的软件库。从概念上简单的说,它们的实现很有挑战并且在计算机科学中经常被认为是暗黑艺术。在这个系列的博文中,我会向你们展示为什么你不需要成为哈利波特就能够很好的控制解释器。但是为了以防万一带上你的魔杖。 -我们将探索一种叫做 Ohm 的新的开源库,它使得搭建解释器很简单并且更加容易再利用。在这个系列里,我们使用 Ohm 去识别数字,构建计算器等等。在这个系列的最后你将已经用少于 200 行的代码发明了一种完整的编程语言。这个强大的工具将让你能够做一些你可能过去认为不可能的事情。 -###为什么解释器很困难? -解释器非常有用。在很多时候你可能需要一个解释器。一种新的文件格式可能出现,你需要去处理但还没有人为它写了一个库;或者你发现了一种老格式的文件但是现存的解释器不能构建你需要的平台。我已经看到这样的事发生无数次。代码会来来去去但数据却是永恒的。 -基础的解释器很简单:只是把一个数据结构转化成另一个。所以为什么感觉你需要成为 邓布利多【魔法师】才能够把它们做出来。 -解释器的一些历史性的挑战是很难写,绝大多数工具很老并且假设了大量晦涩难懂的计算机科学知识。如果你在大学里上过编译器课程那么课本里可能也有从 1970 年以来的技术。幸运的是,解释器技术从那时候起已经提高了很多。 -代表性地,解释器是通过使用一种叫作形式语法的特殊语法来定义你想要解析的东西这样发明的,然后你需要把它放入像 Bison 和 Yacc 的工具中,这些工具能够产生一堆你需要修改的 C 代码或者链接到你实际写入额的编程语言。另外的选择是用你更喜欢的语言亲自动手写一个解释器,这很慢且很容易出错,在你能够真正使用它之前还有许多额外的工作。 -想像一下,是否你关于你想要解析的东西的语法描述也是解释器?如果你能够仅仅直接运行语法,然后在你需要的地方增加挂钩,那是什么?那就是 Ohm 所做的事。 -###解释器简介 -[Ohm][1]是一种新的解析系统。它类似你可能已经在课本里面看到的语法并且它更强大,使用起来更简单。通过 Ohm, 你能够使用一种灵活的语法以 .ohm 文件格式来写格式定义,然后使用你的宿主语言把语义加入到里面。在这篇博文里,我们将用 JavaScript 作为宿主语言。 -Ohm 建立在一个为制造更简单、更灵活的解释器的一个多年调查基础之上。VPRI 的 [STEPS program](pdf) 使用 Ohm 的前驱为许多特殊的工作创造了专门的语言(比如一个有 400 行代码的平行制图描绘器)[Ometa][3]. -Ohm 有许多有趣的特点和符号,但是不是要全部解释它们,我认为我们应该只需投入其中并构建一些东西。 -###解析整数 -让我们来解析一些数字。这看起来会很简单,只需在一个文本串中寻找毗邻的数字,但是让我们尝试去处理所有形式的数字:整数和浮点数,十六进制数和八进制数,科学计数,负数。解析数字很简单,正确解析却很难。 -亲自构建这个代码将会很困难,会有很多故障,会伴随有许多特殊的情况,比如有时会相互矛盾。 -用 Ohm 构建的解释器涉及三个部分:语法、语义和测试。我通常挑选一个问题的一部分为它写测试,然后构建足够的语法和语义来使测试通过。然后我再挑选问题的另一部分,增加更多的测试,更新语法和语义,从而确保所有的测试能够持续通过。即使我们有了新的强大的工具,写解释器从概念上来说依旧很困难。测试是用一种合理的方式来构建解释器的唯一方法。现在,让我们开始工作。 -我们将从整数开始。一个整数由一系列相互毗邻的数字组成。让我们把下面的内容放入一个叫做 grammar.ohm 的文件中: -``` -CoolNums { - // just a basic integer - Number = digit+ -} -``` -这创造了一条撮合一个或多个数字叫作 Number 的单一规则。+ 意味着一个或更多,就像一个常规的表达。当有一个或更多的数字时,这个规则将会撮合它们,如果没有数字或者有一些不是数字的东西将不会撮合。一个数字定义成从 0 到 9 其中的一个字符。数字也是像 Number 一样的规则,但是它是 Ohm 的其中一条构建规则因此我们不需要去定义它。我们可以推翻它如果我们想的话但在这时候这没有任何意义,毕竟我们不打算去发明一种新的数。 -现在,我们可以读入这个语法并用 Ohm 库来运行它。 -把它放入 test1.js -``` -var ohm = require('ohm-js'); -var fs = require('fs'); -var assert = require('assert'); -var grammar = ohm.grammar(fs.readFileSync('src/blog_numbers/syntax1.ohm').toString()); -``` -Ohm 的语法调用将把文件读入并解释成一个语法对象。现在我们可以增加一些语义。把下面内容增加到你的 JavaScript 文件中: -``` -var sem = grammar.createSemantics().addOperation('toJS', { - Number: function(a) { - return parseInt(this.sourceString,10); - } -}); -``` -这创造了一系列叫作 sem with the operation to JS[伴有 JavaScript 操作的语义] 的语义。这些语义至关重要,一群函数和语法中的每一条规则相匹配。一个函数将会被调用当与它相匹配的语法规则被解析时。上面的 Number 函数将会被调用当语法中的 Number 规则被解析时。语法定义在语言中 chunks[大块] 是什么,语义定义当 chunks[大块] 被解析时应该做什么。 -语义函数能够做我们想做的任何事,比如打印初故障信息,创造对象,或者递归调用 toJS 作用于任何子节点。此时我们仅仅想把文本转换成真正的 JavaScript 整数。 -所有的语义函数有一个包含一些有用性质的暗含对象。源属性代表输入文本和这个节点相匹配。这个 sourceString[源串] 是一个匹配输入串,调用构建在 JavaScript 中的parseInt 函数会把这个串转换成一个数。parseInt 中 10 这个参数告诉 JavaScript 我们输入的是一个以 10 为基底的数。如果少了这个参数, JavaScript 也会假定以 10 为基底,但是我们把它包含在里面因为后面我们将支持以 16 为基底的数,所以使之明确比较好。 -既然我们有一些语法,让我们来实际解析一些东西看一看我们的解释器是否能够工作。如果知道我们的解释器工作?通过测试它,许多许多的测试,每一个边缘情况都需要一个测试。 -伴随标准断言 API,有一个测试函数能够匹配一些输入并运用我们的语义把它转换成一个数,然后比较转换生成的数和我们期望的输入。 -``` - function test(input, answer) { - var match = grammar.match(input); - if(match.failed()) return console.log("input failed to match " + input + match.message); - var result = sem(match).toJS(); - assert.deepEqual(result,answer); - console.log('success = ', result, answer); - } -``` -这个函数就是上面这个。现在我们能够为不同的数写一堆测试。如果匹配失败我们的脚本将会丢弃一个例外。如果不能打印成功,让我们尝试一下,把下面这些内容加入到脚本中: -``` - test("123",123); - test("999",999); - test("abc",999); -``` -然后用节点 test.js 运行脚本 -你的输出应该是这样: -``` -success = 123 123 -success = 999 999 -input failed to match abcLine 1, col 1: -> 1 | abc - ^ -Expected a digit -``` -真酷。正如理所当然的那样,前两个成功了,第三个失败了。更好的是,Ohm 自动给了我们一个很棒的错误信息指出匹配失败。 -###浮点数 -我们的解释器工作了,但是它不能做任何非常有趣的事。让我们把它扩展成既能解析整数又能解析浮点数。改变 grammar.ohm 文件使它看起来像下面这样: -``` -CoolNums { - // just a basic integer - Number = float | int - int = digit+ - float = digit+ "." digit+ -} -``` -这把 Number 规则改变成指向一个浮点数或者一个整数。我的意思是,我们把这读成"一个 Number 由一个浮点数或者一个整数构成。”然后整数定义成 digit+, 浮点数定义成 digit+ 后面跟着一个句号然后再跟着另一个 digit+. 这意味着在句号前和句号后都至少要有一个数字。如果一个数中没有一个句号那么它就不是一个浮点数,因此就是一个整数。 -现在,让我们再次看一下我们的语义作用。由于我们现在有了新的规则所以我们需要新的作用函数:一个作为整数的,一个作为浮点数的。 -``` -var sem = grammar.createSemantics().addOperation('toJS', { - Number: function(a) { - return a.toJS(); - }, - int: function(a) { - console.log("doing int", this.sourceString); - return parseInt(this.sourceString,10); - }, - float: function(a,b,c) { - console.log("doing float", this.sourceString); - return parseFloat(this.sourceString); - } -}); -``` -这里有两件事情需要注意。首先,整数,浮点数和数都有相匹配的语法规则和函数。然而,针对数的作用不再有任何意义。它接收子节点 'a' 然后通过子节点返回 toJS 的结果。换句话说,Number 规则简单的返回相匹配的子规则。由于这是在 Ohm 中任何规则的默认行为,因此实际上我们不用去考虑 Number 的作用,Ohm 会替我们来做这件事。 -第二,整数有一个参数然而浮点数有三个:a, b, 和 c. 这是由于规则的参数数量。参数数量意味着一个规则里面有多少参数。如果我们回过头去看语法,浮点数的规则是: -``` - float = digit+ "." digit+ -``` -浮点数规则通过三个部分来定义:第一个 digit+, '.', 还有第二个 digit+. 这三个部分都会作为参数传递给浮点数的作用函数。因此浮点数必须有三个参数否则 Ohm 库给出一个错误。在这种情况下我们不用在意参数因为我们仅仅直接攫取了输入串,但是我们仍然需要列表的参数来回避编译器错误。后面我们将实际使用其中一些参数。 -现在我们可以为新的浮点数支持添加更多的测试。 -``` -test("123",123); -test("999",999); -//test("abc",999); -test('123.456',123.456); -test('0.123',0.123); -test('.123',0.123); -``` -注意最后一个测试将会失败。一个浮点数必须以一个数开始,即使它仅仅是 0, .123 不是有效的,实际上真正的 JavaScript 语言有相同的规则。 -###十六进制数 -现在我们已经有了整数和浮点数,但是有一些其他的数的语法可能能够很好的支持:十六进制数和科学计数。十六进制数是是以 16 为基底2的数。十六进制数的数字能从 0 到 9 和从 A 到 F. 十六进制数经常用在计算机科学中当用二进制数据工作时,因为你可以仅仅使用两个数字表示 0 到 255 的数。 -在绝大多数源自 C 的编程语言(包括 JavaScript), 十六进制数通过在前面加上 ‘0x' 来向编译器表明后面跟的是一个十六进制数。为了让我们的解释器支持十六进制数,我们只需要添加另一条规则。 -``` - Number = hex | float | int - int = digit+ - float = digit+ "." digit+ - hex = "0x" hexDigit+ - hexDigit := "0".."9" | "a".."f" | "A".."F" -``` -我实际上已经增加了两条规则。'hex' 表明十六进制数是一个 'ox' 后面一个或多个 ’hexDigits'[十六进制数子] 的串。一个 'hexDigit' 是从 0 到 9, 或从 a 到 f, 或 A 到 F(包扩大写和小写的情况)的一个字符。我也修改了 Number 规则来识别十六进制数作为其他可能的选择。现在我们只需要另一条针对十六进制数的作用规则。 -``` - hex: function(a,b) { - return parseInt(this.sourceString,16); - } -``` -注意到,在这种情况下,我们把 '16' 作为基数传递给 'parseInt', 因为我们希望 JavaScript 知道这是一个十六进制数。 - -我略过了一些很重要需要注意的事。针对 'hexDigit' 的规则像下面这样: -``` - hexDigit := "0".."9" | "a".."f" | "A".."F" -``` -注意我使用的是 ':=' 而不是 '='. 在 Ohm 中,'=' 是当你需要推翻一条规则的时候使用。证明是 Ohm 已经有了针对 'hexDigit' 的默认规则,就像针对 'digit', 'space' 等一堆其他的东西。如果我使用了 '=', Ohm 将会报告一个错误。这是一个检查从而我不能无意识的推翻一个规则。由于新的 hexDigit 规则和 Ohm 的构建规则一样,所以我们可以仅仅对它添加注释然后让 Ohm 来实现它。我留下这个规则仅仅是因为这样我们可以看到它实际上是如何进行的。 -Now we can add some more tests and see that our hex digits really work: -现在,我们可以添加更多的测试然后看到十六进制数真的工作。 -``` -test('0x456',0x456); -test('0xFF',255); -``` -###科学计数 -最后,让我们来支持科学计数。科学计数是针对非常大或非常小的数比如 1.8×10^3, 在大多数编程语言中,科学计数法表示的数会写成这样:1.8e3 表示 18000, 或者 1.8e-3 表示 .018. 让我们增加另外一对规则来支持这个指数表示: -``` - float = digit+ "." digit+ exp? - exp = "e" "-"? digit+ -``` -上面增加了一个指数规则通过在浮点数规则末尾加上一个 '?'. '?' 表示 0 或 1,所以指数是可选择的但是不能超过一个。增加指数规则也改变了浮点数规则的参数数量,所以我们需要为浮点数作用增加又一个参数,即使我们不使用它。 -``` - float: function(a,b,c,d) { - console.log("doing float", this.sourceString); - return parseFloat(this.sourceString); - }, -``` -现在我们的测试可以通过了: -``` -test('4.8e10',4.8e10); -test('4.8e-10',4.8e-10); -``` -###结论 -Ohm 是构建解释器的一个很棒的工具,因为它很容易开始并且你可以递增的增加规则。Ohm 也还有其他我今天没有写到的很棒的特点,比如故障观察仪和子类化。 -s. -到目前为止,我们已经使用 Ohm 来把字符串翻译成 JavaScript 数,并且 Ohm 经常由于需要把一个表示转化成另一个这一目的而使用。然而,Ohm 还有更多的用途。通过放入以系列不同的语义作用你可以使用 Ohm 来真正处理和计算东西。一个单独的语法可以被许多不同的语义使用,这是 Ohm 其中一个不可思议的特点。 -在这个系列的下一篇文章中,我将向你们展示如何计算像(4.85 + 5 * (238 - 68)/2) 这样的数学表达式,不仅仅是解析数。 -额外的挑战:你能够扩展语法来支持八进制数吗?这些以 8 为基底的数能够只用 0 到 7 这几个数字来表示,前面加上一个数字 0 或者字母 o. 看看针对下面这些测试情况是够正确。下次我将给出答案。 -``` -test('0o77',7*8+7); -test('0o23',0o23); -``` - - - - --------------------------------------------------------------------------------- - -via: https://www.pubnub.com/blog/2016-08-30-javascript-parser-ohm-makes-creating-a-programming-language-easy/?utm_source=javascriptweekly&utm_medium=email - -作者:[Josh Marinacci][a] -译者:[ucasFL](https://github.com/ucasFL) -校对:[校对者ID](https://github.com/校对者ID) - -本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 - -[a]: https://www.pubnub.com/blog/author/josh/ -[1]: https://github.com/cdglabs/ohm -[2]: http://www.vpri.org/pdf/tr2012001_steps.pdf -[3]: http://tinlizzie.org/ometa/ - -