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

简介:

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

    源代码下载 

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

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

 
  1. //  简单的变量输出 
  2. xx45
  3. _ok = 64
  4. print (xx); 
  5. a1 = 65
  6. print (a1); 
  7.   
  8. //  数组演示 
  9.   
  10. $arr = [1,2,3,4,5,"aa"];  //  数值与字符串换搭的数组,$表示全局变量 
  11. print($arr);           //  输出数组的所有元素 
  12. print($arr[2]);         //  输出数组的第3个元素 
  13.   
  14. //  三重for循环 
  15. $x = 0;  //  全局变量 
  16. //  i、j、z都为局部变量,for循环外不可访问 
  17. for(i=0;i<10;ii=i+1) 
  18.     for(j = 0; j < 10jj=j+1) 
  19.     { 
  20.         for(z = 0; z < 10zz = z + 1) 
  21.         { 
  22.             $x = $x + 1; 
  23.         }    
  24.     } 
  25. print($x);   //  输出1000 
  26.   
  27. //  计算10的阶乘,涉及到函数的递归操作和if语句 
  28.   
  29. def jc(n) 
  30.     if(n == 0) 
  31.     { 
  32.         return 1; 
  33.     } 
  34.     else if(n == 1) 
  35.     { 
  36.         return 1; 
  37.     } 
  38.     else 
  39.     { 
  40.         return jc(n - 1) *n; 
  41.           
  42.     } 
  43.       
  44. print("10!"); 
  45. print(jc(10));  //  计算10的阶乘(3628800) 
  46.   
  47. //  冒泡排序(降序) 
  48. $arr = [5,3,1,7,5,4,-56,12]; 
  49. $len = length($arr); 
  50. //  双循环冒泡排序 
  51. for(i = 0; i < $len; i++) 
  52.     for(j = 0; j < $len - 1; j++) 
  53.     { 
  54.         if($arr[j] < $arr[j+1]) 
  55.         {    
  56.             x = $arr[j+1]; 
  57.             $arr[j+1] = $arr[j]; 
  58.             $arr[j] = x; 
  59.         } 
  60.     } 
  61.   
  62. 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语言的代码。

 
  1. 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方法的代码如下:

 
  1. //  CShellAST表示AST节点的类型,一个普通Java类 
  2.   
  3. public Object exec(CShellAST ast) 
  4.     try 
  5.     { 
  6.         switch (ast.getType()) 
  7.         { 
  8.             case CShellParser.BLOCK:  //  处理块操作 
  9.                 block(ast); 
  10.                 break
  11.             case CShellParser.ASSIGN:  //  处理赋值操作 
  12.                 assign(ast); 
  13.                 break
  14.             case CShellParser.LENGTH:   //  处理返回长度操作 
  15.                 return length(ast); 
  16.             case CShellParser.ARRAY:   //  处理数组操作 
  17.                 arrayStat(ast); 
  18.                 break
  19.             case CShellParser.RETURN: 
  20.                 ret(ast); 
  21.                 break
  22.             case CShellParser.PRINT: 
  23.                 print(ast); 
  24.                 break
  25.             case CShellParser.IF:      //  处理if语句 
  26.                 ifstat(ast); 
  27.                 break
  28.             case CShellParser.FOR: 
  29.                 forloop(ast); 
  30.                 break
  31.             case CShellParser.CALL: 
  32.                 return call(ast); 
  33.             case CShellParser.ADD: 
  34.                 return add(ast); 
  35.             case CShellParser.PREV: 
  36.             case CShellParser.SUFFIX: 
  37.                 return incAndDec(ast); 
  38.             case CShellParser.SUB: 
  39.                 return op(ast); 
  40.             case CShellParser.MUL: 
  41.             case CShellParser.DIV: 
  42.                 return op(ast); 
  43.             case CShellParser.EQ: 
  44.                 return eq(ast); 
  45.             case CShellParser.LT: 
  46.                 return lt(ast); 
  47.             case CShellParser.GT: 
  48.                 return gt(ast); 
  49.             case CShellParser.INT: 
  50.                 return Integer.parseInt(ast.getText()); 
  51.             case CShellParser.CHAR: 
  52.                 return new Character(ast.getText().charAt(1)); 
  53.             case CShellParser.FLOAT: 
  54.                 return Float.parseFloat(ast.getText()); 
  55.             case CShellParser.STRING: 
  56.                 String s = ast.getText(); 
  57.                 return s.substring(1, s.length() - 1); 
  58.             case CShellParser.ID: 
  59.             case CShellParser.ARRAY_ELEMENT: 
  60.                 return load(ast); 
  61.             default// catch unhandled node types 
  62.                 throw new UnsupportedOperationException("无法处理" 
  63.                         + ast.getText() + "<" + ast.getType() + ">"); 
  64.         } 
  65.     } 
  66.     catch (Exception e) 
  67.     { 
  68.         listener.error("异常原因: " + ast.toStringTree(), e); 
  69.     } 
  70.     return null

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

 
  1. private void ifstat(CShellAST ast) 
  2.     //  下面的代码需要从当前AST节点(表示if语句根节点)的子节点获取 
  3.     //  if语句的各个组成部分 
  4.   
  5.     //  获取if语句的两个圆括号直接的表达式部分 
  6.     CShellAST expr = (CShellAST) ast.getChild(0); 
  7.     //  获取if条件如果为true要执行的代码块 
  8.     CShellAST ifBlock = (CShellAST) ast.getChild(1); 
  9.     //  获取elseif的部分(包括条件表达式和要执行的块) 
  10.     CShellAST elseifAll = (CShellAST) ast.getChild(2); 
  11.     //  获取else部分要执行的代码块 
  12.     CShellAST elseBlock = (CShellAST) ast.getChild(3); 
  13.     //  利用递归方式再次调用exec方法执行表达式,并返回值 
  14.     Boolean c = (Boolean) exec(expr); 
  15.     //  如果为true,执行if block 
  16.     if (c.booleanValue()) 
  17.     { 
  18.         exec(ifBlock);  //  递归执行if block 
  19.     } 
  20.     else 
  21.     { 
  22.         //  判断有多少个elseif部分,CShell支持有无限多个else if语句 
  23.         if (elseifAll.getChildCount() > 0
  24.         { 
  25.             List<CShellAST> children = elseifAll.getChildren(); 
  26.             //  挨个判断else if后面的表达式是否为true 
  27.             for (CShellAST child : children) 
  28.             { 
  29.                 expr = (CShellAST) child.getChild(0); 
  30.                 ifBlock = (CShellAST) child.getChild(1); 
  31.                 c = (Boolean) exec(expr); 
  32.                 //  如果某个else if条件为true,直接执行else if后面的代码块, 
  33.                 //  最后返回,剩下的都不执行了    
  34.                 if (c.booleanValue()) 
  35.                 { 
  36.                     exec(ifBlock); 
  37.                     return
  38.                 } 
  39.             } 
  40.         } 
  41.         //  最后会执行else语句(因为前面的条件都为false) 
  42.   
  43.         //  判断是否有else语句(最多只能有1个else子句) 
  44.         if (elseBlock.getChildCount() == 1
  45.         { 
  46.             exec((CShellAST) elseBlock.getChild(0));  // 执行else block 
  47.         } 
  48.     } 

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

 
  1. public class CShell 
  2. {   
  3.     public static void main(String[] args) throws Exception 
  4.     { 
  5.         InputStream input = null
  6.         input = new FileInputStream("source/bubble.cs"); 
  7.         Interpreter interp = new Interpreter(); 
  8.         interp.process(input); 
  9.     } 

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






 本文转自 androidguy 51CTO博客,原文链接:http://blog.51cto.com/androidguy/1157584,如需转载请自行联系原作者

相关文章
|
4月前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
76 1
|
4月前
|
算法 C语言 C++
C++语言学习指南:从新手到高手,一文带你领略系统编程的巅峰技艺!
【8月更文挑战第22天】C++由Bjarne Stroustrup于1985年创立,凭借卓越性能与灵活性,在系统编程、游戏开发等领域占据重要地位。它继承了C语言的高效性,并引入面向对象编程,使代码更模块化易管理。C++支持基本语法如变量声明与控制结构;通过`iostream`库实现输入输出;利用类与对象实现面向对象编程;提供模板增强代码复用性;具备异常处理机制确保程序健壮性;C++11引入现代化特性简化编程;标准模板库(STL)支持高效编程;多线程支持利用多核优势。虽然学习曲线陡峭,但掌握后可开启高性能编程大门。随着新标准如C++20的发展,C++持续演进,提供更多开发可能性。
92 0
|
4月前
|
JSON C# 开发者
💡探索C#语言进化论:揭秘.NET开发效率飙升的秘密武器💼
【8月更文挑战第28天】C#语言凭借其强大的功能与易用性深受开发者喜爱。伴随.NET平台演进,C#持续引入新特性,如C# 7.0的模式匹配,让处理复杂数据结构更直观简洁;C# 8.0的异步流则使异步编程更灵活高效,无需一次性加载全部数据至内存。通过示例展示了模式匹配简化JSON解析及异步流实现文件逐行读取的应用。此外,C# 8.0还提供了默认接口成员和可空引用类型等特性,进一步提高.NET开发效率与代码可维护性。随着C#的发展,未来的.NET开发将更加高效便捷。
67 1
|
5月前
编程之路:从代码到架构的心路历程
【7月更文挑战第9天】在数字世界的迷宫中,每一行代码都承载着创造者的梦想与挑战。本文将通过个人技术感悟的镜头,探索编程实践的深层次价值,从最初的代码编写到复杂的系统架构设计,揭示技术成长的内在逻辑和情感变迁。我们将一同穿梭在技术的森林里,寻找那些让代码生动起来的秘密。
37 2
|
7月前
|
算法 程序员
代码与哲学:从技术实践中汲取智慧
【2月更文挑战第18天】 在数字世界的构建过程中,代码不仅仅是一种实现功能的工具,它更是连接现实与理想的桥梁。本文将探讨编程实践如何映射出深刻的哲学思考,揭示通过技术探索所能领悟的人生智慧。我们将透过代码的表象,深入其背后的逻辑结构,从而理解编程不仅是一种职业技能,更是一种对世界认知和自我修炼的方式。
62 7
|
架构师 算法 测试技术
嵌入式系统软件架构设计(长篇深度好文)
嵌入式系统软件架构设计(长篇深度好文)
5045 1
|
存储 缓存 网络协议
强推Linux高性能服务器编程, 真的是后端开发技术提升, 沉淀自身不容错过的一本经典书籍
强推Linux高性能服务器编程, 真的是后端开发技术提升, 沉淀自身不容错过的一本经典书籍
强推Linux高性能服务器编程, 真的是后端开发技术提升, 沉淀自身不容错过的一本经典书籍
|
消息中间件 存储 缓存
架构之美-软件实现分析之道
理解一个实现,是以对模型和接口的理解为前提。 如果想了解一个系统的实现,应从软件结构和关键技术两个方面着手。无论是软件结构,还是关键技术,我们都需要带着自己的问题入手,而问题的出发点就是我们对模型和接口的理解。 了解软件的结构,其实,就是把分层的模型展开,看下一层模型: 要知道这个层次给你提供了怎样的模型 要带着自己的问题去了解这些模型为什么要这么设计 Kafka的实现主要是针对机械硬盘做的优化,现在的SSD硬盘越来越多,成本越来越低,这个立意的出发点已经不像以前那样稳固了。
143 0
架构之美-软件实现分析之道
|
程序员 开发工具 数据格式