疯狂极客前传:用最快的速度设计一种新的编程语言

最近打算写一些列有趣、而且有一定难度的文章。这个系列的名字就叫《疯狂极客》,这一系列的文章大多数与计算机有密切的关系。包括制作编译器、制作OS、Android控制电路板、机器人的制作(通过Android、IOS等设备控制)等等。

    源代码下载 

    在正式开始《疯狂极客》系列文章之前,先来热热身。用最短的时间设计一种简单,但好玩的编程语言CShell(不过不用担心,实现CShell解析器基本上用不着编译原理的知识,但以后的文章就会涉及到很多编译原理的内容了)。从CShell的名称可以猜到,是一种C风格的语言,并且可以像Shell一样解释执行(动态语言)。当然,这种语言不可能像C语言或Shell一样强大,因为C语言的编译器实现起来尽管不复杂(因为是结构化编程语言,没有类、接口这些东西,实现起来要比Java编译器简单得多),但仍然不太可能在很短的时间内完成(一至两天)。不过本文实现的CShell尽管简单,但仍然可以实现一些算法。CShell语言支持输出值和变量、条件语句(if),for循环,自加、自减、+、-、*、/操作,函数(支持递归)。由于CShell是动态语言,所以不需要声明变量,不过支持全局和局部变量,当然,还支持数组(整数、字符串类型数组),所以使用CShell可以很容易实现像冒泡排序、阶乘等算法。

      在讨论CShell的设计原理和实现过程之前,先看一些用CShell编写的程序。单从这些程序所完成的工作来看都太太太简单了,不过这回完全不同,这回是用我们自己发明的新语言来实现这些算法,例如,递归阶乘计算、冒泡排序,是不是很酷呢!!Let’s go!

//  简单的变量输出
xx= 45;
_ok = 64;
print (xx);
a1 = 65;
print (a1);

//  数组演示

$arr = [1,2,3,4,5,"aa"];  //  数值与字符串换搭的数组,$表示全局变量
print($arr);           //  输出数组的所有元素
print($arr[2]);         //  输出数组的第3个元素

//  三重for循环
$x = 0;  //  全局变量
//  i、j、z都为局部变量,for循环外不可访问
for(i=0;i<10;i=i+1)
{
    for(j = 0; j < 10; j=j+1)
    {
        for(z = 0; z < 10; z = z + 1)
        {
            $x = $x + 1;
        }
    }
}
print($x);   //  输出1000

//  计算10的阶乘,涉及到函数的递归操作和if语句

def jc(n)
{
    if(n == 0)
    {
        return 1;
    }
    else if(n == 1)
    {
        return 1;
    }
    else
    {
        return jc(n - 1) *n;

    }

}
print("10!");
print(jc(10));  //  计算10的阶乘(3628800)

//  冒泡排序(降序)
$arr = [5,3,1,7,5,4,-56,12];
$len = length($arr);
//  双循环冒泡排序
for(i = 0; i < $len; i++)
{
    for(j = 0; j < $len - 1; j++)
    {
        if($arr[j] < $arr[j+1])
        {
            x = $arr[j+1];
            $arr[j+1] = $arr[j];
            $arr[j] = x;
        }
    }
}

print($arr);  //  输出[12, 7, 5, 5, 4, 3, 1, -56]

如何设计和实现编程语言

       设计一种编程语言的方法很多,当然,通常的做法是要学好编译原理,然后按部就班地从词法分析器做起,然后是词法分析器、语义分析、中间代码生成、中间代码优化,目标代码生成,如果语言需要使用runtime运行,还需要编写可以运行目标代码的虚拟机(解释目标代码的程序,例如jvm就是解析Java字节码文件的虚拟机)。看着就有点晕。而且估计现在很多科班出身的程序员编译原理学的一塌糊涂。就算编译原理学的很好,光凭编译原理的理论,如果要想编写一个比较复杂的编译器或解析器也是很难办到的(尤其是加入面向对象功能)。这是因为一个复杂的编译器有很多代码几乎不太可能完全通过手工编写,例如,语法分析如果使用LL(*)分析方式,计算大量的first和follow集合就非常恐怖,就算把代码编写完了,如果要为语言增加或修改新的语法,修改这些代码将又是一场恶梦。所以大多数复杂的工业级编程语言都是通过半自动化的方式完成的。

      所谓半自动化,就是指不可能完全通过自动的方式生成编译器,而只能通过自动的方式生成编译器最核心的部分:词法分析器和语法分析器。基本的做法是通过DSL(domain-specific language )指定词法和语法的结构和必要的信息,然后编译器的编译器(生成编译器的程序)会根据DSL自动生成词法和语法解析器,当然,通过DSL可以增加语义部分的代码,这样生成的程序就直接拥有语义解析功能了。

      对于很多世界级的企业,如google、微软、intel、IBM,都会有自己的CC(编译器的编译器),不过对于个人或小企业,完全开发一套CC难度会很大(这东西比开发一套编译器的难度更大)。所以我们可以使用开源免费的CC。例如JavaCC、lex、yacc、antlr等。其中JavaCC只支持Java语言,lex是词法分析器的生成器、yacc是语法分析器的生成器,这两个支持从C语言,而antlr支持多种语言,如Java、C#、ruby、C/C++、JavaScript等等。所以本文使用Antlr来设计和实现CShell语言。

CShell语言是如何练成的

      尽管CShell依靠Antlr来实现,需要自己编写的代码仍然非常多,因此本文只介绍核心的代码和实现原理。更详细的代码请参考本文提供的源代码。

      学过编译原理的读者首先就会想到,设计语言首先就是进行词法分析,然后根据词法分析的结果进行语法分析。幸运的是,这两样都可以利用Antlr自动完成。

      所谓词法分析,就是将语言文本分成最小但愿的词素(称为Token)。例如,下面的是一段CShell语言的代码。

for(i = 0; i < $len; i++)
{
}

如果要对这段代码进行词法分析,就会分解成如下的一系列Token

“for”、“(”、“i”、“=”、“0”、“;”、“i”、“<”、“$len”、“;”、“i”、“++”、“{”、“}”

      当然,要想自己编程实现这个分析,就需要使用到有限自动机(DFA)进行处理,尽管程序不复杂,但还是比较麻烦的。有了Antlr,就容易得多了。通常只要定义这些Token的规则即可。有些Token是与语法规则放到一起的,有些是单独的词法规则。例如,上面代码中有两个变量(i和$len),其中i局部变量、$len为全局变量,这两个变量都属于标识符范畴,所以可以定义一个专门识别标识符号的词法规则。

ID  :   '$'?(LETTER|'_') (LETTER | '0'..'9')*  ;

    其中ID是词法规则名称,词法规则名称的第一个字母必须大写。LETTER表示26个小写和26个大写字母。“?”表示可以有,也可能没有,“*”为星闭包,表示重复0次到N次。

LETTER:   ('a'..'z' | 'A'..'Z')

     从ID的词法规则可以看出,ID就是可能以“$”开头,也可能没有“$”。不管有没有“$”,下一个字符必须是字母或下划线,接下来的字母或者是字母、或者是数字的任意字符串。例如abc、_xyz123、$_23都认为是ID。Antlr会自动根据这个规则生成Java代码。

其他的Token分析也采用类似的方法,例如,识别字符串可以使用下面的规则。

STRING:  '\"' .* '\"' ;

其中“.*”表示任意字符序列。也就是在CShell里一个字符串就是在两个双引号中的任意字符序列。

      词法处理完,就是相应的语法了,词法的分析结果是Token序列,而这个序列正式语法分析的输入。也就是说语法分析和词法分析的方式很像,只是词法分析的输入是单个字符序列,输出是Token序列。而语法分析的输入是Token序列,输出可能有多种,也可能没有输出,在分析的过程中就执行相应的动作(语义处理),也可能生成AST(抽象语法树),然后进一步对其进行优化。本例使用的是AST方式,也就是说将CShell源代码经过语法分析后转换为一颗AST,目的是去掉一些杂质,例如,for循环中只有i、$len、++等标识符和运算符号是有用的,但左右括号就没有任何用处,这些辅助符号是为了区分for语句和其他语句的。

这里只看一个稍微简单的if语句的语法规则。

statement :      'if' '(' expr ')' slist elseif_statement_all  else_statement?

其中slist是另外一个产生式,表示if和else if之间的部分。

slist   // 原内容: ':' NL (statement)+ '.' NL

        :         NL*'{' NL* (statement)* NL* '}'NL*     -> ^(BLOCK statement*)

        ;

      其中NL表示空行。而^(BLOCK statement*)部分表示AST,其中BLOCK为AST的根节点,从这一点可以看出,AST已经将slist中的左右大括号都过滤出去了,只剩下有实际意义的statement。

     从statement和slist的定义可以看出,if语句必须以“if”开头,Antlr会将if作为一个Token返回给语法分析器。然后紧跟着if的是左括号,接触是表达式(expr,另外一个产生式),然后就是if语句的执行体(slist),接着就是elseif部分,剩下的部分就与if部分的定义类似了,请读者参看源代码中的antlr/CShell.g文件。

     那么编写完Antlr需要的DSL,接下来做什么呢?接下来就要自己来做语义部分,这部分内容非常复杂,基本的思想就是通过语法分析将变量、关键字(for、if等)返回,然后由语义部分决定如何做。例如,对于变量,通常做法是定义一个符号表(使用Map对象即可),变量名就是Map的key,先将该变量存储在Map对象中。如果遇到某个变量,会首先到Map对象中查找,如果未找到,就定义该变量(将变量和变量值存入Map对象),如果找到,就直接去除变量值使用。至于for、if语句如何处理,就要利用语法分析生成的AST了。

     其中Interpreter类是分析的核心类,给类有一个exec方法,需要将AST的根节点传入该方法,也就是说执行CShell代码的过程就是遍历AST的过程,AST是多叉树,遍历需要使用广度优先方式遍历。exec方法的代码如下:

//  CShellAST表示AST节点的类型,一个普通Java类

public Object exec(CShellAST ast)
{
    try
    {
        switch (ast.getType())
        {
            case CShellParser.BLOCK:  //  处理块操作
                block(ast);
                break;
            case CShellParser.ASSIGN:  //  处理赋值操作
                assign(ast);
                break;
            case CShellParser.LENGTH:   //  处理返回长度操作
                return length(ast);
            case CShellParser.ARRAY:   //  处理数组操作
                arrayStat(ast);
                break;
            case CShellParser.RETURN:
                ret(ast);
                break;
            case CShellParser.PRINT:
                print(ast);
                break;
            case CShellParser.IF:      //  处理if语句
                ifstat(ast);
                break;
            case CShellParser.FOR:
                forloop(ast);
                break;
            case CShellParser.CALL:
                return call(ast);
            case CShellParser.ADD:
                return add(ast);
            case CShellParser.PREV:
            case CShellParser.SUFFIX:
                return incAndDec(ast);
            case CShellParser.SUB:
                return op(ast);
            case CShellParser.MUL:
            case CShellParser.DIV:
                return op(ast);
            case CShellParser.EQ:
                return eq(ast);
            case CShellParser.LT:
                return lt(ast);
            case CShellParser.GT:
                return gt(ast);
            case CShellParser.INT:
                return Integer.parseInt(ast.getText());
            case CShellParser.CHAR:
                return new Character(ast.getText().charAt(1));
            case CShellParser.FLOAT:
                return Float.parseFloat(ast.getText());
            case CShellParser.STRING:
                String s = ast.getText();
                return s.substring(1, s.length() - 1);
            case CShellParser.ID:
            case CShellParser.ARRAY_ELEMENT:
                return load(ast);
            default: // catch unhandled node types
                throw new UnsupportedOperationException("无法处理"
                        + ast.getText() + "<" + ast.getType() + ">");
        }
    }
    catch (Exception e)
    {
        listener.error("异常原因: " + ast.toStringTree(), e);
    }
    return null;
}

下面只看一个如何处理if语句的ifstat方法的实现代码

private void ifstat(CShellAST ast)
{
    //  下面的代码需要从当前AST节点(表示if语句根节点)的子节点获取
    //  if语句的各个组成部分

    //  获取if语句的两个圆括号直接的表达式部分
    CShellAST expr = (CShellAST) ast.getChild(0);
    //  获取if条件如果为true要执行的代码块
    CShellAST ifBlock = (CShellAST) ast.getChild(1);
    //  获取elseif的部分(包括条件表达式和要执行的块)
    CShellAST elseifAll = (CShellAST) ast.getChild(2);
    //  获取else部分要执行的代码块
    CShellAST elseBlock = (CShellAST) ast.getChild(3);
    //  利用递归方式再次调用exec方法执行表达式,并返回值
    Boolean c = (Boolean) exec(expr);
    //  如果为true,执行if block
    if (c.booleanValue())
    {
        exec(ifBlock);  //  递归执行if block
    }
    else
    {
        //  判断有多少个elseif部分,CShell支持有无限多个else if语句
        if (elseifAll.getChildCount() > 0)
        {
            List<CShellAST> children = elseifAll.getChildren();
            //  挨个判断else if后面的表达式是否为true
            for (CShellAST child : children)
            {
                expr = (CShellAST) child.getChild(0);
                ifBlock = (CShellAST) child.getChild(1);
                c = (Boolean) exec(expr);
                //  如果某个else if条件为true,直接执行else if后面的代码块,
                //  最后返回,剩下的都不执行了
                if (c.booleanValue())
                {
                    exec(ifBlock);
                    return;
                }
            }
        }
        //  最后会执行else语句(因为前面的条件都为false)

        //  判断是否有else语句(最多只能有1个else子句)
        if (elseBlock.getChildCount() == 1)
        {
            exec((CShellAST) elseBlock.getChild(0));  // 执行else block
        }
    }
}

       CShell代码分析器的入口类是CShell,在该类中调用了Interprefer.process方法读者CShell语言源代码。其中bubble.cs就是CShell语言的源代码文件,可以换成其他的源代码文件。调用process方法后,就会根据具体的CShell代码执行相应的操作。例如,print(…)语句会输出相应的字符串。

public class CShell
{
    public static void main(String[] args) throws Exception
    {
        InputStream input = null;
        input = new FileInputStream("source/bubble.cs");
        Interpreter interp = new Interpreter();
        interp.process(input);
    }
}

        如果读者对Antlr还不太理解也没关系,本文只是抛砖引玉,目的并不是讲解Antlr。只是希望读者对Antlr以及设计一种语言的过程有所了解。在后面的一系列文章中将会深度探讨编译原理以及Antlr的使用方法。通过设计自己的专有语言最大的作用是可以显著提高工作效率,例如,可以将常用的工作抽象成某些语句,到时只要一执行脚本就可完成需要数小时,甚至数天才能完成的工作。

时间: 2024-10-02 01:20:54

疯狂极客前传:用最快的速度设计一种新的编程语言的相关文章

打算写技术博客前传之家的寻找

尝试了多个博客最后安家在ITPUB,觉得这个界面简单,打开速度也可以,知名度也行,其它博客或多或少有自己不满意的地方,so就这个了,之后打算每天一篇博客,希望大家多多支持!      另外,觉得每个博客都有一个共同的缺点就是不能直接粘贴图片,这个对于写博客来说有点麻烦,我这人最怕这种麻烦事了...博客发展发展估计以后可以直接粘贴图片了

社交网络:迄今最火的极客电影

一部关于性.金钱与背叛的极客电影征服了从好莱坞到纽约的40份主流报刊. 文|CBN记者 杨樱 人人都对天才感兴趣,尤其当他是史上最年轻的亿万富翁的时候.10月1日,讲述Facebook创始人马克·扎克伯格创建这家公司历程的电影<社交网络>(The Social Network)在美国上映,并以2300万美元拔得周末票房头筹,而整部电影的投入不到5000万. <社交网络>的成功可能不仅仅在于奥斯卡获奖导演大卫·芬奇和编剧阿龙·索金联手,当然他们分别贡献了"快速利落的节奏&q

通过极客精神,在科技与时尚和人文的交界点切入

大家知道极客是什么样的一群人吗?可能很多人认为极客是技术的宅男宅女,或者是一些科学怪人.这对,但也不对.其实我们认为极客是有很多维度的,有些极客他们在技术上很高竿,有的极客只是喜欢好玩的科技产品和极致的体验.但是也有很了不起的极客在通过科技创新改变世界,比如说像苹果的乔布斯,比如Facebook的扎克伯格,还有谷歌的两位创始人,这些极客能够把世界打造成他们想要的模样. 在我们看来,极客是一群特别崇尚技术,特别热爱自由的一批人,他们有强烈的好奇心和探索欲,而且不会去跟随别人去打造世界,而是更希望自

来自极客标签10款最新设计素材

本周我们推荐来自极客标签社区带来的10款免费设计素材,大家可以在这里免费下载你需要的内容.如果你也有更好的作品,欢迎分享到社区中来,在得到帮助的同时,也能与更多人分享来自你的作品. 超棒的UI元素下载网站 - UI Elements http://www.gbtags.com/gb/share/476.htm UI Elements 是一个能够下载很多免费UI元素设计的网站.包含了导航.移动设备UI.布局设计.图表等等.相信你需要的UI设计,都能找到对应的解决方案! 免费图标: Sunday M

来自极客标签10款最新设计素材-系列十一

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 来源:GBin1.com 本周我们推荐来自极客标签社区带来的10款免费设计素材,大家可以在这里免费下载你需要的内容.如果你也有更好的作品,欢迎分享到社区中来,在得到帮助的同时,也能与更多人分享来自你的作品. iPad和iPhone 5的免费线框设计 来自Cat Smith的一套免费的ipad和iphone的线框设计,包含了相关PSD文件. 令

Z极客最疯狂 2013中国科技Geek汇召集令

2013年4月20日,由http://www.aliyun.com/zixun/aggregation/33107.html">中关村在线主办,微星.神州.凯迪拉克等知名厂商共同支持的"中国首届科技Geek汇"活动将在北京新世纪日航饭店(北京市海淀区首都体育馆南路6号)火爆开启! 届时世界顶级战队WE成员SKY将携队友共同出席,到场网友即可与战队互动PK,赢取万元奖金:更有"维多利亚秘密"等中外超模亲临现场,鼻血喷涌的同时听许喜占摄影大师传授人像拍摄

《R的极客理想——高级开发篇 A》一一2.4 R语言中的遗传算法

2.4 R语言中的遗传算法 问题 如何用R语言进行遗传算法的计算? 引言 人类总是在生活中摸索规律,把规律总结为经验,再把经验传给后人,让后人发现更多的规律,每一次知识的传递都是一次进化的过程,最终形成了人类的智慧.自然界的规律,让人类适者生存地活了下来,聪明的科学家又把生物进化的规律,总结成遗传算法,扩展到了更广的领域中.本节将带你走进遗传算法的世界.2.4.1 遗传算法介绍 遗传算法是一种解决最优化的搜索算法,是进化算法的一种.进化算法最初借鉴了达尔文的进化论和孟德尔的遗传学说,从生物进化的

对话唱吧陈华手记:从极客到CEO

1978年出生的陈华曾是一个典型的极客,大学时代,在http://www.aliyun.com/zixun/aggregation/34205.html">北京大学读书时就做出了天网Maze共享和搜索平台.在节目录制之前,一位摄像师说:我在大学时用过你的产品.陈华有些得意地说:那时的很多大学生都是我的用户. 他能敏锐地踩准时代的脉博.把握创业机会.他和我提起,1980年代,广东的经济是如何的活跃,但是1989年后,长达四五年时间,经济十分低迷,直到他1996年离开广东到北京上大学,才开始重

“爆炒”比特币:极客盛宴还是阴谋的开始

中介交易 SEO诊断 淘宝客 云主机 技术大厅 新快报记者 洪文锋 起源 做个"矿工"挖掘比特币 一般认为,比特币的起源来自一个名为"Satoshi Nakamoto"(中本聪)的"昵称".2008年,他发布了一个关于建立点对点电子金融系统的初衷,并将这种虚拟的货币称之为Bitcoin.他希望能创建一套"基于密码学原理而不基于信用,使得任何达成一致的双方,能够直接进行支付,从而不需要第三方中介的参与"的电子支付系统.对应于现实