K语言入门学习5:什么是Modules, Imports, 与 Requires

这节课的目的是解释K定义如何被分解成一个个的模块和文件,以及又如何将这些不同的组件组合成一个完整的K定义。

K的外部语法

回想一下第1.3课, 那里提到过K的语法规范里有两个组件:K的外部语法和K的内部语法。外部语法,如前所述,包括requires(需要)模块(Modules)导入(imports)句子(sentences)。K的语义由定义中包含的一组句子来表达的。该定义中被认为包含的范围由K定义的**主语义模块(main semantics module)**以及包含该模块的文件中存在的require和imports决定。

基本模块语法

K中的模块就是一组句子,它是K定义的一个基本单位。一个模块包含了以下内容:

  1. 模块名;
  2. 属性列表(可选的);
  3. imports列表;
  4. 句子列表。

模块名称由一个或多个字母、数字或“_”组成,中间用“-”隔开。以下是一些有效的模块名称:’ FOO ', ’ FOO- bar ', ’ foo0 ', ’ foo0_bar-Baz9 ‘。这里有一些无效的模块名:’ - ‘,’ -FOO ', ’ BAR- ', ’ FOO——BAR '。从风格上讲,模块名通常都是大写,用连字符分隔单词,但这不是严格强制的。

下面的例子定义了一个空的模块:

module LESSON-05-A

endmodule

下面的模块有一些属性

module LESSON-05-B [attr1, attr2, attr3(value)]

endmodule

下面的模块则有一些句子:

module LESSON-05-C
  syntax Boolean ::= "true" | "false"
  syntax Boolean ::= "not" Boolean [function]
  rule not true => false
  rule not false => true
endmodule

Imports

到目前为止,我们只讨论了包含单个模块的定义。但定义还可以包含多个模块,其中一个模块导入其他模块。

以K模块定义的开头的导入所依赖的模块,也就在其它句子之前。’ imports '关键字后跟一个模块名来指定所依赖的模块。

下面的例子里,定义中就有两个模块 (lesson-05-d.k):

module LESSON-05-D-1
  syntax Boolean ::= "true" | "false"
  syntax Boolean ::= "not" Boolean [function]
endmodule

module LESSON-05-D
  imports LESSON-05-D-1

  rule not true => false
  rule not false => true
endmodule

这个K定义等价于上面单个模块’ LESSON-05-C '的定义。实际上,通过导入一个模块,我们将模块中被导入的所有句子都包含到我们要导入的模块中。导入一个模块和直接将其语句包含在另一个模块中之间有一些细微的区别,但我们将在后面讨论这些区别。从本质上讲,您可以将模块看作是在更大的K定义中对句子进行概念性分组的一种方式。

练习

修改 lesson-05-d.k ,让它其中有4个模块:

  1. 一个模块包含了syntax;
  2. 一个模块包含了一条rule;
  3. 一个模块包含了另一条rule;
  4. 最后的模块,通过imports上面3个模块。

确保该K定义可以编译成功,并通过not函数,验证修改后的K定义是有效的。

解析一个文件中有多个模块的情形

您可能已经注意到,定义中的每个模块都可以表达一组不同的语法。解析模块中的句子时,我们使用该模块的语法,从而丰富了K的基本语法,进而解析该模块中的规则。例如,下面的定义会导致一个解析器错误(lesson-05-e.k):

module LESSON-05-E-1
  rule not true => false
  rule not false => true
endmodule

module LESSON-05-E-2
  syntax Boolean ::= "true" | "false"
  syntax Boolean ::= "not" Boolean [function]
endmodule

这是因为模块’ LESSON-05-E-1 ‘中引用的语法,即’ not ‘、’ true ‘和’ false '没有被该模块导入。您可以通过简单地导入包含您想要在您的句子中使用的语法的模块来解决这个问题。

主语法和语义模块

当我们编译K定义时,我们需要知道从哪里开始。我们指定了两个特定的切入点模块:主语法模块主语义模块。主语法模块,以及它递归导入的所有模块,用于为程序创建解析器,用于解析使用’ krun ‘执行的程序。主要语义模块,以及它递归导入的所有模块,用于确定运行时为了执行程序可以应用的规则。例如,在上面的例子中,如果主要语义模块是模块’ LESSON-05-D-1 ‘,那么’ not ‘是一个无法解析的函数(即,没有与它相关的语法规则),因为模块’ LESSON-05-D-2 '中的语法规则不包括在内。

虽然你可以通过将’–main-module ‘和’–syntax-module ‘标记传递给’ kompile ‘来显式指定入口点模块,但默认情况下,如果你输入’ kompile foo.k ‘,则主要语义模块将是’ FOO ‘,而主要语法模块将是’ FOO- syntax '。

将一个K定义分割到多个文件中

到目前为止,虽然我们已经讨论了将定义分解为多个概念性组件的方法,但这用处有限,因为我们仍然被迫将定义中的所有模块放入单个文件中。这显然是不可取的。因此,K提供了在K定义中组合文件的机制,即需要(requires)指令。

在K中,关键字“requires”有两种含义。第一个是require声明,它出现在K文件的顶部,在任何模块声明之前。它由关键字’ requires '和一个双引号字符串组成。关键字“requires”的第二种含义将在后面的课程中介绍,但它是不同的,因为第二种情况只发生在模块内部。

传递给require语句的字符串包含一个文件名。当你在一个文件上运行’ kompile ‘时,它会查看该文件中所有的’ require ‘语句,查找磁盘上的这些文件,解析它们,然后递归地处理这些文件中的所有require语句。然后,它将所有这些文件中的所有模块组合在一起,并共同使用它们作为’ imports '语句可以引用的模块集。

总结

综上所述,有一种方法可以将“lesson 02-c.k”的定义,从第1.2课中的单文件单模块,拆成多个文件和模块:

colors.k:

module COLORS
  syntax Color ::= Yellow()
                 | Blue()
endmodule

fruits.k:

module FRUITS
  syntax Fruit ::= Banana()
                 | Blueberry()
endmodule

colorOf.k:

requires "fruits.k"
requires "colors.k"

module COLOROF-SYNTAX
  imports COLORS
  imports FRUITS

  syntax Color ::= colorOf(Fruit) [function]
endmodule

module COLOROF
  imports COLOROF-SYNTAX

  rule colorOf(Banana()) => Yellow()
  rule colorOf(Blueberry()) => Blue()
endmodule

然后像原来单模块定义一样,调用 kompile colorOf.k 来完成上述K定义的编译。

练习

修改’ COLOROF ‘模块的名称,然后重新编译该定义。试着理解为什么你现在得到一个编译器错误。然后,通过向kompile传递’–main-module ‘和’–syntax-module '标志来解决这个编译器错误。

Include目录

需要注意的一点是,在’ requires '声明中,路径是如何解析的。

默认情况下,允许指定的路径为绝对路径或相对路径。如果路径是绝对的,则导入确切的文件。如果路径是相对的,匹配文件将在编译器指定的所有**包括目录(include directories)**中查找。默认情况下,包括目录包括当前工作目录,紧随其后的是include/kframework/builtin 目录,这个目录在安装k的路径中 .您也可以通过一个或多个目录“kompile”通过-I命令行标志, 在这种情况下,这些目录都会被放到include目录列表的前面,并成为include 目录列表的一部分 。

练习

  1. 对于1.4课中问题2的解决方案,包括显式的优先级和结合性申明, 把其K定义中的整数和中括号等语法放到了一个模块里,而把语法中的加法,减法,和一元否定放到另一个模块, 然后将乘法和除法的语法放在第三个模块。确保您仍然可以像以前一样解析相同的表达式集。将优先级申明放在main模块中。
  2. 修改课程1.2中的“lesson-02-d.K’,以便规则和语法分别放在单独的文件中各自的模块中。
  3. 将包含问题2中的语法的文件放在另一个目录中,然后重新编译定义。观察编译错误发生的原因。然后通过传递’ -I ',指定该include目录给kompile,来修复该编译器错误。

猜你喜欢

转载自blog.csdn.net/DongAoTony/article/details/125028923