Closed Hoblovski closed 4 years ago
术语: 1. 报错/错误/不能:如果我们要求某种输入会被报错、或某种输入是错误,那编译器对于此输入,不应生成汇编代码。如果可能,打印有用的错误信息。 2. 未定义行为:如果我们规定某种操作是未定义行为,无论编译器对于包含此操作的程序做什么都是合法的。测例代码不会包含未定义行为。 并且所有测例公开,简明起见语义标准仅叙述最重要的几条。 所有测例中未出现的行为都不作为考察内容。 step1. 1.1. MiniDecaf 的 int 类型具体指 32 位有符号整数类型,范围 [-2^31, 2^31-1],补码表示。 1.2. 编译器应当只接受 [0, 2^31-1] 范围内的整数, 我们未来会支持负数。 如果整数超过此范围,编译器应当报错。 step2. 2.1. MiniDecaf 中,负数不被整体作为一个 token,而是一个取负符号,加上它的绝对值。 这意味着我们无法用字面量表示 -2^31,但可以写作 `-2147483647-1`。 2.2. 运算越界是未定义行为。例如 -(-2147483647-1) 是未定义行为。 2.3. 除非特别声明,子表达式求值顺序是不确定的。 我们不知道 `int a=0; (a=1)+(a=a+1);` 之后 a 的值。 step3. 3.1. 除以零和模零是未定义行为。 step4. 4.1. 如果 || 和 && 的子表达式有副作用,那么这就是一个未定义行为。 不对逻辑短路做要求。 4.2. 比较大小是有符号数的比较大小。 step5. var 5.1. 变量重复声明是错误。 5.2. 使用未声明的变量是错误。 5.2. 被声明的变量在声明语句完成后,可以被使用。 不能写 `int a = a`。 5.3. (局部)变量初始值不确定,使用不确定的值是未定义行为。 5.3. main 函数如果没有 return 语句,默认返回 0。 5.4. 表达式被称为左值(lvalue)当且仅当它能被下面两条规则构造出来: 1. 被声明过的变量是左值;2. 如果 e 是左值,那么括号括起的 (e) 也是左值。 5.4. 只有左值能出现在赋值左边。 step6. if 6.1. int 类型的表达式作为 if/三目表达式的条件时,非 0 表示真(例如 1,-1, 1000000007),0 表示假。 6.1. 如果出现悬吊 else(dangling else),要求 else 优先和内层 if 结合。 例如 if (0) if (0) ; else ; 等价于 if (0) { if (0) ; else; } 而非 if (0) { if (0) ; } else ; 6.2. 如果 then/else 子句仅是一个声明,是未定义行为。 例如 if (1) int a; 6.3. 对三目表达式求值,首先对条件求值,如果条件值为真,对 ? 和 : 之间的表达式求值,不得对 : 之后的表达式求值。 如果条件值为假,对 : 之后的表达式求值,不得对 ? 和 : 之间的表达式求值。 step7. block 7.1. [update 5.1] 不得声明当前作用域已经声明过的同名变量。 7.2. [update 5.1] 声明某变量时,只要当前作用于没有同名变量,那么声明即合法。 如果更早的开作用域中有同名变量,那么从声明开始到声明所在作用域结束,更早的同名变量都不可见。 7.3. [update 5.2] 使用不在当前任何开作用域中的变量,是错误。 step8. for while do 8.1. for 循环的控制表达式可以为空,表示循环条件永远为真。 8.2. for 语句自身带一个作用域,给 for 的 init 声明用。 如果它的循环体是 block 语句块,那么循环体再带一个作用域。 8.3. 在循环外使用 break 和 continue 是错误行为。 8.4. break 跳转至最近的循环的结束后那条语句。 如果最近的循环语句是 do 或 while,continue 跳转执行条件判断; 如果是 for,continue 跳转执行 post。 step9. 函数 9.1. 除了 main 以外的语句没有返回,那么返回值不确定。 如果使用这种不确定的返回值,那么是一个未定义行为。 9.2. 函数声明和定义的参数类型(虽然只有 int)以及参数个数、以及返回值类型必须匹配。 9.3. 函数可被声明多次,只能被定义一次,并且对声明和定义的顺序没有任何限制。 9.4. 传参时,参数类型和个数必须匹配。 step10. 全局变量 10.1. 全局变量可以被初始化,但初始值(initializer)只能是整数字面量。 10.2. 如果没有显式给出初始值,全局变量初始值为 0。 10.2. 全局变量没有声明一说,只要写下就是定义。 不允许重复定义全局变量。 step11. 指针 11.1. [update 5.4] 在 5.4 中两条规则外新增一条:如果 e 是类型为 T* 的表达式,那么 *e 是类型为 T 的左值。 11.5. 类型只有 int 和指针类型,禁止隐式类型转换,但显式类型转换只要不违反其他几条就允许。 11.2. & 的操作数必须是左值。(意味着 &*e 等价于 e,但不是左值) 11.2. * 的操作数类型必须是指针类型。 11.3. 指针类型的表达式仅能参与如下运算:类型转换、(一元)&、*、(二元)==、!=。 指针加减算术是下一步的任务。指针不得参与乘除模和、一元、比较大小、逻辑运算。 11.4. 空指针是值为 0 的指针。 为了书写空指针只能对值为 0 的表达式做类型转换,如 (int*) 0。 判断空指针也类似,如 p == (int**) 0。 11.6. 未对齐的指针是未定义行为。 11.7. 如果指针类型和被指向的对象的类型不匹配,例如 int a; (int*) a; 那么是未定义行为,哪怕没有解引用。 此条空指针除外。 step12. 数组 1. 数组定义: 1.1. 可以多维。每一维长度不能是零或负数。 1.2. 没有变长数组 `int a[n];` 也没有不定长数组 `int a[];`。 1.3. 对数组变量取地址是错误,也没有指向指针的数组。 > 这是为了简化实验,否则需要引入 C 中一堆繁复的记号,像 int *a[10] 和 int (*a)[10]。 1.4. 数组声明不能有初始值。局部变量数组初始值未定,全局变量初始值为零。 1.5. 数组的地址一定对齐,各个元素在内存中是连续的,并且多维的情况下排在前面的维度优先。 例如 int a[3][4][5] 内存排布等价于 int b[60],a[1][2][3] 等价于 b[33]。 2. 下标运算: 2.1. 下标运算优先级高于一元运算符。 > 由于下标运算为后缀形式,且优先级比一元运算符高,所以文法中要加一个 `<postfix>`。 2.2. 下标运算的操作数类型必须是指针或数组。 2.3. 允许将数组的前几维单独提出,赋给一个指针:`int a[2][2][2]; int *p = (int*) a[0]; int *q = (int*) a[1][1];`,提出后的类型还是数组类型,需要显式转换。 2.4. 下标必须是 int 类型,不进行下标越界检查,越界是未定义行为。 2.5. 下标运算的结果是左值,即可以 `a[1] = 2; int *p = &a[1];`。 3. 类型检查与转换: * 只允许数组到任意指针类型的显式转换,不支持任何隐式转换。 * 数组类型的表达式只能参与如下运算:下标、类型转换。 * 函数形参不得被声明为数组。 * 没有数组类型的参数,传参时只能显式转换为指针。 3. 指针算术运算: * 指针加法:允许指针加 int、以及 int 加指针,结果的类型和操作数指针的类型相同。 - int 和指针运算的时候,运算前要乘上指针类型的基类型的 sizeof。 - 如果运算越界,那么是未定义行为。 越界指结果不在操作数指针所在数组的范围内,如果操作数指向单个元素视为长为 1 的数组。 * 指针减法:允许指针减 int、以及指针减指针。 - 指针减 int 结果类型同指针类型,类似指针加法。 - 指针 `a` 减指针 `b` 要求 `a` 和 `b` 是同类型的指针,结果是一个 int `c`,满足 `a == b + c`。同上,不对齐指针是未定义行为。 - 指针 `a` 减指针 `b`,只有当 `a` 和 `b` 是指向同一个数组内元素(不越界,或者刚刚超过最后那个元素一个位置)时才有意义,否则是未定义行为。 * 空指针参与的任何指针算术都是未定义行为。
语义 spec 已经加了。
这个作废。