编写一个简单的Python到C编译器:你好,斐波纳契

2020-08-24 02:51:42

在这篇文章中,我们将用Python编写一个Python到C的编译器。这特别容易做到,因为Python有一个内置的解析库,而且许多CPython内部都向扩展编写者公开。

在本文结束时,只要几百行Python代码,我们就可以编译并运行以下程序:

$cat tests/recursive_fib.pydef fib(N):if n==0或n==1:返回n return fib(n-1)+fib(n-2)def main():print(fib(40))$python3 PYC test/recursive_fier.py$。/bin/a.out102334155。

这篇文章实现了一个非常小的Python子集,完全放弃了管理内存的尝试,因为我无法理解手动引用计数。也许有一天我会想办法换一辆像波姆这样简单的GC。

这个程序很可能在Windows、Mac、FreeBSD等系统上都能正常运行,但是我没有费心去测试它(或者提供自定义的编译器指令)。欢迎拉取请求!

在我们进入编译器之前,让我们使用libpython用C手写Fibonacci程序。

如Python Embedded Guide中所述,我们将需要包含libpython并在我们的主机中对其进行初始化。c:

为了针对libpython进行编译,我们将使用python3-confialled作为python3-devel的一部分来告诉我们在编译过程中的每个步骤应该链接什么。

$GCC-c-o main.o$(python3-config--cflag)main.c$GCC$(python3-config--ldflag)main.o$./a.out;echo$?0。

凉爽的!。现在,当我们考虑转换Fibonacci实现时,我们希望尽可能长时间地将所有内容作为Python对象保留。这意味着向和从小函数传递和接收PyObject*,并将所有C整数转换为PyObject*的子类型PyLong。可以想象,在对Python进行操作之前,Python中的所有内容都是一个对象。

有关Python中对象的更多信息,请查看Python文档中的数据模型页面。

要将C整数映射到PyLong*,我们使用PyLong_FromLong。Tomap相反,我们使用pylong_aslong。

要比较两个PyObject*,我们可以使用PyObject_RichCompareBool,它将处理比较,而不考虑两个参数的类型。如果没有这个帮助器,我们将不得不编写复杂的检查来确保两边是相同的,如果是相同的,则将它们解包成它们的底层C值并比较C值。

我们可以使用PyNumber_Addand和PyNumber_Subtract进行基本算术运算,并且有许多类似的辅助程序可用于后续操作。

#DEFINE PY_SSIZE_T_CLEAN#include<;Python.h>;PyObject*fib(PyObject*n){PyObject*Zero=PyLong_FromLong(0);PyObject*one=PyLong_FromLong(1);IF(PyObject_RichCompareBool(n,零,Py_Eq)||PyObject_RichCompareBool(n,one,Py。Return PyNumber_add(左、右);}int main(int argc,char*argv[]){Py_Initialize();PyObject*res=fib(PyLong_FromLong(7));//应为13 return PyLong_aslong(Res);}。

$GCC-c-o main.o$(python3-config--cflag)main.c$GCC$(python3-config--ldflag)main.o$./a.out;echo$?13

那真是太棒了!但我们在一个地方作弊。我们假设fib函数的输入是一个整数,并且我们在编写PyNumber_*操作的任何地方传播这一假设。当我们编写编译器时,在调用数值帮助器之前,我们需要检查这两个参数是否都是整数,否则我们可能需要调用字符串连接帮助器或其他完全不同的东西。

当我使用现有的解析器编写新的编译器时,我几乎总是从入口点和代码生成器开始,这样我就可以探索AST。但是,如果我们先从实用程序开始,解释代码是最容易的。

此C文件将包含三个辅助函数,用于安全地加、减和打印。它将连接到生成的C文件的顶部。我们现在只支持整数,但是这个结构为我们以后支持更多的类型做好了准备。

#DEFINE PY_SSIZE_T_CLEAN#include<;Python.h>;inline PyObject*PYC_Add(PyObject*l,PyObject*r){//TODO:ALLOW__ADD__OVERRIDE//如果(PyLong_Check(L)&;&;PyLong_Check(R){Return PyNumber_Add(l,r);}//TODO:句柄str。}inline PyObject*PYC_Sub(PyObject*l,PyObject*r){//TODO:ALLOW_ADD_OVERRIDE//如果(PyLong_Check(L)&;&;PyLong_Check(R)){return PyNumber_Subtract(l,r);}//TODO:处理字符串等。//TODO:引发异常返回NULL;}INLINE PyObject*PYC_PRINT(。\n";);返回Py_None;}。

就是这样!我们可以在Python中将它们生成为字符串,但这样做会很麻烦。通过使用专用的C文件,我们可以利用语法突出显示,因为该文件只是C代码。由于我们将所有函数标记为内联,因此不将这些函数作为字符串嵌入到Python中没有运行时开销。

该文件将包含一个上下文类,用于作用域中的ManagingiIdentifier和代理到Writer类,该Writer类包含用于编写C代码行的帮助器。

我们将在上下文中拥有Writer类的两个实例,这样我们就可以写入主体(或当前/主要)区域和初始化区域。

如果在顶层声明了任何变量,则初始化区域是必需的。我们不能在函数外部初始化这些变量,因为每个PyObject*都必须在调用Py_Initialize之后创建。在我们进入编译后的Python Main函数之前,这一部分将被写入我们的C Main函数。

导入复制类Writer():content=";";def write(self,exp:str,indent:int=0):self.content+=(";";*indent)+exp def writeln(self,stmt:str,indent:int=0):self.write(stmt+";\n";,indent)def write_Statement(self,stmt:str。,indent)class context():Initialations=Writer()Body=Writer()缩进=0 Scope=0 ret=None namings={}Counter=-1 def__getattr__(self,name:str)->;object:#避免在每次输出时传入self.indentation=[Initialations";,";Body";]对于输出中的输出:if name.startswith(Output):返回lambda s,i=无:getattr(getattr(self,output),name[len(Output)+1:])(s,i如果我不是其他self.indentation)返回对象。__getattr__(self,name)def get_local(self,source_name:str)->;dict:返回self.namings[source_name]def register_global。:loc,";scope";:0,}def register_local(self,local:str=";tmp";)->;str:self.count+=1 self.namings[local]={";name";:F";{local}_{self.counter}";,#命名字典已复制,因此我们需要在声明";scope";处捕获scope#。用法:self.scope,}return self.namings[local][";name";]def copy(Self)参数:new=copy。copy(Self)#由于某种原因Copy。深拷贝不会执行此操作。namings=dict(new.namings)return new def at_toplevel(Self):return self.scope=0。

入口点负责读取源代码、解析源代码、调用代码生成器、将源代码写入C文件并编译。

导入astimport osimport子进程simport Shutilimport sysfrom context import Contextfrom codegen import GenerateBUILTINS={";print";:";pyc_print";,}def main():target=sys.argv[1]with open(Target)as f:source=f.read()tree=ast.parse(source,target)。

...def main()...。Ctx=context()with open(";libpyc.c&34;)as f:ctx.body_write(f.read()+";\n";)for Builtin,FN in BUILTINS.Items():ctx.register_global(builtin,fn)Generate(ctx,tree)。

接下来,我们创建一个干净的输出目录,并使用生成的代码和一个用于初始化Python和任何全局变量的main函数编写main.c:

...def Main():...#创建并移动到工作目录outdir=";bin";Shutil.rmtree(outdir,Ignore_Errors=True)os.mkdir(Outdir)os.chdir(Outdir)with open(";main.c";,";w";)as f:f.write(ctx.body.content)Main=ctx.namings.get(";main";)[";name";]f.write(f";";";int main(int argc,char*argv[]){{Py_Initialize();//初始化全局变量(如果有)。{ctx.initializations.content}PyObject*r={main}();return PyLong_aslong(R);}}";";";)。

...def main():...。Subprocess.run([";clang-format";,";-i";,";main.c";])cflag_raw=subprocess.check_output([";python3-config";,";--cflag";])如果f.strie()]cmd=[";GCC";]中的f的cflag=[f.strip(),则运行(";clang-format";-i";,";,";]cmd=[";GCC";])。,";-c";,";-o";,";main.o";]+cflag+[";main.c";]subprocess.run(Cmd)ldflag_raw=subprocess.check_output([";python3-config";,";--ldflag";])ldflag=[f.strips()for f in ldflag_raw.decode().Split(";";)如果f.strie()]cmd=[";GCC;]+ldflag+[";main.o&34;]subprocess.run(Cmd)。

导入astimport osimport子进程simport Shutilimport sysfrom context import Contextfrom codegen import GenerateBUILTINS={";print";:";pyc_print";,}def main():target=sys.argv[1]with open(Target)as f:source=f.read()tree=ast.parse(source,target)ctx=context()with open(";libpy34;libpyf():target=sys.argv[1]with open(Target)as f:source=f.read()tree=ast.parse(source,target)ctx=context()with open(";libpya。)对于内置目录,BUILTINS.Items()中的fn:ctx.register_global(builtin,fn)GENERATE(CTX,TREE)#创建并移动到工作目录outdir=";bin";Shutil.rmtree(outdir,Ignore_Errors=True)os.mkdir(Outdir)os.chdir(Outdir)with open(";main.c";,";w&#。Name";]f.write(f";";";int main(int argc,char*argv[])){{Py_Initialize();//初始化全局变量(如果有的话)。{ctx.initializations.content}PyObject*r={main}();return PyLong_aslong(R);}}";";";";)subprocess.run([";clang。Main.c";])CFLAGS_RAW=subprocess.check_output([";python3-config";,";--CFLAGS";])CFLAGS=[f.strain()for f in cflag_raw.decode().Split(";";)if f.strie()]cmd=[";GCC";,";-c";,";-o";,";-o";,";Main.o";]+cflag+[";main.c&34;]子进程.run(Cmd)ldflag_RAW=subprocess.check_output([";python3-config";,";--ldflag";])ldflag=[f.strips()for f in ldflag_raw.decode().如果f.strip()]cmd=[";GCC";]+ldflag+[";]+ldflag+[";].分解(";";)如果f.strie()]cmd=[";GCC";]+ldflag+[&。Main.o";]subprocess.run(Cmd)main()

最后,我们编写从Python AST到C的翻译层。我们将其分为10个助手函数。有ASTspec可供参考是很有帮助的。

代码生成器的入口点是Generate(ctx:context,exp)。它为任何具有存储语句列表的body属性的对象生成代码。此函数将为模块、函数体、IF体等对象生成代码。

对于每条语句,我们将简单地将生成传递给一个关联的帮助器函数。不过,在表达式生成的情况下,我们还会在表达式结果上添加noop操作,否则编译器将报告未使用的变量。

Def Generate(ctx:context,module):用于模块中的stmt。body:if isinstance(stmt,ast.Assign):Generate_Assign(ctx,stmt)Elif isinstance(stmt,ast.FunctionDef):Generate_function_def(ctx,stmt)Elif isinstance(stmt,ast.Return):Generate_Return(ctx,stmt)Elif isinstance(stmt,ast.If)://noop隐藏未使用的警告";)ctx.body_write_Statement(f";{r}+=0";)否则:引发异常(f";不支持的语句类型:{type(Stmt)}";)。

记住要积极地抛出异常,否则使用新语法调试程序会很麻烦。

要生成分配代码,我们需要检查我们是否处于顶层。如果我们在顶层,我们可以声明变量,但是我们还不能初始化它。因此,我们将初始化代码添加到程序的初始化部分。

不过,在执行任何一项操作之前,我们都要注册变量名,以便可以设置一个安全的本地名称,以便在生成的代码中使用。然后我们编译右手边,这样我们就可以把它赋给左手边。

Import astfrom context import Contextdef initialize_Variable(ctx:context,name:str,val:str):if ctx.at_toplevel():decl=f";PyObject*{name}";ctx.body_write_Statement(decl,0)init=f";{name}={val}";ctx.initialations_write_Statement(Init):ctx.。)def GENERATE_ASSIGN(ctx:context,stmt:ast.Assign):#TODO:支持分配给元组LOCAL=ctx.register_local(stmt.target[0].id)val=GENERATE_EXPRESSION(ctx,stmt.value)initialize_variable(ctx,local,val)

就像GENERATE中的语句一样,我们需要实现几种表达式:

对于ast.Num,我们只需要将文字数字包装为PyLong*。对于ast.Name,我们只需在上下文中查找本地名称。否则,我们将委托给更多帮助器函数。

Def Generate_Expression(ctx:context,exp)->;str:if isinstance(exp,ast.Num):#TODO:处理非整数tmp=ctx.register_local(";num";)initialize_Variable(ctx,tmp,f";PyLong_FromLong({exp.n})";)return tmp Elif isinstance(exp,ast.BinOp):return Generate_bin_op(ctx,exp)Elif isinstance(exp,ast.BoolOp):return Generate_bool_op(ctx,ast.BoolOp):return Generate_bool_op(ctx,exp)Elif isinstance(exp,ast.Name):return ctx.get_local(exp.id)[";name";]Elif isinstance(exp,ast.Compare):return GENERATE_COMPARE(CTX,ast.Compare)。不支持的表达式:{type(Exp)}";)。

对于每个作为表达式的代码生成帮助器,我们将表达式存储在一个局部变量中,并返回变量的名称,以便AST中的父节点可以引用该子节点。这可能会导致低效的代码生成(无用的分配),但对于这样的项目来说,这并不是什么大不了的事情,而且很可能会被GCC优化掉。更烦人的方面是,无用的赋值只会使生成的代码更难阅读。

对于二元运算符,我们需要支持加法和减法。在ast.Compare和ast.BoolOp中提供了其他二元运算符,如相等或和/或。

Def Generate_bin_op(ctx:context,binop:ast.BinOp)->;str:result=ctx.register_local(";binop";)l=Generate_Expression(CTX,binop.Left)r=Generate_Expression(CTX,binop.right)if isinstance(binop.op,ast.Add):ctx.body_write_Statement(f";PyObject*{result}=PY。)Elif isInstance(binop.op,ast.Sub):ctx.body_write_Statement(f";PyObject*{result}=PYC_Sub({l},{r})";)ELIF:引发异常(f";不支持的二元运算符:{type(binop.op)}";)返回结果。

对于斐波纳契程序,我们只需要支持或,但是在Python中,or比在C中要复杂。在Python中,第一个值为true,将表达式短路,结果是它的值,而不是true。

Def Generate_bool_op(ctx:context,boolop:ast.BoolOp)->;str:result=ctx.register_local(";boolop";)ctx.body_write_Statement(f";PyObject*{result}&34;)if isinstance(boolop.op,ast.Or):Done_or=ctx.register_local(";Done_or"。{result}={v}";)ctx.body_writeln(f";if(PyObject_IsTrue({v})){{";)ctx.body_write_Statement(f";goto{Done_or}";,ctx.indentation+1)ctx.body_writeln(";}";)ctx.body_writeln(f";

现在我把它写下来了,我知道如果我们使用循环的话,我们可能可以把这个函数移到libpyc.c中。也许在下一次迭代中。

此函数处理相等和不平等检查。我们将修改手写翻译中使用的PyObject_RichCompareBool助手。

唯一需要记住的是右侧作为数组传递。因此,我们必须遍历它,并对列表中的所有对象应用相等/不等检查。

Def GENERATE_COMPARE(ctx:context,exp:ast.Compare)->;str:result=ctx.register_local(";Compare";)Left=Generate_Expression(CTX,exp.Left)ctx.body_write_Statement(f";PyObject*{result}={Left}";)for i,op in Enumerate(exp.ops):v=Generate_Expression(CTX,exp.compators[i。{result}=PyObject_RichCompare({result},{v},Py_Eq)";)Elif isInstance(op,ast.NotEq):ctx.body_write_Statement(f";{result}=PyObject_RichCompare({result},{v},Py_NE)";)否则:引发异常(f";不支持的比较:{type(Op)}";)返回结果。

最后一个表达式非常简单。我们首先编译调用的参数,然后编译函数本身,然后像任何C函数一样调用带参数的函数。直接调用C函数会对与Python库交互产生影响(基本上,我们不会与任何库交互),但这是最简单的入门方式。

Def GENERATE_CALL(ctx:context,exp:ast.Call)->;str:args=';,';.join([GENERATE_EXPRESSION(CTX,a)for a in exp.args])FUN=GENERATE_EXPRESSION(CTX,exp.func)res=ctx.register_local(";call_result";)#TODO:lambdas和闭包需要额外的工作ctx.body_write_Statement。

这是一个有趣的故事。首先,我们在作用域中注册函数名。然后我们复制上下文,以便函数体中的变量包含在函数体中。我们扩大了范围,因为我们知道我们已经离开了顶层。最后,我们对正文进行了编译。

Def Generate_Function_def(ctx:context,fd:ast.FunctionDef):name=ctx.register_local(fd.name)Child Ctx=ctx.copy()args=";,";.join([f";PyObject*{Child Ctx.register_local(a.arg)}";for a in fd.args.args])ctx.body_writeln(f"。,0)Child Ctx.scope+=1 Child Ctx.indentation+=1如果不是Child Ctx.ret:Child Ctx.body_write_Statement(";return Py_None";)ctx.body_writeln(";}\n";,0)

对Child Ctx.ret的检查并不是严格必要的,因为即使已经有一个,我们也可以发出一个返回值。要求GENERATE_RETURN设置此属性,并让GENERATE_Function_def检查它,这样只会使生成代码更美观一些。

非常简单,我们只需编译要返回的值,然后发出一条RETURN语句。

我们存储返回值,以便函数定义可以知道是否添加返回PyNone语句。

您知道要做的是:编译测试,如果测试是真实的,则进入编译后的主体。我们改天再处理另一具身体。

Def GENERATE_IF(ctx:context,exp:ast.if):test=Generate_Expression(ctx,exp.test)ctx.body_writeln(f";if(PyObject_IsTrue({test})){{";)ctx.indentation+=1 Generate(ctx,exp)#TODO:Handle exp.orelse ctx.indentation-=1 ctx.body_writeln(";

$cat tests/recursive_fib.pydef fib(N):if n==0或n==1:返回n return fib(n-1)+fib(n-2)def main():print(fib(40))$python3 PYC test/recursive_fier.py$。/bin/a.out102334155。

我提到这一点的唯一原因是,当我为面向C++/libV8的JavaScript做一个类似的编译器项目时,生成的代码在速度上大致相同或稍慢一些。