正则表达式与领域特定语言(DSL)

如何设计一门语言(十)——正则表达式与领域特定语言(DSL)

几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情。

创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的。我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了。当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态。虽然这个游戏需要脚本在剧情里面控制一些人物的走动什么的,但是所幸并不复杂,于是还是完成了任务。一眨眼10年过去了,现在在写GacUI,为了开发的方便,我自己做了一些DSL,或者实现了别人的DSL,渐渐地也明白了一些设计DSL的手法。不过在讲这些东西之前,我们先来看一个令我们又爱(对所有人)又恨(反正我不会)的DSL——正则表达式!

一、正则表达式

正则表达式可读性之差我们人人都知道,而且正则表达式之难写好都值得O’reilly出一本两厘米厚的书了。根据我的经验,只要先学好编译原理,然后按照.net的规格自己撸一个自己的正则表达式,基本上这本书就不用看了。因为正则表达式之所以要用奇怪的方法去写,只是因为你手上的引擎是那么实现的,所以你需要顺着他去写而已,没什么特别的原因。而且我自己的正则表达式拥有DFA和NFA两套解析器,我的正则表达式引擎会通过检查你的正则表达式来检查是否可以用DFA,从而可以优先使用DFA来运行,省去了很多其实不是那么重要的麻烦(譬如说a**会傻逼什么的)。这个东西我自己用的特别开心,代码也放在gac.codeplex.com上面。

正则表达式作为一门DSL是当之无愧的——因为它用了一种紧凑的语法来让我们可以定义一个字符串的集合,并且取出里面的特征。大体上语法我还是很喜欢的,我唯一不喜欢的是正则表达式的括号的功能。括号作为一种指定优先级的方法,几乎是无法避免使用的。但是很多流行的正则表达式的括号竟然还带有捕获的功能,实在是令我大跌眼镜——因为大部分时候我是不需要捕获的,这个时候只会浪费时间和空间去做一些多余的事情而已。所以在我自己的正则表达式引擎里面,括号是不捕获的。如果要捕获,就得用特殊的语法,譬如说(<name>pattern)把pattern捕获到一个叫做name的组里面去。

那我们可以从正则表达式的语法里面学到什么DSL的设计原则呢?我认为,DSL的原则其实很简单,只有以下三个:

  1. 短的语法要分配给常用的功能
  2. 语法要么可读性特别好(从而比直接用C#写直接),要么很紧凑(从而比直接用C#写短很多)
  3. API要容易定义(从而用C#调用非常方便,还可以确保DSL的目标是明确又简单的)

很多DSL其实都满足这个定义。SQL就属于API简单而且可读性好的那一部分(想想ADO.NET),而正则表达式就属于API简单而且语法紧凑的那一部分。为什么正则表达式可以设计的那么紧凑呢?现在让我们来一一揭开它神秘的面纱。

正则表达式的基本元素是很少的,只有连接、分支和循环,还有一些简单的语法糖。连接不需要字符,分支需要一个字符“|”,循环也只需要一个字符“+”或者“*”,还有代表任意字符的“.”,还有代表多次循环的{5,},还有代表字符集合的[a-zA-Z0-9_]。对于单个字符的集合来讲,我们甚至不需要[],直接写就好了。除此之外因为我们用了一些特殊字符所以还得有转义(escaping)的过程。那让我们数数我们定义了多少字符:“|+*[]-{},.()”。用的也不多,对吧。

尽管看起来很乱,但是正则表达式本身也有一个严谨的语法结构。关于我的正则表达式的语法树定义可以看这里:https://gac.codeplex.com/SourceControl/latest#Common/Source/Regex/RegexExpression.h。在这里我们可以整理出一个语法:

复制代码
DIGIT ::= [0-9]
LITERAL ::= [^|+*[]-\{}^,.()]
ANY_CHAR ::= LITERAL | "^" | "|" | "+" | "*" | "[" | "]" | "-" | "" | "{" | "}" | "," | "." | "(" | ")"

CHAR
    ::= LITERAL
    ::= "" ANY_CHAR

CHARSET_COMPONENT
    ::= CHAR
    ::= CHAR "-" CHAR

CHARSET
    ::= CHAR
    ::= "[" ["^"] { CHARSET_COMPONENT } "]"

REGEX_0
    ::= CHARSET
    ::= REGEX_0 "+"
    ::= REGEX_0 "*"
    ::= REGEX_0 "{" { DIGIT } ["," [ { DIGIT } ]] "}"
    ::= "(" REGEX_2 ")"

REGEX_1
    ::= REGEX_0
    ::= REGEX_1 REGEX_0

REGEX_2
    ::= REGEX_1
    ::= REGEX_2 "|" REGEX_1

REGULAR_EXPRESSION
    ::= REGEX_2
复制代码

这只是随手写出来的语法,尽管可能不是那么严谨,但是代表了正则表达式的所有结构。为什么我们要熟练掌握EBNF的阅读和编写?因为当我们用EBNF来看待我们的语言的时候,我们就不会被愈发的表面所困扰,我们会投过语法的外衣,看到语言本身的结构。脱别人衣服总是很爽的。

于是我们也要透过EBNF来看到正则表达式本身的结构。其实这是一件很简单的事情,只要把EBNF里面那些“fuck”这样的字符字面量去掉,然后规则就会分为两种:

1:规则仅由终结符构成——这是基本概念,譬如说上面的CHAR什么的。 
2:规则的构成包含非终结符——这就是一个结构了。

我们甚至可以利用这种方法迅速从EBNF确定出我们需要的语法树长什么样子。具体的方法我就不说了,大家自己联系一下就会悟到这个简单粗暴的方法了。但是,我们在设计DSL的时候,是要反过来做的。首先确定语言的结构,翻译成语法树,再翻译成不带“fuck”的“骨架EBNF”,再设计具体的细节写成完整的EBNF

看到这里大家会觉得,其实正则表达式的结构跟四则运算式子是没有区别的。正则表达式的*是后缀操作符,|是中缀操作符,连接也是中最操作符——而且操作符是隐藏的!我猜perl系正则表达式的作者当初在做这个东西的时候,肯定纠结过“隐藏的中缀操作符”应该给谁的问题。不过其实我们可以通过收集一些素材,用不同的方案写出正则表达式,最后经过统计发现——隐藏的中缀操作符给连接操作是最靠谱的。

为什么呢?我们来举个例子,如果我们把连接和分支的语法互换的话,那么原本“fuck|you”就要写成“(f|u|c|k)(y|o|u)”了。写多几个你会发现,的确连接是比分支更常用的,所以短的那个要给连接,所以连接就被分配了一个隐藏的中缀操作符了。

上面说了这么多废话,只是为了说明白一个道理——要先从结构入手然后才设计语法,并且要把最短的语法分配给最常用的功能。因为很多人设计DSL都反着来,然后做成了屎。

二、Fpmacro

第二个要讲的是Fpmacro。简单来说,Fpmacro和C++的宏是类似的,但是C++的宏是从外向内展开的,这意味着dynamic scoping和call by name。Fpmacro是从内向外展开的,这意味着lexical scoping和call by value。这些概念我在第七篇文章已经讲了,大家也知道C++的宏是一件多么不靠谱的事情。但是为什么我要设计Fpmacro呢?因为有一天我终于需要类似于Boost::Preprocessor那样子的东西了,因为我要生成类似这样的代码。但是C++的宏实在是太他妈恶心了,恶心到连我都不能驾驭它。最终我就做出了Fpmacro,于是我可以用这样的宏来生成上面提到的文件了。

我来举个例子,如果我要生成下面的代码:

int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
cout<<a1<<a2<<a3<<a4<<endl;

就要写下面的Fpmacro代码:

复制代码
$$define $COUNT 4 /*定义数量:4*/
$$define $USE_VAR($index) a$index /*定义变量名字,这样$USE_VAR(10)就会生成“a10”*/

$$define $DEFINE_VAR($index) $$begin /*定义变量声明,这样$DEFINE_VAR(10)就会生成“int a10 = 10;”*/
int $USE_VAR($index) = $index;
$( ) /*用来换行——会多出一个多余的空格不过没关系*/
$$end

$loop($COUNT,1,$DEFINE_VAR) /*首先,循环生成变量声明*/
cout<<$loopsep($COUNT,1,$USE_VAR,<<)<<endl; /*其次,循环使用这些变量*/
复制代码

顺便,Fpmacro的语法在这里,FpmacroParser.h/cpp是由这个语法生成的,剩下的几个文件就是C++的源代码了。不过因为今天讲的是如何设计DSL,那我就来讲一下,我当初为什么要把Fpmacro设计成这个样子。

在设计之前,首先我们需要知道Fpmacro的目标——设计一个没有坑的宏,而且这个宏还要支持分支和循环。那如何避免坑呢?最简单的方法就是把宏看成函数,真正的函数。当我们把一个宏的名字当成参数传递给另一个宏的时候,这个名字就成为了函数指针。这一点C++的宏是不可能完全的做到的,这里的坑实在是太多了。而且Boost::Preprocessor用来实现循环的那个技巧实在是我操太他妈难受了。

于是,我们就可以把需求整理成这样:

  1. Fpmacro的代码由函数组成,每一个函数的唯一目的都是生成C++代码的片段。
  2. 函数和函数之间的空白可以用来写代码。把这些代码收集起来就可以组成“main函数”了,从而构成Fpmacro代码的主体。
  3. 函数可以有内部函数,在代码复杂的时候可以充当一些namespace的功能,而且内部函数都是私有的。
  4. Fpmacro代码可以include另一份Fpmacro代码,可以实现全局配置的功能。
  5. Fpmacro必须支持分支和循环,而且他们的语法和函数调用应该一致。
  6. 用来代表C++代码的部分需要的转义应该降到最低。
  7. 即使是非功能代码部分,括号也必须配对。这是为了定义出一个清晰的简单的语法,而且因为C++本身也是括号配对的,所以这个规则并没有伤害。
  8. C++本身对空格是有很高的容忍度的,因此Fpmacro作为一个以换行作为分隔符的语言,并不需要具备特别精确的控制空格的功能。

为什么要强调转义呢?因为如果用Fpmacro随便写点什么代码都要到处转义的话,那还怎么写得下去呀!

这个时候我们开始从结构入手。Fpmacro的结构是简单的,只有下面几种:

  1. 普通C++代码
  2. 宏名字引用
  3. 宏调用
  4. 连接
  5. 括号
  6. 表达数组字面量(最后这被证明是没有任何意义的功能)

根据上面提到的DSL三大原则,我们要给最常用的功能配置最短的语法。那最短的功能是什么呢?跟正则表达式一样,是连接。所以要给他一个隐藏的中缀运算符。其次就要考虑到转义了。如果Fpmacro大量运用的字符与C++用到的字符一样,那么我们在C++里面用这个字符的时候,就得转义了。这个是绝对不能接受的。我们来看看键盘,C++没用到的也就只有@和$了。这里我因为个人喜好,选择了$,它的功能大概跟C++的宏里面的#差不多。

那我们如何知道我们的代码片段是访问一个C++的名字,还是访问一个Fpmacro的名字呢?为了避免转义,而且也顺便可以突出Fpmacro的结构本身,我让所有的Fpmacro名字都要用$开头,无论是函数名还是参数都一样。于是定义函数就用$$define开始,而且多行的函数还要用$$begin和$$end来提示(见上面的例子)。函数调用就可以这么做:$名字(一些参数)。因为不管是参数名还是函数名都是$开头的,所以函数调用肯定也是$开头的。那写出来的代码真的需要转义怎么办呢?直接用$(字符)就行了。这个时候我们可以来检查一下这样做是不是会定义出歧义的语法,答案当然是不会。

我们定义了$作为Fpmacro的名字前缀之后,是不是一个普通的C++代码(因此没有$),直接贴上去就相当于一个Fpmacro代码呢?结论当然是成立的。仔细选择这些语法可以让我们在只想写C++的时候可以专心写C++而不会被各种转义干扰到(想想在C++里面写正则表达式的那一堆斜杠卧槽)。

到了这里,就到了最关键的一步了。那我们把一个Fpmacro的名字传递给参数的时候,究竟是什么意思呢?一个Fpmacro的名字,要么就是一个字符串,要么就是一个Fpmacro函数,不会有别的东西了(其实还可能是数组,但是最后证明没用)。这个纯洁性要一直保持下去。就跟我们在C语言里面传递一个函数指针一样,不管传递到了哪里,我们都可以随时调用它。

那Fpmacro的函数到底有没有包括上下文呢?因为Fpmacro和pascal一样有“内部函数”,所以当然是要有上下文的。但是Fpmacro的名字都是只读的,所以只用shared_ptr来记录就可以了,不需要出动GC这样的东西。关于为什么带变量的闭包就必须用GC,这个大家可以去想一想。这是Fpmacro的函数像函数式语言而不是C语言的一个地方,这也是为什么我把名字写成了Fpmacro的原因了。

不过Fpmacro是不带lambda表达式的,因为这样只会把语法搞得更糟糕。再加上Fpmacro允许定义内部函数和Fpmacro名字是只读的这两条规则,所有的lambda表达式都可以简单的写成一个内部函数然后赋予它一个名字。因此这一点没有伤害。那什么时候需要传递一个Fpmacro函数呢进另一个函数呢?当然就只有循环了。Fpmacro的内置函数有分支循环还有简单的数值计算和比较功能。

我们来做一个小实验,生成下面的代码:

复制代码
void Print(int a1)
{
    cout<<"1st"<<a1<<endl;
}

void Print(int a1, int a2)
{
    cout<<"1st"<<a1<<", "<<"2nd"<<a2<<endl;
}

....

void Print(int a1, int a2, ... int a10)
{
    cout<<...<<"10th"<<a10<<endl;
}

....
复制代码

我们需要两重循环,第一重是生成Print,第二重是里面的cout。cout里面还要根据数字来产生st啊、nd啊、rd啊、这些前缀。于是我们可以开始写了。Fpmacro的写法是这样的,因为没有lambda表达式,所以循环体都是一些独立的函数。于是我们来定义一些函数来生成变量名、参数定义和cout的片段:

复制代码
$$define $VAR_NAME($index) a$index /*$VAR_NAME(3) -> a3*/
$$define $VAR_DEF($index) int $VAR_NAME($index) /*$VAR_DEF(3) -> int a3*/
$$define $ORDER($index) $$begin /*$ORDER(3) -> 3rd*/
    $$define $LAST_DIGIT $mod($index,10)
    $index$if($eq($LAST_DIGIT,1),st,$if($eq($LAST_DIGIT,2),nd,$if($eq($LAST_DIGIT,3),rd,th)))
$$end
$$define $OUTPUT($index) $(")$ORDER($index)$(")<<$VAR_NAME($index) /*$OUTPUT(3) -> "3rd"<<a3*/
复制代码

接下来就是实现Print函数的宏:

复制代码
$$define $PRINT_FUNCTION($count) $$begin
void Print($loopsep($count,1,$VAR_DEF,$(,)))
{
    cout<<$loopsep($count,1,$OUTPUT,<<)<<endl;
}
$( ) $$end
复制代码

最后就是生成整片代码了:

$define $COUNT 10 /*就算是20,那上面的代码的11也会生成11st,特别方便*/
$loop($COUNT,1,$PRINT_FUNCTION)

注意:注释其实是不能加的,因为如果你加了注释,这些注释最后也会被生成成C++,所以上面那个$COUNT就会变成10+空格+注释,他就不能放进$loop函数里面了。Fpmacro并没有添加“Fpmacro注释”的代码,因为我觉得没必要

为什么我们不需要C++的宏的#和##操作呢?因为在这里,A(x)##B(x)被我们处理成了$A(x)$B(x),而L#A(x)被我们处理成了L$(“)$A(x)$(“)。虽然就这么看起来好像Fpmacro长了一点点,但是实际上用起来是特别方便的。$这个前缀恰好帮我们解决了A(x)##B(x)的##的问题,写的时候只需要直接写下去就可以了,譬如说$ORDER里面的$index$if…。

那么这样做到底行不行呢?看在Fpmacro可以用这个宏来生成这么复杂的代码的份上,我认为“简单紧凑”和“C++代码几乎不需要转义”和“没有坑”这三个目标算是达到了。DSL之所以为DSL就是因为我们是用它来完成特殊的目的的,不是general purpose的,因此不需要太复杂。因此设计DSL要有一个习惯,就是时刻审视一下,我们是不是设计了多余的东西。现在我回过头来看,Fpmacro支持数组就是多余的,而且实践证明,根本没用上。

大家可能会说,代码遍地都是$看起来也很乱啊?没关系,最近我刚刚搞定了一个基于语法文件驱动的自动着色和智能提示的算法,只需要简单地写一个Fpmacro的编辑器就可以了,啊哈哈哈哈。

三、尾声

本来我是想举很多个例子的,还有语法文件啊,GUI配置啊,甚至是SQL什么的。不过其实设计一个DSL首先要求你对领域本身有着足够的理解,在长期的开发中已经在这个领域里面感受到了极大的痛苦,这样你才能真的设计出一个专门根除痛点的DSL来。

像正则表达式,我们都知道手写字符串处理程序经常要人肉做错误处理和回溯等工作,正则表达式帮我们自动完成了这个功能。

C++的宏生成复杂代码的时候,动不动就会因为dynamic scoping和call by name掉坑里而且还没有靠谱的工具来告诉我们究竟要怎么做,Fpmacro就解决了这个问题。

开发DSL需要语法分析器,而且带Visitor模式的语法树可扩展性好但是定义起来特别的麻烦,所以我定义了一个语法文件的格式,写了一个ParserGen.exe(代码在这里)来替我生成代码。Fpmacro的语法分析器就是这么生成出来的。

GUI的构造代码写起来太他妈烦了,所以还得有一个配置的文件。

查询数据特别麻烦,而且就算是只有十几个T的小型数据库也很难自己设计一个靠谱的容器,所以我们需要SQLServer。这个DSL做起来不简单,但是用起来简单。这也是一个成功的DSL。

类似的,Visual Studio为了生成代码还提供了T4这种模板文件。这个东西其实超好用的——除了用来生成C++代码,所以我还得自己撸一个Fpmacro……

用MVC的方法来写HTML,需要从数据结构里面拼HTML。用过php的人都知道这种东西很容易就写成了屎,所以Visual Studio里面又在ASP.NET MVC里面提供了razor模板。而且他的IDE支持特别号,razor模板里面可以混着HTML+CSS+Javascript+C#的代码,智能提示从不出错!

还有各种数不清的配置文件。我们都知道,一个强大的配置文件最后都会进化成为lisp,哦不,DSL的。

这些都是DSL,用来解决我们的痛点的东西,而且他本身又不足以复杂到用来完成程序所有的功能(除了连http service都能写的SQLServer我们就不说了=_=)。设计DSL的时候,首先要找到痛点,其次要理清楚DSL的结构,然后再给他设计一个要么紧凑要么可读性特别高的语法,然后再给一个简单的API,用起来别提多爽了。

编译器相关
 
如何设计一门语言(十)——正则表达式与领域特定语言(DSL)
摘要: 几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情。 创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的。我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了。当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态。虽然这个游戏需要脚本在剧情...阅读全文
posted @ 2013-09-16 09:27 陈梓瀚(vczh) 阅读(397) | 评论 (2) 编辑
 
如何设计一门语言(九)——类型
摘要: 类型是了解编程语言的重要一环。就算是你喜欢动态类型语言,为了想实现一个靠谱的东西,那也必须了解类型。举个简单的例子,我们都知道+和-是对称的——当然这只是我们的愿望了,在javascript里面,"1"+2和"1"-2就不是一回事。这就是由于不了解类型的操作而犯下的一些滑稽的错误。什么,你觉得因为"1"的类型是string所以"1"+2就应该是"12"?啐!"1"的类型是(string | number),这才是正确的做法。了解编程语言的基本原理并不意味着你一定要成为一名编译阅读全文
posted @ 2013-08-17 16:27 陈梓瀚(vczh) 阅读(1941) | 评论 (5) 编辑
 
如何设计一门语言(八)——异步编程和CPS变换
摘要: 关于这个话题,其实在(六)里面已经讨论了一半了。学过Haskell的都知道,这个世界上很多东西都可以用monad和comonad来把一些复杂的代码给抽象成简单的、一看就懂的形式。他们的区别,就像用js做一个复杂的带着几层循环的动画,直接写出来和用jquery的“回调”写出来的代码一样。前者能看不能用,后者能用不能看。那有没有什么又能用又能看的呢?我目前只能在Haskell、C#和F#里面看到。至于...阅读全文
posted @ 2013-07-27 11:13 陈梓瀚(vczh) 阅读(1168) | 评论 (5) 编辑
 
如何设计一门语言(七)——闭包、lambda和interface
摘要: 人们都很喜欢讨论闭包这个概念。其实这个概念对于写代码来讲一点用都没有,写代码只需要掌握好lambda表达式和class+interface的语义就行了。基本上只有在写编译器和虚拟机的时候才需要管什么是闭包。不过因为系列文章主题的缘故,在这里我就跟大家讲一下闭包是什么东西。在理解闭包之前,我们得先理解一些常见的argument passing和symbol resolving的规则。首先第一个就是call by value了。这个规则我们大家都很熟悉,因为流行的语言都是这么做的。大家还记得刚开始学编程的时候,书上总是有一道题目,说的是:void Swap(int a, int b){ in...阅读全文
posted @ 2013-07-05 22:32 陈梓瀚(vczh) 阅读(4336) | 评论 (19) 编辑
 
时隔多年我又再一次体验了一把跟大神聊天的感觉
摘要: 跟大神聊天是很开心的。这不是因为我激动,而是因为大神说出来的每一个字都是有价值的,一针见血,毫无废话。至于为什么说又,当然是这种事情以前发生过。第一次是在高中认识了龚敏敏。那个时候我刚做完那个傻逼的2D ARPG不久,龚敏敏已经是M$RA的实习生了,图形学上的造诣肯定要比我高许多,其中的差距构成了大神跟菜鸟的关系。当然现在我尽管中心已经放在了程序设计语言(programming language,以下简称PL)上,但是还知道一些图形学的内容,跟龚敏敏的差距自然也已经缩小到了不构成大神和菜鸟的关系的程度了。尽管他还是比我多知道很多东西。第二次是在大学的时候认识了g9yuayon。g9菊苣是做形式阅读全文
posted @ 2013-06-26 01:19 陈梓瀚(vczh) 阅读(5093) | 评论 (32) 编辑
 
如何设计一门语言(六)——exception和error code
摘要: 我一直以来对于exception的态度都是很明确的。首先exception是好的,否则就不会有绝大多数的语言都支持他了。其次,error code也没什么问题,只是需要一个前提——你的语言得跟Haskell一样有monad和comonad。你看Haskell就没有exception,大家也写的很开心。为什么呢?因为只要把返回带error code结果的函数给做成一个monad/comonad,那么...阅读全文
posted @ 2013-06-10 15:02 陈梓瀚(vczh) 阅读(1009) | 评论 (1) 编辑
 
如何设计一门语言(五)——面向对象和消息发送
摘要: 面向对象这个抽象的特例总是有说不完的话题,更糟糕的是很多语言都错误地实现了面向对象——class居然可以当一个变量类型什么的这只是让人们写代码写的更糟糕而已。当然这个话题第三篇文章已经说过了,现在来谈谈人们喜欢拿来装逼的另一个话题——消息发送。 按照惯例先来点题外话。说到消息发送,有些人喜欢跳出来说,objective-c的消息做得多优雅啊,代码都可以写成一句话[golang screw:you...阅读全文
posted @ 2013-05-25 11:08 陈梓瀚(vczh) 阅读(1239) | 评论 (4) 编辑
 
如何设计一门语言(四)——什么是坑(操作模板)
摘要: 其实我在写这个系列的第三篇文章的时候就已经发现,距离机器越远,也就是抽象越高的概念,坑的数量是越少的。但是这并不是说,距离机器越近的概念就越强大或者说越接近本质。这是广大的程序员对计算理论的一种误解。大多数人理解编程的知识结构的时候,都是用还原论来理解的,这个方法其实并没有错。但问题在于,“还原”的方法并不是唯一的。很多人觉得,反正你多高级的语言编译完了无非都是机器码嘛。但是还有另一种解释,你无论多低级的语言编译完了无非也就是带CPS变换(continuation passing style)的λ-calculus程序嘛。他们是等价的,不仅能力上也是,“本质”上也是。一个用CPS变换完整地处理阅读全文
posted @ 2013-05-12 16:34 陈梓瀚(vczh) 阅读(1402) | 评论 (5) 编辑
 
如何设计一门语言(三)——什么是坑(面向对象和异常处理)
摘要: 在所有的文字之前,我需要强调一下,我本人对structure typing持反对态度,所以就算文中的内容“看起来很像”go的interface,读者们也最好不要觉得我是在赞扬go的interface。我比较喜欢的是haskell和rust的那种手法。可惜rust跟go一样恨不得把所有的单词都缩成最短,结果代码写出来连可读性都没有了,单词都变成了符号。如果rust把那乱七八糟的指针设计和go的那种屎缩写一起干掉的话,我一定会很喜欢rust的。同理,COM这个东西设计得真是太他妈正确了,简直就是学习面向对象手法的最佳范例,可惜COM在C++下面操作起来有点傻逼,于是很多人看见这个东西就呵呵呵了。上阅读全文
posted @ 2013-05-05 11:29 陈梓瀚(vczh) 阅读(4693) | 评论 (16) 编辑
 
如何设计一门语言(二)——什么是坑(b)
摘要: 我从来没有在别的语言的粉里面看见过这么容易展示人性丑陋一面的粉,就算是从十几年前开始的C++和C对喷,GC和非GC对喷,静态类型动态类型对喷的时候,甚至是云风出来喷C++黑得那么惊天动地的时候,都没有发生过这么脑残的事情。这种事情只发生在go语言的脑残粉的身上,这究竟代表什么呢?想学go语言的人最好小心一点了,学怎么用go没关系,go学成了因为受不了跳到别的语言去也没关系,就算是抖M很喜欢被折腾所...阅读全文
posted @ 2013-04-28 18:28 陈梓瀚(vczh) 阅读(3134) | 评论 (26) 编辑
 
如何设计一门语言(一)——什么是坑(a)
摘要: 这个系列的起因是这样的,王垠写了一篇喷go的博客http://www.yinwang.org/blog-cn/2013/04/24/go-language/,里面说go已经烂到无可救药了,已经懒得说了,所以让大家去看http://www.mindomo.com/view.htm?m=8cc4f95228f942f8886106d876d1b041,里面有详细的解释。然后这篇东西被发上了微博,很多博...阅读全文
posted @ 2013-04-27 17:28 陈梓瀚(vczh) 阅读(5482) | 评论 (32) 编辑
 
可配置语法分析器开发纪事(六)——构造一个真正能用的状态机(下)
摘要: 上一篇文章对大部分文法都构造出了一个使用的状态机了,这次主要来讲右递归的情况。右递归不像左递归那么麻烦,因为大部分右递归写成循环也不会过分的让语法树变得难以操作,不过仍然有少数情况是我们仍然希望保留递归的语法树形状,譬如C++的连等操作,因此这里就来讲一下这个问题。 右递归是怎么形成的呢?在这里我们先不想这个问题,我们来看一个普通的文法。在上一篇文章我们已经说过了,如果一条文法有一个非终结符引用...阅读全文
posted @ 2013-04-13 09:49 陈梓瀚(vczh) 阅读(883) | 评论 (1) 编辑
 
可配置语法分析器开发纪事(五)——构造一个真正能用的状态机(中)
摘要: 上一篇博客写到了如何给一个非终结符的文法规则构造出一个压缩过的下推状态机,那么今天说的就是如何把所有的文法都连接起来。其实主要的idea在(三)和他的勘误(三点五)里面已经说得差不多了。但是今天我们要处理的是带信息的transition,所以还有一些地方要注意。所以在这里我们先把几条文法的最后的状态机都列出来(大图):接下来的这一步,就是要对所有靠非终结符(Exp啊Term这些)进行跳转的transition都执行上一篇文章所说的传说中的交叉链接。在产生链接的时候,我们给shift和reduce的边分别加上shift和reduce。而shift和reduce是有参数的——就是被shift走的状阅读全文
posted @ 2013-01-01 15:55 陈梓瀚(vczh) 阅读(1039) | 评论 (0) 编辑
 
可配置语法分析器开发纪事(四)——构造一个真正能用的状态机(上)
摘要: 本来说这一篇文章要把构造确定性状态机和look ahead讲完的,当我真正要写的时候发现东西太多,只好分成两篇了。上一篇文章说道一个基本的状态机是如何构造出来的,但是根据第一篇文章的说法,这一次设计的文法是为了直接构造出语法树服务的,所以必然在执行状态机的时候就要获得构造语法树的一切信息。如果自己开发过类似的东西就会知道,类似LALR这种东西,你可以很容易的把整个字符串分析完判断他是不是属于这个LALR状态机描述的这个集合,但是你却不能拿到语法分析所走的路径,也就是说你很难直接拿到那颗分析树。没有分析树肯定是做不出语法树的。因此我们得把一些信息插入到状态机里面,才能最终把分析树(并不一定真的要阅读全文
posted @ 2012-12-23 00:30 陈梓瀚(vczh) 阅读(1059) | 评论 (0) 编辑
 
可配置语法分析器开发纪事(三点五)——生成下推自动机的具体步骤
摘要: 刚刚发了上一篇文章之后就发现状态机画错了。虽然LiveWriter有打开博客并修改文章的功能,不过为了让我留下一个教训,我还是决定发一篇勘误。这个教训就是,作分析的时候不要随便“跳步”,该一步一步来就一步一步来。其实人呢,就是很容易忘掉以前的教训的了。第一个告诉我不能这么干的人其实是小学三年级的数学老师。当时我因为懒得写字,所以计算应用题的时候省了几步,被批评了。故事就从状态机开始。文法我就不重复了,见上一篇文章。现在我们从状态机开始。第一个状态机是直接从文法变过来的:然后我们把所有的非终结符跳转都通过Shift和Reduce连接到该非终结符所代表的状态机的状态上面,就会变成下面的图。具体的做阅读全文
posted @ 2012-12-07 18:50 陈梓瀚(vczh) 阅读(295) | 评论 (0) 编辑
 
可配置语法分析器开发纪事(三)——生成下推自动机
摘要: 上一篇博客讲到了构造符号表的事情。构造完符号表之后,就要进入语义分析的后一个阶段了:构造状态机。跟我以前写的如何实现正则表达式引擎的两篇文章讲的一样,自动机先从Epsilon Nondeterministic Automaton开始,然后一步一步构造成Deterministic Automaton。但是语法分析和正则表达式有很大不同,那么这个自动机是什么样子的呢?(对学术感兴趣的人可以去wiki一下“下推自动机”)下推自动机和有限自动机的区别是,下推自动机扩展成普通的自动机的时候,他的状态的数目是无限的(废话)。但是无限的东西是没办法用编程来表达的,那怎么办呢?那就加入一个不定长度的“状态描述阅读全文
posted @ 2012-12-07 16:44 陈梓瀚(vczh) 阅读(1066) | 评论 (3) 编辑
 
可配置语法分析器开发纪事(二)——构造符号表
摘要: 上一篇博客讲到了构造语法树的问题。有朋友在留言问我,为什么一定要让语法分析器产生语法树,而不是让用户自己决定要怎么办呢?在这里我先解答这个问题。 1、大部分情况下都是真的需要有语法树 2、如果要直接返回计算结果之类的事情的话,只需要写一个visitor运行一下语法树就好了,除去自动生成的代码以外(反正这不用人写,不计入代价),代码量基本上没什么区别 3、加入语法树可以让文法本身描述...阅读全文
posted @ 2012-11-29 00:51 陈梓瀚(vczh) 阅读(1230) | 评论 (7) 编辑
 
可配置语法分析器开发纪事(一)——构造语法树
摘要: 就像之前的博客文章所说的,(主要还是)因为GacUI的原因,我决定开发一个更好的可配置轻量级语法分析器来代替之前的落后的版本。在说这个文章之前,我还是想在此向大家推荐一本《编程语言实现模式》,这的确是一本好书,让我相见恨晚。 其实说到开发语法分析器,我从2007年就已经开始在思考类似的问题了。当时C++还处于用的不太熟练的时候,难免会做出一些傻逼的事情,不过总的来说当年的idea还是能用的。从那...阅读全文
posted @ 2012-11-21 22:46 陈梓瀚(vczh) 阅读(1768) | 评论 (6) 编辑
 
做了一个画f(x,y)=0函数图像的算法,果断codeplex之
摘要: 代码上传到了http://vlpp.codeplex.com/的CandidateGamesFunctionVisualizer文件夹下面,VS2010,.NET 4.0。做这个的目的只要是前几天看到了batman equaltion,然后浑身不舒服,因此就想起了这片新闻(http://news.cnblogs.com/n/106212/)里面的东西。就花了一个晚上和一个早上的时间做了出来。当然这里面有点瑕疵,不过大概还是好的。 在CandidateGamesFunctionVisualizerFvCalculationRawExpression.cs里面可以看到用来表达函数的语.阅读全文
posted @ 2011-08-11 15:05 陈梓瀚(vczh) 阅读(1848) | 评论 (9) 编辑
 
Vczh Library++ 语法分析器开发指南
摘要: Vczh Library++ 语法分析器开发指南陈梓瀚前言在日常的开发工作中我们总是时不时需要写一些语法分析器。语法分析器不一定指的是一门语言的编译器前端,也有可能仅仅是一个自己设计格式的配置文件的读写程序,或者是一门用来简化我们开发的DSL(领域专用语言)。我们可以选择使用XML,不过因为XML的噪音实在是太多,所以自己写语法分析器在有些情况下是必要的,特别是那种经常需要修改的文件,使用XML有...阅读全文
posted @ 2010-04-28 13:46 陈梓瀚(vczh) 阅读(1163) | 评论 (2) 编辑
 
Vczh Library++3.0 开源啦!
摘要: 项目主页:http://vlpp.codeplex.com/ Vczh Library++从2006年就开始开发,到现在经历了一些版本变迁,到现在已经正式步入3.0了。现在Vczh Library++ 3.0的基础部分已经成型,我的目标是将Vczh Library++ 3.0做成一个在性能不是极端苛刻情况下使用的数据处理库,附带一个高速的脚本引擎。未来可能会提供更多的东西,但主要围绕着这两个目标走...阅读全文
posted @ 2009-12-31 11:06 陈梓瀚(vczh) 阅读(741) | 评论 (2) 编辑
 
Syngram Helper实验:读入文法文件动态生成语法分析器
摘要: 我们知道Yacc和Bison都是产生C++的代码作为编译器的前端的。但是有时候我们需要动态地产生一个编译器前端,极端一点讲,譬如“文法调试器”。调试器总不能动态生成.y文件,让yacc编译,让gcc再度编译,然后execute,最后将程序的输出结果读进来。这样就太麻烦了,于是我们需要重新写一个生成编译器前端的程序。阅读全文
posted @ 2008-09-06 18:56 陈梓瀚(vczh) 阅读(1363) | 评论 (2) 编辑
 
Syngram Helper开始设计:一个能用来写编译器的工具
摘要: 大概一年前曾经用C++开发了一个可以在C++中直接写上下文无关文法的上下文无关文法分析器。这玩意儿叫Syngram。Syngram曾经做了两次,第一次做成了用一个类去读文法文件,后来不爽就改成了直接在C++里面写的。我弄了一个叫Term的类,重载了一些操作符,于是你可以搞分支、可选、错误处理等复杂的文法推导式。现在打算做一个周边工具。阅读全文
posted @ 2008-08-27 21:07 陈梓瀚(vczh) 阅读(2591) | 评论 (18) 编辑
原文地址:https://www.cnblogs.com/Leo_wl/p/3324157.html