在 C89 编译器中实现三字母表

Implementing trigraphs in a C89 compiler

我正在尝试编写一个简单的 C89 --> x86_64 编译器,基于 this C89 standard draft, in C89, for learning's sake. So far, I am implementing translation phase 1。我的理解是这包括

  1. 正在将代码读入字符串。
  2. 替换三字母序列。

我已经尝试用一个程序来实现它(请原谅我犯的任何风格错误):

char *trigraph_replacement(char *code)
{
        char *temp = calloc(1, strlen(code));
        char *temp_1 = temp;
        char *code_1 = code;
        for (; *code_1; code_1++)
        {
                if (strncmp(code_1, "??", 2) == 0)
                {
                        code_1 += 2;
                        switch (*code_1)
                        {
                        case '<':
                                *(temp_1) = '{';
                                break;
                        case '>':
                                *(temp_1) = '}';
                                break;
                        case '(':
                                *(temp_1) = '[';
                                break;
                        case ')':
                                *(temp_1) = ']';
                                break;
                        case '=':
                                *(temp_1) = '#';
                                break;
                        case '/':
                                *(temp_1) = '\';
                                break;
                        case '\'':
                                *(temp_1) = '^';
                                break;
                        case '!':
                                *(temp_1) = '!';
                                break;
                        case '-':
                                *(temp_1) = '~';
                                break;
                        default:
                                break;
                        }
                }
                else
                {
                        *temp_1 = *code_1;
                }
                temp_1++;
        }

        free(code);
        return temp;
}

现在,直觉上,这似乎应该做应该做的事情,替换所有三字母。然而,the gcc docs 说“Trigraphs 并不流行,许多编译器错误地实现了它们”。它继续指出“可移植代码不应依赖于转换或忽略的三字母”

结果,我很纳闷

Reading the code into a string.

是的,从源文件使用的符号 table 转换为编译器使用的符号。理想情况下,他们会使用同一个。

Is my implementation sufficient or did I err in some way?

  • calloc(1, strlen(code)); 几乎可以肯定是一个错误,因为您没有为空终止符分配空间。
  • 具有堆分配、分支 switch 等的实现对于解析器来说非常幼稚,这段代码会非常慢。尽可能使用查找 tables,执行速度优先于内存消耗。
  • 不需要所有讨厌的指针算法,因为您已经决定调用 strlen(该调用也会产生额外的执行时间)。存储该调用的结果并将整个循环更改为 for(size_t i=0; i<length; i++)。您应该能够大大削减此功能。

Are trigraphs worth implementing in the first place, or are they not used, even in the most ancient legacy programs?

除了在那些古老的程序中,它们几乎没有被使用过。它们在 C 中的存在是一个备受批评的语言缺陷。这就是 gcc 在遇到它们时发出警告的原因。

但是,如果您希望以某种方式声明符合 ISO C90,则需要支持它们。它们(出于未知原因)甚至没有被当前的 ISO C17 标准标记为过时,因此它们似乎会保留下来。

抛开一些技术问题和效率低下的问题,我稍后会谈到,你的问题的答案:

did I err in some way?

是,“是的,你做到了”。

1。此代码中的错误

这是 trigraph_replacement 中循环的高度简化视图(您可能讨厌我的大括号格式;我这样做是为了减少垂直 space 浪费):

for (; *code_1; code_1++) {
    if (strncmp(code_1, "??", 2) == 0) {
        code_1 += 2;
        switch (*code_1) {
            case '<': *(temp_1) = '{'; break;
            /* ...  */
            default:                   break;
        }
    } /* end if (strncmp(...)) */
    else { *temp_1 = *code_1; }
    temp_1++;
}

如果 strncmp return 是非零值,则没有问题;效果是:

    *temp_1 = *code_1;
    temp_1++;
    code_1++;  /* From the for loop */

如果 strncmp returns 0 和第一个(或任何其他指定的)case 匹配,同样没问题:

    code_1 += 2; 
    *temp_1 = '{';   /* The parentheses around temp_1 were unnecessary */
    temp_1++;
    code_1++;  /* From the for loop */

但是如果 strncmp returns 0 但是下一个字符是 space:

    printf("Can this be correct?? N has the value %d\n", N);

default 案例涉及以下序列:

    code_1 += 2; 
    /* default case does nothing */
    temp_1++;
    code_1++;  /* From the for loop */

所以代码跳过了??和后面的space,并没有将任何内容存储到输出缓冲区的相应位置。结果将是:

    printf("Can this be correctØN has the value %d\n", N);

Ø 代表一个实际的 NUL 字符;因为 code 是用 calloc 分配的,未设置的字符位置将为 0。这不太可能与下一个一起玩翻译阶段;它可能会过早地终止源代码,或者产生无效的源字符错误,或者只是掉在地上,但其中 none 将是用户想要的。)

也许更糟糕的是以下示例(标准中的§5.2.1.1para3):

    printf("Eh???/n");

该示例中的三字母序列从第二个 ? 开始,您的代码永远不会注意到它(即使您修复了默认大小写,也可能不会注意到,具体取决于您修复它的方式)。预期的翻译是 printf("Eh?\n");,在三字母组用单个反斜杠替换 ??/ 之后,这显然必须在解释反斜杠转义之前发生。

这确实是 GCC 文档提到的经典实现错误之一。

2。其他实施困难

如果您按顺序执行翻译阶段,您的代码将不会遇到另一个常见错误,那就是在某些上下文中无法处理三字母。这通常发生在词法分析器中,它试图在词法分解的同时处理三字母(和行拼接),我认为在某处的评论中提到过。

我认为合理的论点是,由于三字母和拼接都不是很常见 [注 1],即使是词法分析器中非常缓慢的实现(仅在必要时才会发生)也可能比浪费快得多在几乎不需要的翻译过程中循环。

但是一些没有任何明智的程序员会实际使用的病态情况很容易出错。考虑以下因素:

/??/
* This is a comment
*/

如果不是很明显,那是注释,因为三字母替换处于第 1 阶段;线路拼接在phase2;评论识别在第3阶段。第1阶段之后,输入是

/\
* This is a comment
*/

在第 2 阶段之后,

/* This is a comment
*/

如果阶段 1 和阶段 2 独立于词法分析器,这很容易做到。但是将 /??/<newline>* 识别为注释起始符并将 *??/<newline>/ 识别为注释终止符的标记正则表达式,以及所有其他古怪的变体,写起来不是很愉快 [Note 2].

最后,我知道将整个输入文件吞入内存以便将其解析为单个连续字符串很诱人,但这并不是一个很好的模式。非常常见的反模式“查找文件末尾并调用 tell 以查看文件的长度”既是对缓冲区溢出的邀请(实际读取文件时文件可能更长,因为它仍然被写入)和一种使 Unix 管道无法用作输入源的方法。但即使您更仔细地阅读文件,您最终还是会使用比必要更多的内存,并且在所有可用内存之前您将无法开始处理源文件。你可能不在乎,现在我可能会接受这个论点,但这是需要考虑的事情。

另一方面,如果你逐个缓冲区读取文件,你需要处理三字母组的开头在缓冲区末尾但最后一个字符尚未被读取的烦人情况.幸运的是,trigraphs 和 splices 都很短,所以你可以用一个小的预留缓冲区甚至一个非常小的状态机来解决这个问题。但这是需要编写和调试的额外代码。

3。不完全是代码审查

正如所写,原型

char* trigraph_replacement(char* tmp);

要求 trigraph_replacement 的参数是一个可变的动态分配的缓冲区,其所有权转移给函数,最终通过 freeing 它和 returning 一个新的动态分配的缓冲区。

这种所有权转移是灾难的根源(尤其是在没有记录的情况下)。除非你有充分的理由,否则接受字符串参数的函数应该保持不变,或者就地修改它们。就地修改是一种提高效率的方法;这并不总是可能的——事实上,通常是不可能的——但在这种特殊情况下,这是可能的,所以你可能要考虑一下。由于三字母的实际使用可能仅限于您自己编写的测试用例,因此通过毫无意义的双重分配和复制整个输入来给生产代码增加负担真的没有多大意义。

可以进行就地修改,因为所有三字母替换都将三个字节替换为一个字节 [注 3]。行拼接是另一个总是使输出更短的过程,只要小心一点,您就可以同时进行这两个过程。因此,如果您记录了您计划修改(但不是释放!)输入的事实,那么将函数编写为这样的东西是完全合理的:

void trigraph_replacement(char* code) {
  /* Trigraphs in `code` are modified in-place. */

  /* Use a look-up table rather than a massive case statement. This is
   * not necessarily faster, but the code is shorter.}
   */
  /* Designated initializers are C99; in C89 you'd need to write this out */
  static const char trigraphs[128] = {
    ['='] = '#',  ['('] = '[', ['/'] = '\', [')'] = ']',
    ['\''] = '^', ['<'] = '{', ['!'] = '|',  ['>'] = '}',
    ['-'] = '~'
  };
  for (char* in = code, *out = code; ; ++in, ++out) {
    if (in[0] == '?' &&
        in[1] == '?' &&  /* Safe because code must be nul-terminated */
        in[2] > 0    &&  /* Ditto */
        in[2] < 128  &&
        trigraphs[in[2]] != 0) {
      *out = trigraphs[in[2]];
      in += 2;
    }
    else {
      *in = *out;
    }
    if (*in == 0) break;
  }
}

当它验证 ?? 启动了一个三字母表时,该循环会小心地将输入指针推进到 ?? 上。我没有尝试对您可以复制和推进一个或两个字符的各种可能性进行特殊处理,因为它们发生的频率不足以证明额外的代码复杂性是合理的。

4。如果不接受就地修改怎么办?

不是每个人都喜欢就地修改(有时包括我),所以值得考虑一个替代方案。无论您选择哪种替代方案,重要的是要考虑(和 文档)动态存储分配策略。

例如,在没有遇到三字母的常见情况下,很容易直接 return 不修改参数。但这让调用者的生活变得困难,因为他们不知道输出字符串是一个额外的动态分配的内存区域,还是同一区域。因此,无条件地将输入复制到新分配的输出是有充分理由的。 (调用者可能会利用复制是无条件的这一事实,例如通过消除不必要的输入缓冲区副本。)

然而,C 提供的计算副本长度的唯一机制是 strlen,这需要对缓冲区进行完整扫描(回想一下,缓冲区是整个程序,它可能非常大) .实际上,调用者很可能知道输入了多长时间,因为他们一定是从某个地方获取的,并且大多数输入函数会告诉您读取了多少数据 [注 4]。所以要求调用者告诉你输入有多长是完全合理的,从而产生像

这样的原型
char* trigraph_replacement(const char* code, size_t codelen);

如果出于某种原因,调用者不知道 code 有多长,他们可以调用 strlen,但没有必要惩罚知道的调用。无论如何,为整个字符串 分配足够的 space 是至关重要的,包括终止 NUL 字符 。所以你会想要使用 malloc(codelen + 1)calloc(1, codelen + 1)。 (如果你小心点,你真的不需要 calloc。)

备注

  1. 绝大多数线拼接被白色space包围,这些可以通过对白色space正则表达式模式进行小的修改来轻松处理。字符串文字中的拼接也是如此。更成问题的是位于标记中间的拼接,特别是像 pp-numbers 这样的复杂标记,或者下面描述的由三字母组成的拼接的可怕情况。

  2. 这并非不可能,我知道有些读者会把它当作一个挑战。这很酷。但是您确定您涵盖了所有案例吗?我希望看到 lot 个测试用例:-)

  3. 三字母转换成的字符都是基本字符集中的字符,必须编码成一个字节。参见§5.2.1.2。

  4. fgets 不应用于读取多行输入,因为它涉及对换行符的额外扫描。除了它不会告诉您它读取了多少个字符这一事实之外,还需要再次扫描空字符。