当前位置:首页|资讯

设计编译型版本的Python.

作者:全民编程是有意义发布时间:2024-10-20

来自对此问题的回答:  https://www.zhihu.com/question/672537543 .

我知道你想问什么, 你想获得编译型的Python版本.


Python一样可以编译为机器码, 不需要到字节码那一步, 只需要在AST后, 遍历完树后, 直接生成IR或汇编. 随后调用汇编器as nasm等等,或自己手写动态汇编器, 就能生成机器码, 跑原生机器码了.


源码字符串 → (token;我称之为牌) → 抽象句法(AST) → 汇编或抽象汇编 → 机器码 .

字符串被翻译为牌, 要用到数据结构: 队列, 哈希表, 栈.

牌翻译为抽象句法树, 要用到数据结构: 树, 队列, 哈希表, 栈.

AST翻译为汇编, 要用到数据结构: 队列, 哈希表, 栈, 图(大型编译/解释情况下.

汇编翻译为机器码, 要用到数据结构: 树, 队列, 哈希表, 栈. (因为汇编器也是一个小型的编译器)

其实这些过程都离不开增删改查, 所以设计编译器/解释器之前, 首先设计恰当的算法与数据结构会事半功倍, 当你设计好后, 只要调用来用即可.

不管是编译还是解释, 都离不开一个调度结构, 也就是元循环求值; 在一个循环里跑Switch...case... , 或 if ... else ... , lisp则更简单, 在 cond ... else 里. 这个调度其实很简单, 就是遇到条件(键)做出相应的动作(值), 而这个是递归的, 也就是值里还可以有键或多个平行的键, 键后自然还要有值, 或空操作也行.

而编译/解释的四个翻译过程, 其中每阶段都有这样一个调度结构(大型项目还有多个), 解释器还需要多一个虚拟机, 虚拟机里也有一个这样的调度, 虚拟机其实是用软件实现CPU的前端, 就是拾取 → 解码 → 译码. 执行可以翻译为处理器的指令, 或者实现为虚拟机原语(就是每个操作写成函数,而这个函数又可以内联汇编或实时翻译为汇编(JIT的一种实现方式)), 因为"解释"还有一个意思是 直译.

你觉得很复杂吗? 其实就是这样简单, 就是键值对匹配与做相应操作, 觉得复杂是因为功能多, 每种编程语言提供的功能不一样, 有的很多比如C++ rust, 对于每样功能(键)都要做相应的解析(一个值或多个值,每个值可能还有一个或多个键) ,要不然这些语言里的各种符号加字符的组合(关键字或保留字), 你怎么识别呢. 所谓解析parse, 其实翻译不对, 更多的意思是; 剖开 解构. 编译compile也一样, 更多的意思其实是收集 搜集 集中 编织, 偏向于集与编的意思.

有的编译器/解释器还有IR, 就是中间码, 是什么呢? 其实是一种机器无关的汇编, 可移植 便携的汇编, 你把中间码看作是一种伪代码也可以, 相当于保留谓词与名词与流程控制的伪代码. 图灵机其实很简单, 状态 顺序 跳转, 没了. 图灵机为无限状态机, 变量存储状态, 指针是变量的一种, 函数就是对变量集合的封装, 操作码(操作者)与操作数(操作范围)都可以存储在变量内. 所以它是递归的, 一个状态可以像卷积网一样引出无限多个状态, 周而复始, 且是上下文相关的.

回到Python这里, 当Python解释器走到抽象句法树(AST)这一步的时候, 通过遍历获取树上的值, 遍历分为两种, 一种是监听器, 一种是访问器, 解释器里监听用得多, 反之访问器用之于编译器, 监听器监听到树上的值后, 发送到虚拟机, 也可以直接解释(事先写好解释函数,当然你也可以就在本地代码处写业务逻辑), 访问器则是对树上的值进行翻译为中间码/汇编/机器码. 树可能是普通树,也可能是二叉树, 一般会把普通树简化为二叉树, 因为操作二叉树简单. 遍历的时候, 就是把[节点] [左子树] [右子树]当做键, 读取并存储到值里, 语法糖看起来就是赋值运算, 汇编或机器码看起来就是mov移操作. 存储的值一般按照固定的形式排列, 比如三地址码 四地址码 多地址码, 也有零地址码, 就是栈机用的形式. 现在流行中间码, 流行静态单复制, 其实跟三地址码很像. 三地址码就是[操作码地址] [目的地址] [源地址], 分别对应节点 左子树 右子树, CISC一般都这样, RISC还多一个[值地址], 像这样; [操作码地址] [值地址] [目的地址] [源地址], CISC把目的地地址当做值地址用. 因为现在没有物理的硬件栈机, 一般都是寄存器机器, 所以Python的零地址码一般都被翻译为三地址码或四地址码(假如你想搞编译型Python的话), 我的意思是其它基于栈设计的编程语言同时又做了JIT或编译版本的, 就会是这样的设计形式. 很多, JS是这样, wasm, 早期Java, 还有那些流行的解释型语言. 还有脚本语言.

所以关键的地方来了. Python在这一小步时, 并不是非得生成零地址码, 你明白吧? 它可以生成机器无关汇编(IR), 机器相关汇编(比如x86 ARM), 甚至机器码(TinyCC), 每种方式的简易度不一样, 理论上来说, 所有解释型语言都可以编译为机器码, 包括bsh这种脚本, 甚至cmd bat都可以, 因为它们都有AST, 只要有AST, 就都会走完上面的每一步.


好了,解答完毕,你学会了吗?

其实很简单, 就是个翻译过程, 简化来说, 就是一键一值对应的键值对, 因为翻译就是这样啊, 水 : water , 火 : fire , 苹果 : Apple , 每个键有对应的值, 反过来翻译也是一样.

掌握键值对的思维, 你就掌握了计算机90%以上的技术, 有很多人理解不了指针, 其实指针就是键值对的一种, 递归也是, 键值对本身也是可以递归的, 它递归的时候就形成了树,

地址码就是指针, 指针存储在变量里, 变量就是状态, 我们通过键值对来描述状态.

有很多技术都是直接应用了键值对思维, 比如JSON, HTML, CSS, 哈希表, lisp的符号表达式, 简化到最根本的状态时, 就是键值对.

我们的汉语(中文汉字汉语)也可以简化为键值对, 谓词当键, 名词当值, 主谓宾-名动名, 所以用中文汉字编程不在话下, 设计中文编程是原生具备的.

我是20年的时候, 悟出这句话的: "键是值的值, 值是键的键".

这几年又深入学了编译和lisp, sicp这些, 对此认识更加深刻.


另外: 中文编程的设计其实很简单, 只要在"源码字符串 → " 这一步设计i18n就行了, 因为现在的这些编译器/解释器都是直接把字符硬编码进去的(特别是ASCII, utf8也是为此优化), 在这一步里的算法与数据结构也是为其专门设计, 所以若是要做i18n适配, 只需要重新设计一种算法就是了, 目前我还没看到有人能创造出来. 最佳做法就是用数字编码替代牌, 在相应的槽(位置处)用数字表示,比如10进制, 16进制, "源码字符串 → " 这一步得到牌后, 翻译为数字表示, 数字固定不变, 每种自然语言所用字符跟这套数字编码对齐(可以写个软路由算法,在源码里指定开关,软路由读取开关切换到相应的自然语言所用配置), 之后AST读取和处理这些数字组, 其实这跟后面步骤的指令是一样的, 一个指令我们可以用二进制表示(00000001), 16进制表示(01), 字符串表示(ADD).也可以用10进制, 但是业界流行用16进制, 因为8位 16位时代,存储空间很贵. mov 8B 10001011‬, mov也可以用汉字"移"表示.


纯手打.



Copyright © 2024 aigcdaily.cn  北京智识时代科技有限公司  版权所有  京ICP备2023006237号-1