传统的源代码简单文本表示对于编程人员来说很方便,但需要进行语法分析来揭示程序的深层结构。尽管某些复杂的软件工具通过分析源代码可以访问程序的结构,但许多像 grep 这样的轻量级编程辅助工具却仅仅依赖于源代码的词法结构。我说明的是一种新的 XML 应用程序,该应用程序提供了另一种 Java 源代码表示法。这种基于 XML 的表示法叫做 JavaML,对工具软件来说显得更加自然,它利用丰富的 XML 工具和技术,可以方便地对大量软件工程分析进行规范。使用 Jikes Java 编译器框架构建的强健的转换器,可将传统的源代码表示转换为 JavaML;而使用 XSLT 样式表,又可将 JavaML 转回到传统的文本格式。
简介
从第一种计算机编程语言开始,编程人员就已经开始将文本表示用作软件结构和计算过程的编码媒介。这些年来,技术已得到充分发展,使编译器的前端大大自动化了;编译器的前端就是执行词法分析和语法分析的那部分,是揭示以简单文本表示的编程语言的结构所必不可少的。借助于具有牢固基础的正规表达式的概念和语法,Lex/Flex 和 Yacc/Bison [42] 之类的工具使这些单调乏味的分析工作走向了自动化。正规表达式说明单个字符如何组合成表单记号,语法则描述更高级别的结构是如何由其他结构和原始表单记号递归构成的。这些过程一起将字符系列转换成一种叫做 抽象语法树 (AST) 的数据结构,这种数据结构可以更直接地反映程序的结构。
源代码的文本表示有几种良好的特性。文本表示相当简明,并且与自然语言相似,往往比较容易阅读。文本还是一种通用的数据格式,因而可以方便地使用大量软件工具转换和处理源代码,这些软件工具包括文本编辑器、版本控制系统以及 grep, awk 和 wc 之类的命令流水线实用程序。
然而,传统的源代码表示有许多问题。诸如 C++ 和 Perl 之类的当前十分流行的语言的语法结构,更加重了对语法分析能力的限制。尽管有许多工具的支持,但是为这些语言构造一个编译器的前端仍很困难。更为困难的也许是,要发展一种语言的语法,就常常需要处理脆弱的文法。这种局限性使得对一种正在发展的语言的处理复杂化了。
文本表示和软件工具
传统的源代码表示的最大局限性是,只有在语法分析后,才能搞清楚程序的结构。这一缺陷使得,某种语言专用的分析功能,在每一个工具中都要重复配置,只要这些工具在进行程序的词法分析之外,还要对程序进行语法推理。编译器必然需要与抽象结构树(AST)一起使用,而许多其他软件工程工具将从访问源代码的结构化表示中获益。遗憾的是,许多软件工程工具没有嵌入语法分析器,因而仅限于执行一些词法分析任务。
有几种原因使得开发人员常常避免在工具软件中嵌入语法分析器。正如前面所提到的,对于语法结构复杂的语言,构建一个完整的编译器前端是具有挑战性的。虽然重复使用(例如重复使用语法定义)简化了实现的过程,但产生的抽象结构树(AST)并不总是直观的。抽象结构树(AST)通常反映的是奇特的人为技巧,而不是直接表示编程层次上的结构。此外,如果您的目标是使用词法信息就能完成得“相当好”的简单语法分析,嵌入编译器的前端无异于“杀鸡用牛刀”。
如果希望转换源代码的格式,则会出现其他的复杂问题:抽象结构树(AST)的变化最终必定反映到传统的源代码表示中,因为后者是主要的长期存储格式。要从一个抽象结构树(AST)重新创建一种文本表示,最直接了当的方法就是不进行语法分析,但这样会产生不受欢迎的副作用,如缩排或空白的更改。开发人员所依赖的其他词法工具会分不清这些更改,例如,版本控制系统就无法区分一个有意义的修改和一个意外产生的无理修改。
最后,在某种工具中使用语法分析器必定会把该工具定位于那种特定的语言,因而会减弱它的适用性和通用性。更糟的是,由于源程序没有标准的结构化的外部表示法,甚至对于以同一种编程语言为目标的不同工具来说,要对它们之间的互操作性进行支持,都是很困难的。
这些复杂因素的最终结果是,开发人员往往不得不使用简单的、面向词法分析的工具,比如编辑器中的 grep 或者“搜索并替换”。这种方法牺牲了准确性:试想将一个局部变量从 result 重命名为 answer。借助简单的“搜索并替换”,每一处出现这个单词的地方都会替换,即使是注释、文本串或者毫不相干的实例字段中的字符,也不例外。
一些开发人员采用的一种替代方法是,依靠集成开发环境 (IDE) 中提供的一套固定的工具,该开发环境能够通过一个集成的语言专用分析器访问其源程序的结构,但这种方法牺牲了灵活性。集成开发环境(IDE)一般只提供一套有限的功能,而且这些功能难于扩展。此外,使用现有的交互式环境,难以自动或批量分析和转换源代码。一些更高级的集成开发环境 (IDE) 如 IBM VisualAge for C++ [ 48],将应用程序编程接口提供给程序的表示法,虽然这也算是一种改进,但这项技术仍然有弊端,因为它不能将简单工具从一种复杂的环境中分离出来,此外还产生了对专有技术的依赖,这是我们所不希望看到的。
解决方案
隐藏在上述问题背后的一个基本问题是,源代码缺乏规范的结构化表示方法。我们需要有一种通用的格式来直接表示程序结构,以便于软件工具分析和处理。我们观察到的一个关键之处是,XML 这种可扩展标记语言(eXtensible Markup Language) [ 9],正好提供了这种功能,而且它是一种功能无比强大的源代码补充表示。
在本文中,我要介绍 Java 标记语言,也即 JavaML ? 一种用于描述 Java 源程序的 XML 应用程序。JavaML 文档类型定义 (DTD) 规定了有效的 JavaML 文档的元素,以及这些元素组合的方式。在这些元素、元素的属性以及用这些元素编写的编程语言结构之间,有一种自然的对应关系。在 JavaML 文档中,源程序的结构反映在元素的嵌套中。借助这种表示,我们就可以利用处理和查询 XML 和 SGML 文档的大量工具,提供一种功能丰富的、开放式的基本结构,进行 Java 源代码的软件工程转换和分析。
JavaML 很适合用作工具软件的规范的 Java 源代码表示。它保留了传统表示的大部分优点,而且克服了这些表示法的许多弱点。下一节讲述 Java 语言和 XML 的有关特性, “Java 标记语言 (JavaML)” 一节则详细描述标记语言,以及传统表示与 JavaML 之间的转换器的实现。 “使用 XML” 一节举了许多例子来说明,如何利用现有的 XML 和 SGML 工具,在 JavaML 提供的功能更为丰富的表示法的基础上,分析源代码和进行格式转换。 “相关工作”和 “下一步工作” 讲述相关的工作,并推荐完成更令人兴奋的下一步工作的途径。 附录 A 中有 JavaML 的完整的文档类型定义 (DTD),而已转换的源代码的更多实例可从作者 的 JavaML Web 页 [ 4 ] 上获得。
背景
Java 标记语言在 Java 和 XML 这两种技术之间架起了一座桥梁,它受到这两种技术的大量特性的影响,获益匪浅。
Java 技术
虽然基于 XML 的编程语言结构的表示法与语言无关,但是 Java 语言是试验这些理念和技术的最佳选择。
Java 语言是一种流行的面向对象的编程语言,由 Sun Microsystems 在 90 年代中期开发 [ 3][ 25 ]。它是一个基于 Java 虚拟机 (JVM) 的、与操作平台无关的执行模型,由于用作万维网应用程序的编程语言而很快被广泛的接受。Java 语言将一种令人联想到 Smalltalk [ 26 ] 的简单对象模型,与 Algol 语言块结构、一种类似 C++ [ 49] 的语法,一种静态类型系统和一种由 Modula-2 [ 10] 产生的软件包系统结合在一起。
在 Java 语言中,与在大多数其他面向对象 (OO) 的语言中一样,对程序语言进行解析所得到的基本单元是 类(class),类规定了一组对象的行为。每一个类可定义几种 方法,或者称之为行为,类似于函数或过程。一个类还可以定义 域,或称之为状态变量,这些域与类的 实例相关联,而这些实例叫做 对象。类可以从 超类继承行为和状态,从而形成互相关的类的层次结构,该层次结构允许在其顶部将相关的代码分解为类,从而有利于重复使用。行为是通过向目标接受者对象发送一个 消息来调用的,此消息就是执行一个为该类定义的方法的请求。选择执行什么方法来响应一个消息叫做 动态调用 ,依据的是接收消息的对象的运行时类。例如 ColoredBall 类的一个实例,可能用一种与 Ball 类的实例不同的方法响应 draw 消息。这种收到同一消息而做出不同响应的能力,主要得益于 Java 语言的可扩展性优势,而这种优势正是面向对象(OO)的团体所大力称道的。
Java 语言在工业和教育领域都得到了广泛应用,并且仍然是一种广受欢迎的 Web 编程语言。与 C++ 不同,Java 类定义放在一个单独的自含式文件中,既没有单独的头文件也没有执行文件,并且 Java 语言基本上没有定义的次序相关性。在出现方法体时,它总是紧随方法特征声明之后定义。此外,Java 语言缺少集成处理器。这些特性合在一起,使 Java 源程序在语法上很简洁,从而使 Java 语言成为使用 XML 表示的最理想的语言。(这种方法对其他语言的适应能力将在 “下一步工作” 中进一步讨论。)
XML:可扩展标记语言
XML 是一种标准的可扩展标记语言 [ 9 ],是 SGML(标准通用标记语言)的子集 [ 37 ]。万维网联盟 (W3C) 将 XML 设计得既小巧又简单,同时仍保持与 SGML 兼容。尽管 HTML(超文本标记语言)当前是标准的 Web 文档语言,W3C 正在将 XML 定位为 HTML 替代语言。标记文档时,HTML 仅允许编程人员使用预先定义的一套固定标记,而 XML 却可以方便地使用用户定义的标记,来适应手头文档和数据的标记要求 [ 27][ 28]。
XML 文档只是由使用标记符进行标记的文本简单地组成,这些标记符含在尖括号内。下面是一个简单例子:
<?xml version="1.0"?><!DOCTYPE email SYSTEM "email.dtd"><email><head><to>Mom</to><to>Dad</to><from>Greg</from><subject>My trip</subject></head><body encoding="ascii">The weather is terrific!</body></email>
<email> 是 email 元素的开启标记,本例结尾的 </email> 是对应的关闭标记。文本和其他嵌套的标记可出现在开启和关闭标记结构之间。出现空元素是允许的;空元素以专门的形式缩略,将开启标记和关闭标记合并在一起: <tag-name/>。在上述文档中,email 元素包含两个直接子元素:head 和 body。此外, XML 开启标记可以与元素的属性/值对相关联,例如,上面 body 元素的 encoding 属性值为 ascii。若要一个 XML 文档是 合式 的,则该文档必须完全遵循 XML 文档要求的大量的语法规则(例如标记必须配对并正确嵌套,属性值必须格式正确并放在引号内,等等)。
XML 文档的一个更严格的特点是 有效性 。当且仅当一个 XML 文档既是合式的,又符合其指定的 文档类型定义 (或称 DTD )时,该 XML 文档才是有效的。文档类型定义是一种形式描述,用于一类 XML 文档使用的特定语言的语法,它定义所有已允许的元素名称,并描述每一类元素可以拥有的属性,同时,它还限制一个有效的 XML 文档内的嵌套结构。就下面的文档定义类型 (DTD) 而言,前面的 XML 示例是有效的:
<!-- email DTD --><!ENTITY % encoding-attribute"encoding (ascii|mime) #REQUIRED"><!ELEMENT email (head,body)><!ELEMENT head (to+,from,subject?)><!ELEMENT to (#PCDATA)><!ELEMENT from (#PCDATA)><!ELEMENT subject (#PCDATA)><!ELEMENT body (#PCDATA)><!ATTLIST bodyencrypted (yes|no) #IMPLIED%encoding-attribute;>
按照此文档类型定义 (DTD),有六种元素类型。email 元素必须正好包含一个 head,后面正好跟一个 body 元素。Head 也同样必须包含一个或多个 to 元素,然后是一个 from 元素,后面跟一个可选的 subject 元素。元素的次序必须是指定的次序。这些元素中的每一个都可以包含文本(又称 已分析的字符数据 或 PCDATA)。文档定义类型 (DTD) 中的单一 ATTLIST 声明规定,body 元素 可以 为 encrypted 属性指定一个值,并且 必须为 encoding 属性指定 ascii 或 mime。encoding-attribute 的 ENTITY 声明(在 DTD 顶部)是一种分解出冗余文本的简单方法;即引号之间给出的文本,可按原样替换到下面的 ATTLIST 声明中(并且,重要的是,它可以用在多个 ATTLIST 中)。
如果上述准则有任何一个未得到满足,则声明遵循此文档定义类型(DTD)的 XML 文档无效。例如,如果某个 email 文档中缺少 from 元素,则该文档是无效的,虽然它可能仍然是合式的。
当您在 XML 中制作数据模型时,主要的设计决策是在嵌套元素或使用属性之间作出选择。在上例中,如果我们这样选择的话,本来也可以把 head 中包含的全部元素都合并到 email 元素的各个属性中去。在使用属性和嵌套元素之间有一些重要区别:属性/值对是无序的,而嵌套的子元素具有规定的次序。 属性的值可以只包含字符数据,不可以包括其他标记,而嵌套的子元素可以随意地进一步嵌套。 只能给每个属性一个值,而一个父元素可以包含多个属于同一类的元素(例如,我们可以让多个 to 元素包含在 head 中)。
虽然上面的区别有时可以决定使用这种技术还是那种技术,但最初的决策往往是一个爱好问题。但是,以后使用由此产生的文档的经验,可能会启发人们重新去考察这个决策,以便使文档中所需的某些操作变得容易或简单。
XML 的另一种有用的数据模型编制特性是,通过 id 属性给元素附上一个唯一标识符,然后其他元素的 idref 就可以引用这些元素。一个合式的 XML 文档,必须让每一个 idref 值与文档中的一个给定 id 匹配。id/idref 链接描述一些优势,这些优势能够使 XML 表示通用的定向图,而不只是表示树目录。
一些工具(如 Emacs 编辑模式、基于结构的编辑器、DTD 语法分析器、验证实用程序、查询系统、格式转换和样式语言,以及许多其他工具)能够很好地支持 XML,这在一定程度上是由于 XML 是从 SGML 继承而来的。许多其他 W3C comments都与 XML 有关,包括级联样式表 [ 8]、XSL(可扩展样式表语言)[ 19]、XSLT (用于转换的 XSL)[ 14]、XPath [ 16 ] 和 DOM(文档对象模型)[ 2]。
Java 标记语言 (JavaML)
Java 标记语言提供了一套完整的 Java 源代码自描述表示。与传统的基于字符的程序表示不同,JavaML 以基于 XML 语法的元素嵌套方式,直接反映了软件产品的结构。此外,通过使用 XML 的 id 和 idref 链接,JavaML 还在程序图中表现出了更多的侧面。
由于 XML 是一种基于文本的表示,所以它保留了传统源代码表示的许多优点。而 JavaML 是一种 XML 的应用程序,所以它易于进行语法分析;并且,现有的所有用于 XML 的工具,都可用于以 JavaML 表示的 Java 源代码。JavaML 的各种工具可以利用现有的基础结构和规范表示法来增强其互操作性。
可能的方法
虽然使用 XML 应用程序来编制源代码的基本方法是相当直接的,但到底使用哪种可能的标记语言,仍然有一个很大的设计空间。最明显的可能性是直接将 XML 用作一个典型的抽象语法树文本转储格式,该语法树源于对源代码的语法分析。现考虑下面这个简单的 Java 程序:
import java.applet.*;import java.awt.*;public class FirstAppletextends Applet {public void paint(Graphics g) {g.drawString("FirstApplet", 25, 50);}}
执行上例中抽象语法树明显的(但极不令人满意的)转换,可能会产生 仅第一行代码 的如下 XML 形式:
<compilation-unit><ImportDeclarationsopt><ImportDeclarations><ImportDeclaration><TypeImportOnDemandDeclaration>import<Name><QualifiedName><Name><SimpleName>java</SimpleName></Name>.<Name><SimpleName>applet</SimpleName></Name><QualifiedName></Name>. * ;</TypeImportOnDemandDeclaration></ImportDeclaration></ImportDeclarations></ImportDeclarationsopt>...</compilation-unit>
毫无疑问,这种转换与理想相去甚远:它冗长得令人无法接受,并且它给出了底层语法的许多令人乏味的细节,这些语法原本是用来分析传统源代码表示的。
另一种可能性是直接标记 Java 源程序而不更改程序的文本(即仅添加标记)。这种方法可能把 FirstApplet.java 实现转换为:
<java-source-program><import-declaration>import java.applet.*;</import-declaration><import-declaration>import java.awt.*;</import-declaration><class-declaration><modifiers>public</modifiers> class<class-name>FirstApplet</class-name>extends<superclass>Applet</superclass> {<method-definition><modifiers>public</modifiers><return-type>void</return-type><method-name>paint</method-name>(<formal-arguments><type>Graphics</type><name>g</name></formal-arguments>)<statements>{g.drawString("FirstApplet", 25, 50);} </statements></method-definition>}</class-declaration></java-source-program>
这一格式朝着一种更有用的标记语言的方向迈了一大步。我们已经肯定地向源代码添加了值,而转回传统的表示法也很容易:只要删除所有 tag,并留下元素的内容(这种删除标记的方法,与 stripsgml [ 31 ] 实用程序的作法,如出一辙)。虽然这种表示似乎对许多任务有用,但它仍然存在一些问题。首先,编码的许多详细信息包含在元素的文本内容中,如果我们想确定要导入什么软件包, XML 查询将需要对声明导入的元素进行词法分析。这种分析用起来不方便,而且没有利用 XML 提供的能力。也许更为重要的一点是,上述的 XML 表示法保留了传统源代码的人为因素,而另一种表示法则可能允许我们不考虑语法细节进行抽象,使我们自己从这些语法重负中完全解脱出来。
选择的表示
我选择原型 JavaML 表示,旨在制定 Java 语言(实际上包括类似的面向对象)的编程语言结构模型,而不依赖特定语言的语法。人们可以很容易想到有一个 Smalltalk 标记语言(SmalltalkML),与此想法非常类似,甚至可以想到应该有一个面向对象标记语言(OOML),它既可以转换成传统的 Java 源代码,又可以转换成 Smalltalk 的外部定义格式。心中有了这个目标,于是首先按这些结构原则设计出 JavaML,然后对它不断地进行提炼,使它在功能和可读性方面都堪称优秀的标记语言。
JavaML 由 附录 A 中的文档类型定义 (DTD) 来确定,但最好还是用示例来说明。对于上面列出的 FirstApplet.java 源代码,我们给出如图 1 所示的 JavaML 程序。
图 1.转换成 JavaML 的 FirstApplet.java
1 <?xml version="1.0" encoding="UTF-8"?>2 <!DOCTYPE java-source-program SYSTEM "java-ml.dtd">34 <java-source-program name="FirstApplet.java">5 <import module="java.applet.*"/>6 <import module="java.awt.*"/>7 <class name="FirstApplet" visibility="public">8 <superclass class="Applet"/>9 <method name="paint" visibility="public" id="meth-15">10 <type name="void" primitive="true"/>11 <formal-arguments>12 <formal-argument name="g" id="frmarg-13">13 <type name="Graphics"/></formal-argument>14 </formal-arguments>15 <block>16 <send message="drawString">17 <target><var-ref name="g" idref="frmarg-13"/></target>18 <arguments>19 <literal-string value="FirstApplet"/>20 <literal-number kind="integer" value="25"/>21 <literal-number kind="integer" value="50"/>22 </arguments>23 </send>24 </block>25 </method>26 </class>27 </java-source-program>
在 JavaML 中,诸如方法、超类、消息发送和字面值数字之类的概念,都是直接在文档内容的元素和属性中表示的。JavaML 的表示法用元素的嵌套来反映编程语言的结构。例如,文本串 “FirstApplet” 是消息发送的一部分,这样 literal-string 元素就嵌套在 send 元素内。如果像图 2a 和图 2b 那样,以可视形式给出源代码时,这种嵌套就更明显了。有关的更多例子,请参见 JavaML 网页 [ 4]。
细心的读者会看到, JavaML 表示的源代码的长度约比传统的源代码长三倍。源代码变长是转化成自描述数据格式(如 XML)的基本代价。有一点很重要,即编程人员在某些任务(包括一般开发和程序编辑)中能够使用简明扼要的传统表示法,尽管 JavaML 可能是底层表示法。JavaML 是对传统源代码表示的补充,并且特别适合在工具软件中使用,同时仍然可由开发人员访问并直接读取。
图 2a. 由 XML Notepad 实用程序 [ 44 ] 显示的 FirstApplet 示例 JavaML 表示法树形图
图 2b. XML Spy [ 36 ]显示 的 FirstApplet 示例的 JavaML 表示法树形图
设计决策
JavaML 所提供的不只是源程序的结构。请注意,将图 1 第 17 行的形参 g 用作了消息发送的目标,该 var-ref 标记的 idref 属性向后指向所引用的 formal-argument 元素(通过其 id 属性)。(在一个文档内,为要引用的元素选择的 id 值必须是唯一的,以便每一个标识符都用一个整数标记,从而使其值各不相同。)这种链接是标准的 XML,这样 XML 工具就能够从一个变量的使用追溯到变量的定义,例如,可获得变量的类型信息。局部变量(即代码段内声明的变量)也有类似的链接,程序结构图的其他方面还可以有更多链接。尽管单一的 var-use 标记已足以指示在任何地方出现的一个变量,但 JavaML 能够在变量值的引用和用作左值的变量之间进行区分:var-ref 元素用于前者,var-set 用于后者。
在整个 JavaML 中,除非元素值的结构比简单文本字符串更加复杂,否则程序员随时可以使用元素的属性。元素的属性可用于诸如 synchronized 和 final 之类的修饰符,也可用于诸如 public 或 private 之类的可视性设置,但属性不用于类型之类的特性,因为类型具有某种结构形式:类型可由一个基本名和一个维数组成,并且它还可以引用实现该类型的类的定义,如果您想这样做的话。如果,比方说,一个返回类型只是方法元素的一个属性值,那么最终用户将不得不对属性的值 "int[][]" 执行字符串处理(这是令人不能接受的),以确定该二维数组的基本类型是原始类型 int。实际上,类型编写为显式子元素,如 <type name="int" dimensions="2">。
JavaML 推广了一些相关的概念,使某些分析过程得以简化,但保留了实现其他任务可能需要的一些特征。例如,45 和 1.9 分别表示为 <literal-number kind="integer" value="45"> 和 <literal-number kind="float" value="1.9">。另一种可能的标记是 <literal-integervalue="45"> 和 <literal-float value="1.9">,但分别使用不同的元素类就淡化了两个值都是数字这层紧密关系,并且表示法的使用更加复杂化。相反,使用单一元素标记,并依据一个 kind 属性来区分这些文字,这样,我们仍可以区分浮点型文字和整型文字,而在通常情况下,我们得到的是与 Java 语言类似的数字类型的灵活性。
JavaML 对程序语言结构的另一处推广见之于循环。for 循环 和 while 循环均可视为广义的循环结构:以 0 或其他值作的初始化值、每次循环之前进行一次检测、对 0 或其他值进行更新操作,以及一组包括循环结束指令的语句。因此,JavaML 不分别使用 for-loop 和 while-loop 两个元素类,而是只使用单一的 loop 元素,这个循环元素具有 kind 属性,它的值可以是 for 或是 while。当转换一个 while 循环时,它既没有 initializer,也没有 update 子元素,而一个 for 循环可能包含许多 initializer 和 update 子元素。另外, do 循环所具有的一些独特的 do-loop 元素仍用于 do 循环,因为 do 循环把检测放在循环的末尾而不是循环的开始。
作为另外一个例子,我们把实例字段和类(即静态的)字段都表示成具有 static 属性的 field 元素,以便对它们进行区分。虽然实例字段和类字段这两个概念之间的差异远远超过 do 循环和 while 循环之间的差异,但是用单一元素表示这两种字段仍然是大有好处的。
局部变量声明是一种语法速记,它给我们提出了有关声明底层表示的有趣的问题。代码段 int dx, dy; 定义了两个 int 类型的变量,但是这个代码段可能隐隐约约带有这样的意思:这两个变量具有相同的类型。相反,请看一看代码段 int weight, i;。这段代码可能就没有这种暗示的意味,它使用语法速记仅仅是为了语言的简洁。因为很难自动识别这两种情况,故对于使用这种速记法的变量声明语句,JavaML 运用了属性 continued="true",从而直接保留了这一语法特性。
在 JavaML 中,处理源代码中的注释语句尤其麻烦。目前,DTD 允许某些“重要”元素(包括 class、anonymous-class、interface、method、 field、block、loop)指定一个 comment 属性。确定将哪些注释附加到哪些元素上,是一项具有挑战性的工作;当前的实施方案只是简单地将注释排队,并且将上一个“重要”元素指定属性以来出现的所有注释,都包含到当前这个元素的 comment 属性中。
另外一个解决注释问题的可能方案是,简单地把注释语句插入 JavaML 表示法中,在进行语法分析时,注释语句仅仅作为散布在正常程序结构中的字符数据,这样就把语义分析的问题留给了其他软件工具。遗憾的是,这种方法将使各种元素具有“混合内容”,在检查 DTD 一致性时,验证能力将会降低。使用 XML 模式 [ 51 ] 来取代 DTD,可以使这种方法更为有效。
转换器实现
为了试验 JavaML 的设计效果并获得使用这种表示法的经验,必须实现从 Java 传统源代码表示到 JavaML 的转换器。在 IBM Jikes Java 编译器框架 [ 35 ] 中,我为每个抽象结构树(AST)节点都添加了一个 XMLUnparse 方法。这种更改,再加上使用一些小的代码段来管理用于请求 XML 输出的选项,形成了一个强健而快速的 JavaML 转换器。我总共向 Jikes 框架添加了大约 1650 行既非注释也非空行的 C++ 代码,以支持 JavaML。
该转换器已经测试过,共转换了 15,000 行多种样本程序,其中包括 4,300 行的 Cassowary Constraint Solving Toolkit [ 5 ] 和各式各样的 applet [ 50 ] 20 多个。然后,使用 James Clark 的 Jade 软件包的 nsgmls 工具 [ 12 ],对每个转换的文件做有关 JavaML 的 DTD 验证。在基于 RedHat6 的双 Pentium III-450 机器上,整个回归测试的处理只需要 12 秒钟。
此外还实现了 XSLT 样式表转换,该样式表输出了用 JavaML 表示法表示的传统源代码。此样式表由 65 个模板规则和不到 600 行代码构成。对许多程序是这样测试(既用 Saxon [ 39] 又用 XT [ 15 ])的:将一个文件转换为 JavaML 文件,再把它转回原文件,然后重新转换为 JavaML 文件 ? 在最后结果和第一次转换成的 JavaML 文件之间不存在差别。所有的源代码均可从 JavaML 主页 [ 4] 获得。
利用 XML
JavaML 将 XML 用作 Java 源程序可供替换的另一种结构化表示法。仅从语法抽象不必考虑 Java 语言的语法细节这一点来说,已经非常方便了;但 JavaML 还有一个更重要的好处:那就是能够利用为支持 SGML 和 XML 而开发的丰富的基础结构。我们可以使用、组合及扩展现有的 SGML 和 XML 工具,而不必从暂存区构建分析和转换工具,来处理程序的专用二进制结构化格式。XML 工具含有丰富的功能部件,这些功能部件包括查询和转换工具、文档识别和合并工具 [ 32 ] 以及一些简单的 API,可直接处理文档。限于篇幅,本文仅讨论三种组合工具的使用:爱丁堡大学(Edinburgh University [ 52 ]) 的 XML 工具箱,包含 sgcount、sgrpg、sggrep 等等
XSLT [ 14] 处理器(例如 XT [ 15] 和 Saxon [ 39 ])和 XML 语法分析器 XP [ 13 ]
Perl XML:: DOM 软件包 [ 20 ],向 XML 树展示了一个 DOM 一 级 [ 2 ] 接口
这只是在使用 JavaML 时,非常有用的工具的很少一部分。在下面的示例中,我们将查询 Hangman.java.xml,它是 Hangman applet 的 JavaML 表示,该 applet 可从 Sun Microsystems 的 applet 页 [ 50 ] 获得,也可从 JavaML 主页 [ 4 ] 获得。虽然从现实世界的标准来看,这些示例不大,但是 XML 工具 和 SGML 工具的目标是处理像大部头的书那么长的程序,所以实现时设计了很强的可伸缩性。
软件工程的一项通用任务(不论好坏)是积累关于原文件的标准。借助 JavaML,SGML 实用程序 sgcount 就出色地总结了 Java 程序的结构(输出的命令已经过删节并略加编辑,以便于在此展示):
% sgcount Hangman.java.xml
outputs:
arguments 103array-initializer 4assignment-expr 60catch 3class 1if 27true-case 27false-case 7field 28field-access 18import 5java-source-program 1literal-char 5literal-boolean 5literal-null 5literal-number 127literal-string 61local-variable 23loop 13method 18new 4new-array 5return 5send 99type 96var-ref 262var-set 52...
在上面的输出中,每一行列出一个元素类和该元素在文档中出现的次数。这样,我们很容易看出共有 18 个方法元素,因而就有 18 种 method 定义。类似地,我们可以看出有 1 个类定义、262 个变量引用、99 个消息发送和 61 个字符串文字。此摘要与其说是一个典型的词法分析(例如代码行数字),还不如说是对程序内容的说明。
如果我们想看一看一个程序包含的字符串文字,我们可以在该程序的 JavaML 表示中使用 sggrep 来完成这项琐碎的工作:
% sggrep '.*/literal-string' < Hangman.java.xml
outputs:<literal-string value='audio/dance.au'/><literal-string value='img/dancing-duke/T'/><literal-string value='.gif'/><literal-string value='img/hanging-duke/h'/><literal-string value='.gif'/><literal-string value='Courier'/><literal-string value='Courier'/>...
请注意 sggrep 的输出仍然是一个(不一定有效,甚至不一定是合式的) XML 文档。因此,我们可以在 UNIX 的管道中排列 SGML 和 XML 工具,使这些工具以一种新颖的,有用的方式结合在一起。例如,在某些情况下,把结果转回普通的 Java 源程序表示是非常必要的,这样可以帮助我们的软件工程师。我们可以使用 results-to-plain-source 实现这个功能,results-to-plain-source 是一个围绕 XSLT 样式表(该样式表可以把 JavaML 转回简单源代码)的外壳:
% sggrep '.*/literal-string' < Hangman.java.xml | results-to-plain-source
outputs:"audio/dance.au""img/dancing-duke/T"".gif""img/hanging-duke/h"".gif""Courier""Courier"...
也可以根据元素的属性在 JavaML 源程序中查询一个元素。例如,如果想找到所有的消息发送setFont,可以很容易地得到精确的结果:
% sggrep '.*/send[message=setFont]' < Hangman.java.xml
outputs:
<send message='setFont'><target><var-ref name='g' idref='frmarg-212'/></target><arguments><var-ref name='font' idref='locvar-611'/></arguments></send><send message='setFont'><target><var-ref name='g' idref='frmarg-212'/></target><arguments><var-ref name='wordFont'/></arguments></send>
在这种结构性标记中,七个字符 "setFont" 出现的地方有注释,文本串,或者变量名,但是这些信息都不会在查询结果中得到体现。一个类似的操作就是利用词法工具去检索信息,其结果将包含同样的错误的正数。请试想如果你仅仅使用词法工具,去查找各种强制类型转换的表达式,将是什么情形 -- Java 表达式中大量使用的括号将使这个工作非常困难,但是,对 JavaML 来说那只是小菜一碟,因为可以使用 cast-expr 元素。
另外一种常见的分析是,在实现转换之前由编译器进行语法检查。例如,在 Java 代码中,只有抽象类才具有抽象方法。在编译过程中,如果程序违反了这条规则,那么编译器会标记一个语法错误。我们可以在一个 JavaML 文档中查询出包含抽象方法的具体类(也就是非抽象类):
% sggrep -q '.*/class[abstract!=true]/method[abstract=true]' < Hangman.java.xml
当然,输出的结果肯定是空的,因为我们的目标文档(也就是被分析的程序)没有违反这条语法规则。
Java 编程新手经常犯的错误之一就是,在应该使用等值检测符 == 的地方不小心使用了赋值运算符 =。 虽然在编译阶段中,Java 的类型检查器可以捕获绝大部分此类错误,但是如果被赋值的变量是 boolean 型变量,那么这种错误将会逃过类型检查器的检测。如果想发现这种结构性错误,那么 JavaML 中的 sggrep 可以使我们很轻松地完成工作:
% sggrep -q '.*/if/test/assignment-expr' < Hangman.java.xml
程序 sgrpg (SGML RePort Generator) 可以把高水平的查询,对子元素的约束和查询结果的输出格式结合起来(一个查询工具的通用语句变化表 [ 24 ])。例如:
% sgrpg '.*/method' '.*/send[message=drawLine]' '' '%s %s' visibility name < Hangman.java.xml
outputs:public paint
可搜索包含消息 drawLine 的消息发送的方法定义。如上所示,程序立即输出匹配元素的的 visibility 属性和 name 属性,这正好同我们的直觉相符,因为 paint() 是唯一调用 drawLine 的方法。
只需利用标准的 XML 工具提供的查询能力,就可以进行大量的分析。我们能做的其他事情包括从循环体内部的返回,所有整型变量的定义,所有不符合项目命名惯例的字符串变量,等等。
前面的查询反映了当前 XML 查询工具的一个缺点: 大多数反应只是对匹配的元素作出的 -- 它们不能提供元素在文档中出现处的上下文信息。虽然在把 XML 文档严格地看作一个数据库时,这种操作是合适的,但是软件工程师可能想知道查询结果发生在 JavaML 文件中什么地方,这样可以把结果映射回原始文档,以便对原始文档进行手工编辑和阅读。为了阐明 JavaML 中存在的这种困境,我把有关原始程序代码结构位置的信息附加在各种元素的属性中。位置信息包括起始行,终止行以及结构的列数(文件名放在祖先类的 java-class-file 元素中)。
除了查询之外,当对软件产品进行修改和扩展时,转换源代码是非常有用的。一般来说,查询工具仅仅是从源文档中捡出符合条件的元素,或者从多个文档中捡出几个元素的组合。功能更为强大的转换工具包括 XSLT [ 14], DSSSL [ 38 ], 或者使用一个能访问多种语言(例如 Perl, Python, Java, 和 C++ )的 DOM (文件对象模型 Document Object Model) 接口直接处理文档 [ 2 ]。例如,我们可以直接使用 XSLT 样式表把所有名为 isBall 的方法重命名为 FIsBall :
<xsl:stylesheet ......><xsl:param name="oldname"/><xsl:param name="newname"/><!-- mostly do an identity transform --><xsl:template match="*|@*|text()"><xsl:copy><xsl:apply-templates select="*|@*|text()"/></xsl:copy></xsl:template><xsl:template match="method[@name=$oldname]"><method name="{$newname}"><xsl:apply-templates/></method></xsl:template><xsl:template match="send[@message=$oldname]"><send message="{$newname}"><xsl:apply-templates/></send></xsl:template></xsl:stylesheet>
执行过程如下:
xt source.java.xml method-rename.xsl oldname=isBall newname=FIsBall
尽管使用文本编辑器或者 Sed 可以获得类似的文本转换的结果,但是这类工具会改过了头,它们会修改所有顺序出现 isBall 这六个字符的地方。以文本为基础的转换可能会错误地影响变量名,文本串,注释和包。而 JavaML 表示的主要优点之一是: 我们可以对影响到的结构进行更为精细的,以语义为基础的控制。
其他可能的转化包括使用类型表,输出一个程序的可浏览的 HTML 格式的页面(见图 3 )或者强调语法的 PostScript。在函数的入口和出口增加调试或者测试代码也是一件非常容易的事情(见图 4)。
图 3. Hangman.java.xml,经过 XSLT HTML 极妙的打印和索引程序处理
方法索引同每个方法定义的开头相链接,使用彩色编码和斜体字,可以起到突出语法的显示效果。
图 4. 调用 Tracer.StartMethod(方法名)和 Tracer.Exitmethod(方法名) 检测某个 Java 类的所有方法的 Perl 程序
#!/usr/bin/perl -wuse XML::DOM;use IO::Handle;my $filename = shift @ARGV;my $parser = new XML::DOM::Parser;my $doc = $parser->parsefile ($filename);my $nodes = $doc->getElementsByTagName("method");for (my $i = 0; $i < $nodes->getLength(); $i++){my $method = $nodes->item($i);my $block = $method->getElementsByTagName("block")->item(0);my $name = $method->getAttribute("name");my $start_code= SendMessageBlock($doc,"Tracer","StartMethod", $name);my $exit_code= SendMessageBlock($doc,"Tracer","ExitMethod", $name);$block->insertBefore($start_code, $block->getFirstChild());$block->appendChild($exit_code);}print $doc->toString;sub SendMessageBlock {my ($doc,$target_var,$method_name,$data) = (@_);# insert, e.g: Tracer.StartMethod("paint");return parseXMLFragment($doc,<<"__END_FRAGMENT__"
相关工作
JavaML 的重要优点之一,就是它可以利用不断扩展的同 SGML 和 XML 相关的软件工具,作为自己的基础,这些软件在前面已经介绍过。许多研究人员几乎都采用类似的方法,来解决软件工程升级和开发工具升级问题,他们已经取得了不同程度的成功。
通过匹配 C 程序中的抽象结构树 (AST)模式,TAWK [ 29] 对 AWK [ 21 ] 的语句变化表进行了扩展。许多 XML 查询工具为 JavaML 提供了相同的功能,而且其中的事件操作框架同 SAX (XML 的简单 API) [ 43] 使用的框架非常类似。
ASTLog [ 18] 扩展了 Prolog [ 17 ] 逻辑编程语言,这种语言可以对模拟抽象结构树(AST)的外部数据库进行推理。与 Prolog 不同,ASTLog 根据一个当前的对象求值。把 Crew 所使用的方法用于 XML 可能非常有趣,但是大量的 XML 工具软件已经按照一种更为传统(可能不太方便)的框架,提供了相同的功能。
GRAS [ 40 ] 是一个面向图像的,用于软件工程环境的数据库系统。编译器的前端可以把普通的 C, Modula-3, 和 Modula-2 程序表示的源代码整合到数据库中去。这种数据库方法在存储 XML 方面已经证明是非常有用的,尤其是用 JavaML 设计软件工程应用程序时,数据库方法更是显示出它的优越性。
软件开发基础 (Software Development Foundation [ 46 ]) 基于 XML 的开放式体系结构, 它用来设计编程环境下的开发工具。有一种称为 CSF (代码结构格式)的 XML 数据库格式,可以用来存储关系,但是它不包括任何已经执行的计算过程的细节。 Chava [ 41 ] 也采用类似的方法,不过它是以 C 程序数据库为基础的[ 11 ]。 Chava 也允许通过对字节代码执行逆向工程来查询 Java 代码。
CCEL [ 22 ] 为 C++ 编写的软件产品提供了一种表达非语言意义(也就是说不能用语言表达)的超语言。JavaML 通过以下方法来提供相同的功能:编写一些简单的查询以搜索指定常量的违规,同时在整个开发周期的各个阶段,如编辑,编译和回归测试过程中,报告查询结果。
微软公司的意念编程小组 (Intentional Programming group [ 47 ])一直在努力开发一种更为抽象的,与具体语法无关的计算过程表示。他们的目标看来是允许软件开发人员描述新的抽象方法,并且找到一种技术使得这些抽象精简为已知的原语。从本质上讲,他们感兴趣的是让软件开发人员在编写软件的过程中创造出域专用的语言。作为这种方法的一个代表,JavaML 特别令人激动。我们可以把新的抽象方法作为文档类型定义(DTD)的渐次扩展。为了使新类型的文档(可以称为 Java++ML)仍能在已有的 Java 编译器中编译,开发人员只需要简单地编写一个从 Java++ML 到 JavaML 的转换。由于 DTD非常容易扩展,因此这种方法应该是站得住脚的,而且它很可能为进一步的工作开创富有成果的途径。运用这种技术时,有几种实用程序会有用处,例如 perlSGML 包的 dtd2html 和 dtddiff [ 31],它们用于文档定义类型(DTD)的文档编制和比较。
下一步工作
虽然本文只是为 Java 语言提供了一种标记语言,但是本文提出的基本方法也可以用于其他语言,甚至可以在不同的语言之间进行转换。根据表示法忽略具体语法细节进行抽象的程度,JavaML 也可能允许导入可视化表示(例如统一模型语言图 [ 1][ 35 ])。由于 XSL 和 DSSSL 的强大功能,生产关于软件产品重要特性的可视化表示已经是指日可待的事情了。
把本文提出的方法应用于 C++ 语言,即另外一个流行的面向对象的编程语言时,一个非常棘手的问题是 C 语言的预处理程序。C 语言的预处理程序首先通过一个文本处理,允许使用那些不能用核心的 C++ 语言表达的抽象。这些抽象对代码的可理解性和可维护性都非常重要,但是他们同语法分析技术 [ 23][ 6] 不能很好地交互。
对当前转换系统的有益的扩展之一,就是对更多的元素进行横向链接。类型元素可以在其他 JavaML 文件中引用他们定义的类。导入声明可以为导入的包引用顶层的文档。还有许多可能性都是行得通的。
当前能把 JavaML 转回传统源代码的转换器是基于 XSLT 的。增加一个 Jikes 前端就可以使编译器直接阅读 JavaML。为实现这种功能,将使用一个 XML 语法分析器(例如 XML4C++ [ 33 ]),从 JavaML 源代码构建 XML DOM, 然后利用 DOM API 可以很方便地循环建立 Jikes 内部抽象结构树 (AST)。运用 Jikes 以前就拥有的、传统的逆向解析器,可以把 JavaML 转回简单源代码。
使用 JavaML 作为主要的源代码表示将具有简化编译器的潜在功能,这不仅仅限于对传统的前端编译器进行简化。一旦编译器知道输入的是一份有效的 JavaML 文档,编译器就可以省略某些语法分析。如果能说明在给定的条件下不可能出现哪些语法错误,那将是非常有用的。由于 XML Schema [ 51][ 7 ] 给出了一套更为精致的 XML 文档有效性规范,因此在工作文档最后确定之后,把 JavaML 而不是 DTD 移植进来将可能非常有益。另外,编辑环境中可以很方便地以直接查询的形式(例如前面 "利用 XML" 中给出的一些例子)移入更多的语法分析功能。
用简洁的文本表示法书写源程序是程序员最乐于接受的方法,因此他们不可能在很短的时间抛弃他们心爱的文本编辑器。我们必须研究更好的方法,以实现传统的源程序表示法和 JavaML 之间的交互式的和渐进式的转换。然后,这种功能就能直接用来支持使用简单文本格式的 XML 表示的交互式编辑,而简单文本格式是工程师们情有独钟的。关于结构性文本编辑器,已有大量的工作 [ 30][ 45 ],它们之间有紧密的联系,并且最终可望获得承认,同时提供无限的资源。由于 XML 技术在商业上的重要性日益增长,这些资源目前就会用来解决问题。
结论
JavaML 是一个可选的、基于 XML 的 Java 源程序表示。与传统的文本源程序表示不同,JavaML 可以使软件工具对 Java 程序中的结构,很方便地进行编程水平的分析推理。这是因为 JavaML 可以更为直接地表示程序的结构。
有了 JavaML,大量既有的 XML 和 SGML 工具就能对 Java 源程序执行各种有趣的和有用的分析与转换。XML 工具软件正在不断地改进,以支持基于 XML 的文档的不断更新的基础结构。最终,JavaML 将代替 Java 程序的传统源代码表示,成为 Java 程序的存储格式,而在整个程序开发过程中,当开发人员同软件产品的结构化表示进行交互时,文本语法分析将仅仅成为几种可能的途径之一。
致谢
我衷心地感谢 Zack Ives,感谢他的批评,他同我的探讨,以及他为我进行的录入工作。我还要感谢 Corin Anderson 和 Alan Borning,感谢他们对这篇论文初稿提出的宝贵的批评comments。我还要感谢 Miguel Figueroa, Karl-Trygve Kalleberg, Craig Kaplan, Todd Millstein, Stig E. Sand 和 Stefan Bjarni Sigurdsson,感谢他们使我受益匪浅的讨论。非常感谢 IBM 构建了 Jikes 编译器框架并把它公之于众,同时非常感谢 Mike Ernst 指导我如何使用 Jikes 编译器。以上工作受到了华盛顿大学计算机科学与工程系 Wilma Bradley 奖学金和国家科学基金第 IIS-9975990 号基金 (NSF Grant No. IIS-9975990)的资助。