﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-loop_in_codes-随笔分类-编译原理</title><link>http://www.cppblog.com/kevinlynx/category/13254.html</link><description>低调做技术__
C/C++\MMORPG服务器\模块架构__ TODO：编译原理\linux env __Kevin Lynx</description><language>zh-cn</language><lastBuildDate>Mon, 15 Mar 2010 17:01:19 GMT</lastBuildDate><pubDate>Mon, 15 Mar 2010 17:01:19 GMT</pubDate><ttl>60</ttl><item><title>[总结]LL(1)分析法及其实现</title><link>http://www.cppblog.com/kevinlynx/archive/2010/03/15/109765.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Mon, 15 Mar 2010 13:33:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2010/03/15/109765.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/109765.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2010/03/15/109765.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/109765.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/109765.html</trackback:ping><description><![CDATA[<p><font size=2>LL(1)分析法和递归下降分析法同属于自顶向下分析法。相对于递归下降而言，LL通过显示<br>地维护一个栈来进行语法分析，递归下降则是利用了函数调用栈。 </font>
<p><font size=2>LL分析法主要由分析栈、分析表和一个驱动算法组成。其实LL的分析算法还是很容易懂的，<br>主要就是一个匹配替换的过程。而要构造这里的分析表，则还涉及计算first集和follow集<br>的算法。 </font></p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/LL1_12DFD/1_2.jpg"><img style="BORDER-BOTTOM: 0px; BORDER-LEFT: 0px; DISPLAY: inline; BORDER-TOP: 0px; BORDER-RIGHT: 0px" title=1 border=0 alt=1 src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/LL1_12DFD/1_thumb.jpg" width=391 height=346></a> </p>
<p><font size=2>个人觉得龙书在解释这些算法和概念时都非常清楚细致，虽然也有人说它很晦涩。 </font>
<p><font size=2>first集和follow集的计算，抛开书上给的严密算法，用人的思维去理解（对于compiler<br>compiler则需要用程序去构造这些集合，这是让计算机去理解），其实很简单： </font>
<p><font size=2>1、对于某个非终结符A的first集（first(A)），简单地说就是由A推导得到的串的首符号的<br>集合：A-&gt;aB，那么这里的a就属于first(A)，很形象。<br>2、follow(A)，则是紧随A的终结符号集合，例如B-&gt;Aa，这里的a就属于follow(A)，也很形<br>象。 </font>
<p><font size=2>当然，因为文法符号中有epsilon，所以在计算上面两个集合时则会涉及到一种传递性。例<br>如，A-&gt;Bc, B-&gt;epsilon，B可以推导出epsilon，也就是基本等同于没有，那么first(A)中<br>就会包含c符号。 </font>
<p><font size=2>在了解了first集和follow集的计算方法后，则可以通过另一些规则构造出LL需要的分析表。 </font>
<p><font size=2>编译原理里总有很多很多的理论和算法。但正是这些理论和算法，使得编译器的实现变得简<br>单，代码易维护。 </font>
<p><font size=2>在某个特定的编程语言中，因为其文法一定，所以对于其LL(1)实现中的分析表就是确定的<br>。我们也不需要在程序里动态构造first和follow集合。 </font>
<p><font size=2>那么，要实现一个LL(1)分析法，大致步骤就集中于：设计文法-&gt;建立该文法的分析表-&gt;编<br>码。 </font>
<p><font size=2>LL分析法是不能处理左递归文法的，例如：expr-&gt;expr + term，因为左递归文法会让对应<br>的分析表里某一项存在多个候选式。这里，又会涉及到消除左递归的方法。这个方法也很简<br>单，只需要把文法推导式代入如下的公式即可： </font>
<p><font size=2>A -&gt; AB | C 等价于：A -&gt; CX, X -&gt; BX | epsilon </font>
<p><font size=2>最后一个问题是，如何在LL分析过程中建立抽象语法树呢？虽然这里的LL分析法可以检查文<br>法对应的语言是否合法有效，但是似乎还不能做任何有意义的事情。这个问题归结于语法制<br>导翻译，一般在编译原理教程中语法分析后的章节里。 </font>
<p><font size=2>LL分析法最大的悲剧在于将一棵在人看来清晰直白的语法树分割了。在递归下降分析法中，<br>一个树节点所需要的属性（例如算术运算符所需要的操作数）可以直接由其子节点得到。但<br>是，在为了消除左递归而改变了的文法式子中，一个节点所需要的属性可能跑到其兄弟节点<br>或者父节点中去了。貌似这里可以参考&#8220;继承属性&#8221;概念。 </font>
<p><font size=2>不过，综合而言，我们有很多业余的手段来处理这种问题，例如建立属性堆栈。具体来说，<br>例如对于例子代码中计算算术表达式，就可以把表达式中的数放到一个栈里。 </font>
<p><font size=2>例子中，通过在文法表达式中插入动作符号来标识一个操作。例如对于文法：<br>expr2-&gt;addop term expr2，则可以改为：expr2-&gt;addop term # expr2。当发现分析栈的栈<br>顶元素是'#'时，则在属性堆栈里取出操作数做计算。例子中还将操作符压入了堆栈。 </font>
<p><font size=2><a href="http://www.cppblog.com/Files/kevinlynx/LL1.rar" target=_blank>下载例子</a>，例子代码最好对照arith_expr.txt中写的文法和分析表来看。 </font>
<p><font size=2>PS，最近在<a href="http://blog.codingnow.com/2005/12/compare_with_lua_5.html#comment-338" target=_blank>云风博客中看到他给的一句评论</a>，我觉得很有道理，并且延伸开来可以说明我们<br>周围的很多现象： </font>
<p><font size=2>&#8221;很多东西，意识不到问题比找不到解决方法要严重很多。比如one-pass 这个，觉得实现<br>麻烦不去实现，和觉得实现没有意义不去实现就是不同的。&#8220; </font>
<p><font size=2>对于以下现象，这句话都可以指明问题：<br>1、认为造轮子没有意义，从不考虑自己是否能造出；<br>2、常告诉别人某个技术复杂晦涩不利于团队使用，却并不懂这个技术；<br>3、笼统来说，【觉得】太多东西没有意义，虽然并不真正懂这个东西。 </font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/109765.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2010-03-15 21:33 <a href="http://www.cppblog.com/kevinlynx/archive/2010/03/15/109765.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>简要实现正则表达式匹配字符串</title><link>http://www.cppblog.com/kevinlynx/archive/2010/02/20/108094.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Sat, 20 Feb 2010 06:53:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2010/02/20/108094.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/108094.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2010/02/20/108094.html#Feedback</comments><slash:comments>4</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/108094.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/108094.html</trackback:ping><description><![CDATA[<p><font size=2>之所以说是&#8220;简要实现&#8221;一方面是因为算法不算高深，算法的实现也不精致，甚至连我对其的理解也不够本质。</font></p>
<p><font size=2>我只不过不想在工作若干年后还是一个只会打字的程序员。学点什么东西，真正精通点什么东西才对得起喜欢</font></p>
<p><font size=2>技术的自己。</font></p>
<p><font size=2></font>&nbsp;</p>
<p><font size=2>附件中的代码粗略实现了《编译原理》龙书中的几个算法。包括解析正则表达式，建立NFA，然后用NFA去匹</font></p>
<p><font size=2>配目标字符串；或者从NFA建立DFA，然后匹配。解析正则表达式我用了比较繁琐的方法，有词法和语法分析</font></p>
<p><font size=2>过程。词法分析阶段将字符和一些操作符整理出来，语法分析阶段在建立语法树的过程中对应地建立NFA。</font></p>
<p><font size=2>当然，因为语法树在这里并没有用处，所以并没有真正地建立。</font></p>
<p><font size=2></font>&nbsp;</p>
<p><font size=2>从正则表达式到NFA比较简单，很多编译原理书里都提到过，如：s|t表达式对应于下面的NFA:</font></p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/1_2.jpg"><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=1 border=0 alt=1 src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/1_thumb.jpg" width=296 height=161></a> </p>
<p>代码中用如下的结构描述状态和状态机中的转换：</p>
<p>#define E_TOK (0)
<p>/* transition */<br>struct tran<br>{<br>&nbsp;&nbsp;&nbsp; char c;<br>&nbsp;&nbsp;&nbsp; struct state *dest;<br>&nbsp;&nbsp;&nbsp; struct tran *next;<br>};
<p>struct state<br>{<br>&nbsp;&nbsp;&nbsp; /* a list of transitions */<br>&nbsp;&nbsp;&nbsp; struct tran *trans;<br>&nbsp;&nbsp;&nbsp; /* inc when be linked */<br>&nbsp;&nbsp;&nbsp; int ref;<br>};
<p>即，每一个状态都有一个转换列表，每个转换都有一个目标状态（即该转换指向的状态）以及转换字符。</p>
<p>貌似通过以上方法建立出来的状态机每个状态最多只会有2个转换？</p>
<p>&nbsp;</p>
<p>建立好NFA后，由NFA匹配目标字符串使用了一种构造子集法（《编译原理》3.7.2节）：</p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/2_2.jpg"><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=2 border=0 alt=2 src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/2_thumb.jpg" width=266 height=214></a> </p>
<p>这个算法里针对NFA的几个操作，如e-closure、move等在由NFA转换DFA时也被用到，因此代码里单独</p>
<p>做了封装（state_oper.c）。这个算法本质上貌似就是一次步进（step）多个状态。</p>
<p>&nbsp;</p>
<p>至于由NFA转DFA,则是相对简单的子集构造法：</p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/3_2.jpg"><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=3 border=0 alt=3 src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/3_thumb.jpg" width=396 height=229></a> </p>
<p>在我以前编译原理课考试的前一天晚上（你懂的）我就对这些算法颇为疑惑。在以后看各种编译</p>
<p>原理教材时，我始终不懂NFA是怎么转到DFA的。就算懂了操作步骤（我大学同学曾告诉我这些步骤，虽然</p>
<p>不知道为什么要那样做），一段时间后依然搞忘。很喜欢《编译原理》龙书里对这个算法最本质的说明：</p>
<p>&nbsp;</p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/4_2.jpg"><img style="BORDER-RIGHT-WIDTH: 0px; DISPLAY: inline; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" title=4 border=0 alt=4 src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/64211484befa_D161/4_thumb.jpg" width=596 height=31></a> </p>
<p>&nbsp;</p>
<p>源代码我是用GCC手工编译的，连makefile也没有。三个test_XXX.c文件分别测试几个模块。test_match.c</p>
<p>基本依赖除掉test外所有c文件，全部链接在一块即可。当然，就经验而言我知道是没几个人会去折腾我的这些</p>
<p>代码的。这些在china的领导看来对工作有个鸟用的代码读起来我自己也觉得费力，何况，我还不伦不类地用了</p>
<p>不知道算哪个标准的c写了这些。</p>
<p>&nbsp;</p>
<p>你不是真想<a href="http://www.cppblog.com/Files/kevinlynx/reg_expr_match.zip">下载。</a>对于这种代码，有BUG是必然的，你也不用在此文若干个月后问我多少行是什么意思，因为</p>
<p>那个时候我也忘了:D。</p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/108094.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2010-02-20 14:53 <a href="http://www.cppblog.com/kevinlynx/archive/2010/02/20/108094.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>kl中的错误处理</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/26/77963.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 26 Mar 2009 09:17:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/26/77963.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/77963.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/26/77963.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/77963.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/77963.html</trackback:ping><description><![CDATA[<p><font size=2>kl中的错误处理 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 之前我一直说错误处理是kl里的软肋，由于一直在关注一些具体功能的改进，也没有对<br>这方面进行改善。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 我这里所说的错误处理，包括语言本身和作为库本身两方面。<br>&nbsp;&nbsp;&nbsp; 语言本身指的是对于脚本代码里的各种语法错误、运行时错误等的处理。好的处理应该<br>不仅仅可以报告错误，而且还能忽视错误让处理过程继续。<br>&nbsp;&nbsp;&nbsp; 而把kl解释器作为一个库使用时，库本身也应该对一些错误情况进行报告。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 整体上，kl简单地通过回调函数指针来把错误信息传给库的应用层。而因为我希望整个<br>kl实现的几层（词法分析、语法分析、符号表、解释器等）可以尽可能地独立。例如虽然语<br>法分析依赖于词法分析（依赖于词法分析提供的接口），但是因为词法分析并不对语法分析<br>依赖，所以完全可以把词法分析模块拿出来单独使用。所以，在日志方面，我几乎为每一层<br>都附加了个error_log函数指针。<br>&nbsp;&nbsp;&nbsp; 而用户层在通过kllib层使用整个库时，传入的回调函数会被间接地传到词法分析层。<br>实际上，当kl作为一个库时，kllib正是用于桥接库本身和用户层的bridge。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 另一方面，语言本身在处理错误的脚本代码时，错误分为几大类型层次：<br>&nbsp;&nbsp;&nbsp; 1.词法错误 lex error，如扫描字符串出错<br>&nbsp;&nbsp;&nbsp; 2.语法错误 syntax error，整理语法树时出错<br>&nbsp;&nbsp;&nbsp; 3.运行时错误 runtime error，在解释执行代码时出错<br>&nbsp;&nbsp;&nbsp; 4.库错误 lib error，发生在kllib这个bridge层的错误<br>&nbsp;&nbsp;&nbsp; kl在报告错误信息时，会首先附加该错误是什么类型的错误。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 这里最麻烦的是语法错误的处理。因为语法分析时发生错误的可能性最大，错误类型也<br>有很多。例如你少写了分号，少写了括号，都会导致错误。这个阶段发生错误不仅要求能准<br>确报告错误，还需要忽略错误让整个过程尽量正确地下去。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 语法分析阶段最根本的就是符号推导（单就kl的实现而言），所谓的符号推导是这样一<br>个过程，例如有赋值语句：a = 1;语法分析时，语法分析器希望（所谓的推导）等号后面会<br>是一个表达式，当分析完了表达式后，又希望接下来的符号(token)是分号作为该语句的结<br>束。<br>&nbsp;&nbsp;&nbsp; 所以，klparser.c中的syn_match正是完成这个过程。每次你传入你希望的符号，例如<br>分号，该函数就检查词法分析中当前符号(token)是否是分号。当然，对于正确的脚本代码，<br>它是一个分号，但是如果是错误的代码，syn_match就会打印诸如：<br>&nbsp;&nbsp;&nbsp; &gt;&gt;syntax error-&gt;unexpected token-&gt; .... <br>&nbsp;&nbsp;&nbsp; 即当前的符号是不被期望的。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 上面完成了错误的检测。对于错误的忽略，或者更高级点地对错误的校正，kl中处理得<br>比较简单，即：直接消耗掉这个不是期望中的符号。例如：<br>&nbsp;&nbsp;&nbsp; a = 1 /* 忘加了分号 */<br>&nbsp;&nbsp;&nbsp; b = 1;<br>&nbsp;&nbsp;&nbsp; 上面两句代码被处理时，在处理完a=1后，发现当前的符号(token)b(是一个ID token)不<br>是期望(expect)中的分号，首先报告b不是期望的符号，然后kl直接掠过b，获取下个符号=。<br>然后处理a=1这个过程结束。当然，下次处理其他语句时，发现=符号，又会继续发生错误。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 错误信息中比较重要的还有行号信息。之前kl这方面一直存在BUG，我在写贪食蛇例子<br>的时候每次新加代码都不敢加太多。因为解释器报告的错误行号总是错误的，我只能靠有没<br>有错误来找错误，而不能通过错误信息找错误。<br>&nbsp;&nbsp;&nbsp; 行号信息被保存在词法分析状态中(lexState:lineno)，语法分析中获取token时，会取<br>出当前的行号，保存到语法树树节点中。因为包括解释模块都是基于树节点的，所以词法分<br>析语法分析解释器三层都可以准确报告行号。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 但是之前解释器报告的行号始终很诡异。症结在于我在载入脚本代码文件时，以rb方式<br>载入，即二进制形式。于是，在windows下，每行文本尾都会有\r\n两个字符。而在词法分<br>析阶段对于行号的增加是：<br>&nbsp;&nbsp;&nbsp; case '\n':<br>&nbsp;&nbsp;&nbsp; case '\r':<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ls-&gt;lineno ++;<br>&nbsp;&nbsp;&nbsp; 不同OS对于文本文件的换行所添加的字符都不一样，例如windows用\r\n，unix系用\n<br>，貌似Mac用\r。所以，词法分析这里写应该可以准确地处理行号。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 但是对于windows，这里就直接将行号增加了两次，所以也就导致了行号出错的问题。查<br>了下文档，发现以文本方式打开文件("r")，调用fread函数读入文件内容时，就会自动把<br>\r\n替换为\n。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 代码改后，又出问题。这个时候，通过fseek和ftell获取到的文件尺寸，貌似包括了<br>\r\n，而fread出来的内容却因为替换\r\n为\n而没有这么多。<br>&nbsp;&nbsp;&nbsp; 不过文件载入不属于kl库本身，kl只接收以字符串形式表示的脚本代码，所以也算不了<br>核心问题。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 同样，最新代码可以从google SVN获取。当然，我也在考虑是否换一个新的项目地址。 </font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/77963.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-26 17:17 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/26/77963.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>kl sample:贪食蛇</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/25/77872.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Wed, 25 Mar 2009 13:17:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/25/77872.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/77872.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/25/77872.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/77872.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/77872.html</trackback:ping><description><![CDATA[<p><font size=2></font>&nbsp;
<p><font size=2>&nbsp;&nbsp;&nbsp; 貌似最近CPPBLOG写一门脚本语言比较流行，连我这种山寨程序员都搞出一个像C又像<br>BASIC的所谓脚本语言，可见其流行程度。</font></p>
<p><font size=2><br>&nbsp;&nbsp;&nbsp; 这个kl脚本例子，是一个具有基本功能的贪食蛇游戏。这个例子中使用了两个插件：<br><a href="http://hge.relishgames.com/" target=_blank>HGE引擎</a>、以及一个撇脚的二维数组插件。因为kl对于数组的实现不是那么漂亮，而我实在<br>不想因为加入二维数组的支持而让代码看起来更乱，所以直接不支持这个特性。考虑到二维<br>数组的应用在一些小游戏中还是比较重要（例如这个贪食蛇，总需要个容器去保存游戏区域<br>的属性），所以撇脚地加了个支持number的二维数组插件。 </font></p>
<p><font size=2>&nbsp;&nbsp;&nbsp; HGE插件我只port了部分接口，也就是注册了一部分函数到脚本里，提供基本的贴图功<br>能。（port--我实在找不到一个合适的词语来形容这种行为---HGE到一门脚本语言里，我似<br>乎做过几次） </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 不知道有没必要提供贪食蛇的实现算法，这似乎说出来有点弱智。- - 不过为了方便别<br>人阅读kl脚本代码，我还是稍微讲一下。游戏中使用一个二维数组保存整个游戏区域，所谓<br>的游戏区域就是蛇可以活动到的地方。每一个二维数组元素对应游戏区域中的一个格子，姑<br>且称为tile。每个tile有一个整数值表示其属性，如BODY、WALL、FOOD、NONE。蛇体的移动<br>归根结底就是蛇头和蛇尾的移动。蛇头和蛇尾属性一样，但是蛇头负责把所经过的tile设置<br>为BODY，而蛇尾则把经过的tile设置为NONE。蛇头的移动方向靠玩家控制，每次蛇头转弯时<br>，都会记录一个转弯点到一个队列。转弯点包括转弯XY坐标以及转向的方向。蛇尾每次移动<br>时都会检查是否到达了一个转弯点，是的话就设置自己的移动方向为该转弯点记录的方向。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 虽然我写了kl这个脚本语言，但是语言特性并不是我设计的。我只是取了C语言的一些<br>特性。所以在写这个sample的时候，我对于kl这个脚本语言的感觉，就是一个像basic的C。<br>因为它太单一，就像BASIC一样只拥有语言的一些基本功能，不能定义复杂的结构，没有天<br>生的对各种数据结构的支持（例如某些语言直接有list, tuple之类）。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 以前中学的时候在电子词典上用GVBASIC写小游戏，当时除了BASIC什么也不知道。今天<br>写这个贪食蛇例子，感觉就像以前用BASIC。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 回头说说一些kl脚本里的特性。从这个例子里（见下载包里的snake.kl），诸如while，<br>for，if...else if...被支持（之前发布的版本里还不支持for和else if）。全局变量支持<br>赋初值（上个版本不支持）。当然，还演示了如何使用插件函数。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 但是，仍有一些特性在我的懒惰之下被置之不理。例如return后必须跟一个表达式，这<br>意味着单纯的return;将被视为语法错误。对于if( a &amp;&amp; b )，kl会计算所有的表达式，而<br>别的语言也许会在a会false后不计算b，这也许不算个问题，但起码我还没修正。还有，kl<br>内部对于错误的报告依然没被修复，少打一个分号你会得到一系列错误的报告，但是却没有<br>准确的行号。甚至，你会看到解释器崩掉。不要紧，在我心里，它作为当年电子词典上那个<br>GVBASIC而言，已经很强大的了。:DD</font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 最近接触了很多UNIX和GNU之类的东西，发觉没有提供版权说明的&#8216;开源&#8217;，原来都是伪<br>开源。虽然我也想按照<a href="http://www.gnu.org/prep/standards/" target=_blank>GNU编码标准</a>里所说为kl的发布包里附加Changelog之类的说明，但是<br>出于懒惰，还是以后再说吧。同样，这次提供的下载里包含了一些编译好的东西，所以我不<br>保证它在你的机器上依然可以运行。我使用了MingW来编译这些，并且提供有点丑陋的Makefile。<br>HGE使用了1.81版本。<br>&nbsp;&nbsp;&nbsp; 贴张图给懒得下载的人： </font></p>
<p><a href="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/klsample_12B5E/snake_screenshot.jpg"><img style="BORDER-RIGHT-WIDTH: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px" border=0 alt=snake_screenshot src="http://www.cppblog.com/images/cppblog_com/kevinlynx/WindowsLiveWriter/klsample_12B5E/snake_screenshot_thumb.jpg" width=670 height=433></a> </p>
<p><font size=2>&nbsp;&nbsp;&nbsp; <a href="http://www.cppblog.com/Files/kevinlynx/snake_sample.zip" target=_blank>下载例子</a>，包含脚本代码。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 如果要获取kl实现代码，建议从我在google的SVN获取：<br></font><a href="http://code.google.com/p/klcommon/"><font size=2>http://code.google.com/p/klcommon/</font></a></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/77872.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-25 21:17 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/25/77872.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（七）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/12/76302.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 12 Mar 2009 01:35:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/12/76302.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/76302.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/12/76302.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/76302.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/76302.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.12.2009 </font>
<p><font size=2><strong>脚本与C语言交互</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 这其实是这一系列的最后一篇，因为我觉得没什么其他需要写的了。<br>&nbsp;&nbsp;&nbsp; 一般而言，脚本语言同C语言交互，包括在C语言中注册C函数到脚本，从而扩展脚本的<br>功能，以及在C语言中调用脚本函数。<br>&nbsp;&nbsp;&nbsp; 为了扩展脚本的功能，这里引入插件的概念。kl在这方面大致上实现得和lua相似。kl<br>支持静态插件和动态插件。<br>&nbsp;&nbsp;&nbsp; 在C语言中调用脚本函数，kl中提供了一些简单的接口用于满足需求。 </font>
<p><font size=2><strong>静态插件</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 静态插件其意思是在C代码中注册函数到脚本中，并随脚本库一起编译链接成最终执行<br>程序。因为其绑定是在开发一个程序的过程中，所以被称为静态的。<br>&nbsp;&nbsp;&nbsp; 一个插件函数，指的是可以被注册进脚本的C函数。这种函数必须原型一样，在kl中这<br>个函数的原型为：typedef struct TValue (*kl_func)( ArgType arg_list );&nbsp;&nbsp;&nbsp; <br>&nbsp;&nbsp;&nbsp; 当你定义了一个这样的原型的函数时，可以通过kl库提供的:<br>&nbsp;&nbsp;&nbsp; int kl_register( struct klState *kl, kl_func f, const char *name )来注册该<br>函数到kl脚本中。该函数参数很简单，第三个参数指定注册进脚本中时的名字。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 原理比较简单：在解释器中保存着一个插件符号表，该符号表的符号名就是这个函数提<br>供的名字，符号对应的值就是第二个参数，也就是插件函数的函数地址。<br>&nbsp;&nbsp;&nbsp; 解释器解释到函数调用时，先从插件符号表中查找，如果找到符号，就将符号的值转换<br>为插件函数，并调用之。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 插件函数的参数其实是一个参数链表。脚本里调用插件函数时，所传递的参数将被解释<br>器整理成参数链表并传递给插件函数。kl库中(集中在kllib.h中)提供了一些方便的接口用<br>于获取每个参数。<br>&nbsp;&nbsp;&nbsp; 插件函数的返回值也将被解释器转换为脚本内部识别的格式，并在必要的时候参与运算<br>。 </font>
<p><font size=2><strong>动态插件</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 动态插件同静态插件的运作方式相同，所不同的是动态插件的插件函数被放在动态运行<br>时库里，例如windows下的dll。<br>&nbsp;&nbsp;&nbsp; kl插件编写标准里要求每个动态插件必须提供一个lib_open函数。kl解释器（或者kl库<br>--当被用作库时）载入一个动态插件时，会直接调用lib_open函数。lib_open函数的主要目<br>的就是把该插件中的所有函数都注册进脚本里。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 因为动态插件在设计之初没有被考虑，所以我并没有为kl加入一些原生的关键字用于导<br>入动态插件，例如import、require之类。我在静态插件层次提供了这个功能。即我提供了<br>一个libloader静态插件，链接进kl解释器程序。该静态插件提供脚本一个名为import的函<br>数。该函数负责动态载入dll之类的动态库，并调用里面的lib_open函数完成动态插件的注<br>册。 </font>
<p><font size=2><strong>C程序里调用脚本函数</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 这个比较简单，通常C语言想调用一个脚本函数时，会传入脚本函数名。因为脚本函数名<br>都保存在全局符号表里，kl库从全局符号表找到该函数符号，并转换其值为语法树节点指针<br>，然后传入解释器模块解释执行。<br>&nbsp;&nbsp;&nbsp; kl库提供struct TValue kl_call( struct klState *kl, const char *name, ArgType args );<br>用于在C里调用脚本函数。 </font>
<p><font size=2><strong>代码导读</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kllib.h/kllib.c作为一个桥接层，用于封装其他模块可以提供给外部模块使用的接口，<br>如果将kl作为一个库使用，用户代码大部分时候只需要使用kllib.h中提供出来的接口。<br>&nbsp;&nbsp;&nbsp; 源码目录plugin下的kllibbase.c中提供了静态插件的例子，kllibloader.c提供了装载<br>动态插件的功能。<br>&nbsp;&nbsp;&nbsp; 源码目录plugin/hge目录下是一个封装2D游戏引擎HGE部分接口到kl脚本中的动态插件<br>例子。<br>&nbsp;&nbsp;&nbsp; 源码目录test/kl.c是一个简单的kl解释程序，它用于执行一段kl代码。这个程序同之前<br>说的解释器不是同一回事。当我说到解释器时，它通常指的是klinterpret.c中实现的解释<br>模块，而解释器程序则指的是一个使用了kl库的独立解释器可执行程序。</font>
<p><font size=2></font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/76302.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-12 09:35 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/12/76302.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（六）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/11/76176.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Wed, 11 Mar 2009 01:12:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/11/76176.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/76176.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/11/76176.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/76176.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/76176.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.11.2009 </font>
<p><font size=2><strong>解释器</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 整理出语法树后，我们就可以根据语法树，并配合符号表开始解释执行脚本代码。这就<br>是接下来要涉及到的解释器。 </font>
<p><font size=2><strong>工作原理</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在第四节中讲语法树时，其实就已经提到解释器的大致工作原理。<br>&nbsp;&nbsp;&nbsp; 一个kl的hello world例子代码大致为：<br>&nbsp;&nbsp;&nbsp; function main()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; print( "hello world\n" );<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 在第二节中我描述了kl代码整体上的结构，是以函数为单位的。因此，对于一个完整的<br>kl脚本代码，其经过语法处理后，将建立一棵大的语法树，该语法树大致结构为：<br>&nbsp;&nbsp;&nbsp; fn1_node<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stmt_node1<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stmt_node2<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br>&nbsp;&nbsp;&nbsp; fn2_node<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stmt_node1<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; stmt_node2<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ... </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; fn1_node和fn2_node同属于同一个作用域，fn1_node的sibling指针指向fn2_node，即在<br>整个树结构中，每一个node通过child[3]成员连接其子节点，通过sibling指针连接其相邻<br>的节点。&nbsp;&nbsp;&nbsp; <br>&nbsp;&nbsp;&nbsp; 解释器解释执行时，就是从main函数所对应的节点开始递归执行的。对于每个节点，都<br>可以知道该节点对应了哪种程序逻辑：是加法运算、比较运算、还是一些控制语句等等。<br>&nbsp;&nbsp;&nbsp; 以这样的控制语句举例：<br>&nbsp;&nbsp;&nbsp; if( 1 ) print( "true" );<br>&nbsp;&nbsp;&nbsp; 对if语句而言，其语法树结构为：<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if_node<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp; \<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /&nbsp;&nbsp;&nbsp; |&nbsp;&nbsp;&nbsp;&nbsp; \ <br>&nbsp;&nbsp;&nbsp; con_exp&nbsp;&nbsp;&nbsp; then_stmt else_stmt </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 即，if语句有最多有三个子节点(child[3])，child[0]指向if的条件表达式，child[1]<br>指向条件表达式为真时执行的语句序列，如果if有else部分，那么child[2]就指向else部分<br>的语句序列。<br>&nbsp;&nbsp;&nbsp; 那么，在发现某个节点是if节点时，就首先计算其条件表达式节点。这个节点的计算方<br>式同脚本中其他所有表达式的计算方式相同，当然，它也是一个递归操作。计算完后判断该<br>表达式的值是否为真，为真则递归执行if节点的child[1]节点，否则检查是否有else节点，<br>有的话就执行child[2]节点。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 其他所有节点的解释方式都是相同的。</font></p>
<p><font size=2><br><strong>解释器环境</strong> </font></p>
<p><font size=2>&nbsp;&nbsp;&nbsp; 解释器环境指的是解释器在解释执行脚本代码时，所需要的运行时环境。kl中主要是符<br>号表信息。一个解释器环境会有三个符号表：全局符号表，主要保存全局变量以及脚本函数<br>符号；函数局部符号表，在解释调用一个脚本函数时，会建立临时的符号表；插件符号表，<br>用于保存插件注册的函数。 </font>
<p><font size=2><strong>如何解释执行函数</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 函数主要有两大类型：脚本内定义的函数以及插件注册进符号表的函数。无论是哪种函<br>数，都会在符号表中建立对应的符号。对于前者，符号被保存于全局符号表，其保存的内容<br>是该函数节点的节点指针；而对于后者，则保存的插件函数的函数地址值。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 每一次解释器解释到一个函数调用节点时，会优先在插件符号表中查找该函数符号。如<br>果找到，就将其值转换为约定的插件函数类型（如同lua里注册的C函数一样），然后整理参<br>数调用之。这个时候代码执行权转接到插件函数里。如果没找到，就在全局符号表里查找，<br>找到后就强转为语法树节点指针，并解释执行该节点下的语句。 </font>
<p><font size=2><strong>代码导读</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 解释器的代码位于klinterpret.h/klinterpret.c中。整体上而言没什么特别的地方，<br>主要是利用语法树的特点。<br>&nbsp;&nbsp;&nbsp; 完成了这一节后，kl就已经可以解释执行所有的脚本语句。当然，因为没有输出功能，<br>只能在调试器里看看计算结果。下一节里会讲到将脚本结合进C语言，从而可以让C语言注册<br>所谓的插件函数到脚本里，也就可以让脚本具有print这样的输出函数。 </font>
<p><font size=2></font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/76176.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-11 09:12 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/11/76176.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（五）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/10/76078.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Tue, 10 Mar 2009 00:58:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/10/76078.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/76078.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/10/76078.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/76078.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/76078.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.10.2009 </font>
<p><font size=2><strong>符号表</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在上一节中，当我们的解释器解释执行age=age+1这个语法树时，会涉及到变量age的值<br>。实际上我们还需要个保存脚本中相关变量的模块，当我们的解释器获取到一个ID树节点时<br>，需要从这个模块中获取出该变量的值，并参与运算。<br>&nbsp;&nbsp;&nbsp; 这个我称之为符号表。我想到这里，我所说的概念很可能和教科书有点不一样了。 </font>
<p><font size=2><strong>什么是符号表？</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 符号表(symbol table)就如同其字面意思一样，是一个表，更宽泛地说是一个保存符号<br>的容器。<br>&nbsp;&nbsp;&nbsp; 脚本中诸如变量函数之类的东西都算作符号，例如age。符号表就是保存这些符号的容<br>器。<br>&nbsp;&nbsp;&nbsp; 在kl中，符号表保存着某一个作用域里的变量。其全局符号表还保存着函数符号，对于<br>函数符号而言，其值为语法树树节点的指针值。当调用一个函数时，将该值转换为树节点，<br>然后执行。当然，这应该算做解释执行一节的细节，不多说。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 再明确下符号表的作用，举例，在上一节中，涉及到这么一个例子函数：<br>&nbsp;&nbsp;&nbsp; value factor( TreeNode *node )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch( node-&gt;type )<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case ID:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 在这里，发现一个树节点类型为ID，就需要根据ID对应的名字，也就<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 是age，在符号表中查找age的值 */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return age；&nbsp;&nbsp;&nbsp; <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* ... */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 以上注释阐述了符号表的作用。 </font>
<p><font size=2><strong>符号表的实现</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 其实不管符号表如何实现，对于其他模块而言，对符号表的唯一要求就是提供几个类似<br>这样的接口：<br>&nbsp;&nbsp;&nbsp; value sym_lookup( const char *name );<br>&nbsp;&nbsp;&nbsp; void sym_insert( const char *name, value val ); <br>&nbsp;&nbsp;&nbsp; 也就是说，提供查找符号值，以及插入新符号的接口。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在kl中，使用了&lt;编译原理与实践&gt;中相同的符号表数据结构实现。即使用了hash表，<br>hash数组中每个元素保存的是一个链表头节点。每一个符号字符串通过散列函数得到hash数<br>组索引，然后在该索引里进行一次线性查找。很典型的hash结构。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 另一方面，因为kl支持全局和函数局部两个作用域。所以kl中有一个全局符号表，用于<br>保存全局变量以及所有的函数符号；同时每一次进入一个函数时，就会创建一个临时的局部<br>符号表，用于存储局部变量；后来，为了支持插件，插件函数被特定地保存在另一个全局符<br>号表里。 </font>
<p><font size=2><strong>代码导读</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kl中的符号表实现代码在klsymtab.h/klsymtab.c中，实现比较简单，无需多言。 </font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/76078.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-10 08:58 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/10/76078.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（四）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/09/75962.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Mon, 09 Mar 2009 03:12:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/09/75962.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/75962.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/09/75962.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/75962.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/75962.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.9.2009 </font>
<p><font size=2><strong>语法分析</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 语法分析接收词法分析阶段的token集合为输入，将这些没有关系的tokens整理为相互<br>之间有关系的结构。书面点的说法叫语法树。<br>&nbsp;&nbsp;&nbsp; 每一次让我写这些文绉绉的概念真让我受不了:D。 </font>
<p><font size=2><strong>语法树</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 语法树简单来说就是一个以token作为每个节点的树型结构。例如我们有表达式age =<br>age + 1;，在词法阶段它被整理为token集合：age, =, age, +, 1。那么在经过语法分析后<br>，这些tokens将被整理为大致如下的树形结构：<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; =<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /&nbsp;&nbsp; \<br>&nbsp;&nbsp;&nbsp; age&nbsp;&nbsp;&nbsp; +<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /&nbsp;&nbsp; \<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; age&nbsp;&nbsp;&nbsp;&nbsp; 1 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 整理成这样的结构有什么好处？就kl解释器而言，最直接的好处就是我可以递归地解释<br>这棵树执行。例如： </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; value compute( TreeNode *root )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* child[0]保存结果值age，child[1]是那个+表达式 */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return op_exp( root-&gt;child[1] ); <br>&nbsp;&nbsp;&nbsp; } </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; value op_exp( TreeNode *node )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch( node-&gt;op )<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case '+':<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* + 表达式必然有左右操作数 */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; value left = factor( node-&gt;child[0] );<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; value right = factor( node-&gt;child[1] );<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return left + right;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; value factor( TreeNode *node )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch( node-&gt;type )<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case ID:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 查找age的值 */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return age; </font>
<p><font size=2>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case CONST:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 1 是常量 */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return node-&gt;cvalue;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; } </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 如你所见，当我们完成了语法分析阶段，我们就可以完成我们的解释器了。后面我会单<br>独讲解下整个解释过程，包括每个模块是如何协作的。我不知道其他解释器是怎么做的，但<br>是我这样做，起码结果是对的。 </font>
<p><font size=2><strong>如何整理出语法树？</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 这里不得不提到所谓的BNF文法，很明显你还是无法从我这里获取编译原理里某个概念<br>的讲解。我这里提这个概念完全是方便我提到这个东西。<br>&nbsp;&nbsp;&nbsp; 每一种语言都有其自己的BNF文法，因为万恶的先知告诉我们，每一门语言都需要建立<br>其语法树。- -!<br>&nbsp;&nbsp;&nbsp; 就像词法分析一样，因为大部分语言的结构都差不多，所以我觉得词法分析和语法分析<br>基本上都没有任何特别之处。也就是说，别的语言的BNF你可以直接拿来改改用。<br>&nbsp;&nbsp;&nbsp; 抄个BNF如下：<br>&nbsp;&nbsp;&nbsp; exp -&gt; exp adop term | term<br>&nbsp;&nbsp;&nbsp; addop -&gt; + | -<br>&nbsp;&nbsp;&nbsp; term -&gt; term mulop factor | factor<br>&nbsp;&nbsp;&nbsp; mulop -&gt; *<br>&nbsp;&nbsp;&nbsp; factor -&gt; (exp) | number<br>&nbsp;&nbsp;&nbsp; 这个BNF用来描述一般的算数表达式(+-*/)。简单来说，一门语言的BNF就是用于描述该<br>语言所有语句的东西，包括if、while、函数定义之类。建议你google一下C语言的BNF，并<br>改造之用于你自己的语言。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 那么有了BNF之后，该如何整理出语法树呢？<br>&nbsp;&nbsp;&nbsp; 通常，我们的代码里都会直接有对应exp、term、addop之类的函数。按照我这句话的意<br>思，上面抄的BNF被翻译为程序代码后，就可能为：<br>&nbsp;&nbsp;&nbsp; exp()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if( ... ) left = exp()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; right = term();<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; left addop right;<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; term()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if( ... ) left = term()<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; right = factor();<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; left mulop right;<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; factor()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if( ... ) return exp();<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; else return number;<br>&nbsp;&nbsp;&nbsp; } </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; (可能还会涉及到EBNF，用于处理重复和选择的一些情况---不用管这句话) </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 每一个函数基本上都会返回一个树节点，当然，该节点下可能会有很多子节点。&nbsp;&nbsp;&nbsp; </font>
<p><font size=2><strong>总结</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 语法分析基本上就是以上信息。它将词法分析输出的token集合整理成一颗语法树。为<br>了整理出这棵语法树，你需要找一份用于描述你语言的BNF，然后根据BNF翻译成处理代码。 </font>
<p><font size=2><strong>代码导读</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kl中的整个语法分析代码位于klparser.c/klparser.h中，其BNF基本上取自&lt;编译原理与<br>实践&gt;附录中的C_语言。</font>
<p><font size=2></font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/75962.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-09 11:12 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/09/75962.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（三）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/07/75816.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Sat, 07 Mar 2009 05:43:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/07/75816.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/75816.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/07/75816.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/75816.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/75816.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.7.2009</font>
<p><font size=2><strong>词法分析</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 词法分析属于整个编译流程中的第一个阶段。为什么要把编译过程分为多个阶段，这就<br>如同软件分层一样，个人觉得是出于降低复杂性的考虑。<br>&nbsp;&nbsp;&nbsp; 再次声明我不会告诉你任何编译原理的理论知识，因为坦率地说我也不会:D。所以我努<br>力将我们需要了解的概念尽可能简单地告诉你。当然，可能会与教科书不吻合。 </font>
<p><font size=2><strong>什么是词法分析?</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 词法分析就是把一段话整理成单词集合。举个简单的例子，例如有代码:age = age + 1;，<br>经过词法分析后，将得到：age、=、age、+、1、;几个符号。为了方便，我称每个单词为一<br>个token。 </font>
<p><font size=2><strong>词法分析的作用</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 词法分析分析出来的单词集合，直接作为编译流程中接下来的语法分析的输入。那么语<br>法分析阶段面对的将是一个一个的token，而不是单个的字符。<br>&nbsp;&nbsp;&nbsp; 例如，在处理age = age + 1;这种语句时，当我们获取到token "="时，我们直接期望接<br>下来的token应该是个表达式。以单词为单位地处理，比直接处理单个字符简单很多。 </font>
<p><font size=2><strong>词法分析的过程</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 词法分析的输入是单个字符流，一般我们fopen一个源代码文件，保存在一个char缓存<br>里，这就是词法分析的输入。而词法分析的最终输出结果就是一个一个的token。<br>&nbsp;&nbsp;&nbsp; 为了处理方便，token并不是单纯的单词。通常我们会将源代码中的所有单词分类，例<br>如变量名其实都属于一类token。简单的token可定义为：<br>&nbsp;&nbsp;&nbsp; struct Token<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; int type;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char value[256];<br>&nbsp;&nbsp;&nbsp; };<br>&nbsp;&nbsp;&nbsp; type用于表示token的类型，例如一个变量名token的类型是一个标识符。value可以用<br>来具体地保存这个变量的名字。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 对于type的处理，通常会事先定义一组枚举值，例如：<br>&nbsp;&nbsp;&nbsp; enum&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;&nbsp; ID, NUM, STRING, IF, ELSE, WHILE, RETURN, FUNCTION }等等用于标示<br>在一个源代码中可能出现的所有token。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 虽然说词法分析的结果是一个token集合，但事实上我们并不是一次做完词法分析。通常<br>词法分析模块提供一个get_token函数。每次调用该函数时，都返回源代码中下一个token。<br>例如，有源代码：age = age + 1;<br>&nbsp;&nbsp;&nbsp; 第一次调用get_token将获得 { ID, "age" }，第二次获得 { ASSIGN, "=" }，第三次<br>获得{ ID, "age" }，等等。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 那么，词法分析该如何实现？也就是struct Token get_token()函数如何实现？其实很<br>简单，你告诉我：给你一个字符串，你如何判断这个字符串全部是数字？<br>&nbsp;&nbsp;&nbsp; int is_num( const char *str )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while( *str != 0 ) <br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if( !isdigit( *str++ ) ) return 0;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 1;<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 所以，基本上，词法分析的过程也就是这个过程。就拿标识符举例，典型的标识符一般<br>以字符开头，然后接着是数字或字符或_，当遇到非法字符时，这个标识符的扫描即结束。<br>&nbsp;&nbsp;&nbsp; 词法分析一般是个while+switch：<br>&nbsp;&nbsp;&nbsp; struct Token get_token()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while( current_char != 0 )<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; switch( current_char )<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case CHARACTER:<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 扫描一个标识符 token */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break; </font>
<p><font size=2>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; case '=':<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* 获得一个 ASSIGN token */<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; break; </font>
<p><font size=2>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; } </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 现在，试着去总结一门语言里的每一个token的规则，然后自己去写写看。 </font>
<p><font size=2><strong>代码导读</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在本节我将提供kl在googlecode的SVN上的代码，先不要去管代码包中的其他东西。关于<br>词法的代码可以在kllex.c kllex.h中找到。lex_token是提供给其他模块的接口，用于获取<br>当前扫描的token。扫描结果可以通过lexState结构体获取。<br>&nbsp;&nbsp;&nbsp; 再次提下版权问题，代码文件以及代码包中我并没有加入任何版权说明，哪怕是GPL。<br>但是如同我之前说的一样，我不介意你传播、改动此代码，但是请保留原作者信息。当然，<br>我并不介意你加上@modified by xxx:)。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 下载kl源代码：</font><a href="http://klcommon.googlecode.com/files/kllan_0.1.0.zip"><font size=2>http://klcommon.googlecode.com/files/kllan_0.1.0.zip</font></a></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/75816.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-07 13:43 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/07/75816.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（二）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75751.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Fri, 06 Mar 2009 08:01:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75751.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/75751.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75751.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/75751.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/75751.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.6.2009 </font>
<p><font size=2><strong>语言特性</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在正式讨论实现细节前明确下这个脚本语言的一些语言特性，基本上可以让我们预见将<br>来会遇到哪些难题。总的来说，它（脚本）将同我们平时接触的如lua一样的脚本语言：拥<br>有一般的编程语言特性，如变量、各种控制流程、也许还有函数，另一方面它还应该和它的<br>宿主语言结合，如作为一个库被用进C，这还涉及到给这门语言设计一种插件方式，最好能<br>通过独立的解释程序让脚本载入一些插件运行。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 以下在描述我写的这个脚本语言时，将以kl表示它的名字，以方便描述。 </font>
<p><font size=2><strong>代码块：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 首先从整体风格上，kl如同C语言一样被划分为函数块，如：<br>&nbsp;&nbsp;&nbsp; function func1()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; function func2()<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; ...<br>&nbsp;&nbsp;&nbsp; kl支持以{}隔离代码块，但是这并不意味着kl有多个独立的局部堆栈，如同C语言一样。<br>这些细节暂不讨论。本节描述的所有内容你都不必深究，因为我只要求你对kl有个感性上的<br>认识。<br>&nbsp;&nbsp;&nbsp; 函数块之外没有可执行的语句(statement)。那么你可能会想到程序的入口点也许会是<br>main。事实上从kl提供的库来看，并没有这种硬性要求。但是，kl的独立解释程序是这样要<br>求的。&nbsp;&nbsp;&nbsp; </font>
<p><font size=2><strong>变量：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kl允许你在任何地方使用一个变量。变量不需要事先定义，任何地方出现一个合<br>法的标识符时，就意味着kl内部会增加这个变量，并给予初值。变量也没有静态类型，也不<br>会固定为某一类型。就一门最简单的语言来看，我觉得数据类型无非就是字符串和数字类型<br>。<br>&nbsp;&nbsp;&nbsp; 所以，kl的变量在某一时刻必然是数字，或者字符串。在脚本里，你无法获知一个变量<br>的类型，事实上也没这个必要。说变量拥有一个类型属性，倒不如说值(value)有一种类型<br>属性。<br>&nbsp;&nbsp;&nbsp; 当字符串值与数字值参与运算时，如1+"a"，其运算结果将自动转换为字符串，也就是<br>"1a"。<br>&nbsp;&nbsp;&nbsp; 一个只有标识符的语句(statement)通常意味着你想定义一个变量。这种无聊的手段通<br>常被用于定义全局变量。 </font>
<p><font size=2><strong>运算符：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kl支持一般的C语言风格的算术、比较、逻辑运算符。例如加减乘除、大于小于、逻辑<br>与逻辑或。 </font>
<p><font size=2><strong>作用域：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; kl脚本里只有两个作用域：全局的和局部的。<br>&nbsp;&nbsp;&nbsp; 位于所有函数块外的变量处于全局作用域；位于函数内的变量处于局部作用域，位于函<br>数块内的代码块变量，还是处于局部作用域。<br>&nbsp;&nbsp;&nbsp; 当局部作用域内出现一个全局里的同名变量时，优先取局部作用域里的变量。这同C语<br>言一样。 </font>
<p><font size=2><strong>控制语句if：<br></strong>&nbsp;&nbsp;&nbsp; if的语法同C语言一样，如：<br>&nbsp;&nbsp;&nbsp; if( a &gt; 10 )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; else<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; if( a &gt; 10 )中的a&gt;10被我成为条件语句，所有条件语句，包括下面的while，都不能<br>为字符串。例如if( "a" )将被视为非法语句。（我为什么要这样考虑？- -!） </font>
<p><font size=2><strong>控制语句while:</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; c-like while:<br>&nbsp;&nbsp;&nbsp; while( a &gt; 10 )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 很遗憾，我暂时没有加入对for的支持。因为我觉得既然有了while，有了循环控制，在<br>没有更多无聊时间的前提下，我没有必要加入for。 </font>
<p><font size=2><strong>函数：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 很遗憾，函数的定义和调用和C语言有点不一样。这是因为kl没有变量类型，那就意味<br>着函数定义如果和C语言一样，就会出现语法歧义，如：<br>&nbsp;&nbsp;&nbsp; func( a )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 就会和函数调用func(a)出现混淆。所以，我加入了function关键字。定义函数的语法<br>为：<br>&nbsp;&nbsp;&nbsp; function func( a, b )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; 如你所见，函数支持参数传递，当然也支持return a;返回值。kl是简陋的，因为它没<br>有指针之类的概念，所以你无法为函数传递一块数据。当然，kl也不能像lua一样让函数可<br>以返回多个值。<br>&nbsp;&nbsp;&nbsp; 函数调用的语法相对熟悉：<br>&nbsp;&nbsp;&nbsp; func( 1, 3 ); </font>
<p><font size=2><strong>数组：</strong> </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 从一开始我就没考虑为kl加入数组。事实证明加入数组是一个不明智的做法。数组的支<br>持让代码在很多地方变得脏乱。无论如何，kl后来支持一维数组了。为了让代码保持那么一<br>点点的干净，我甚至为定义数组加入dim的关键字。这意味着，在kl里，数组和一般的变量<br>总有点不一样：变量无需定义，数组却必须事先定义。<br>&nbsp;&nbsp;&nbsp; 数组的长度不支持动态扩充。如果支持，我得让kl内部更好地去管理内存。<br>&nbsp;&nbsp;&nbsp; 数组元素的类型没有硬性的规定，这意味着a[0] = 1; a[1] = "a";是允许的。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 语言特性上就描述这些，在本节末尾我决定贴一段kl计算阶乘的代码： </font>
<p><font size=2>/* fac.kl */<br>function main()<br>{<br>&nbsp;&nbsp;&nbsp; n = input( "%d" );<br>&nbsp;&nbsp;&nbsp; print( "fac(" + n + ") = " + fac( n ) );<br>} </font>
<p><font size=2>function fac( n )<br>{<br>&nbsp;&nbsp;&nbsp; if( n == 1 )<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return 1;<br>&nbsp;&nbsp;&nbsp; }<br>&nbsp;&nbsp;&nbsp; else<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return fac( n - 1 ) * n;<br>&nbsp;&nbsp;&nbsp; }<br>} </font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/75751.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-06 16:01 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/06/75751.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>实现一种解释性脚本语言（一）</title><link>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75749.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Fri, 06 Mar 2009 07:58:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75749.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/75749.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2009/03/06/75749.html#Feedback</comments><slash:comments>4</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/75749.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/75749.html</trackback:ping><description><![CDATA[<p><font size=2>author: Kevin Lynx email: zmhn320#163.com date: 3.6.2009 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; （相信我，这一节全是废话。）<br>&nbsp;&nbsp;&nbsp; 我不是标题党，但是有必要解释下这个标题。综合来说我就是想与你分享我所学到的。<br>我会将我实现的这个简单的脚本语言的实现细节展示给你。它将涵盖：词法分析、语法分析<br>、符号表管理、语法树解释执行、插件管理等内容。<br>&nbsp;&nbsp;&nbsp; 我并不擅长传授编译原理知识。我没有听过编译原理课，所以我也不会编译原理（也许<br>即使我听了也不会:D）。所以对于这方面的能手而言，我口中的&#8216;DFA&#8216;可能会贻笑大方。<br>&nbsp;&nbsp;&nbsp; 显然，CPPBLOG上有编译原理上的大牛。如果你想学习更深入的知识，可以去请教他们。<br>vczh(http://www.cppblog.com/vczh/) 看起来是我所说的这个人。在致谢名单里我将真诚地<br>写上他的名字。他的&#8217;手把手xxx脚本&#8216;系列多多少少还是给了我一些有用的信息。<br>&nbsp;&nbsp;&nbsp; 其次是FOX，在词法分析的DFA和NFA那里我请教了他一些问题。虽然我现在又忘了。如<br>你们所知，理论和实现之间总会隔着鸿沟。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 推荐《编译原理与实践》(&lt;Compiler Construction:Principles and Practice&gt;<br>Kenneth C. Louden)这本书。在你将来阅读我的脚本语言的实现代码时，你会发现有很一些地<br>方同这本书里的TINY语言实现代码有相似之处。建议你阅读TINY的代码。<br>&nbsp;&nbsp;&nbsp; 感谢VIM、GCC、GDB、MingW，我用这些软件在工作之余写出了这个东西的几千行C代码。<br>很明显我是个开源文化的爱好者。但是我不会告诉你unix有多么多么好，因为我也是个初学<br>者，我还不懂unix。开源在我看来更是一种分享知识的精神。让这种精神如同GPL一样病毒<br>式地传染下去。<br>&nbsp;&nbsp;&nbsp; 还有版权问题。但也许它不是个问题。我不会添加任何版权信息。我允许你任意传播、<br>改动我所散播的东西，但是唯一的基本条件是：保留作者的信息---不要告诉别人，这东西<br>是你做的。 </font>
<p><font size=2>&nbsp;&nbsp;&nbsp; 在所有的文章发布后，我都可能会再次修改。也许通过RSS或者日志日期之类你可以获<br>得修改提醒。 </font>
<p><font size=2></font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/75749.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2009-03-06 15:58 <a href="http://www.cppblog.com/kevinlynx/archive/2009/03/06/75749.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>小写了个XML解析器</title><link>http://www.cppblog.com/kevinlynx/archive/2008/12/10/69079.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Wed, 10 Dec 2008 08:22:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2008/12/10/69079.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/69079.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2008/12/10/69079.html#Feedback</comments><slash:comments>8</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/69079.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/69079.html</trackback:ping><description><![CDATA[<p><font size=2>&nbsp;&nbsp;&nbsp; 开始用FLEX做词法分析，然后在此基础上稍微做些符号匹配（实在称不上语法分析），即完成了XML<br>文件的简单解析。<br>&nbsp;&nbsp;&nbsp; 我把XML文件拆分成：&lt;, &gt;, /&gt;, &lt;/, =, ID, STRING 等token。这样一整理，用FLEX直接生成词法<br>分析程序。每一次getToken就返回这些token。上层的语法匹配就变得比较简单。例如当得到"/&gt;"token<br>时，我就可以判断这是一个节点的结束；当得到ID token时，就可以推测下一个token为"="，再下一个<br>是个STRING。不过对于部分token，也需要做一两个token的回溯，例如当遇到"&lt;"时，并不一定表示一个<br>新节点的开始，它可能是新节点的开始，同样也可能是上一个节点的结束("&lt;/")。<br>&nbsp;&nbsp;&nbsp; 以我薄弱的编译原理知识来看，解析XML变得非常容易。除此之外，还需要写一些上层代码来保存<br>XML结构，以方面更上层代码获取XML文件的配置信息。因为我打算用纯C来写这个东西，所以数据结构方<br>面只有自己处理。这里我以一种变相的树结构来保存：每一个节点有两个域：first child, sibling。<br>其实这样做是一个很明显的通用做法，因为XML种每一个节点都可能拥有不定数量的children节点，如果<br>让parent直接去保存，显然很笨。例如：<br>&nbsp;&nbsp;&nbsp; &lt;Resource&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;bmp file="1.bmp"/&gt;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;bmp file="2.bmp"/&gt;<br>&nbsp;&nbsp;&nbsp; &lt;/Resource&gt;<br>&nbsp;&nbsp;&nbsp; 可以使用这样的数据结构来存储：<br>&nbsp;&nbsp;&nbsp; struct xmlNode<br>&nbsp;&nbsp;&nbsp; {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; struct xmlNode *child;<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; struct xmlNode *sibling;<br>&nbsp;&nbsp;&nbsp; };<br>&nbsp;&nbsp;&nbsp; 对于Resource这个node而言，其child域指向第一个bmp节点(file属性为1.bmp那个节点)；对于第一<br>个bmp节点而言，其sibling域则指向了第二个bmp节点。<br>&nbsp;&nbsp;&nbsp; 这个简单的xml解析器是在公司外网机器上写的，没有VC，没有任何IDE。代码我是用VIM敲的，敲好<br>后写makefile，用mingw里的gcc、make来生成程序，用gdb来调试程序。这算是第一次离开VC写的一个非<br>练习程序(起码用makefile来组织工程)。- -| makefile写的比较烂，gdb用得很不熟，不过好歹调试出来<br>了。越来越想换个平台，只可惜工作还是得在windows vc下，很扫兴。<br>&nbsp;&nbsp;&nbsp; 后来发觉词法分析也很简单，用FLEX的时候正则表达式都写出来了。前段时间一直在看编译原理，虽然不<br>用功。但是就这里而言，基本可以直接根据正则表达式画出DFA。终于不用接触那恶心的从NFA转DFA的<br>过程，因为我至今不会，更不会写代码转。- - 总而言之，自己手写了词法分析。边写边参考编译原理<br>与实践中附带的tiny-c编译器的词法分析部分，最终发现我抄了一遍。MD，一点技术含量都没有。 </font></p>
<p><font size=2>附上全部源代码（对于代码我还是比较满意的:D），<a href="http://www.cppblog.com/Files/kevinlynx/klxmlparser.rar" target=_blank>下载</a></font></p>
<p><font size=2></font></p>
<img src ="http://www.cppblog.com/kevinlynx/aggbug/69079.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2008-12-10 16:22 <a href="http://www.cppblog.com/kevinlynx/archive/2008/12/10/69079.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>