??xml version="1.0" encoding="utf-8" standalone="yes"?> LL分析法主要由分析栈、分析表和一个驱动算法组成。其实LL的分析算法还是很Ҏ(gu)懂的Q?br>主要是一个匹配替换的q程。而要构造这里的分析表,则还涉及计算first集和follow?br>的算法? 个h觉得龙书在解释这些算法和概念旉非常清楚l致Q虽然也有h说它很晦涩?
first集和follow集的计算Q抛开书上l的严密法Q用人的思维ȝ解(对于compiler 1、对于某个非l结WA的first集(first(A)Q,单地说就是由A推导得到的串的首W号?br>集合QA->aBQ那么这里的a属于first(A)Q很形象?br>2、follow(A)Q则是紧随A的终l符号集合,例如B->AaQ这里的a属于follow(A)Q也很Ş 当然Q因为文法符号中有epsilonQ所以在计算上面两个集合时则会涉及到一U传递性。例 在了解了first集和follow集的计算Ҏ(gu)后,则可以通过另一些规则构造出LL需要的分析表?
~译原理里L很多很多的理论和法。但正是q些理论和算法,使得~译器的实现变得 在某个特定的~程语言中,因ؓ(f)其文法一定,所以对于其LL(1)实现中的分析表就是确定的 那么Q要实现一个LL(1)分析法,大致步骤集中于Q设计文?>建立该文法的分析?>~?br>码?
LL分析法是不能处理左递归文法的,例如Qexpr->expr + termQ因为左递归文法会让对应 A -> AB | C {h(hun)于:A -> CX, X -> BX | epsilon
最后一个问题是Q如何在LL分析q程中徏立抽象语法树(wi)呢?虽然q里的LL分析法可以检查文 LL分析法最大的(zhn)剧在于一在人看来清晰直白的语法?wi)分割了。在递归下降分析法中Q?br>一个树(wi)节点所需要的属性(例如术q算W所需要的操作敎ͼ可以直接由其子节点得到。但 不过Q综合而言Q我们有很多业余的手D|处理q种问题Q例如徏立属性堆栈。具体来_ 例子中,通过在文法表辑ּ中插入动作符h标识一个操作。例如对于文法: 下蝲例子Q例子代码最好对照arith_expr.txt中写的文法和分析表来看?
PSQ最q在云风博客中看Cl的一句评?/a>Q我觉得很有道理Qƈ且g伸开来可以说明我?br>周围的很多现象:
”很多东西Q意识不到问题比找不到解x法要严重很多。比如one-pass q个Q觉得实?br>ȝ不去实现Q和觉得实现没有意义不去实现是不同的?#8220;
对于以下现象Q这句话都可以指明问题:
地维护一个栈来进行语法分析,递归下降则是利用了函数调用栈?
compiler则需要用E序L造这些集合,q是让计机ȝ解)Q其实很单:
象?
如,A->Bc, B->epsilonQB可以推导出epsilonQ也是基本{同于没有,那么first(A)?br>׃包含cW号?
单,代码易维护?
。我们也不需要在E序里动态构造first和follow集合?
的分析表里某一存在多个候选式。这里,又会涉及到消除左递归的方法。这个方法也很简
单,只需要把文法推导式代入如下的公式卛_Q?
法对应的语言是否合法有效Q但是似乎还不能做Q何有意义的事情。这个问题归l于语法?br>导翻译,一般在~译原理教程中语法分析后的章节里?
是,在ؓ(f)了消除左递归而改变了的文法式子中Q一个节Ҏ(gu)需要的属性可能跑到其兄弟节点
或者父节点中去了。貌D里可以参?#8220;l承属?#8221;概念?
例如对于例子代码中计算术表辑ּQ就可以把表辑ּ中的数放C个栈里?
expr2->addop term expr2Q则可以改ؓ(f)Qexpr2->addop term # expr2。当发现分析栈的?br>元素是'#'Ӟ则在属性堆栈里取出操作数做计算。例子中q将操作W压入了堆栈?
1、认为造轮子没有意义,从不考虑自己是否能造出Q?br>2、常告诉别h某个技术复杂晦涩不利于团队使用Q却q不懂这个技术;
3、笼l来_【觉得】太多东西没有意义,虽然q不真正懂这个东ѝ?
我只不过不想在工作若q年后还是一个只会打字的E序员。学点什么东西,真正_N点什么东西才对得起喜?/font>
技术的自己?/font>
附g中的代码_略实现了《编译原理》龙书中的几个算法。包括解析正则表辑ּQ徏立NFAQ然后用NFAd
配目标字W串Q或者从NFA建立DFAQ然后匹配。解析正则表辑ּ我用了比较繁琐的Ҏ(gu)Q有词法和语法分?/font>
q程。词法分析阶D将字符和一些操作符整理出来Q语法分析阶D在建立语法?wi)的q程中对应地建立NFA?/font>
当然Q因法树(wi)在这里ƈ没有用处Q所以ƈ没有真正地徏立?/font>
从正则表辑ּ到NFA比较单,很多~译原理书里都提到过Q如Qs|t表达式对应于下面的NFA:
代码中用如下的结构描q状态和状态机中的转换Q?/p>
#define E_TOK (0)
/* transition */
struct tran
{
char c;
struct state *dest;
struct tran *next;
};
struct state
{
/* a list of transitions */
struct tran *trans;
/* inc when be linked */
int ref;
};
卻I每一个状态都有一个{换列表,每个转换都有一个目标状态(卌转换指向的状态)以及转换字符?/p>
貌似通过以上Ҏ(gu)建立出来的状态机每个状态最多只会有2个{换?
建立好NFA后,由NFA匚w目标字符串用了一U构造子集法Q《编译原理?.7.2节)Q?/p>
q个法里针对NFA的几个操作,如e-closure、move{在由NFA转换DFA时也被用刎ͼ因此代码里单?/p>
做了装Qstate_oper.cQ。这个算法本质上貌似是一ơ步q(stepQ多个状态?/p>
至于由NFA转DFA,则是相对单的子集构造法Q?/p>
在我以前~译原理课考试的前一天晚上(你懂的)我就对这些算法颇为疑惑。在以后看各U编?/p>
原理教材Ӟ我始l不懂NFA是怎么转到DFA的。就懂了操作步骤(我大学同学曾告诉我这些步骤,虽然
不知道ؓ(f)什么要那样做)Q一D|间后依然搞忘。很喜欢《编译原理》龙书里对这个算法最本质的说明:
源代码我是用GCC手工~译的,qmakefile也没有。三个test_XXX.c文g分别试几个模块。test_match.c
基本依赖除掉test外所有c文gQ全部链接在一块即可。当Ӟq验而言我知道是没几个h会去折腾我的q些
代码的。这些在china的领导看来对工作有个鸟用的代码读h我自׃觉得费力Q何况,我还不u不类地用?/p>
不知道算哪个标准的c写了q些?/p>
你不是真?a href="http://www.shnenglu.com/Files/kevinlynx/reg_expr_match.zip">下蝲?/a>对于q种代码Q有BUG是必然的Q你也不用在此文若干个月后问我多行是什么意思,因ؓ(f)
那个时候我也忘?D?/p>
之前我一直说错误处理是kl里的软肋Q由于一直在x一些具体功能的改进Q也没有?br>q方面进行改善?
我这里所说的错误处理Q包括语a本n和作为库本n两方面?br> 语言本n指的是对于脚本代码里的各U语法错误、运行时错误{的处理。好的处理应?br>不仅仅可以报告错误,而且q能忽视错误让处理过El?br> 而把kl解释器作Z个库使用Ӟ库本w也应该对一些错误情况进行报告?
整体上,kl单地通过回调函数指针来把错误信息传给库的应用层。而因为我希望整个
kl实现的几层(词法分析、语法分析、符可、解释器{)可以可能地独立。例如虽然语
法分析依赖于词法分析Q依赖于词法分析提供的接口)Q但是因法分析ƈ不对语法分析
依赖Q所以完全可以把词法分析模块拿出来单独用。所以,在日志方面,我几乎ؓ(f)每一?br>都附加了个error_log函数指针?br> 而用户层在通过kllib层用整个库Ӟ传入的回调函C被间接地传到词法分析层?br>实际上,当kl作ؓ(f)一个库Ӟkllib正是用于桥接库本w和用户层的bridge?
另一斚wQ语a本n在处理错误的脚本代码Ӟ错误分ؓ(f)几大cd层次Q?br> 1.词法错误 lex errorQ如扫描字符串出?br> 2.语法错误 syntax errorQ整理语法树(wi)时出?br> 3.q行旉?runtime errorQ在解释执行代码时出?br> 4.库错?lib errorQ发生在kllibq个bridge层的错误
kl在报告错误信息时Q会首先附加该错误是什么类型的错误?
q里最ȝ的是语法错误的处理。因法分析时发生错误的可能性最大,错误cd?br>有很多。例如你写了分P写了括P都会D错误。这个阶D发生错误不仅要求能?br>报告错误,q需要忽略错误让整个q程量正确C厅R?
语法分析阶段最Ҏ(gu)的就是符h|单就kl的实现而言Q,所谓的W号推导是这样一
个过E,例如有赋D句:a = 1;语法分析Ӟ语法分析器希望(所谓的推导Q等号后面会
是一个表辑ּQ当分析完了表达式后Q又希望接下来的W号(token)是分号作语句的结
束?br> 所以,klparser.c中的syn_match正是完成q个q程。每ơ你传入你希望的W号Q例?br>分号Q该函数检查词法分析中当前W号(token)是否是分受当Ӟ对于正确的脚本代码,
它是一个分P但是如果是错误的代码Qsyn_match׃打印诸如Q?br> >>syntax error->unexpected token-> ....
卛_前的W号是不被期望的?
上面完成了错误的。对于错误的忽略Q或者更高点地寚w误的校正Qkl中处理得
比较单,卻I直接消耗掉q个不是期望中的W号。例如:
a = 1 /* 忘加了分?*/
b = 1;
上面两句代码被处理时Q在处理完a=1后,发现当前的符?token)b(是一个ID token)?br>是期?expect)中的分号Q首先报告b不是期望的符P然后kl直接掠过bQ获取下个符??br>然后处理a=1q个q程l束。当Ӟ下次处理其他语句Ӟ发现=W号Q又会l发生错误?
错误信息中比较重要的q有行号信息。之前klq方面一直存在BUGQ我在写贪食蛇例?br>的时候每ơ新加代码都不敢加太多。因释器报告的错误行hL错误的,我只能靠有没
有错误来N误,而不能通过错误信息N误?br> 行号信息被保存在词法分析状态中(lexState:lineno)Q语法分析中获取tokenӞ会取
出当前的行号Q保存到语法?wi)?wi)节点中。因为包括解释模块都是基于树(wi)节点的,所以词法分
析语法分析解释器三层都可以准报告行受?
但是之前解释器报告的行号始终很诡异。症l在于我在蝲入脚本代码文件时Q以rb方式
载入Q即二进制Ş式。于是,在windows下,每行文本N会有\r\n两个字符。而在词法?br>析阶D对于行L(fng)增加是:
case '\n':
case '\r':
ls->lineno ++;
不同OS对于文本文g的换行所d的字W都不一P例如windows用\r\nQunixpȝ\n
Q貌似Mac用\r。所以,词法分析q里写应该可以准地处理行号?
但是对于windowsQ这里就直接行号增加了两次Q所以也导致了行号出错的问题。查
了下文档Q发C文本方式打开文g("r")Q调用fread函数d文g内容Ӟ׃自动?br>\r\n替换为\n?
代码改后Q又出问题。这个时候,通过fseek和ftell获取到的文g寸Q貌似包括了
\r\nQ而fread出来的内容却因ؓ(f)替换\r\n为\n而没有这么多?br> 不过文g载入不属于kl库本w,kl只接收以字符串Ş式表C的脚本代码Q所以也不?br>核心问题?
同样Q最C码可以从google SVN获取。当Ӟ我也在考虑是否换一个新的项目地址?
貌似最qCPPBLOG写一门脚本语a比较行Q连我这U山寨程序员都搞Z个像C又像
BASIC的所谓脚本语aQ可见其行E度?/font>
q个kl脚本例子Q是一个具有基本功能的贪食蛇游戏。这个例子中使用了两个插Ӟ
HGE引擎、以及一个撇脚的二维数组插g。因为kl对于数组的实C是那么漂亮,而我实在
不想因ؓ(f)加入二维数组的支持而让代码看v来更乱,所以直接不支持q个Ҏ(gu)。考虑Cl?br>数组的应用在一些小游戏中还是比较重要(例如q个贪食蛇,总需要个容器M存游戏区?br>的属性)Q所以撇脚地加了个支持number的二l数l插件?
HGE插g我只port了部分接口,也就是注册了一部分函数到脚本里Q提供基本的贴图?br>能。(port--我实在找不到一个合适的词语来Ş容这U行?--HGEC门脚本语a里,我似
乎做q几ơ)
不知道有没必要提供贪食蛇的实现算法,q似乎说出来有点弱智? - 不过Z方便?br>人阅读kl脚本代码Q我q是E微讲一下。游戏中使用一个二l数l保存整个游戏区域,所?br>的游戏区域就是蛇可以zd到的地方。每一个二l数l元素对应游戏区域中的一个格子,?br>且称为tile。每个tile有一个整数DC其属性,如BODY、WALL、FOOD、NONE。蛇体的Ud
归根l底是蛇头和蛇Ud。蛇头和蛇尾属性一P但是蛇头负责把所l过的tile讄
为BODYQ而蛇ֈ把经q的tile讄为NONE。蛇头的Ud方向靠玩家控Ӟ每次蛇头转弯?br>Q都会记录一个{弯点C个队列。{弯点包括转弯XY坐标以及转向的方向。蛇每ơ移?br>旉会检查是否到达了一个{弯点Q是的话p|自qUd方向转弯点记录的方向?
虽然我写了klq个脚本语言Q但是语aҎ(gu)ƈ不是我设计的。我只是取了C语言的一?br>Ҏ(gu)。所以在写这个sample的时候,我对于klq个脚本语言的感觉,是一个像basic的C?br>因ؓ(f)它太单一Q就像BASIC一样只拥有语言的一些基本功能,不能定义复杂的结构,没有?br>生的对各U数据结构的支持Q例如某些语a直接有list, tuple之类Q?
以前中学的时候在?sh)子词典上用GVBASIC写小游戏Q当旉了BASIC什么也不知道。今?br>写这个贪食蛇例子Q感觉就像以前用BASIC?
回头说说一些kl脚本里的Ҏ(gu)。从q个例子里(见下载包里的snake.klQ,诸如whileQ?br>forQif...else if...被支持(之前发布的版本里q不支持for和else ifQ。全局变量支持
赋初|上个版本不支持)。当Ӟq演CZ如何使用插g函数?
但是Q仍有一些特性在我的懒惰之下被置之不理。例如return后必跟一个表辑ּQ这
意味着单纯的return;被视ؓ(f)语法错误。对于if( a && b )Qkl会计所有的表达式,?br>别的语言也许会在a会false后不计算bQ这也许不算个问题,但v码我q没修正。还有,kl
内部对于错误的报告依然没被修复,打一个分号你会得Cpd错误的报告,但是却没?br>准确的行受甚臻I你会看到解释器崩掉。不要紧Q在我心里,它作为当q电(sh)子词怸那个
GVBASIC而言Q已l很强大的了?DD
最q接触了很多UNIX和GNU之类的东西,发觉没有提供版权说明?#8216;开?#8217;Q原来都是伪
开源。虽然我也想按照GNU~码标准里所说ؓ(f)kl的发布包里附加Changelog之类的说明,但是
Z懒惰Q还是以后再说吧。同Pq次提供的下载里包含了一些编译好的东西,所以我?br>保证它在你的机器上依然可以运行。我使用了MingW来编译这些,q且提供有点丑陋的Makefile?br>HGE使用?.81版本?br> 贴张囄懒得下蝲的hQ?
下蝲例子Q包含脚本代码?
如果要获取kl实现代码Q徏议从我在google的SVN获取Q?br>http://code.google.com/p/klcommon/
脚本与C语言交互
q其实是q一pd的最后一,因ؓ(f)我觉得没什么其他需要写的了?br> 一般而言Q脚本语a同C语言交互Q包括在C语言中注册C函数到脚本,从而扩展脚本的
功能Q以及在C语言中调用脚本函数?br> Z扩展脚本的功能,q里引入插g的概ckl在这斚w大致上实现得和lua怼。kl
支持静态插件和动态插件?br> 在C语言中调用脚本函敎ͼkl中提供了一些简单的接口用于满需求?
静态插?/strong>
静态插件其意思是在C代码中注册函数到脚本中,q脚本库一L(fng)译链接成最l执?br>E序。因为其l定是在开发一个程序的q程中,所以被UCؓ(f)静态的?br> 一个插件函敎ͼ指的是可以被注册q脚本的C函数。这U函数必d型一P在kl中这
个函数的原型为:typedef struct TValue (*kl_func)( ArgType arg_list );
当你定义了一个这L(fng)原型的函数时Q可以通过kl库提供的:
int kl_register( struct klState *kl, kl_func f, const char *name )来注册该
函数到kl脚本中。该函数参数很简单,W三个参数指定注册进脚本中时的名字?
原理比较单:在解释器中保存着一个插件符可Q该W号表的W号名就是这个函数提
供的名字Q符号对应的值就是第二个参数Q也是插g函数的函数地址?br> 解释器解释到函数调用Ӟ先从插gW号表中查找Q如果找到符P将W号的D{?br>为插件函敎ͼq调用之?
插g函数的参数其实是一个参数链表。脚本里调用插g函数Ӟ所传递的参数被解释
器整理成参数链表q传递给插g函数。kl库中(集中在kllib.h?提供了一些方便的接口?br>于获取每个参数?br> 插g函数的返回g被解释器{换ؓ(f)脚本内部识别的格式,q在必要的时候参与运?br>?
动态插?/strong>
动态插件同静态插件的q作方式相同Q所不同的是动态插件的插g函数被放在动态运?br>时库里,例如windows下的dll?br> kl插g~写标准里要求每个动态插件必L供一个lib_open函数。kl解释器(或者kl?br>--当被用作库时Q蝲入一个动态插件时Q会直接调用lib_open函数。lib_open函数的主要目
的就是把该插件中的所有函数都注册q脚本里?
因ؓ(f)动态插件在设计之初没有被考虑Q所以我q没有ؓ(f)kl加入一些原生的关键字用于导
入动态插Ӟ例如import、require之类。我在静态插件层ơ提供了q个功能。即我提供了
一个libloader静态插Ӟ链接qkl解释器程序。该静态插件提供脚本一个名为import的函
数。该函数负责动态蝲入dll之类的动态库Qƈ调用里面的lib_open函数完成动态插件的?br>册?
CE序里调用脚本函?/strong>
q个比较单,通常C语言惌用一个脚本函数时Q会传入脚本函数名。因本函数名
都保存在全局W号表里Qkl库从全局W号表找到该函数W号Qƈ转换其gؓ(f)语法?wi)节?gu)?br>Q然后传入解释器模块解释执行?br> kl库提供struct TValue kl_call( struct klState *kl, const char *name, ArgType args );
用于在C里调用脚本函数?
代码D
kllib.h/kllib.c作ؓ(f)一个桥接层Q用于封装其他模块可以提供给外部模块使用的接口,
如果kl作ؓ(f)一个库使用Q用户代码大部分时候只需要用kllib.h中提供出来的接口?br> 源码目录plugin下的kllibbase.c中提供了静态插件的例子Qkllibloader.c提供了装?br>动态插件的功能?br> 源码目录plugin/hge目录下是一个封?D游戏引擎HGE部分接口到kl脚本中的动态插?br>例子?br> 源码目录test/kl.c是一个简单的kl解释E序Q它用于执行一Dkl代码。这个程序同之前
说的解释器不是同一回事。当我说到解释器Ӟ它通常指的是klinterpret.c中实现的解释
模块Q而解释器E序则指的是一个用了kl库的独立解释器可执行E序?/font>
解释?/strong>
整理法树(wi)后,我们可以根据语法树(wi)Qƈ配合W号表开始解释执行脚本代码。这?br>是接下来要涉及到的解释器?
工作原理
在第四节中讲语法?wi)时Q其实就已经提到解释器的大致工作原理?br> 一个kl的hello world例子代码大致为:
function main()
{
print( "hello world\n" );
}
在第二节中我描述了kl代码整体上的l构Q是以函Cؓ(f)单位的。因此,对于一个完整的
kl脚本代码Q其l过语法处理后,徏立一大的语法树(wi)Q该语法?wi)大致结构?f)Q?br> fn1_node
stmt_node1
stmt_node2
...
fn2_node
stmt_node1
stmt_node2
...
fn1_node和fn2_node同属于同一个作用域Qfn1_node的sibling指针指向fn2_nodeQ即?br>整个?wi)结构中Q每一个node通过child[3]成员q接其子节点Q通过sibling指针q接其相?br>的节炏V?nbsp;
解释器解释执行时Q就是从main函数所对应的节点开始递归执行的。对于每个节点,?br>可以知道该节点对应了哪种E序逻辑Q是加法q算、比较运、还是一些控制语句等{?br> 以这L(fng)控制语句举例Q?br> if( 1 ) print( "true" );
对if语句而言Q其语法?wi)结构?f)Q?br> if_node
/ | \
/ | \
con_exp then_stmt else_stmt
卻Iif语句有最多有三个子节?child[3])Qchild[0]指向if的条件表辑ּQchild[1]
指向条g表达式ؓ(f)真时执行的语句序列,如果if有else部分Q那么child[2]指向else部分
的语句序列?br> 那么Q在发现某个节点是if节点Ӟ首先计其条g表达式节炏V这个节点的计算?br>式同脚本中其他所有表辑ּ的计方式相同,当然Q它也是一个递归操作。计完后判断该
表达式的值是否ؓ(f)真,为真则递归执行if节点的child[1]节点Q否则检查是否有else节点Q?br>有的话就执行child[2]节点?
其他所有节点的解释方式都是相同的?/font>
解释器环?/strong>
解释器环境指的是解释器在解释执行脚本代码Ӟ所需要的q行时环境。kl中主要是W?br>可信息。一个解释器环境会有三个W号表:全局W号表,主要保存全局变量以及脚本函数
W号Q函数局部符可Q在解释调用一个脚本函数时Q会建立临时的符可Q插件符可Q?br>用于保存插g注册的函数?
如何解释执行函数
函数主要有两大类型:脚本内定义的函数以及插g注册q符可的函数。无论是哪种?br>敎ͼ都会在符可中徏立对应的W号。对于前者,W号被保存于全局W号表,其保存的内容
是该函数节点的节Ҏ(gu)针;而对于后者,则保存的插g函数的函数地址倹{?
每一ơ解释器解释C个函数调用节Ҏ(gu)Q会优先在插件符可中查找该函数W号。如
果找刎ͼ将其D{换ؓ(f)U定的插件函数类型(如同lua里注册的C函数一PQ然后整理参
数调用之。这个时候代码执行权转接到插件函数里。如果没扑ֈQ就在全局W号表里查找Q?br>扑ֈ后就{法树(wi)节点指针Qƈ解释执行该节点下的语句?
代码D
解释器的代码位于klinterpret.h/klinterpret.c中。整体上而言没什么特别的地方Q?br>主要是利用语法树(wi)的特炏V?br> 完成了这一节后Qkl已l可以解释执行所有的脚本语句。当Ӟ因ؓ(f)没有输出功能Q?br>只能在调试器里看看计结果。下一节里会讲到将脚本l合qC语言Q从而可以让C语言注册
所谓的插g函数到脚本里Q也可以让脚本hprintq样的输出函数?
W号?/strong>
在上一节中Q当我们的解释器解释执行age=age+1q个语法?wi)时Q会涉及到变量age的?br>。实际上我们q需要个保存脚本中相兛_量的模块Q当我们的解释器获取C个ID?wi)节?gu)
Q需要从q个模块中获取出该变量的|q参与运?br> q个我称之ؓ(f)W号表。我惛_q里Q我所说的概念很可能和教科书有点不一样了?
什么是W号表?
W号?symbol table)如同其字面意思一P是一个表Q更宽泛地说是一个保存符?br>的容器?br> 脚本中诸如变量函Ccȝ东西都算作符P例如age。符可是保存q些W号的容
器?br> 在kl中,W号表保存着某一个作用域里的变量。其全局W号表还保存着函数W号Q对?br>函数W号而言Q其gؓ(f)语法?wi)?wi)节点的指针倹{当调用一个函数时Q将该D{换ؓ(f)?wi)节点?br>然后执行。当Ӟq应该算做解释执行一节的l节Q不多说?
再明下W号表的作用QD例,在上一节中Q涉及到q么一个例子函敎ͼ
value factor( TreeNode *node )
{
switch( node->type )
{
case ID:
/* 在这里,发现一个树(wi)节点cd为IDQ就需要根据ID对应的名字,也就
是ageQ在W号表中查找age的?*/
return ageQ?nbsp;
/* ... */
}
}
以上注释阐述了符可的作用?
W号表的实现
其实不管W号表如何实玎ͼ对于其他模块而言Q对W号表的唯一要求是提供几个cM
q样的接口:
value sym_lookup( const char *name );
void sym_insert( const char *name, value val );
也就是说Q提供查扄号|以及插入新符L(fng)接口?
在kl中,使用?lt;~译原理与实?gt;中相同的W号表数据结构实现。即使用了hash表,
hash数组中每个元素保存的是一个链表头节点。每一个符号字W串通过散列函数得到hash?br>l烦引,然后在该索引里进行一ơ线性查找。很典型的hashl构?
另一斚wQ因为kl支持全局和函数局部两个作用域。所以kl中有一个全局W号表,用于
保存全局变量以及所有的函数W号Q同时每一ơ进入一个函数时Q就会创Z个(f)时的局?br>W号表,用于存储局部变量;后来Qؓ(f)了支持插Ӟ插g函数被特定地保存在另一个全局W?br>可里?
代码D
kl中的W号表实C码在klsymtab.h/klsymtab.c中,实现比较单,无需多言?
语法分析
语法分析接收词法分析阶段的token集合入,这些没有关pȝtokens整理为相?br>之间有关pȝl构。书面点的说法叫语法?wi)?br> 每一ơ让我写q些文绉l的概念真让我受不了:D?
语法?/strong>
语法?wi)简单来说就是一个以token作ؓ(f)每个节点的树(wi)型结构。例如我们有表达式age =
age + 1;Q在词法阶段它被整理为token集合Qage, =, age, +, 1。那么在l过语法分析?br>Q这些tokens被整理为大致如下的?wi)Şl构Q?br> =
/ \
age +
/ \
age 1
整理成这L(fng)l构有什么好处?kl解释器而言Q最直接的好处就是我可以递归地解?br>q棵?wi)执行。例如:
value compute( TreeNode *root )
{
/* child[0]保存l果值ageQchild[1]是那?表达?*/
return op_exp( root->child[1] );
}
value op_exp( TreeNode *node )
{
switch( node->op )
{
case '+':
{
/* + 表达式必然有左右操作?*/
value left = factor( node->child[0] );
value right = factor( node->child[1] );
return left + right;
}
}
}
value factor( TreeNode *node )
{
switch( node->type )
{
case ID:
/* 查找age的?*/
return age;
case CONST:
/* 1 是常?*/
return node->cvalue;
}
}
如你所见,当我们完成了语法分析阶段Q我们就可以完成我们的解释器了。后面我会单
独讲解下整个解释q程Q包括每个模块是如何协作的。我不知道其他解释器是怎么做的Q但
是我q样做,L(fng)l果是对的?
如何整理法树(wi)Q?/strong>
q里不得不提到所谓的BNF文法Q很明显你还是无法从我这里获取编译原理里某个概念
的讲解。我q里提这个概念完全是方便我提到这个东ѝ?br> 每一U语a都有其自qBNF文法Q因Z恶的先知告诉我们Q每一门语a都需要徏?br>其语法树(wi)? -!
像词法分析一P因ؓ(f)大部分语a的结构都差不多,所以我觉得词法分析和语法分?br>基本上都没有M特别之处。也是_别的语言的BNF你可以直接拿来改改用?br> 抄个BNF如下Q?br> exp -> exp adop term | term
addop -> + | -
term -> term mulop factor | factor
mulop -> *
factor -> (exp) | number
q个BNF用来描述一般的数表达?+-*/)。简单来_一门语a的BNF是用于描述?br>语言所有语句的东西Q包括if、while、函数定义之cR徏议你google一下C语言的BNFQƈ
攚w之用于你自q语言?
那么有了BNF之后Q该如何整理法树(wi)呢?
通常Q我们的代码里都会直接有对应exp、term、addop之类的函数。按照我q句话的?br>思,上面抄的BNF被翻译ؓ(f)E序代码后,可能ؓ(f)Q?br> exp()
{
if( ... ) left = exp()
right = term();
left addop right;
}
term()
{
if( ... ) left = term()
right = factor();
left mulop right;
}
factor()
{
if( ... ) return exp();
else return number;
}
(可能q会涉及到EBNFQ用于处理重复和选择的一些情?--不用这句话)
每一个函数基本上都会q回一个树(wi)节点Q当Ӟ该节点下可能会有很多子节炏V?nbsp;
ȝ
语法分析基本上就是以上信息。它?yu)词法分析输出的token集合整理成一颗语法树(wi)。ؓ(f)
了整理出q棵语法?wi),你需要找一份用于描qC语言的BNFQ然后根据BNF译成处理代码?
代码D
kl中的整个语法分析代码位于klparser.c/klparser.h中,其BNF基本上取?lt;~译原理?br>实践>附录中的C_语言?/font>
词法分析
词法分析属于整个~译程中的W一个阶Dcؓ(f)什么要把编译过E分为多个阶D,q就
如同软g分层一P个h觉得是出于降低复杂性的考虑?br> 再次声明我不会告诉你M~译原理的理论知识,因ؓ(f)坦率地说我也不会:D。所以我?br>力将我们需要了解的概念可能简单地告诉你。当Ӟ可能会与教科书不d?
什么是词法分析?
词法分析是把一D话整理成单词集合。D个简单的例子Q例如有代码:age = age + 1;Q?br>l过词法分析后,得刎ͼage?、age???几个W号。ؓ(f)了方便,我称每个单词Z
个token?
词法分析的作?/strong>
词法分析分析出来的单词集合,直接作ؓ(f)~译程中接下来的语法分析的输入。那么语
法分析阶D面对的是一个一个的tokenQ而不是单个的字符?br> 例如Q在处理age = age + 1;q种语句Ӟ当我们获取到token "="Ӟ我们直接期望?br>下来的token应该是个表达式。以单词为单位地处理Q比直接处理单个字符单很多?
词法分析的过E?/strong>
词法分析的输入是单个字符,一般我们fopen一个源代码文gQ保存在一个char~存
里,q就是词法分析的输入。而词法分析的最l输出结果就是一个一个的token?br> Z处理方便Qtokenq不是单U的单词。通常我们会将源代码中的所有单词分c,?br>如变量名其实都属于一ctoken。简单的token可定义ؓ(f)Q?br> struct Token
{
int type;
char value[256];
};
type用于表示token的类型,例如一个变量名token的类型是一个标识符。value可以?br>来具体地保存q个变量的名字?
对于type的处理,通常会事先定义一l枚丑ր|例如Q?br> enum { ID, NUM, STRING, IF, ELSE, WHILE, RETURN, FUNCTION }{等用于标示
在一个源代码中可能出现的所有token?
虽然说词法分析的l果是一个token集合Q但事实上我们ƈ不是一ơ做完词法分析。通常
词法分析模块提供一个get_token函数。每ơ调用该函数Ӟ都返回源代码中下一个token?br>例如Q有源代码:age = age + 1;
W一ơ调用get_token获?{ ID, "age" }Q第二次获得 { ASSIGN, "=" }Q第三次
获得{ ID, "age" }Q等{?
那么Q词法分析该如何实现Q也是struct Token get_token()函数如何实现Q其实很
单,你告诉我Q给你一个字W串Q你如何判断q个字符串全部是数字Q?br> int is_num( const char *str )
{
while( *str != 0 )
{
if( !isdigit( *str++ ) ) return 0;
}
return 1;
}
所以,基本上,词法分析的过E也是q个q程。就拿标识符举例Q典型的标识W一?br>以字W开_然后接着是数字或字符或_Q当遇到非法字符Ӟq个标识W的扫描即结束?br> 词法分析一般是个while+switchQ?br> struct Token get_token()
{
while( current_char != 0 )
{
switch( current_char )
{
case CHARACTER:
/* 扫描一个标识符 token */
break;
case '=':
/* 获得一?ASSIGN token */
break;
...
}
}
}
现在Q试着Lȝ一门语a里的每一个token的规则,然后自己d写看?
代码D
在本节我提供kl在googlecode的SVN上的代码Q先不要ȝ代码包中的其他东ѝ关?br>词法的代码可以在kllex.c kllex.h中找到。lex_token是提供给其他模块的接口,用于获取
当前扫描的token。扫描结果可以通过lexStatel构体获取?br> 再次提下版权问题Q代码文件以及代码包中我q没有加入Q何版权说明,哪怕是GPL?br>但是如同我之前说的一P我不介意你传播、改动此代码Q但是请保留原作者信息。当Ӟ
我ƈ不介意你加上@modified by xxx:)?
下蝲kl源代码:http://klcommon.googlecode.com/files/kllan_0.1.0.zip
语言Ҏ(gu)?/strong>
在正式讨论实现细节前明确下这个脚本语a的一些语aҎ(gu),基本上可以让我们预见?br>来会遇到哪些N。ȝ来说Q它Q脚本)同我们qx接触的如lua一L(fng)脚本语言Q拥
有一般的~程语言Ҏ(gu),如变量、各U控制流E、也许还有函敎ͼ另一斚w它还应该和它?br>宿主语言l合Q如作ؓ(f)一个库被用qCQ这q涉及到l这门语a设计一U插件方式,最好能
通过独立的解释程序让脚本载入一些插件运行?
以下在描q我写的q个脚本语言Ӟ以kl表示它的名字Q以方便描述?
代码块:
首先从整体风gQkl如同C语言一栯划分为函数块Q如Q?br> function func1()
{
}
function func2()
{
}
...
kl支持以{}隔离代码块,但是qƈ不意味着kl有多个独立的局部堆栈,如同C语言一栗?br>q些l节暂不讨论。本节描q的所有内容你都不必深IӞ因ؓ(f)我只要求你对kl有个感性上?br>认识?br> 函数块之外没有可执行的语?statement)。那么你可能会想到程序的入口点也怼?br>main。事实上从kl提供的库来看Qƈ没有q种性要求。但是,kl的独立解释程序是q样?br>求的?nbsp;
变量Q?/strong>
kl允许你在M地方使用一个变量。变量不需要事先定义,M地方出现一个合
法的标识W时Q就意味着kl内部会增加这个变量,q给予初倹{变量也没有静态类型,也不
会固定ؓ(f)某一cd。就一门最单的语言来看Q我觉得数据cd无非是字符串和数字cd
?br> 所以,kl的变量在某一时刻必然是数字,或者字W串。在脚本里,你无法获知一个变?br>的类型,事实上也没这个必要。说变量拥有一个类型属性,倒不如说?value)有一U类?br>属性?br> 当字W串g数字值参与运时Q如1+"a"Q其q算l果自动{换ؓ(f)字符Ԍ也就?br>"1a"?br> 一个只有标识符的语?statement)通常意味着你想定义一个变量。这U无聊的手段?br>常被用于定义全局变量?
q算W:
kl支持一般的C语言风格的算术、比较、逻辑q算W。例如加减乘除、大于小于、逻辑
与逻辑或?
作用域:
kl脚本里只有两个作用域Q全局的和局部的?br> 位于所有函数块外的变量处于全局作用域;位于函数内的变量处于局部作用域Q位于函
数块内的代码块变量,q是处于局部作用域?br> 当局部作用域内出C个全局里的同名变量Ӟ优先取局部作用域里的变量。这同C?br>a一栗?
控制语句ifQ?br> if的语法同C语言一P如:
if( a > 10 )
{
}
else
{
}
if( a > 10 )中的a>10被我成ؓ(f)条g语句Q所有条件语句,包括下面的whileQ都不能
为字W串。例如if( "a" )被视ؓ(f)非法语句。(我ؓ(f)什么要q样考虑Q? -!Q?
控制语句while:
c-like while:
while( a > 10 )
{
}
很遗憾,我暂时没有加入对for的支持。因为我觉得既然有了whileQ有了@环控Ӟ?br>没有更多无聊旉的前提下Q我没有必要加入for?
函数Q?/strong>
很遗憾,函数的定义和调用和C语言有点不一栗这是因为kl没有变量cdQ那意?br>着函数定义如果和C语言一P׃出现语法歧义Q如Q?br> func( a )
{
}
׃和函数调用func(a)出现h。所以,我加入了function关键字。定义函数的语法
为:
function func( a, b )
{
}
如你所见,函数支持参数传递,当然也支持return a;q回倹{kl是简陋的Q因为它?br>有指针之cȝ概念Q所以你无法为函C递一块数据。当Ӟkl也不能像lua一栯函数?br>以返回多个倹{?br> 函数调用的语法相对熟(zhn):
func( 1, 3 );
数组Q?/strong>
从一开始我没考虑为kl加入数组。事实证明加入数l是一个不明智的做法。数l的?br>持让代码在很多地方变得脏乱。无论如何,kl后来支持一l数l了。ؓ(f)了让代码保持那么一
点点的干净Q我甚至为定义数l加入dim的关键字。这意味着Q在kl里,数组和一般的变量
L点不一P变量无需定义Q数l却必须事先定义?br> 数组的长度不支持动态扩充。如果支持,我得让kl内部更好地去理内存?br> 数组元素的类型没有硬性的规定Q这意味着a[0] = 1; a[1] = "a";是允许的?
语言Ҏ(gu)上描q这些,在本节末我军_贴一Dkl计算阶乘的代码:
/* fac.kl */
function main()
{
n = input( "%d" );
print( "fac(" + n + ") = " + fac( n ) );
}
function fac( n )
{
if( n == 1 )
{
return 1;
}
else
{
return fac( n - 1 ) * n;
}
}
Q相信我Q这一节全是废话。)
我不是标题党Q但是有必要解释下这个标题。综合来说我是想与你分享我所学到的?br>我会我实现的这个简单的脚本语言的实现细节展C给你。它?yu)涵盖:词法分析、语法分?br>、符可理、语法树(wi)解释执行、插件管理等内容?br> 我ƈ不擅长传授编译原理知识。我没有听过~译原理课,所以我也不会编译原理(也许
即我听了也不会:DQ。所以对于这斚w的能手而言Q我口中?#8216;DFA‘可能会贻W大斏V?br> 昄QCPPBLOG上有~译原理上的大牛。如果你惛_?fn)更深入的知识,可以去请教他们?br>vczh(http://www.shnenglu.com/vczh/) 看v来是我所说的q个人。在致谢名单里我真诚地
写上他的名字。他?#8217;手把手xxx脚本‘pd多多少q是l了我一些有用的信息?br> 其次是FOXQ在词法分析的DFA和NFA那里我请教了他一些问题。虽然我现在又忘了。如
你们所知,理论和实C间M隔着鸿沟?
推荐《编译原理与实践?<Compiler Construction:Principles and Practice>
Kenneth C. Louden)q本书。在你将来阅L的脚本语a的实C码时Q你会发现有很一些地
方同q本书里的TINY语言实现代码有相g处。徏议你阅读TINY的代码?br> 感谢VIM、GCC、GDB、MingWQ我用这些Y件在工作之余写出了这个东西的几千行C代码?br>很明显我是个开源文化的爱好者。但是我不会告诉你unix有多么多么好Q因为我也是个初?br>者,我还不懂unix。开源在我看来更是一U分享知识的_。让q种_如同GPL一L(fng)?br>式地传染下去?br> q有版权问题。但也许它不是个问题。我不会dM版权信息。我允许你Q意传播?br>改动我所散播的东西,但是唯一的基本条件是Q保留作者的信息---不要告诉别hQ这东西
是你做的?
在所有的文章发布后,我都可能会再ơ修攏V也?dng)R过RSS或者日志日期之cM可以?br>得修Ҏ(gu)醒?
附上全部源代码(对于代码我还是比较满意的:DQ,下蝲