【问题标题】:How to transpile python Compare ast nodes to c?如何编译 python 将 ast 节点与 c 进行比较?
【发布时间】:2020-11-14 02:02:22
【问题描述】:

让我们从考虑python3.8.5's grammar开始,在这种情况下,我有兴趣弄清楚如何将python比较转换为c。

为了简单起见,假设我们正在处理一个非常小的 python 平凡子集,我们只想转换平凡的比较表达式:

expr = Compare(expr left, cmpop* ops, expr* comparators)

如果我没记错的话,在 python 中,a<b<c 之类的表达式被转换为 a<b && b<c 之类的东西,其中 b 只被评估一次......所以我猜在 c 中你应该做类似bool v0=a<b; bool v1=v0<c in为了防止在第一个子句为真的情况下对 b 进行多次评估。

不幸的是,我不知道如何将其放入代码中,到目前为止,这就是我所拥有的:

import ast
import shutil
import textwrap
from subprocess import PIPE
from subprocess import Popen


class Visitor(ast.NodeVisitor):
    def visit(self, node):
        ret = super().visit(node)
        if ret is None:
            raise Exception("Unsupported node")
        return ret

    def visit_Expr(self, node):
        return f"{self.visit(node.value)};"

    def visit_Eq(self, node):
        return "=="

    def visit_Lt(self, node):
        return "<"

    def visit_LtE(self, node):
        return "<="

    def visit_Load(self, node):
        return "//load"

    def visit_Name(self, node):
        return f"{node.id}"

    def visit_Compare(self, node):
        left = self.visit(node.left)
        ops = [self.visit(x) for x in node.ops]
        comparators = [self.visit(x) for x in node.comparators]

        if len(ops) == 1 and len(comparators) == 1:
            return f"({left} {ops[0]} {comparators[0]})"
        else:
            lhs = ",".join([f"'{v}'" for v in ops])
            rhs = ",".join([f"{v}" for v in comparators])
            return f"cmp<{lhs}>({rhs})"

    def visit_Call(self, node):
        func = self.visit(node.func)
        args = [self.visit(x) for x in node.args]
        # keywords = [self.visit(x) for x in node.keywords]
        return f"{func}({','.join(args)})"

    def visit_Module(self, node):
        return f"{''.join([self.visit(x) for x in node.body])}"

    def visit_Num(self, node):
        return node.n


if __name__ == "__main__":
    out = Visitor().visit(
        ast.parse(
            textwrap.dedent(
                """
            1 == 1<3
            1 == (1<3)
            1 == (0 < foo(0 <= bar() < 3, baz())) < (4 < 5)
            foo(0 <= bar() < 3, baz())
        """
            )
        )
    )

    if shutil.which("clang-format"):
        cmd = "clang-format -style webkit -offset 0 -length {} -assume-filename None"
        p = Popen(
            cmd.format(len(out)), stdout=PIPE, stdin=PIPE, stderr=PIPE, shell=True
        )
        out = p.communicate(input=out.encode("utf-8"))[0].decode("utf-8")
        print(out)
    else:
        print(out)

如您所见,输出将是某种不可编译的 c 输出:

cmp<'==', '<'>(1, 3);
(1 == (1 < 3));
cmp<'==', '<'>((0 < foo(cmp<'<=', '<'>(bar(), 3), baz())), (4 < 5));
foo(cmp<'<=', '<'>(bar(), 3), baz());

问题,允许我转换 python 比较表达式的算法是什么(一个 python 工作示例在这里是理想的,但只是一些允许我改进提供的 sn-p 的通用伪代码也可以)到c?

【问题讨论】:

  • 没有标准的 C 表达式可以得到你想要的行为。
  • "所以我想在 c 中你应该做类似 bool v0=a
  • 原来转译并不像改变语法那么简单!
  • 应该类似于int i1 = 1; int i2 = 1; int i3 = 3; if(i1 == i2 &amp;&amp; i2 &lt; i3)。如果您使编译器更智能,它可能会判断 1 是否被评估两次并不重要,因此它可以将其更改为 if(1 == 1 &amp;&amp; 1 &lt; 3)
  • return f"cmp&lt;{lhs}&gt;({rhs})" 的目的是什么?这不是 C 代码的样子。

标签: python c compiler-construction abstract-syntax-tree transpiler


【解决方案1】:

正确的翻译:

if 1 == 2 < 3:

是:

int i1 = 1;
int i2 = 2;
int i3 = 3;
if(i1 == i2 && i2 < i3) {

(编辑:这仍然不正确,因为它没有短路)

或者,最后一个不必是临时变量:

int i1 = 1;
int i2 = 2;
if(i1 == i2 && i2 < 3) {

或者:(此版本将被比较的表达式保留在比较表达式中)

int i1;
if(1 == (i1 = 2) && i2 < 3) {

您的编译器需要知道被比较的值是ints,以便它可以声明临时变量。而且它还需要选择每次不同的临时变量名,所以如果你有两个这样的比较,那么它就不会尝试生成多个同名的变量。

您可能意识到可以多次计算表达式 2 是可以的,因此编译器可以生成以下代码:

if(1 == 2 && 2 < 3) {

但这是一个可选的附加功能。

请注意,同一表达式中可能有多个:

if 1 < (2 if (6 < 7 < 8) else 3) < 4:

翻译成这样的:

int i1 = 1;
    int i2 = 6;
    int i3 = 7;
    int i4 = 8;
int i5 = (i2 < i3 && i3 < i4 ? 2 : 3);
int i6 = 4;
if(i1 < i5 && i5 < i6) {

或:

int i1;
int i2;
if(1 < (i1 = (6 < (i2 = 7) && i2 < 8 ? 2 : 3)) && i1 < 4) {
//            ^^^^^^^^^^^^^^^^^^^^^^ inside
// ^^^^^^^^^^^                               ^^^^^^^^^^^ outside

【讨论】:

  • @BPL 你有没有考虑过 NodeVisitor 没有做你想做的事情的可能性?您可以直接查看节点,不必经过NodeVisitor。
【解决方案2】:

转换比较表达式时的另一个复杂情况是,您希望防止在拆分后多次使用的子表达式被多次计算,如果存在诸如函数调用之类的副作用,这一点尤其重要。

可以将子表达式提前声明为变量以避免多次计算。

有一个聪明的方法可以将 Python 比较表达式转换为 JavaScript,来自一个名叫 亚历山大·舍帕诺夫斯基。他在他的博客文章中详细解释了他的整个解决方案:http://hackflow.com/blog/2015/04/12/metaprogramming-beyond-decency-part-2/

基本上同样的方法可以应用到 C 的转译中。

他确定成对的相邻操作数。这对于将链式比较转换为单独的比较是必要的,其中“中间”操作数随后被复制,并且是拆分的第二个子比较的左操作数。

可以使用一种符号表将变量与子表达式相关联。变量的命名可以通过一个简单的计数器来完成。

访问表达式节点时可以输出变量。要获得问题中作为示例给出的表达式的 C 输出,您可以简单地发出 printf。

为了进一步简化,我们可以假设假设的小而平凡的 Python 子集只需要处理 int 表达式。

Python 代码

我采用了您的 sn-p 并根据上述几点对其进行了轻微修改,使其成为一个独立的示例,为您的示例表达式输出可编译的 C 代码。

import ast
import itertools
import textwrap


def pairwise(iterable):
    """s -> (s0,s1), (s1,s2), (s2, s3), ..."""
    a, b = itertools.tee(iterable)
    next(b, None)
    return zip(a, b)


class Visitor(ast.NodeVisitor):
    def __init__(self):
        self.varCounter = 0
        self.varTable = []

    def visit_Expr(self, node):
        code = self.visit(node.value)
        variables = '\n'.join(self.varTable)
        self.varTable = []
        return f'{variables}\nprintf("%d\\n", {code});\n'

    def visit_Eq(self, node):
        return "=="

    def visit_Lt(self, node):
        return '<'

    def visit_LtE(self, node):
        return '<='

    def visit_Gt(self, node):
        return ">"

    def visit_GtE(self, node):
        return ">="

    def visit_Name(self, node):
        return str(node.id)

    # see http://hackflow.com/blog/2015/04/12/metaprogramming-beyond-decency-part-2/
    def visit_Compare(self, node):
        ops = node.ops
        operands = [node.left] + node.comparators
        variables = []
        for o in operands:
            self.varCounter += 1
            num = self.varCounter
            op = self.visit(o)
            variables.append((num, op))
            self.varTable.append(f'int t{num} = {op};')

        pairs = pairwise(variables)  # adjacent pairs of operands

        return ' && '.join('%s(%s %s %s)' %
                             ('!' if isinstance(op, ast.NotIn) else '',
                              f't{l[0]}', self.visit(op), f't{r[0]}')
                             for op, (l, r) in zip(ops, pairs))

    def visit_Call(self, node):
        args = [self.visit(x) for x in node.args]
        return self.visit(node.func) + "(" + ", ".join(args) + ")"

    def visit_Num(self, node):
        return str(node.n)


def main():
    analyzer = Visitor()
    tree = ast.parse(
        textwrap.dedent(
            """
            1 == 1<3
            1 == (1<3)
            1 == (0 < foo(0 <= bar() < 3, baz())) < (4 < 5)
            foo(0 <= bar() < 3, baz())
            """
        )
    )

    # print(ast.dump(tree))

    for node in ast.iter_child_nodes(tree):
        c = analyzer.visit(node)
        print(c)


if __name__ == '__main__':
    main()

测试运行

运行 Python 程序时,调试控制台会显示以下内容:

int t1 = 1;
int t2 = 1;
int t3 = 3;
printf("%d\n", (t1 == t2) && (t2 < t3));

int t4 = 1;
int t6 = 1;
int t7 = 3;
int t5 = (t6 < t7);
printf("%d\n", (t4 == t5));

int t8 = 1;
int t10 = 0;
int t12 = 0;
int t13 = bar();
int t14 = 3;
int t11 = foo((t12 <= t13) && (t13 < t14), baz());
int t9 = (t10 < t11);
int t16 = 4;
int t17 = 5;
int t15 = (t16 < t17);
printf("%d\n", (t8 == t9) && (t9 < t15));

int t18 = 0;
int t19 = bar();
int t20 = 3;
printf("%d\n", foo((t18 <= t19) && (t19 < t20), baz()));

当然有一种方法可以进一步简化这一点。例如,常量表达式不需要分配给变量。当然,还有更多细节需要考虑。但这应该是为您的示例数据输出可编译 C 代码的起点。

【讨论】:

  • And of course there are many more details to consider - 主要是没有短路的副作用会有所不同。
【解决方案3】:

您正在寻找的方法,尝试去上课,永远不会与 C 一起工作,因为 C 不支持课程。如果您想将逻辑和对象捆绑到一个简洁的单元中,您正在寻找 C++

我能想到的最好的方法是创建一个全新的Compare.c 文件,其头文件声明struct Compare,它对Python 中的操作和比较器具有可变大小,但即使使用flexible array members,你也是仍然在小河上,因为你不能在一个结构中拥有两个灵活的数组,这意味着你需要一个包含元素 operandcomparator 的子结构(可能称为 op) - 现在引入另一个问题(假设 Python 的库支持它们) - 处理一元和三元运算符。

您可以使用union 来处理支持comparator 结构中的一元和三元运算符,但除此之外,您正在涉足非常深的水域。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-21
    • 2019-04-20
    • 1970-01-01
    • 2019-02-27
    • 2021-05-28
    • 1970-01-01
    • 2013-05-21
    • 1970-01-01
    相关资源
    最近更新 更多