附录B Diksam语言的设计

以下将要介绍的是在本书中Diksam最终版本(book_ver.0.4)的设计。与附录A一样,内容并不是很严谨。

B.1 程序结构

B.1.1 构成程序的要素

Diksam的程序由以下要素构成。

1. 声明部分

声明部分由require声明和 rename声明组成。声明部分必须在源文件的开头,但也可以省略。

2. 顶层结构

顶层结构(toplevel)由 语句  (statement)组成。Diksam的程序从处理器中指定的源文件的顶层结构开始执行。

3. 函数定义或声明

函数定义(function definition)定义可以(可能)从其他位置调用的函数。在顶层结构的语句顺序执行时会忽略函数定义。因为函数允许回溯引用,所以其定义的位置既可以在调用的位置之前,也可以在调用的位置之后。

函数声明(function declaration)只声明了函数的签名(signature)。对于函数声明来说,必须存在与其对应的定义。

4. 类型定义

类型定义包括类定义(class definition)、接口定义(interface definition)、枚举类型定义(enumerated type definition)和 delegate类型定义 (delegate type definition)。

类型定义是为了定义可能在其他位置使用的数据类型。与函数一样,会在顶层结构的语句执行时被忽略,并且允许回溯引用。

下面是几个示例。

仅由顶层结构组成的Diksam程序:

println("hello, world.");

开头含有声明部分的Diksam程序:

// 将标准函数println改名为print_line

rename diksam.lang.println print_line;

print_line("hello, world.");

包含函数定义的Diksam程序:

// 函数定义

void func() {

println("hello, world.");

}

// 调用定义的函数

func();

包含类定义的Diksam程序:

public class Point {

private double x;

private double y;

void print() {

println("x.." + this.x + ", y.." + this.y);

}

constructor initialize(double x, double y){

this.x = x;

this.y = y;

}

}

// 创建Point类的实例

Point p = new Point(10, 20);

// 输出p的内容

p.print();

B.1.2 分割编译

通过声明部分的 require 声明指定的包(package),达到能够使用在其他源文件中定义的函数和类的目的。

在Diksam 中,包由一个源文件(扩展名 .dkh)或者两个源文件(扩展名 .dkh和 .dkm)的组合组成。

下面的例子通过 require了 hoge.piyo.fuga包,达到了可以使用 fuga. dkh中定义的类和函数的目的。

require hoge.piyo.fuga;

在.dkh文件中描述了只有在函数的签名声明的情况下,实际调用函数的时候处理器才会对 fuga.dkh对应的实现文件 fuga.dkm进行搜索并编译/加载。这种机制被称为 动态加载(dynamic load)。

被 require文件的搜索路径,即环境变量 DKM_REQUIRE_SEARCH_PATH,在UNIX环境中使用逗号分隔,在Windows环境中使用分号分隔。因为Diksam的包名和文件路径的层级是一致的,所以只需要指定搜索路径就可以以此为起点,通过包名的层级搜索文件了。

在没有设定 DKM_REQUIRE_SEARCH_PATH的情况下,当前目录将成为唯一的搜索路径。

另外,动态加载时的搜索路径可以通过环境变量 DKM_LOAD_SEARCH_PATH取得。

diksam.lang 包已经默认为 require,因此使用者无需自己再进行 require。

被require的源文件中,顶层结构将被忽略,不会执行。

B.1.3 解决命名冲突

在 require了多个包的时候,很可能会发生类名、函数名的命名冲突。在这个时候,可以通过声明部分的 rename声明为标识符改名,以此方法来回避命名冲突。

下面是将 diksam.lang包下面的 print函数改名为 myprint的例子。

rename diksam.lang.print myprint;

B.1.4 关于全局变量的链接

在顶层结构中声明的变量(全局变量)相对于源文件独立的,不能进行链接。如果想要让某个包的全局变量被其他的包访问的话,就必须要声明 get_xxx()、 set_xxx()这样的函数。

B.2 语法规则

B.2.1 源文件的字符编码

Diksam的源文件使用与处理器相同的字符编码。当前确定的是在UNIX环境中为EUC和UTF-8,在Windows环境中为GB2312。除了写注释和定义字符串常量,其他情况下不能使用非ASCII字符。

B.2.2 关键字

下面这些单词作为Diksam的关键字,不能作为变量名、函数名、类名等使用。

abstract boolean break case catch class const constructor continue default delegate do double else elsif enum false final finally forforeach if instanceof int interface native_pointer new null override private public rename require return string super switch this throw throws true try virtual void while

(注) foreach是为将来准备的关键字,现在并没有使用。

B.2.3 空白字符的处理

空格(ASCII码的0x20)、制表符和换行符在源文件中除了区分不同的标识符外没有其他含义。

B.2.4 注释

在Diksam中可以使用以下两种注释方式。

以 /开始以 /结束的注释,这种注释不能嵌套使用。

以 //开始直到本行结束的注释

B.2.5 标识符

被当做变量名、函数名、类名等使用的标识符,需要遵从下面的规则。

第一个字符必须是英文字母(A~Z, a~z)或者是下划线。

从第二个文字开始可以是英文字母、下划线或数字(0~9)。

Diksam的标识符中不允许使用中文等未被ASCII字符集涵盖的字符。

B.2.6 常量

1. 真假值常量

真假值常量只有 true(真)和 false(假)。

2. 整数常量

整数常量由“ 0”或者“ 1~9后面跟着0个或以上的 0~9”组成。

如果在前面加上“ 0x”或者“ 0X”前缀的话,可以表示十六进制整数。

目前为止还不支持八进制表示方式。

3. 实数常量

实数常量由“1个以上的 0~9的组合、点、1个以上的 0~9的组合”组成。

.5或者 1.都不是实数常量。

4. 字符串常量

字符串常量是由双引号括起来的字符串。在字符串常量中\n表示换行,\t表示制表符,\表示 \。

5. null常量

null常量用来表示引用类型没有指向任何值。

6. 数组常量

在花括号 {} 中将元素用逗号分开并对元素进行初始化就创建了数组常量。数组类型为该常量中第一个元素的类型的数组。最后一个元素的后面有没有逗号都可以。

B.3 数据类型

B.3.1 基础类型

Diksam存在以下 基础类型 。

  1. void 类型(void)

void类型表示函数是没有返回值的特殊类型。只能作为函数或者方法的返回值使用。

2. 逻辑类型(boolean)

可以是 true或者 false。

3. 整数类型(int)

表示整数的类型。能够表达的范围与编译器以及编译VM的C语言环境的 int类型一致。

4. 实数类型(double)

表示实数的类型。能够表达的范围与编译器以及编译VM的C语言环境的 double类型一致。

5. 字符串类型(string)

表示字符串的类型。其内部表现形式为编译器以及编译VM时C语言环境下的宽字符串。字符串类型属于引用类型。但是,因为字符串本身不能改变(immutable),所以使用者没有必要意识到它是一个引用类型。

B.3.2 类/接口

类和接口都属于用户自定义类型。

关于类定义和接口定义的详细内容请参考B.7节。

类和接口都是引用类型。

B.3.3 派生类

派生类型是由基础类型、类、接口派生出来的类型。

存在以下两种派生类型。

1. 数组类型

Diksam的数组类型,其元素数无须在声明时决定,是可以动态创建的引用类型。

2. 函数类型

函数和方法都是函数类型。函数类型不存在变量,并可以赋值给适当的 delegate类型。

数组类型可以使用递归。

int 类型数组的数组: 

int[][] a;

函数类型通过 delegate 类型可以实现“函数的数组”、“返回函数的函数”等。

B.3.4 枚举类型

枚举类型通过下面的方式定义。在最后一个元素后面有没有逗号都可以。

enum 枚举类型名称 {

枚举名称,

枚举名称,

枚举名称,

}

枚举类型的值(枚举)如下所示,使用枚举类型名称.枚举名称的形式表示。

Fruits.ORANGE

枚举类型在参与加法运算(使用+运算符)时,如果左边是字符串(枚举类型在右边)的话,枚举类型会转换为字符串。

枚举类型可以使用比较运算符(==、!=、 >、 >=、 <、 <=)和 switch 判断分支。

B.3.5 delegate类型

delegate类型是指向函数的引用类型。通过在关键字 delegate后面加上与函数签名相同的形式定义。

下面的实例以一个 Window、两个 int 以及一个 MouseButton 枚举为参数,返回值为 void的delegate类型(在后述Windows版的Diksam中会被实际使用到)。

// 在Window按下鼠标的事件

delegate void MouseButtonDownProc(Window window, int x, int y,

MouseButton button);

通过上述类型定义,就可以像下面这样声明 delegate类型的变量了。

MouseButtonDownProc mouse_button_handler;

也可以定义把 delegate类型作为参数或者函数类型的返回值。

在表达式中,通过单独的函数名就可以创建函数类型的值。函数类型的值可以赋值给与其具有互换性的 delegate类型的变量,还可以像函数的实际参数那样,作为参数进行传递。

void mouse_button_func(Window window, int x, int y, MouseButton button){

}

// 在Window对象中按下鼠标的时候调用

// 为了处理这个事件将mouse_button_func作为参数传递进去

window.set_mouse_button_down_proc(mouse_button_func);

delegate类型的变量不仅可以赋函数,还可以赋方法。在赋方法的时候, delegate类型的变量会将方法对应的类实例一并保存起来。

给delegate类型的变量赋值,需要满足以下条件。

1. 赋值的函数或者方法,必须与被赋值的delegate类型的参数数量一致。

  1. 赋值的函数或者方法的参数类型(相对应地),必须与被赋值的 delegate的参数类型一致,或者是 delegate的参数类型的父类。

  2. 赋值的函数或者方法的返回值类型,必须与被赋值的 delegate的返回值类型一致,或者是 delegate返回值类型的子类。

  3. 赋值的方法在 throws中列出的异常范围要比被赋值的 delegate在 throws中列出的范围小。

B.3.6 内建方法

在数组和字符串类型中,拥有一些内建方法。

数组的内建方法如下所示。

figure_0362_0093

字符串的内建方法如下所示。

figure_0363_0094

B.4 表达式

表达式(expression)是由运算符连接起来的单目表达式。

B.4.1 单目表达式

单目表达式有以下几种。

常量。

标识符。虽然变量名、常量名、函数名是单目表达式,但类名等类型的名称不是表达式。 

this。表示在类的方法内指向当前实例的引用。

super。在类的方法内,调用超类的方法时使用。

new表达式。具体的使用方法请参考B.7.6节。

枚举。

B.4.2 类型转换

在Diksam中,具备了以下条件时会进行自动转型。

双目运算符的类型转换 

双目算数运算符(+、 -、 *、 /、 %)以及比较运算符(==、!=、 >、 >=、 <、 <=)在两边类型不一致的时候,会根据以下规则进行类型扩展。

  1. 无论左边还是右边,只要其中一边是整数(int),另外一边是实数(double)的时候,整数(int)会转换为实数(double)。

  2. 在左边是字符串(string)的情况下,仅限于加法运算(运算符为 +)的时候,右边会转换为字符串。

上述类型转换,在算术赋值运算符(+=、 -=、 *=、 /=、 %=)的情况下同样适用。

赋值时的类型转换 

  1. 把类赋值给类的时候,当左边是右边的超类或者被实现的接口时,会进行 向上转型  (up cast)。

2. 右边是 int类型,左边是 double类型的时候,右边会转换为 double类型。

3. 右边是 double类型,左边是 int类型的时候,右边会转换为 int类型。

B.4.3 运算符一览

Diksam的运算符一览如下所示(优先顺序从高至低)。

figure_0364_0095

在Diksam中,只存在后置的自增和自减运算符。并且,其动作与C等语言中前置的运算符效果相同(不会等到最后,而是立刻自增表达式的值)。

B.5 语句

B.5.1 声明语句

声明语句(declaration statement)是进行变量声明的语句。

声明语句为

类型 变量名;

或者

类型 变量名 = 初始化表达式;

这样的形式(“=初始化表达式”的部分被称为初始化(initializer))。

为声明语句加上 final,可以防止对应变量的初始值被覆盖(禁止再次赋值)。

final int a = 10;

在 final的声明语句中,如果不进行初始化就会发生编译错误。

使用 const关键字声明常量。

const HOURS_IN_DAY = 24;// 一天24小时

const MINUTES_IN_DAY = HOURS_IN_DAY * 60;// 1天24 × 60分

在常量的声明中,因为通过初始值可以判断类型,所以 const无需指定类型。

B.5.2 表达式语句

所谓表达式语句(expression statement)就是在表达式后面加上分号。

// 调用print()函数的表达式后面加上分号

print("hello,world.");

// 自增表达式后面加上分号

i++;

// 无副作用的表达式也可以成为表达式语句,但没有任何意义

5;

B.5.3 if语句

if语句是根据条件判断并处理分支的语句。

if (条件表达式1) {

// 条件表达式1为真时执行的处理。

} elsif (条件表达式2) {

// 条件表达式1不为真,且条件表达式2为真时执行的处理。

} else {

// 所有条件表达式都不为真时执行的处理。

}

elsif 子句和 else 子句都是可以省略的。另外,可以编写任意多个 elsif子句。与C等语言不同的是,只包含一行语句也不能省略花括号({})。

B.5.4 switch语句

switch语句是进行多分支处理的语句。

switch(a)

case 1 {

// a等于1时执行

} case 2,3 {

// a等于2或3时执行

} default {

// a不等于上面的1、2、3时执行

}

Diksam的 switch语句与C语言的fall through不同。在匹配了一个 case之后,即使没有在分支中写 break,也不会交由下一个 case 处理。另外,各 case子句必须使用花括号({})括起来。

在针对多个值进行相同处理的时候,值之间以逗号分隔。

如果没有匹配任何一个 case子句的话,就会执行 default子句。

各 case子句在以下代码成立时就会执行。

switch中描述的表达式 == case中描述的表达式

虽然只能使用 ==运算符,但是表达式的类型没有限制。然而,在 ==两边为了匹配类型会进行类型转换,在 switch中却不会进行类型转换。

B.5.5 while语句

while语句是进行循环操作的语句。

标识符:

while (条件表达式) {

// 在条件表达式为真的情况下,此处的代码会反复执行。

}

“标识符:”的部分被称为标签(label)。标签可以省略。

与 if语句相同,即使只包含一行语句也不能省略花括号({})。

B.5.6 do while语句

do while语句是进行后判断型的循环操作语句。

标识符:

do {

// 这个位置的代码最初必须执行一次,

// 之后,在条件表达式为真的情况下,此处的代码会反复执行。

} while (条件表达式);

“标识符:”的部分被称为标签(label)。标签可以省略。

与 if语句相同,即使只包含一行语句也不能省略花括号({})。

B.5.7 for语句

for语句是进行循环操作的语句。

标识符:

for(第1表达式; 第2表达式; 第3表达式) {

// 在第2表达式为真的情况下,此处的代码会反复执行。

}

“标识符:”的部分被称为标签(label)。标签可以省略。上述 for语句,除了在 continue时会执行第3表达式之外,其他与下面的 while语句效果相同。

第1表达式;

while (第2表达式) {

语句

第3表达式;

}

第1表达式、第2表达式、第3表达式都是可以省略的。在省略了第2表达式时,意味着永远返回真。

B.5.8 return语句

return语句是从函数中跳出的语句。

return 表达式;

在函数为 void类型的时候,如果写了 return表达式的话会报错。

B.5.9 break语句

break语句是用于跳出循环的语句。

break 标识符;

标识符可以省略。在省略的情况下,跳出最内侧的循环。

在指定了标识符的情况下,跳出持有同样标识符的循环。

B.5.10 continue语句

continue语句用于跳转到循环的末尾。

continue 标识符;

标识符可以省略。在省略的情况下,以最内侧的循环为对象。

在指定了标识符的情况下,以持有同样标识符的循环为对象。

在以 for为对象进行 continue的时候, continue后会紧接着对 for语句的第3表达式进行计算。

在以 do while为对象进行 continue的时候,会对下一次的条件表达式进行计算,如果结果为假则终止循环。

B.5.11 try语句

try语句是用于执行异常处理的语句。

try {

// 语句

} catch (异常类型 变量名) {

// 语句

} catch (异常类型 变量名) {

// 语句

} finally {

// 语句

}

在 try子句中如果发生了异常,就会执行 catch子句,会在 catch 子句中按照从上到下的顺序搜索与发生的异常类匹配的 catch 子句。如果有匹配的 catch 子句,则会转移处理位置(抛给上一层或者终止处理)。发生的异常会被设置到 catch 子句声明的变量中。这个变量默认为 final,不可以再次赋值。

finally子句无论异常是否发生,也不论是否有匹配的 catch子句,都一定会执行。在 try子句或者 catch子句中即使是执行了 break、 continue、 return来中断处理,也会执行 finally子句。如果使用 break、 continue、 return来中断 finally子句的话,就会发生编译错误。

B.5.12 throw语句

throw语句是用于抛出异常的语句。

在通常情况下,可以像下面这样显式地为表达式指定异常。

throw e;

在 try 子句内发生的异常,如果存在与其对应的 catch 子句的话,就会被 catch 子句捕获。如果不存在与其对应的 catch 子句,或者异常发生在 try语句之外的时候,会终止当前正在执行的函数并返回到调用者(或者叫上一层)的处理中。如果调用者也没有 catch这个异常的话,则会沿着调用层级回溯直到顶层结构。如果顶层结果中也没有捕捉这个异常的话,就会输出栈轨迹(stack trace)并终止处理。

Diksam的栈轨迹在异常 throw的时候会被清空,并在返回到调用函数的层级或者中断顶层结构执行的时候被再次设置。

因此,在 catch子句中直接再次抛出捕获的异常时,如果不想清除当前的栈轨迹,可以像下面这样单独使用 throw;。

throw;

B.6 函数

B.6.1 函数定义

函数通过如下的形式进行定义。

类型 函数名(参数序列) throws子句 {

语句

}

throws子句可以省略。 throws描述了在这个函数中有可能会发生的异常,用逗号分隔并一一列出。

如下例所示。

// 接收两个整数型参数并返回它们的和的函数

int sum (int a, int b) {

return a + b;

}

// 以数组和索引值为参数,

// 返回指定位置的元素的函数。

int get_at(int[] array, int index)

throws ArrayIndexOfBoundsException {

return array[index];

}

函数的形式参数与已经赋值的 final局部变量的使用方法一致。因此,不能赋值给形式参数。

B.6.2 函数的签名声明

当函数被定义在其他源文件中的时候,就要像下面这样,以描述 签名声明 的方式,让调用了这个函数的表达式可以顺利编译。

int func(bouble x);

与C语言的原型声明不同,在Diksam中不能省略形式参数的名称(上面的 x)。另外,形式参数的名称也属于这个类型的一部分,在与函数定义不一致的时候就会发生错误。

例如下面的签名声明。

void draw_line(double x1, double y1, double x2, double y2);

与此相对,就不允许像下面这样的函数定义。

void draw_line(double x1, double x2, double y1, double y2);

B.6.3 局部变量

在函数内声明的变量就是局部变量(local variable)。

局部变量的作用范围是满足下面两个条件的范围(现在,局部变量的作用范围不受程序块的影响)。

在声明之后

在包含其声明的程序块内

局部变量的生命周期随着它所在函数的退出而终止。

B.7 类/接口的定义

B.7.1 类定义

类通过以下的形式进行定义。

类修饰符 class 类名

: 超类,要实现的接口(多个) {

字段、方法、构造方法的定义

}

类修饰符有以下两种。

访问修饰符 (access modifier)。可以指定为 public或者不指定。

abstract修饰符 。可以指定或者不指定。

访问修饰符与abstract修饰符的顺序不同。

当访问修饰符指定了 public的时候,这个类就可以被别的包使用了。如果不指定的话,就只能在当前包中使用。

指定了abstract的类被视为抽象类(abstract class)。抽象类不能通过 new将其实例化。相反,没有指定abstract的类被称为具象类(concrete class),其中不能包含 abstract方法。

超类和要实现的接口与C++、C#相同,在冒号后面以逗号分隔并一一列出(顺序不同)。在不需要继承的情况下,可以省略冒号。

在Diksam中对于类来说只能单继承。另外,在Diksam中不能继承具象类。

接口可以被多继承。

B.7.2 接口定义

接口通过以下的形式定义。

访问修饰符 interface 接口名 {

// 方法定义

}

接口除了只能记录 abstract方法外,其他的功能与类相同。

访问修饰符与类相同,可以指定或者不指定 public。接口一定要加上 abstract,如果不指定 abstract就会发生编译错误。

在Diksam中,接口不能继承其他接口。

B.7.3 字段定义

字段通过以下的形式定义。

访问修饰符 final修饰符 类型 字段名 初始化语句(表达式);

针对字段的访问修饰符有 public、 private 和不指定。它们拥有不同的含义, public 可以在其他包中使用,不指定的话可以在本包内被使用, private只能在当前类内被使用。

如果指定 final修饰符,这个字段就不能被赋值了。

B.7.4 方法定义

方法通过以下的方式定义。

方法修饰符 类型 方法名(参数,可以是多个) throws子句 {

语句

}

与函数定义一样, throws子句可以省略。另外,在加上了 abstract修饰符后,可以不描述方法体(包含了语句的代码块)。

方法修饰符有以下几种。

访问修饰符 。 public、 private、不指定。

abstract修饰符 。可以指定或者不指定。

virtual修饰符 、 override修饰符 。

没有指定 virtual 修饰符的方法不能重写。另外,如果要重写方法的话,必须指定 override修饰符。

B.7.5 方法重写

在子类中定义与父类或者所继承接口的方法同名的方法被称为方法重写(method override)。方法重写必须满足以下条件。

1. 重写与被重写的方法,参数个数必须一致。

  1. 进行重写的方法的参数类型(相对应地),必须与被重写方法的参数类型一致,或者是被重写方法的参数类型的父类。

  2. 进行重写的方法的返回值类型,必须与被重写方法的返回值类型一致,或者是被重写方法的返回值类型的子类。

  3. 进行重写的方法在 throws中列出的异常的范围要比被重写方法在 throws中列出的范围小。

5. 进行重写的方法的访问修饰符,要比被重写方法的访问修饰符的限制宽松许多。

B.7.6 构造器

构造器是在实例创建时自动调用的方法。

在Diksam中,构造器的定义需要使用 constructor修饰符。

public constructor initialize(){

// 记录构造器的处理

}

上面的例子用 constructor修饰符描述了与类型名名称相同的构造方法,因此必须要在方法前加上 public 和 virtual 方法修饰符,以便在之后进行重写。

与Java、C++、C#不同,在Diksam中可以给构造器任意起名字。在 new实例的时候,使用以下的形式指定构造器。

// myinit()是用户自定义的构造器

Point p = new Point.myinit(x, y);

如果不指定方法名,只写成 new Point(x, y);的话,会调用名为 initialize的构造器。另外,在类定义的时候,如果一个构造器也没有定义的话,编译器会自动创建如下的默认构造器(default constructor)。

public virtual constructor initialize(){

super.initialize(); ← 仅限于存在超类的情况

}

Diksam的构造器可以在子类中进行重写。但是,构造器并不会进行B.7.5节中写到的参数和返回值的检查。

另外,构造器仅限于在创建实例的时候使用,在实例被创建以后,构造器并不能像普通方法那样被调用。

B.8 程序库

在这里收录了Diksam标准程序库(library)中具有代表性的函数和类。

B.8.1 函数

void print(string str);

向标准输出中输出作为参数接收的字符串。

void println(string str);

向标准输出中输出作为参数接收的字符串,并在结尾处加上换行符。

File fopen(string file_name, string mode);

打开文件。参数的设计以C语言的 fopen()为基准。

string fgets(File file);

从 file指定的文件中读取一行,并作为返回值返回。

void fputs(string str, File file);

向 file指定的文件中输出 str。

void fclose(File file);

关闭参数 file指定的文件。

double to_double(int int_value);

将 int转换为 double。

int to_int(double double_value);

将 double转换为 int。

B.8.2 内建类

File

File类相当于C语言的 File类型。其内部以 native_pointer类型保存了C语言的 File,因此并没有存在特别的方法等内容。

Exception

Exception类是所有异常类的类层级的顶端。

它具有以下的字段和方法。

public string message;

保存了异常信息的字段。

public StackTrace[] stack_trace;

保存了栈轨迹的字段。

void print_stack_trace();

输出栈轨迹的方法。

另外, StackTrace类的定义如下所示。

class StatckStrace {

int line_number;

string file_name;

string function_name;

}