From 72162ca3351aaaa9d4e7fb9992ac692293802c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=81=95=E5=B1=B1?= Date: Wed, 6 Dec 2023 16:02:35 +0800 Subject: [PATCH] update explanation about `=` operator --- doc/4_godelscript_language.md | 182 +++++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 34 deletions(-) diff --git a/doc/4_godelscript_language.md b/doc/4_godelscript_language.md index b2919ec3..885aafb9 100644 --- a/doc/4_godelscript_language.md +++ b/doc/4_godelscript_language.md @@ -21,7 +21,7 @@ GödelScript 编译器主要应用场景为: 1. 面向用户编写简单或复杂查询,提供更便捷的写法,提高编写查询的效率; 2. 提供严格类型检查与类型推导,给予更智能的代码修改提示; -3. 提供严格的 [ungrounded](#ungrounded-error) 检测,避免容易触发的 Soufflé Ungrounded Error; +3. 提供严格的 [ungrounded(未赋值/未绑定)](#ungrounded-error-未赋值未绑定错误) 检测,避免触发 Soufflé Ungrounded Error; 4. Language Server 以及 IDE Extension 支持。 ### 基本程序构成 @@ -105,11 +105,9 @@ GödelScript 采用类 C 语言的注释方式。 #### `main` 函数 -GödelScript 查询脚本可以包含`main`函数,该函数无返回值。 +GödelScript 查询脚本可以包含`main`函数,该函数无返回值。在不实现`main`函数,且没有写 query 声明的情况下,程序不会输出。 -在`main`中可以多次使用`output(...)`来表明要输出多个查询结果。 - -`main`函数只允许使用`output`,其他语句会导致编译错误。 +更多详细内容请看 [`main`函数](#gödelscript-main-函数)。 ```rust fn main() { @@ -118,11 +116,9 @@ fn main() { } ``` -在不实现`main`函数,且没有写 query 声明的情况下,程序不会输出。 - ### 基础类型和编译器内建函数 -GödelScript 包含基础类型`int``string`,`bool`属于基础类型,但是不能作为值存储。 +GödelScript 包含基础类型`int` `string`,`bool`属于基础类型,但是不能作为值存储。 #### `int`类型 native 函数 @@ -204,7 +200,7 @@ GödelScript 包含基础类型`int``string`,`bool`属于基础类型,但是 | to | (self) -> T | 转换到其他类型的 schema,采用 duck type 检测。 | | is | (self) -> bool | 判断是否可以是其他类型的 schema,采用 duck type 检测。如果自身 schema 有主键,则底层只会通过主键判断是否可以是其他类型。 | | key_eq | (self, T) -> bool | 检查两个 schema 实例的主键是否相等。 | -| key_neq | (self, T) -> bool | 检查两个 schema 实例的主键是否**不**相等。 | +| key_neq | (self, T) -> bool | 检查两个 schema 实例的主键是否不等。 | schema native 函数实例: @@ -232,42 +228,101 @@ fn convert() -> *ElementParent { ### 函数 -#### `main` 函数 +#### GödelScript `main` 函数 -在上文中已经提及,该函数是 GödelScript 中唯一不需要声明返回值的函数。 +`main`函数是 GödelScript 中唯一不声明返回值的函数。`main`函数只允许使用`output`,其他语句会导致编译错误;多次使用`output(...)`可以输出多个查询结果,查询结果会分表显示,表名即为`output`中调用的查询函数的函数名。 #### 查询函数 查询函数的返回值类型推荐为`bool`,需要输出查询结果时,需要使用`output()`函数。 +在`output()`中调用的查询函数不再是常规思路中的用传参调用函数。参数列表在此时会变化为输出表的表结构,下面是两个查询函数的应用实例: + +1. 单表`output` + + 单表`output`特指在`main`函数中,只使用一次`output`来输出。 + + ```rust + fn example(a: int, b: string) -> bool {...} + + fn main() { + output(example()) // 此时参数列表变为输出表结构,不需要传参 + } + ``` + + 对应的输出表结构为: + + ```json + [ + {"a": 0, "b": "xxx"}, + {"a": 1, "b": "xxx"} + ] + ``` + +2. 多表`output` + + 多表`output`是指在`main`函数中,使用多次`output`来输出。在这种情况下,输出数据会附带对应的表名。 + + ```rust + fn example0(a: int, b: string) -> bool {...} + fn example1(a: string, b: int) -> bool {...} + + fn main() { + output(example0()) + output(example1()) + } + ``` + + 对应的输出表结构为: + + ```json + { + "example0":[ + {"a": 0, "b": "xxx"}, + {"a": 1, "b": "xxx"} + ], + "example1":[ + {"a": "xxx", "b": 0}, + {"a": "xxx", "b": 1} + ] + } + ``` + +下面是一个比较详细的例子,在这个例子中,我们直接构造了两组数据并输出。在下列代码中,需要注意的是: + +1. GödelScript 中,布尔值可以使用`true`和`false`关键字。 + +2. `=`符号在 GödelScript 中是比较特殊的符号,不能用常规的编程语言的思路来理解。GödelScript 是一种 Datalog 语言。在这里,`=`符号同时具备两种语义,一个是 __赋值__ 一个是 __判等__。详情可看[`=`运算符](#赋值和判等运算符)。 + +3. 在这个例子的条件语句中,`a`和`b`均使用了`=`的赋值语义,因为`int`和`string`类型参数在函数体中被认为是`ungrounded(未赋值/未绑定)`,必须要被赋值才能使用。 + +4. `=`赋值语句的返回值是`true`。 + ```rust -fn myQuery(a: int, b: string) -> bool { - if (a = 1 && b = "hello") { +fn example(a: int, b: string) -> bool { + // = 符号同时具有赋值和比较的功能,取决于此时的左值是否已经“被赋值” + // 这里的 a 和 b 所用的 = 符号均是赋值语义 + if (a = 1 && b = "1") { + // GödelScript 使用关键字 true 和 false 来表示布尔值 return true } - if (...) { - ... + if (a = 2 && b = "2") { + return true } - ... } fn main() { - output(myQuery()) // 这里无需填写传入参数 + output(example()) } ``` -`output()`会根据该函数的参数列表格式来输出表结构。所以在`main`中使用时,`output()`并不需要你提供这个函数的传入参数。对应的输出表结构大致如下: +预期的输出结果应该为: -| a | b | -| --- | --- | -| 1 | "hello" | -| ... | ... | - -GödelScript 使用`true`和`false`关键字来代表返回的`bool`类型结果: - -```rust -return true -return false +```json +[ + {"a": 1, "b": "1"}, + {"a": 2, "b": "2"} +] ``` #### 普通函数 @@ -352,11 +407,70 @@ if (f.getName().contains("util") || f.getName().contains("com")) { 条件可以使用这些逻辑运算符进行连接:`!`取反,`||`或,`&&`与。 -条件中的比较运算符:`>`大于,`<`小于,`>=`大于等于,`<=`小于等于,`=`等于,`!=`不等于。 +条件中的比较运算符:`>`大于,`<`小于,`>=`大于等于,`<=`小于等于,`=`等于或者赋值,`!=`不等于。 常规算术运算可以使用如下运算符:`+`加法,`-`减法/取负,`*`乘法,`/`除法。 -**注意:比较运算符中的**`**=**`**在左侧变量没有被绑定数据时,会执行绑定操作并返回**`**true**`**,类似于赋值操作。** +##### 赋值和判等运算符`=` + +`=`符号在 GödelScript 中具有两种不同的语义:赋值和判等,具体的语义需要分情况进行讨论: + +1. 赋值 + + 赋值一般出现在`int` `string`这类基础类型的变量参数上,这类变量作为函数的参数出现时,一般被认为是未赋值的。而具有这类变量的函数被调用时,传入的参数,实际上是作为筛选条件存在。 + + ```rust + fn example(a: int) -> bool { + // 这里比较反直觉,在过程式语言中,这里通常会被认为是判断 a == 1 + // 但是在 datalog 方言中,datalog 的每个函数实际上都是在算一个中间表 (view) + // 所以这个函数本质上是生成了一个 view,数据为 [{"a": 1}] + return a = 1 // assign a = 1 + } + + fn test() -> bool { + // 这里看似是在通过传参让 a = 2,实际上并不是 + // example() 自己会返回 view: [{"a": 1}] + // 然后通过 a = 2 来约束结果,可以看到,我们这里没有拿到任何结果 + // 所以返回了 false + return example(2) // false + } + ``` + +2. 判等 + + 对于 schema 类型来说,任何一个 schema 背后都有一个全集,所以参数列表中的 schema 类型一般被认为是已经被赋值的。对于已经赋值的变量来说,`=`就是判等操作。 + + ```rust + // 声明 schema + schema A {...} + + // 实现 schema 的成员函数 + impl A { + // 这里定义了 schema A 的全集 + @data_constraint + pub fn __all__() -> *A {...} + } + + fn example(a: A) -> bool { + for(temp in A::__all__()) { + if (a = temp) { + return true + } + } + } + ``` + + 同样,对于中间声明的有初始值的`int`或者`string`,`=`也是判等操作。 + + ```rust + fn example() -> bool { + let (a = 1) { // assign a = 1 + if (a = 1) { // compare a = 1 + return true + } + } + } + ``` #### match 语句 @@ -944,9 +1058,9 @@ fn class_method(className: string, methodName: string, methodSignature: string) } ``` -### Ungrounded Error +### Ungrounded Error: 未赋值/未绑定错误 -GödelScript 会将未与集合绑定的符号判定为`ungrounded`。基本判定规则为: +GödelScript 会将未与数据绑定的符号判定为`ungrounded(未赋值/未绑定)`。基本判定规则为: - 未初始化的/未被使用的/未与集合绑定的符号 - 未被绑定的`int``string`参数 @@ -2086,9 +2200,9 @@ e, p 的笛卡尔积就变成了 e, i 的笛卡尔积,从运算的层面来看 ### 不要滥用`@inline`/必须用`@inline`的优化策略 inline 函数的底层机制是在**调用处展开**,如果该函数不存在大量的 schema 传参,并且在很多位置都被调用,inline 可能会导致**编译结果膨胀且重复计算次数指数级增加**,有时反而不利于减少运行时间。 -如果存在必须要使用 inline 的情况 (比如规避 ungrounded),但是使用之后反而出现运行速度变慢的情况,可以采取将内嵌语句拆分为 predicate 的方式来避免展开导致的编译结果膨胀。 +如果存在必须要使用 inline 的情况 (比如规避`ungrounded`),但是使用之后反而出现运行速度变慢的情况,可以采取将内嵌语句拆分为 predicate 的方式来避免展开导致的编译结果膨胀。 -下面的例子中,`getValueByAttributeNameByDefaultValue`为了避免`attributeName`被识别为 ungrounded 所以标注 inline,后续在 if 分支中添加了一个条件语句,但是导致了执行时间从 3 秒变成 35 秒: +下面的例子中,`getValueByAttributeNameByDefaultValue`为了避免`attributeName`被识别为`ungrounded`所以标注`inline`,后续在 if 分支中添加了一个条件语句,但是导致了执行时间从 3 秒变成 35 秒: ```rust impl XmlElementBase {