个有趣的项目中,其中包括位于牛津的OS6操作系统[Stoy 72],和施乐公司PARC研究中心的创造性项目Alto的一部分[Thacker 79]。我们熟悉该语言,是因为Richards工作过的MIT CTSS系统[Corbato 62]被用于Multics开发。最初的BCPL编译器被Rudd Canaday和贝尔实验室的一些人们移植到Multics和GE-635 GECOS系统上[Canaday 69];在贝尔实验室的Multics项目奄奄一息阶段,它成为随后转入Unix的一帮人选择的语言。
BCPL、B和C都归属于以Fortran和Algol 60为代表的传统过程式家族。它们格外地倾向面向系统编程、小巧、描述简洁,而且可被简单的编译器轻易地翻译。它们接近机器,它们所引入的抽象以传统计算机所提供的具体数据类型和操作为基础,它们依赖于例程库以输入输出以及与操作系统的其它交互。尽管不太成功,它们还使用库程序指定其他有趣的控制结构,如协程和过程关闭。同时,它们的抽象层次足够高,足够用心的话,能达到机器间的可移植性。
BCPL, B和C在很多细节上存在语法差异,但总体上它们是相似的。程序由一系列的全局声明和函数(过程)声明组成。BCPL中,过程能够嵌套,但不能引用定义在外包过程中的非静态对象。B和C通过更强行的限制避免了它:基本就没有嵌套过程。每一种语言(除了早期的B版本)都认可(文件的)分别编译,并提供了从指定文件中包含(including)文本的方式。
BCPL中的若干语法和词法机制较B和C中的更优雅和正式的。例如,BCPL的过程和数据声明拥有更一致的结构,并且它提供了一套更完整的循环结构。尽管BCPL程序名义上是由分界的字符流构成,聪明的规则使得以每一行结束的语句可以省略其分号。B和C忽视了这种便利,大多数语句以分号来结束。刨除这些差异,BCPL的大多数语句和操作符直接对应B和C中的相应物。
BCPL和B之间的一些结构化的差异源于介质存储的限制。比如,BCPL声明采用这样的形式
let P1 be command
and P2 be command
and P3 be command
...
此处的由命令表示的程序文本包含完整过程。这些子声明相互关联而且同时出现,所以名字P3在过程P1内可见。相似地,BCPL能在求得一个值的表达式里包含一组声明和语句,例如
E1 := valof ( declarations ; commands ; resultis E2 ) + 1
BCPL编译器在产生输出前,通过存储和分析内存中该完整程序的解析过的表示,可以容易地处理此类构造。B编译器所受的存储限制决定了一种一次通过( one-pass)技术,由此尽可能快生成输出,而这一语法上的重新设计将这种可能带入到C。
BCPL中一些不令人满意的地方归因于它的技术问题,在B的设计中它们被有意识的避免了。例如,BCPL使用一个“全局向量”(global vector)机制以在分离编译的程序间通信。在这种模式中,程序员使用一个全局向量的数值偏移量,显式关联每个外部可见过程和数据对象的名字。链接使用这些数值偏移量,在被编译过的代码上完成。B起初坚持,整个程序一次性全部传递给编译器,来规避这个麻烦。B的后期实现,和C的全部实现,使用一个传统的链接器,来解决出现在分离编译文件中的外部名字,而不是把指定偏移量的负担推给程序员。
BCPL到B的转换中引入的其它变化,大概是因为风格的缘故,一些仍是有争议的,例如赋值使用单个字符=代替:=。类似地,B使用/**/来括起注释,而B使用//注释直至行末的文本。这显然是从PL/I继承来的。(C++重新启用了BCPL的注释惯例。) Fortran影响了声明的语法:B的声明以一个auto, static这样的类型指定符开始,跟着一列名字,C不仅遵循这种风格,还把它的类型关键字,加入这种声明的开始处。
在Richards的书中文档化的BCPL与B之间的差别,并非都是经过深思熟虑的;我们是从一个BCPL[Richards 79]的早期版本开始工作的。例如,用于跳离switchon语句的endcase在我们1960年代开始学习该语言时,并没有出现,所以B和C中重复出现的,用于跳离switch语句的关键字break,乃是一种背离的发展,而不是清醒的改变。
对比B产生过程中发生的普遍的语法变化,BCPL的核心语义内容——类型结构和表达式求值——保持不变。它们两种语言都是无类型的,或更恰当地说有一种单一的数据类型,“字”(word)或“单元”(cell),一个固定长度的位模式。这些语言中的内存由此类单元的线形数组组成,每一个单元的内容的含义与应用的运算相关。例如,求和运算符使用机器的整数加法指令,简单相加其运算对象,其它算术运算同样不清楚它们运算对象的含义。因为内存是一个线形数组,只可能解析单元的值为该数组的索引,并且BCPL为这个目的提供一个运算符。在最初的语言中,它被拼写为rv,后来为!,但是B使用一元*。因此,如果p是单元,包含另一个单元的索引(其地址,或指向的指针),*p引用被指向单元的内容,作为表达式的值或赋值对象。
因为指针在BCPL和B中只不过是整型内存数组的索引,对它们进行算术运算是有意义的:如果p是一个单元的地址,那么p+1是下一个单元的地址。这种约定是两种语言中数组语义的基础。在BCPL中,一个人这样写
let V = vec 10
或在B中,
auto V[10];
效果是一样的:分配了一个名字为V的单元,然后保留另一组10个连续单元,它们中第一个的内村索引,被存放在V中。按照一般的规则,在B中的表达式
*(V+i)
把V和i相加,并指向V后第i个位置。BCPL和B都增加了特别的符号,使这种对数组的访问更简洁;在B中的等价表达式是
V[i]
在BCPL中是
V!i
这种引用数组的方法甚至在当时仍是不常见的;C后来同化它为一种更不常规的方式。
BCPL,B或C都没有强烈支持字符数据;每一个都把字符串当作整型数组,并通过一些惯例提供了一些一般规则。字符串字面值在BCPL和B中表示一个使用串内字符初始化的静态区的地址,被包装成单元。在BCPL中,第一个被包装的单元包含串所拥有的字符个数;在B中,没有此计数,字符串以一个特别的字符终结,在B中杯拼写为“*e”。这个改变部分是为了避免把计数值放在一个8位或9位槽(slot)产生的串长度限制,部分是因为维护这个计数,从我们的经验看来,不如使用一个终结符方便。
在BCPL,串中每个字符的使用,是通过被展开为另一个数组,一个字符对应一个单元,然后进行再次包装;B提供了对应的例程,但人们更多地使用,另外的访问或替换一个串内字符的库函数。
更多历史
在TMG版本B工作后,Thompson利用B重写了B(编译器)(一个bootstrapping步骤)。在开发中,他不断与内存限制作斗争:每次语言版本使编译器膨胀令内存几乎不够使用,但每次重写利用语言特征的优点,减少了它的尺寸。例如,B引入通用赋值运算符,使用x=+y来把y加入x。这个符号经过McIlroy引自Algol 68[Wijngaarden 75],他将它合并到他实现的一个TMG版本。(在B和早期C,该运算符被拼作=+而不是+=;这个由B的词法分