之前我一直说错误处理是kl里的软肋Q由于一直在x一些具体功能的改进Q也没有?br>q方面进行改善?
我这里所说的错误处理Q包括语a本n和作为库本n两方面?br> 语言本n指的是对于脚本代码里的各U语法错误、运行时错误{的处理。好的处理应?br>不仅仅可以报告错误,而且q能忽视错误让处理过El?br> 而把kl解释器作Z个库使用Ӟ库本w也应该对一些错误情况进行报告?
整体上,kl单地通过回调函数指针来把错误信息传给库的应用层。而因为我希望整个
kl实现的几层(词法分析、语法分析、符可、解释器{)(j)可以可能地独立。例如虽然语
法分析依赖于词法分析Q依赖于词法分析提供的接口)(j)Q但是因法分析ƈ不对语法分析
依赖Q所以完全可以把词法分析模块拿出来单独用。所以,在日志方面,我几乎ؓ(f)每一?br>都附加了个error_log函数指针?br> 而用户层在通过kllib层用整个库Ӟ传入的回调函C(x)被间接地传到词法分析层?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会(x)首先附加该错误是什么类型的错误?
q里最ȝ(ch)的是语法错误的处理。因法分析时发生错误的可能性最大,错误cd?br>有很多。例如你写了分P写了括P都会(x)D错误。这个阶D发生错误不仅要求能?br>报告错误,q需要忽略错误让整个q程量正确C厅R?
语法分析阶段最Ҏ(gu)的就是符h|单就kl的实现而言Q,所谓的W号推导是这样一
个过E,例如有赋D句:(x)a = 1;语法分析Ӟ语法分析器希望(所谓的推导Q等号后面会(x)
是一个表辑ּQ当分析完了表达式后Q又希望接下来的W号(token)是分号作语句的结
束?br> 所以,klparser.c中的syn_match正是完成q个q程。每ơ你传入你希望的W号Q例?br>分号Q该函数检查词法分析中当前W号(token)是否是分受当?dng)对于正确的脚本代码?br>它是一个分P但是如果是错误的代码Qsyn_match׃(x)打印诸如Q?br> >>syntax error->unexpected token-> ....
卛_前的W号是不被期望的?
上面完成了错误的(g)。对于错误的忽略Q或者更高点地寚w误的校正Qkl中处理得
比较单,卻I(x)直接消耗掉q个不是期望中的W号。例如:(x)
a = 1 /* 忘加了分?*/
b = 1;
上面两句代码被处理时Q在处理完a=1后,发现当前的符?token)b(是一个ID token)?br>是期?expect)中的分号Q首先报告b不是期望的符P然后kl直接掠过bQ获取下个符??br>然后处理a=1q个q程l束。当?dng)下次处理其他语句Ӟ发?W号Q又?x)l发生错误?
错误信息中比较重要的q有行号信息。之前klq方面一直存在BUGQ我在写贪食蛇例?br>的时候每ơ新加代码都不敢加太多。因释器报告的错误行hL错误的,我只能靠有没
有错误来N误,而不能通过错误信息N误?br> 行号信息被保存在词法分析状态中(lexState:lineno)Q语法分析中获取tokenӞ?x)?br>出当前的行号Q保存到语法?wi)?wi)节点中。因为包括解释模块都是基于树(wi)节点的,所以词法分
析语法分析解释器三层都可以准报告行受?
但是之前解释器报告的行号始终很诡异。症l在于我在蝲入脚本代码文件时Q以rb方式
载入Q即二进制Ş式。于是,在windows下,每行文本N?x)有\r\n两个字符。而在词法?br>析阶D对于行L(fng)增加是:(x)
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内容Ӟ׃(x)自动?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获取。当?dng)我也在考虑是否换一个新的项目地址?
貌似最qCPPBLOG写一门脚本语a比较行Q连我这U山寨程序员都搞Z个像C又像
BASIC的所谓脚本语aQ可见其行E度?/font>
q个kl脚本例子Q是一个具有基本功能的贪食蛇游戏。这个例子中使用了两个插Ӟ(x)
HGE引擎、以?qing)一个撇脚的二维数组插g。因为kl对于数组的实C是那么漂亮,而我实在
不想因ؓ(f)加入二维数组的支持而让代码看v来更乱,所以直接不支持q个Ҏ(gu)。考虑Cl?br>数组的应用在一些小游戏中还是比较重要(例如q个贪食蛇,总需要个容器M存游戏区?br>的属性)(j)Q所以撇脚地加了个支持number的二l数l插件?
HGE插g我只port了部分接口,也就是注册了一部分函数到脚本里Q提供基本的贴图?br>能。(port--我实在找不到一个合适的词语来Ş容这U行?--HGEC门脚本语a里,我似
乎做q几ơ)(j)
不知道有没必要提供贪食蛇的实现算法,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都?x)记录一个{弯点C个队列。{弯点包括转弯XY坐标以及(qing)转向的方向。蛇每ơ移?br>旉?x)检查是否到达了一个{弯点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。全局变量支持
赋初|上个版本不支持)(j)。当?dng)q演CZ如何使用插g函数?
但是Q仍有一些特性在我的懒惰之下被置之不理。例如return后必跟一个表辑ּQ这
意味着单纯的return;被视ؓ(f)语法错误。对于if( a && b )Qkl?x)计所有的表达式,?br>别的语言也许?x)在a?x)false后不计算bQ这也许不算个问题,但v码我q没修正。还有,kl
内部对于错误的报告依然没被修复,打一个分号你?x)得Cpd错误的报告,但是却没?br>准确的行受甚臻I你会(x)看到解释器崩掉。不要紧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以?qing)在C语言中调用脚本函数?br> Z扩展脚本的功能,q里引入插g的概c(din)kl在这斚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中这
个函数的原型为:(x)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三个参数指定注册进脚本中时的名字?
原理比较单:(x)在解释器中保存着一个插件符可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会(x)直接调用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会(x)传入脚本函数名。因本函数名
都保存在全局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>是接下来要涉?qing)到的解释器?
工作原理
在第四节中讲语法?wi)时Q其实就已经提到解释器的大致工作原理?br> 一个kl的hello world例子代码大致为:(x)
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>可信息。一个解释器环境?x)有三个W号表:(x)全局W号表,主要保存全局变量以及(qing)脚本函数
W号Q函数局部符可Q在解释调用一个脚本函数时Q会(x)建立临时的符可Q插件符可Q?br>用于保存插g注册的函数?
如何解释执行函数
函数主要有两大类型:(x)脚本内定义的函数以及(qing)插g注册q符可的函数。无论是哪种?br>敎ͼ都会(x)在符可中徏立对应的W号。对于前者,W号被保存于全局W号表,其保存的内容
是该函数节点的节Ҏ(gu)针;而对于后者,则保存的插g函数的函数地址倹{?
每一ơ解释器解释C个函数调用节Ҏ(gu)Q会(x)优先在插件符可中查找该函数W号。如
果找刎ͼ将其D{换ؓ(f)U定的插件函数类型(如同lua里注册的C函数一P(j)Q然后整理参
数调用之。这个时候代码执行权转接到插件函数里。如果没扑ֈQ就在全局W号表里查找Q?br>扑ֈ后就{法树(wi)节点指针Qƈ解释执行该节点下的语句?
代码D
解释器的代码位于klinterpret.h/klinterpret.c中。整体上而言没什么特别的地方Q?br>主要是利用语法树(wi)的特炏V?br> 完成了这一节后Qkl已l可以解释执行所有的脚本语句。当?dng)因?f)没有输出功能Q?br>只能在调试器里看看计结果。下一节里?x)讲到将脚本l合qC语言Q从而可以让C语言注册
所谓的插g函数到脚本里Q也可以让脚本hprintq样的输出函数?
W号?/strong>
在上一节中Q当我们的解释器解释执行age=age+1q个语法?wi)时Q会(x)涉及(qing)到变量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>然后执行。当?dng)q应该算做解释执行一节的l节Q不多说?
再明下W号表的作用QD例,在上一节中Q涉?qing)到q么一个例子函敎ͼ(x)
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样的接口:(x)
value sym_lookup( const char *name );
void sym_insert( const char *name, value val );
也就是说Q提供查扄号|以及(qing)插入新符L(fng)接口?
在kl中,使用?lt;~译原理与实?gt;中相同的W号表数据结构实现。即使用了hash表,
hash数组中每个元素保存的是一个链表头节点。每一个符号字W串通过散列函数得到hash?br>l烦(ch)引,然后在该索引里进行一ơ线性查找。很典型的hashl构?
另一斚wQ因为kl支持全局和函数局部两个作用域。所以kl中有一个全局W号表,用于
保存全局变量以及(qing)所有的函数W号Q同时每一ơ进入一个函数时Q就?x)创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)执行。例如:(x)
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我们就可以完成我们的解释器了。后面我?x)?br>独讲解下整个解释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特别之处。也是_(d)别的语言的BNF你可以直接拿来改改用?br> 抄个BNF如下Q?br> exp -> exp adop term | term
addop -> + | -
term -> term mulop factor | factor
mulop -> *
factor -> (exp) | number
q个BNF用来描述一般的数表达?+-*/)。简单来_(d)一门语a的BNF是用于描述?br>语言所有语句的东西Q包括if、while、函数定义之cR徏议你google一下C语言的BNFQƈ
攚w之用于你自q语言?
那么有了BNF之后Q该如何整理法树(wi)呢?
通常Q我们的代码里都?x)直接有对应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会(x)涉及(qing)到EBNFQ用于处理重复和选择的一些情?--不用这句话)
每一个函数基本上都会(x)q回一个树(wi)节点Q当?dng)该节点下可能会(x)有很多子节炏V?nbsp;
ȝ
语法分析基本上就是以上信息。它?yu)词法分析输出的token集合整理成一颗语法树(wi)。ؓ(f)
了整理出q棵语法?wi),你需要找一份用于描qC语言的BNFQ然后根据BNF译成处理代码?
代码D
kl中的整个语法分析代码位于klparser.c/klparser.h中,其BNF基本上取?lt;~译原理?br>实践>附录中的C_语言?/font>
词法分析
词法分析属于整个~译程中的W一个阶Dc(din)ؓ(f)什么要把编译过E分为多个阶D,q就
如同软g分层一P个h觉得是出于降低复杂性的考虑?br> 再次声明我不?x)告诉你M~译原理的理论知识,因ؓ(f)坦率地说我也不会(x):D。所以我?br>力将我们需要了解的概念可能简单地告诉你。当?dng)可能会(x)与教科书不d?
什么是词法分析?
词法分析是把一D话整理成单词集合。D个简单的例子Q例如有代码:age = age + 1;Q?br>l过词法分析后,得刎ͼ(x)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的单词。通常我们?x)将源代码中的所有单词分c,?br>如变量名其实都属于一ctoken。简单的token可定义ؓ(f)Q?br> struct Token
{
int type;
char value[256];
};
type用于表示token的类型,例如一个变量名token的类型是一个标识符。value可以?br>来具体地保存q个变量的名字?
对于type的处理,通常?x)事先定义一l枚丑ր|例如Q?br> enum { ID, NUM, STRING, IF, ELSE, WHILE, RETURN, FUNCTION }{等用于标示
在一个源代码中可能出现的所有token?
虽然说词法分析的l果是一个token集合Q但事实上我们ƈ不是一ơ做完词法分析。通常
词法分析模块提供一个get_token函数。每ơ调用该函数Ӟ都返回源代码中下一个token?br>例如Q有源代码:(x)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开_(d)然后接着是数字或字符或_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代码文件以?qing)代码包中我q没有加入Q何版权说明,哪怕是GPL?br>但是如同我之前说的一P我不介意你传播、改动此代码Q但是请保留原作者信息。当?dng)?br>我ƈ不介意你加上@modified by xxx:)?
下蝲kl源代码:(x)http://klcommon.googlecode.com/files/kllan_0.1.0.zip
语言Ҏ(gu)?/strong>
在正式讨论实现细节前明确下这个脚本语a的一些语aҎ(gu),基本上可以让我们预见?br>来会(x)遇到哪些N。ȝ来说Q它Q脚本)(j)同我们qx接触的如lua一L(fng)脚本语言Q拥
有一般的~程语言Ҏ(gu),如变量、各U控制流E、也许还有函敎ͼ另一斚w它还应该和它?br>宿主语言l合Q如作ؓ(f)一个库被用qCQ这q涉?qing)到l这门语a设计一U插件方式,最好能
通过独立的解释程序让脚本载入一些插件运行?
以下在描q我写的q个脚本语言Ӟ以kl表示它的名字Q以方便描述?
代码块:(x)
首先从整体风gQkl如同C语言一栯划分为函数块Q如Q?br> function func1()
{
}
function func2()
{
}
...
kl支持以{}隔离代码块,但是qƈ不意味着kl有多个独立的局部堆栈,如同C语言一栗?br>q些l节暂不讨论。本节描q的所有内容你都不必深IӞ因ؓ(f)我只要求你对kl有个感性上?br>认识?br> 函数块之外没有可执行的语?statement)。那么你可能?x)想到程序的入口点也怼(x)?br>main。事实上从kl提供的库来看Qƈ没有q种性要求。但是,kl的独立解释程序是q样?br>求的?nbsp;
变量Q?/strong>
kl允许你在M地方使用一个变量。变量不需要事先定义,M地方出现一个合
法的标识W时Q就意味着kl内部?x)增加这个变量,q给予初倹{变量也没有静态类型,也不
?x)固定?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:(x)
kl支持一般的C语言风格的算术、比较、逻辑q算W。例如加减乘除、大于小于、逻辑
与逻辑或?
作用域:(x)
kl脚本里只有两个作用域Q全局的和局部的?br> 位于所有函数块外的变量处于全局作用域;位于函数内的变量处于局部作用域Q位于函
数块内的代码块变量,q是处于局部作用域?br> 当局部作用域内出C个全局里的同名变量Ӟ优先取局部作用域里的变量。这同C?br>a一栗?
控制语句ifQ?br> if的语法同C语言一P如:(x)
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׃(x)出现语法歧义Q如Q?br> func( a )
{
}
׃(x)和函数调用func(a)出现h。所以,我加入了function关键字。定义函数的语法
为:(x)
function func( a, b )
{
}
如你所见,函数支持参数传递,当然也支持return a;q回倹{kl是简陋的Q因为它?br>有指针之cȝ概念Q所以你无法为函C递一块数据。当?dng)kl也不能像lua一栯函数?br>以返回多个倹{?br> 函数调用的语法相对熟(zhn):(x)
func( 1, 3 );
数组Q?/strong>
从一开始我没考虑为kl加入数组。事实证明加入数l是一个不明智的做法。数l的?br>持让代码在很多地方变得脏乱。无论如何,kl后来支持一l数l了。ؓ(f)了让代码保持那么一
点点的干净Q我甚至为定义数l加入dim的关键字。这意味着Q在kl里,数组和一般的变量
L点不一P(x)变量无需定义Q数l却必须事先定义?br> 数组的长度不支持动态扩充。如果支持,我得让kl内部更好地去理内存?br> 数组元素的类型没有硬性的规定Q这意味着a[0] = 1; a[1] = "a";是允许的?
语言Ҏ(gu)上描q这些,在本节末我军_贴一Dkl计算阶乘的代码:(x)
/* 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这一节全是废话。)(j)
我不是标题党Q但是有必要解释下这个标题。综合来说我是想与你分享我所学到的?br>我会(x)我实现的这个简单的脚本语言的实现细节展C给你。它?yu)涵盖?x)词法分析、语法分?br>、符可理、语法树(wi)解释执行、插件管理等内容?br> 我ƈ不擅长传授编译原理知识。我没有听过~译原理课,所以我也不?x)编译原理(也?br>即我听了也不会(x):DQ。所以对于这斚w的能手而言Q我口中?#8216;DFA‘可能?x)贻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(x)隔着鸿沟?
推荐《编译原理与实践?<Compiler Construction:Principles and Practice>
Kenneth C. Louden)q本书。在你将来阅L的脚本语a的实C码时Q你?x)发现有很一些地
方同q本书里的TINY语言实现代码有相g处。徏议你阅读TINY的代码?br> 感谢VIM、GCC、GDB、MingWQ我用这些Y件在工作之余写出了这个东西的几千行C代码?br>很明显我是个开源文化的爱好者。但是我不会(x)告诉你unix有多么多么好Q因为我也是个初?br>者,我还不懂unix。开源在我看来更是一U分享知识的_。让q种_如同GPL一L(fng)?br>式地传染下去?br> q有版权问题。但也许它不是个问题。我不会(x)dM版权信息。我允许你Q意传播?br>改动我所散播的东西,但是唯一的基本条件是Q保留作者的信息---不要告诉别hQ这东西
是你做的?
在所有的文章发布后,我都可能?x)再ơ修攏V也?dng)R过RSS或者日志日期之cM可以?br>得修Ҏ(gu)醒?