跟vczh看实例学编译原理——零:序言

在《如何设计一门语言》里面,我讲了一些语言方面的东西,还有痛快的喷了一些XX粉什么的。不过单纯讲这个也是很无聊的,因此我开了这个《跟vczh看实例学编译原理》系列,意在科普一些编译原理的知识,尽可能让你们能够在创造语言以后,本身写一个原型。在这里我拿我创造的一门颇有趣的语言 https://github.com/vczh/tinymoe/ 做为实例。html

 

商业编译器对功能和质量的要求都是很高的,里面大量的东西其实都跟编译原理不要紧。一个典型的编译原理的原型有什么特征呢?node

  1. 性能低
  2. 错误信息难看
  3. 没有检查全部状况就生成代码
  4. 优化作得烂
  5. 几乎没有编译选项

 

等等。Tinymoe就知足了上面的5种状况,由于个人目标也只是想作一个原型,向你们介绍编译原理的基础知识。固然,我对语法的设计仍是尽可能靠近工业质量的,只是实现没有花太多心思。jquery

 

为何我要用Tinymoe来做为实例呢?由于Tinymoe是少有的一种用起来简单,并且库能够有多复杂写多复杂的语言,就跟C++同样。C++11额标准库在一块儿用简直是愉快啊,Tinymoe的代码也是这么写的。可是这并不妨碍你能够在写C++库的时候发挥你的想象力。Tinymoe也是同样的。为何呢,我来举个例子。git

 

Hello, world!

Tinymoe的hello world程序是很简单的:github

 

module hello world 编程

using standard library 数组

 

sentence print (message) ruby

    redirect to "printf" 函数式编程

end 函数

 

phrase main

    print "Hello, world!"

end

 

module指的是模块的名字,普通的程序也是一个模块。using指的是你要引用的模块——standard library就是Tinymoe的STL了——固然这个程序并无用到任何standard library的东西。说到这里你们可能意识到了,Tinymoe的名字能够是不定长的token组成的!没错,后面你们会慢慢意识到这种作法有多么的强大。

 

后面是print函数和main函数。Tinymoe是严格区分语句和表达式的,只有sentence和block开头的函数才能做为语句,并且同时只有phrase开头的函数才能做为表达式。因此下面的程序是不合法的:

 

phrase main

    (print "Hello, world!") + 1

end

 

缘由就是,print是sentence,不能做为表达式使用,所以他不能被+1。

 

Tinymoe的函数参数都被写在括号里面,一个参数须要一个括号。到了这里你们可能会以为很奇怪,不过很快就会有解答了。为何要这么作,下一个例子就会告诉咱们。

 

print函数用的redirect to是Tinymoe声明FFI(Foreign Function Interface)的方法,也就是说,当你运行了print,他就会去host里面找一个叫作printf的函数来运行。不过你们不要误会,Tinymoe并无被设计成能够直接调用C函数,因此这个名字实际上是随便写的,只要host提供了一个叫作printf的函数完成printf该作的事情就好了。main函数就不用解释了,很直白。

1加到100等于5050

这个例子能够在Tinymoe的主页(https://github.com/vczh/tinymoe/)上面看到:

 

module hello world

using standard library

 

sentence print (message)

redirect to "printf"

end

 

phrase sum from (start) to (end)

set the result to 0

repeat with the current number from start to end

add the current number to the result

end

end

 

phrase main

print "1+ ... +100 = " & sum from 1 to 100

end

 

为何名字能够是多个token?为何每个参数都要一个括号?看加粗的部分就知道了!正是由于Tinymoe想让每一行代码均可以被念出来,因此才这么设计的。固然,你们确定都知道怎么算start + (start+1) + … + (end-1) + end了,因此应该很容易就能够看懂这个函数里面的代码具体是什么意思。

 

在这里能够稍微多作一下解释。the result是一个预约义的变量,表明函数的返回值。只要你往the result里面写东西,只要函数一结束,他就变成函数的返回值了。Tinymoe的括号没有什么特殊意思,就是改变优先级,因此那一句循环则能够经过添加括号的方法写成这样:

 

repeat with (the current number) from (start) to (end)

 

你们可能会想,repeat with是否是关键字?固然不是!repeat with是standard library里面定义的一个block函数。你们知道block函数的意思了吧,就是这个函数能够带一个block。block有一些特性可让你写出相似try-catch那样的几个block连在一块儿的大block,特别适合写库。

 

到了这里你们心中可能会有疑问,循环为何能够作成库呢?还有更加使人震惊的是,break和continue也不是关键字,是sentence!由于repeat with是有代码的:

 

category

    start REPEAT

    closable

block (sentence deal with (item)) repeat with (argument item) from (lower bound) to (upper bound)

    set the current number to lower bound

    repeat while the current number <= upper bound

        deal with the current number

        add 1 to the current number

    end

end

 

前面的category是用来定义一些block的顺序和包围结构什么的。repeat with是属于REPEAT的,而break和continue声明了本身只能直接或者间接方在REPEAT里面,所以若是你在一个没有循环的地方调用break或者continue,编译器就会报错了。这是一个花边功能,用来防止手误的。

 

你们可能会注意到一个新东西:(argument item)。argument的意思指的是,后面的item是block里面的代码的一个参数,对于repeat with函数自己他不是一个参数。这就经过一个很天然的方法给block添加参数了。若是你用ruby的话就得写成这个悲催的样子:

 

repeat_with(1, 10) do |item|

    xxxx

end

 

而用C++写起来就更悲催了:

 

repeat_with(1, 10, [](int item)

{

    xxxx

});

 

block的第一个参数sentence deal with (item)就是一个引用了block中间的代码的委托。因此你会看到代码里面会调用它。

 

好了,那repeat while老是关键字了吧——不是!后面你们还会知道,就连

 

if xxx

    yyy

else if zzz

    www

else if aaa

    bbb

else

    ccc

end

 

也只是你调用了if、else if和else的一系列函数而后让他们串起来而已。

 

那Tinymoe到底提供了什么基础设施呢?其实只有select-case和递归。用这两个东西,加上内置的数组,就图灵完备了。图灵完备就是这么容易啊。

 

多重分派(Multiple Dispatch)

讲到这里,我不得不说,Tinymoe也能够写类,也能够继承,不过他跟传统的语言不同的,类是没有构造函数、析构函数和其余成员函数的。Tinymoe全部的函数都是全局函数,可是你可使用多重分派来"挑选"类型。这就须要第三个例子了(也能够在主页上找到):

 

module geometry

using standard library

 

phrase square root of (number)

    redirect to "Sqrt"

end

 

sentence print (message)

    redirect to "Print"

end

 

type rectangle

    width

    height

end

 

type triangle

    a

    b

    c

end

 

type circle

    radius

end

 

phrase area of (shape)

    raise "This is not a shape."

end

 

phrase area of (shape : rectangle)

    set the result to field width of shape * field height of shape

end

 

phrase area of (shape : triangle)

    set a to field a of shape

    set b to field b of shape

    set c to field c of shape

    set p to (a + b + c) / 2

    set the result to square root of (p * (p - a) * (p - b) * (p - c))

end

 

phrase area of (shape : circle)

    set r to field radius of shape

    set the result to r * r * 3.14

end

 

phrase (a) and (b) are the same shape

    set the result to false

end

 

phrase (a : rectangle) and (b : rectangle) are the same shape

    set the result to true

end

 

phrase (a : triangle) and (b : triangle) are the same shape

    set the result to true

end

 

phrase (a : circle) and (b : circle) are the same shape

    set the result to true

end

 

phrase main

    set shape one to new triangle of (2, 3, 4)

    set shape two to new rectangle of (1, 2)

    if shape one and shape two are the same shape

        print "This world is mad!"

    else

        print "Triangle and rectangle are not the same shape!"

    end

end

 

这个例子稍微长了一点点,不过你们能够很清楚的看到我是如何定义一个类型、建立他们和访问成员变量的。area of函数能够计算一个平面几何图形的面积,并且会根据你传给他的不一样的几何图形而使用不一样的公式。当全部的类型判断都失败的时候,就会掉进那个没有任何类型声明的函数,从而引起一场。嗯,其实try/catch/finally/raise都是函数来的——Tinymoe对控制流的控制就是如此强大,啊哈哈哈哈。就连return均可以本身作,因此Tinymoe也不提供预约义的return。

 

那phrase (a) and (b) are the same shape怎么办呢?没问题,Tinymoe能够同时指定多个参数的类型。并且Tinymoe的实现具备跟C++虚函数同样的性质——不管你有多少个参数标记了类型,我均可以O(n)跳转到一个你须要的函数。这里的n指的是标记了类型的参数的个数,而不是函数实例的个数,因此跟C++的状况是同样的——由于this只能有一个,因此就是O(1)。至于Tinymoe究竟是怎么实现的,只须要看《如何设计一门语言》第五篇(http://www.cppblog.com/vczh/archive/2013/05/25/200580.html)就有答案了。

Continuation Passing Style

为何Tinymoe的控制流均可以本身作呢?由于Tinymoe的函数都是写成了CPS这种风格的。其实CPS你们都很熟悉,当你用jquery作动画,用node.js作IO的时候,那些嵌套的一个一个的lambda表达式,就有点CPS的味道。不过在这里咱们并无看到嵌套的lambda,这是由于Tinymoe提供的语法,让Tinymoe的编译器能够把同一个层次的代码,转成嵌套的lambda那样的代码。这个过程就叫CPS变换。Tinymoe虽然用了不少函数式编程的手段,可是他并非一门函数是语言,只是一门普通的过程式语言。可是这跟C语言不同,由于它连C#的yield return均可以写成函数!这个例子就更长了,你们能够到Tinymoe的主页上看。我这里只贴一小段代码:

 

module enumerable

using standard library

 

symbol yielding return

symbol yielding break

 

type enumerable collection

    body

end

 

type collection enumerator

    current yielding result

    body

    continuation

end

 

略(这里实现了跟enumerable相关的函数,包括yield return)

 

block (sentence deal with (item)) repeat with (argument item) in (items : enumerable collection)

    set enumerator to new enumerator from items

    repeat

        move enumerator to the next

        deal with current value of enumerator

    end

end

 

sentence print (message)

    redirect to "Print"

end

 

phrase main

    create enumerable to numbers

        repeat with i from 1 to 10

            print "Enumerating " & i

            yield return i

        end

    end

 

    repeat with number in numbers

        if number >= 5

            break

        end

        print "Printing " & number

    end

end

 

什么叫模拟C#的yield return呢?就是连惰性计算也一块儿模拟!在main函数的第一部分,我建立了一个enumerable(iterator),包含1到10十个数字,并且每产生一个数字还会打印出一句话。可是接下来我在循环里面只取前5个,打印前4个,所以执行结果就是

当!

 

CPS风格的函数的威力在于,每个函数均可以控制他如何执行函数结束以后写在后面的代码。也就是说,你能够根据你的须要,干脆选择保护现场,而后之后再回复。是否是听起来很像lua的coroutine呢?在Tinymoe,coroutine也能够本身作!

 

虽然函数最后被转换成了CPS风格的ast,并且测试用的生成C#代码的确也是原封不动的输出了出来,因此运行这个程序耗费了大量的函数调用。但这并不意味着Tinymoe的虚拟机也要这么作。你们要记住,一个语言也好,类库也好,给你的接口的概念,跟实现的概念,有可能彻底不一样。yield return写出来的确要花费点心思,因此《序言》我也不讲这么多了,后续的文章会详细介绍这方面的知识,固然了,还会告诉你怎么实现的。

 

尾声

这里我挑选了四个例子来展现Tinymoe最重要的一些概念。一门语言,要应用用起来简单,库写起来能够发挥想象力,才是有前途的。yield return例子里面的main函数同样,用的时候多清爽,清爽到让你彻底忘记yield return实现的时候里面的各类麻烦的细节。

 

因此为何我要挑选Tinymoe做为实例来科普编译原理呢?有两个缘由。第一个缘由是,想要实现Tinymoe,须要大量的知识。因此既然这个系列想让你们可以看完实现一个Tinymoe的低质量原型,固然会讲不少知识的。第二个缘由是,我想经过这个例子向你们将一个道理,就是库和应用 、编译器和语法、实现和接口,彻底能够作到隔离复杂,只留给最终用户简单的部分。你看到的复杂的接口,并不意味着他的实现是臃肿的。你看到的简单的接口,也不意味着他的实现就很简洁

 

Tinymoe目前已经能够输出C#代码来执行了。后面我还会给Tinymoe加上静态分析和类型推导。对于这类语言作静态分析和类型推导又不少麻烦,我如今尚未彻底搞明白。譬如说这种能够本身控制continuation的函数要怎么编译成状态机才能避免掉大量的函数调用,就不是一个容易的问题。因此在系列一边作的时候,我还会一边研究这个事情。若是到时候系列把编译部分写完的同时,这些问题我也搞明白的话,那我就会让这个系列扩展到包含静态分析和类型推导,继续往下讲。

相关文章
相关标签/搜索