有两个星期没有更新博客了,主要是最近在研究一种更灵活的代码编辑框的框架设计,修了很多bug,还有公司的事情多了起来。现在全部都解决了,因此开始写这一篇博客。上一篇文章提到了我搞定了一个智能提示的原型,当然现在已经在Vczh Library++ 3.0上面添加了鼠标指向一个对象显示声明代码和打括号的时候提示函数参数(部分完成)的功能了。今天来说一下我是如何实现这些功能的。当然我不会讲所有细节,只会讲重点,如何实现那个界面也不包括在这里。我要说的是,如何立刻知道任意一个位置所在的代码究竟是什么东西。
如果你没有读过之前的几篇文章的话,建议去翻一翻,因为我之前提到了一些背景,还有我实现的C#版yacc(当然只是指功能,并不兼容),IDE和编译器的语法分析器的异同和实现一个IDE用的语法分析器要注意的地方。
语法分析总是产生语法树或者分析树的,无论开发什么能够感应代码内容的工具,都逃不过语法分析。因此可以肯定的是,在你敲代码的时候,IDE真的在背后生成了一棵树,只不过为了要达到普通文本框的输入性能,很多东西都要移动到后台去做,但是为了瞬间响应并作智能提示,有一些东西要移动到前台做。他们之间的分界线想要界定清楚其实也不是很难。
假设我们要编辑一份超大文件(几万行吧,再超过要开除的哈),每当你打字修改它的时候,一定会进行语法分析并产生语法树。对于这么大规模的代码要产生语法树肯定不是瞬间就能完成的(我那个东西大概要一秒钟多一点),因此这一步是在后台完成的。但是当你打一个"."的时候,你肯定希望立刻就要弹出列表的内容。为了知道列表的内容,你肯定得先知道那个"."出现在了什么表达式里面,以及"."前面的那个表达式究竟是什么类型,这是离不开全文分析的。但是全文分析又太慢,所以我引入了一个技术。
为了完成这个技术,你必须在前台分析得到那个表达式。我们很容易就知道,我们是不可能等待后台分析给我们提供数据的。所以在这里我们要做的是,缓存当前我们感兴趣的代码。在这里简单化一下,如果我们只需要提供按"."弹出列表的话,我们只需要缓存语句(statement)就可以了。怎么做呢?假设我们已经可以通过所在的位置得到代码的内容(下面会讲),那么我们显然可以知道光标的位置所在的语句的语法树对象究竟是什么。有了这个语法树对象,我们就可以从代码里面直接把这个语句的代码文字复制出来,然后缓存语句的代码、语句所在的全文位置和语句所在的作用域。作用域是语法树的一部分,在做完语法分析之后,只需要做简单的语义分析建立作用域就可以计算很多东西了。这个缓存会在光标位置移动的时候更新,也会在当前的全文分析结束的时候更新。
一旦缓存下来之后,你往里面打了一个字符,那我不仅可以更新文本框里面的内容,我还可以更新缓存里面的代码的内容,同时还可以知道新的缓存开始结束位置。一个语句通常都是很短的,最多也就一百来个字符,因此我们立刻在前台对它做语法分析。而且往一个语句里面打字的话,99%以上的情况是不会影响到上下文的,所以这个语句的旧作用域对象仍然可用。这个时候我们用旧的作用域对象来对新的语句做语义分析,那么就可以知道这个语句每一个表达式的类型了,从而知道了"."前面的表达式究竟是什么类型。然后利用旧作用域对象,我们就可以知道这个类型包含了多少成员。到了这一步,列表里面的对象就构造完毕了。
然而后台的全文分析总是会结束的,所有的信息在这个时候就准备好了,然后发个消息给前台让它更新缓存。两种更新缓存都是用GUI的消息驱动的,所以不可能同时发生,只会先后发生。之前谈到的临时更新跟后台的全文分析是并行的,不过这个不会影响我们。只要我们正确处理后台跟前台的信息交换,那么整个智能感应的计算过程就可以做得十分安全,不会发生死锁。我相信这一点应该不是很难。
那么,现在回到了两个最原始的问题。第一个是如何通过位置查找语法树。这个很容易解决,只要在语法分析的时候把所有跟位置有关的信息都记录在树里面就可以了。第二个问题是我们如何处理用户写错的代码。平时编译原理里面所教授的自动错误恢复其实是不好用的,你看看VC++的编译器在你写错了什么东西之后,大部分的错误信息基本上都没法看,因此如何进行错误恢复肯定要我们自己进行精心设计。但是问题来了,我们如何实现它呢?显然手写语法分析器会让我们心烦意乱根本做不下去(还要处处记得记录位置信息……),因此我们需要一个语法分析器生成器。
在这里我建议大家去阅读我博客上的两篇文章,你可以从这两篇文章所给的链接看到一些其他的东西,讲的是如何用组合子开发语法分析器。我这里给语法树添加了一个新属性,也就是一种组合起来强大但是又容易指定的错误恢复技术了。这里的错误恢复技术分为两种,一种是针对循环的,这个大家看代码就可以了,因为跟第二种——也就是序列关系的文法的错误恢复——非常相似,只是一个理论上的变换而已。
内容是这样的。假设我们需要分析下面的表达式:EXPRESSION + "." + MEMBER,那么我们总是希望在残缺不全的代码里面恢复出尽可能正确的信息。我们知道一旦出现了".",用户想要写的必然是一个访问对象成员的表达式,因此我们在"."那里表上记号,变成EXPRESSION + "." + MEMBER。标记有一个副作用,也就是一旦标记所包含的语法分析成功了,那么整条语法会保证产生出指定的语法树结构。如果用户出现了错误,那么所有的错误都会被当成用户少输入了什么东西而引起的。虽然这一个假设对于编译器来说不太合适,但是对于IDE来说显然是合适的。但是这种做法很容易在分析列表结构的代码里引起死循环,所以需要做很多测试来保证你的标记不会造成问题。
下面的例子也可以辅助说明这种方法的有效性。举个例子,你需要做一个函数。你在写函数的过程中显然会临时或者不小心少些一些东西——有时候我们并不是把所有的事情都想清楚了才开始写代码的。这个时候为了正确分析出函数的结构,我们做下面的语法并标记:
FUNCTION_DECLARATION ::= TYPE + NAME + "(" + list<TYPE + NAME, ","> + ")" + COMPOSITE_STATEMENT
VARIABLE_DECLARATION ::= TYPE + NAME + optional("=" + EXPRESSION) + ";"
然后总是保证FUNCTION_DECLARATION的优先级比VARIABLE_DECLARATION更高,我们就总是可以恢复出最正确的语法结构了。这一种做法对于你在连续输入代码的过程中进行正确的提示是相当好用而且方便的。
至于代码生成器本身怎么实现,还是去Vczh Library++ 3.0下载代码吧。
posted on 2010-11-22 03:29
陈梓瀚(vczh) 阅读(13579)
评论(14) 编辑 收藏 引用 所属分类:
开发自己的IDE