Skip to content

Instantly share code, notes, and snippets.

@FrankHB
Created August 8, 2022 09:42
Show Gist options
  • Save FrankHB/c3eaf1fe4b61f6eb89a9329a9bfd110b to your computer and use it in GitHub Desktop.
Save FrankHB/c3eaf1fe4b61f6eb89a9329a9bfd110b to your computer and use it in GitHub Desktop.
关于所谓“弱类型”的误导问题

缘起

因为 Bilibili 莫名其妙吃回复,那就在这里存档。

原始内容见这里

如无另行指定,默认回复 @LanceMoe

预备 +TODO 改换文体,+FAQ

(不过虽然议题有相关性,仍然远远不够继续升级话题就是了。)

引用内容

(无关内容略。)

LanceMoe回复 @喜羊羊与刘备 :本来设计之初就是强类型的语言一般都不需要三等号,因为三等号本来就是给弱类型时候设计的双等号擦屁股用的[doge]

2022-07-16 18:44

LanceMoe回复 @星海如昼 :这个属于运算符重载,不是隐式转换 [doge]

2022-07-16 18:45

LanceMoe回复 @STEStellar :除0和对0取mod在类型安全的语言都是要抛异常的,建议你了解一下PL的基础知识:类型理论

2022-07-16 18:46

LanceMoe回复 @不氪金之后 :是b不能为0,除0和对0取mod都是应该抛异常的

2022-07-16 18:47

LanceMoe回复 @--诺安-- :没有说需要c和c++,况且c语言是弱类型,c++是强类型,c++和c早就是两门完全不同的语言了。做项目当然要选合适的语言和框架,现在正常来说,预判项目是大型需要多人合作的,新起项目都是首选强类型语言,比如前端原来用js现在基本上都是用ts(ts整体还算类型比较完备的),原来可能没得选必须用c起步的现在起步尽量用rust。现代语言基本上都有类型推论,所以也不需要显式声明类型,没有历史包袱的情况下写起来方便度和弱类型的东西也没什么太大区别了。

2022-07-16 18:54回复

幻の上帝回复 @LanceMoe :除0抛异常……C:?C++:?

2022-08-06 22:04回复

幻の上帝回复 @LanceMoe :如果你真正经学过PL,就不要用所谓强类型/弱类型这样的混沌说法。所谓弱类型也不是指类型检查强度低下,而是指根本上瞎指派类型+滥用多态。

2022-08-06 22:06回复

LanceMoe回复 @幻の上帝 :上来就喷?强弱类型虽然没绝对界限,但是几门语言放在一起比很容易判断。你只要看这个语言设计本意,是不是编写代码时需要“经常”做隐式cast、或者类型的decay是不是“通常”是隐式的就能轻松判断,我从来也没说什么“类型检查强度低下”这种话。比如c的malloc返回void*可以分配给任意type的pointer,数组传递会decay成指针,这就是弱类型。c++相当于c的差异点都是强类型的特征,如果这两门语言一比,那c++显然是强类型,但是跟rust这种新出的设计更先进的语言比,没有除0异常之类的点,那么跟rust比c++略弱,但是依旧是强类型,c++不搞除零异常只是为了最大程度兼容历史的包袱。有什么混沌的?难道有人认为js和lua这种是强类型?正经学过PL你告诉我什么叫瞎指派类型,我是没听说过。你的意思是隐式cast?更加通俗的说,弱类型简单来说就是非常容易让人在不注意的时候,因为类型问题犯错的语言。我回复也是因为这个层主在这边说是因为“动态语言”的0和"0"导致的,我只是想说动态语言也有强类型,静态语言也有弱类型的,跟动态静态无关。弱类型本来就是方便大家沟通产生的概念,不然你出去咋描述,把强类型的特征和弱类型的特征全部说一遍?

2022-08-07 13:14回复

幻の上帝回复 @LanceMoe :我不知道你是哪来的PL基础,总之少拿民科理论开腔误导。

动态语言跟强弱类型自然无关,但是很遗憾你的理解错的离谱,虽然这种错误理解流行得像模像样,以至于我仍然不得不见一个就纠正一个以免继续流毒。

所谓强类型(strongly typed)就是在负责在typing environment给出type assignment的type formation rules里明确指派nominal types并且要求接口约束能确保类型保持compatible的设计,对立的就是所谓的无类型(untyped)或者单一类型(unityped),而本来就没什么弱类型,就跟本来就不存在什么面向过程一样。你杜撰的差别是在强行扭曲原意,架空原本需要描述的性质。类型检查强度低下就是分不清typing和typechecking导致的常见脑补出所谓强弱类型的例子,但你既然没这样想,那么就更没有什么借口使用什么弱类型。

C和C++都是典型的强类型语言,而且符合最早的Liskov那时候定义的强类型仅限在函数签名的狭义的情况。你提及的所谓C和C++的差异属于type compatible rules/polymorphism机制的不同,根本没什么模型能给出可比较的强弱性——比如说C++根本没像C定义compatible type,却又多了conversion function之类的自定义polymorphism,你何德何能能把这些具体的不同设计抵消比较出“强弱”?Rust差得更大,就更加没可比性了。

C和C++没除零异常是你典型想当然的遗漏。刻意不用异常也不是你所说的理由——要知道,除零异常的标准化是所有UB中相对容易的一类(最简单地,搞成信号)。真正的理由虽然还是接口语义上的决策,但明确和想要什么类型系统达到什么目的无关。用WG21的话,这是所谓的narrow contract并且不应当被widen,宁可故意允许UB,也不添加用户程序无法回避的检查开销。

幻の上帝回复 @LanceMoe :你所谓的“方便沟通”很大程度是伪需求。你确定正经的讨论中真有把所谓“弱类型”一网打尽的共性存在(除了刻意设计一门表现可以烂到什么程度的语言专门拿来黑的情况)?尽管把0和"0"混同的类似设计在不止一个语言中存在,但这基本就是没搞清楚在自己设计什么的无意识跟风。这样的一个现象本来就是artifact,堪称反模式。但另一方面,这种性质没流行到需要专门污染词汇表去命名的程度(非得硬说,那就是一种type confusion,但这一般是描述用户程序的)。

另一方面,没有混淆0和"0"的正儿八斤的动态语言多得是,甚至Lua的设计者都知道不好在Lua5.4给改了。

至于具体的转换特性本身,我已经说过了,就叫强制(coercion)。这是个比“强类型”还老资格得多的跨语言标准术语,也是Lua手册中的用词。(某些不上道的C用户学歪了导致没意识到“强制”是指隐式转换这样的常识……就不要怪我咯。)

不懂也没什么,但是还要出来乱说给正经科普增加工作量,就是你的错了。

注释 这里还有个关于 LanceMoe 对 void* 的理解偏差描述的回复被 Bilibili 吃了没显示。

LanceMoe回复 @幻の上帝 :挺有意思,先说强弱类型是民科说法,然后你花了这么大篇幅讲你认为的“强类型”是什么,然后又说“方便沟通”是伪需求,合着是在这说废话。[OK]别说什么除0不抛异常是为了零开销,要真是一切零开销,现在c++这套异常系统就别做了。除0定为ub本来就是历史包袱,在大部分主流处理器里都产生硬件中断,就算不抛异常也可以根据ieee754返回非数常量,没必要定为ub,事实就是改掉包袱远比新起一个复杂。然后也别说什么你要做“正经讨论”,真要做“科普”麻烦你写篇论文给个你的建议,你再造个概念来“方便沟通”,我支持,否则就别说什么“方便沟通”是伪需求。欢迎多做做你觉得是对的“科普”。我本来那层只是想提醒“弱类型”不代表是“动态语言”,无论怎么说这点不接受反驳,无论你怎么定义,这两个都不能划等号,所以不知道你在杠什么。还有我给你提个建议,个人觉得像你这种上来就给人扣“民科”帽子的,对“科普”是最大的阻力。“正经讨论”就别在这盖楼回复了。

正论

直球回复

我是觉得挺无聊的,你自己明摆着不搭边的外行,非得强行出头连续回复一堆人,推销你自己的观点,动机是什么,显摆你理解更深刻?“弱类型”不代表是“动态语言”没问题我没反对,但是你其它地方这槽点出现率得也太离谱了,还特意扛着PL基础知识的旗号…… exm ,PL 你说而是这样的?

我花篇幅是因为即便这种性质恶劣的误导(尤其是当事人九成九毫无自觉)平时一两句话能忍则忍,但是集中连续输出错误忍无可忍无需再忍,就逮到猛薅以确保源头上的效率最大限度打击这种谎话说多了就是真理的心态而已。这也是主流民科的态度,只不过典型的民科更加刻意。另一方面,稍微细究一点就发现PL界本来就没你所谓的这种需求,“弱类型”的提法过一遍发现不是以讹传讹就全是一知半解的外行口嗨的发明,你非得无中生有,别人不当你民科无理取闹还能咋地?

强类型到底什么东西,弱类型的说法到底多不靠谱,这种随便一搜二手文献都马上七七八八的东西,对知道什么“PL 的基础问题”的人来讲,真需要我再继续指出来源吗?还有你真能指望一个中括号都会替换歪的地方里贴啥靠谱的文献引用?

除零问题我看你是彻底的外行,不知道这里的开销到底有多大——不止是运行时开销,还有维护spec上消除硬件实现差异的问题。虽然我说过“最简单的”做法,这仍然工程上极端复杂,跟往 C++ 里加 Windows SEH 没太大(不)可行性上的差别。这种异常要整清楚就得把常见 CPU 的异步异常机制在 C/C++ 的层次上整明白,干掉所有不一致的部分,光这坨就比整个 C++ 异常都复杂了。而刻意拒绝包装内建操作符的错误条件为 C++ 这样而同步异常则是明确实现开销上的考虑。(当然你也可以硬说 C 没 C++ 那种异常算“历史包袱”所以连这种机会都没有……那你开心就好。题外话,我不反对缺乏控制作用是 PL 设计史上的包袱,但支持控制作用的实现方式就不应该钦定是“异常”。)关于 C 或 C++ 这方面设计决策不服的,建议直接提交 proposal 。

Issue 1

首先的问题是为何“弱类型”之类的话题自然被视为民科,这个最基本的一般理由上面已经提及不再解释。

可能需要补充的是如何断定“弱类型”这个说法有资格被视为民科内容、应当被视为民科内容以及属于伪需求的理由或推理过程。

其一在于 PL 界的见闻。不管是正式的论文还是还算正经的 PL 讨论(比如 LtU 上的)实际上都难以看到会提到这个说法,乃至可以认为相关人士已经对这种提法的离谱性和搞乱说话人含义的危害程度有充分认识而刻意回避了。

其二,更直接的理由是有不止一个独立来源给出反对“弱类型”的提法,基本上是这方面比较内行去深究过这些问题人的共识。

其中的技术理由各有不同,但共同点是同一“弱类型”这个概念来源可疑、定义模糊且不大有希望挽救,不如彻底放弃。

TODO 补充参考文献的内联展开片段。

当然作为已经流行的讹用术语,它的存在感已经无法让一般二手文献忽视了。但是,其中除了出处相对地靠谱以外,说法仍然不得细究——有很多判定标准上无法推敲的地方,例如“什么算弱”“如何衡量相对的强弱”。

如果要以“类型检查的强度”来建模而允许明确定义相对意义上的“弱类型”弱在哪,其实并不是不行。但至今没找到这种归纳强度并建立包含序理论意义上的模型的正式工作。(作为对比,抽象理论就有,所以谁比谁更能抽象在 PL 的一般意义上是可比的——虽然也没落实到具体 PL 上。)

退一步讲,就算有这种研究,也不可能建立放之四海皆准的全序关系(而且不限于类型检查)。这在先前提到的 C 和 C++ 类型系统的差异的例子上就已经暗示过了。

然而这个方向更大的问题是把弱类型和类型检查混淆根本就是说不通。关于这些讹传的背景成因,更严肃的技术分析和观点概述很早就已经有了,不再赘述。

Issue 2

C 和 C++ 不标准化(整数)除零异常显然不是单纯的历史包袱。

因为部分历史原因以及现在仍然强调的“性能优先”的习惯,C 和 C++ 的 /% 内建操作通常直接实现为可被 ISA(指令集架构)支持的操作。

在遇到除零的错误条件时,ISA 操作通常规定会引起硬件实现支持的异常。这种异常通常是异步的。这基本不是历史包袱,而是这种方式允许利用的硬件实现意义上的空间分散性允许物理同时直接支持并行处理。其中的历史包袱只有具体异常到底是单独的硬件器件(比如协处理器)还是和其它中断或异常机制一同实现的问题,但这在 ISA 以上的软件原则上是不可见的。

当然,以 C 和 C++ 为代表的 ISA 以上的软件实现难以利用硬件的并行红利,那就是另一个问题了。不过这也不是历史包袱,而是这类语言设计的抽象机语义本身就和并行的体系结构模型不搭——要说历史包袱,那么所有 ALGOL-like 语言整个都是。

这个意义上,不以 ISA 异常标准化除零异常的应该容易理解。考虑到具体设计的细节,比如如何与其它异常交互之类的问题,ISA 的异常远比现有的几乎所有高级 PL 中的异常机制都复杂。除了先天异步+并行和大多数语言的语义模型不搭这个麻烦的问题(比如考虑如何通知事件完成或取消——注意这里的延迟限制比外部 I/O 严格得太多,实用上这里不能引入本机线程),还在于客观上无法确保不同的 ISA 一定会有相同的设计。对标准化 PL 来说评估兼容不同的可能实现,又要尽量不引入间接层次的性能开销,这很要命。

所以强调兼容性、连调用栈都直接架空的 C ,此路不通,对一般的异常充其量就是提供了一个信号表示不是无事发生,但除零这个问题没到 SIGILL 这种程度还是完全无法保证。继承 C 的 C++ 更加不待见信号上下文对程序的运行时操作的限制,更加没扩充的余地。而 C++ 的同步异常扩充为异步异常在工作量上也是异想天开——不说绝对意义上 C++ 同步异常的复杂性已经非常离谱了(比如考虑 ABI 兼容性)。

另一方面,不想要 UB 去实现替代手法本身的工作量实在很低——最简单地,加个 if 判断,返回错误码。但 C 这样的抽象捉急的设计中,if 加在哪也是有讲究的。加上早期连内联都不健全的年代(这确实是历史包袱,但是也不全是——现在也不可能保持 ABI 解决跨翻译单元内联兼容问题),标准化封装个带有 if 的“安全”操作实在没现实吸引力。

让 C++ 异常包装错误条件看上去更正常(因为除零异常逻辑上其实是可以同步的,所以可行),但是 C++ 异常机制的一些过度设计(比如过于具体的异常对象类型和过强的 unwind 保证之类)和现实(非历史)的包袱(比如异常运行时 ABI 兼容性)会让八成用户明确反对这里使用异常的必要性。甚至还有不少 C++ 用户至今以此反对整个 C++ 异常机制,那么自然更加无可救药。

这些背景下,保持内建操作整数除零 UB 自然是共识。(说实话,我很奇怪 C 和 C++ 用户会有这方面认知偏差。)

(至于浮点数除零 inf 那是成熟的硬件方案里早就支持了,不用白不用。但是浮点数这玩意儿细节远远更头疼,不过这完全是另外的问题了。)

原则上,更重要的问题在于 UB 是被期望的。与之相对,滥用检查避免包括 UB 之类的“错误”的所谓防御性编程不受待见,因为它本质上不信任任何接口的用户,以至于用户即便完全清晰无误地理解了需求,也无法干预次等的实现——除非把实现拆了自己改了。但是既然我都得自己实现了,那么要你何用?

WG21 的讨论中,这种预期的无错误检查的接口和存在“防御性”检查的接口被分别称为 narrow contracts 和 wide contracts 。一般地,只提供 wide contracts 是有害的,因为检查的复杂性基本可以忽略,总是存在一个更自由的 narrow contract 的版本的接口,而同时满足 narrow 和 wide 的需求——封装底层的 narrow contract 的接口并添加检查就得到了后者。而反过来只要不是知道实现细节,一般是做不到的。

一个根本原因是,C++ 这样的语言不足以精确建模像度量性能这样的性质(顶天能在文档里约定一个复杂度要求,连检查都做不到),而相同接口的劣等实现的却又实实在在地影响程序的行为(甚至可能作为攻击向量),这就是一种抽象泄漏。

这种不对称性揭示了一些更深刻的普遍规律:如果想要隔离接口及其实现,又不想有实现开销的惩罚,那么 widening contracts considered harmful 。同理,一般意义上依赖这种问题操作的所谓防御性编程是会自动破产的:任何无条件地设定前置条件而拒绝让它的直接用户配置是否启用,是一种无能的反模式。

更正常一点的做法是,把接口的检查集中到(不)信任边界上。例如,不能信任最终用户的输入,就集中检查最终用户直接决定的输入,但是不要再嵌套调用中重复检查——这不仅多少在持续损害最终用户的体验,还可能会掩盖遗漏检查的根因,造成更大的问题。再者,还有不少严重的灾难性错误(比如硬件失败)是根本无法检查的,对系统状态一致性的维护天然地就依赖其它的手段。

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

@yangbowen
Copy link

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

这个。我倒想说,

当你思考如何正确纠正历史包袱的时候,比你不用心的人又用充满无可饶恕的低级bug的产品抢占了第十个市场。

@FrankHB
Copy link
Author

FrankHB commented Jul 26, 2023

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

这个。我倒想说,

当你思考如何正确纠正历史包袱的时候,比你不用心的人又用充满无可饶恕的低级bug的产品抢占了第十个市场。

其实不见得需要很有所谓。为什么我非要关心我八竿子打不着的所谓“市场”呢?只要我没靠这个所谓的市场吃饭,我就有底气保持无所谓;至于是不是实际无所谓就看心情了。

正常来讲,只要不是有别的更离谱的缺陷,不犯同等低级错误的一方就能代表有生命力的先进事物。很多东西错的就是错的,只要认知缺陷不改进,就不会有实质性变化。但若不是有生态资源冲突,为什么需要在乎这个呢?让它们自生自灭便罢。

尔曹身与名俱灭,不废江河万古流。

根本上,观点的生命力跟用户多少和市场容量占比根本没有关系。否则,Worse Is Better 早就赢了,连争议都不会有。正是因为不确定的任何涉众都有发现客观缺陷并克服之以达成进步的刚需,即便是占据市场绝对少数,也会不断前赴后继。这个过程中不断会有人发现前人的错误而反复提出相同的问题。这种现象的不断发生,是不以个别参与者意志为转移的客观规律。

我也就是意识到这部分重复劳动过多之后,刻意采取一些措施降低这部分的成本罢了。而真正能够体现独创性的新的东西平时我都大不屑于放到一起讲。

所以,如果所谓的市场没有什么喧宾夺主的显著性的问题,各玩各的相安无事也无关紧要;但如果上门来往我这拉屎撒尿,指鼠为鸭地给我增加无谓成本,那么就得打回去了。

道路是曲折的,斗争是艰难的;未来是有希望的,前景是能乐观的;所以,对不会进化的猴子投降是不可能的。

@FrankHB
Copy link
Author

FrankHB commented Jul 26, 2023

考虑这个操作的开销的时候值得考虑两种情况。一种是除数并不为 0 的情况的开销,一种是除数确实为 0 的情况的开销。 考虑产生硬件中断的机器(主要说整数除法指令),然后就是识别这种除数为 0 的情况大致有两种策略。一种是给前面加一个分支判断,这样两种情况下都会增加一个分支的开销(如果除数为 0 的情况 cold-path ,那么这个分支会被准确预测,开销不会太大);另一种就是捕获机器产生的硬件中断,这样(应该是 hot-path 的)除数不为 0 的情况下就没有额外开销(除去捕获并抛出的代码带来的一点空间开销),而除数为 0 的情况下就产生抛出异常的很大的开销。但添加分支判断属于用户也很容易自行完成的事( widen 一个 narrow contract ),而“未发生零除的情况下无额外开销,同时检测偶尔的零除”则是只有实现成抛出异常才能办到的。所以从开销的角度讲,我认为把整数除法除数为 0 改成抛出异常是最合理的。

ISA 的异常不是 C++ 异常,涉及到非常多的没有在 C++ 中定义的机器实现细节,标准化的工作量很大。现在明显还有大把别的工作要完成。所以这条技术路线几无可能落地。

实际的结果就是没人去提案。

不管是设计是否必要承担这些复杂度,还是实际无法负担标准化开销的问题,都可以概括为“得不偿失”。

不止是运行时开销,还有维护spec上消除硬件实现差异的问题。虽然我说过“最简单的”做法,这仍然工程上极端复杂,跟往 C++ 里加 Windows SEH 没太大(不)可行性上的差别。这种异常要整清楚就得把常见 CPU 的异步异常机制在 C/C++ 的层次上整明白,干掉所有不一致的部分,光这坨就比整个 C++ 异常都复杂了。

不论 CPU 对异常采取何种具体的机制,它总归

  1. 达成条件的情况下发生,发生之后通过某种机制能够让程序知道并处理
  2. 发生的情况下,不能继续执行之后的指令,要先处理(异常处理的时候,异常的指令后续的对该指令依赖的指令不能影响架构状态)
    只要这两点成立,我想这东西被封装到 C++ 异常这个抽象当中,应是不难的。

这种程度的模型太略了,描述清楚行为都不大够,没有多少实用性。即便是 C++ 已有的异常特性,都比这更复杂。看看各种 ABI 中的约定,会让你对这里的设计为什么不够用的理由大开眼界。

更直接地,如果坚持这样做,你首先就得提供在 C++ 中处理异步异常的整套机制。因为你没法排除包括除零在内的这些异常实际上在 ISA 层次上就已经是异步的(无条件假装同步异常会有和预期不一样的行为),更没法去说服硬件厂商回收已经生产的支持异步异常的硬件。

最直接的工作量是描述清楚兼容异步异常处理流程的架构状态。C++ 现有的设计中,暴露异常处理状态(当前异常),反而造成无法优化,有过度设计的问题。约定架构状态在这里更加麻烦。在语言层面上,没有类似 continuation 的抽象,原则上想简单透明支持异步操作基本是不可能的,会有各种各样的 corner case(有 C++ coroutines 的经验,这已经很显然了)。

而一旦按 ISA 字面意义公开能够直接映射状态,还会有人会想该不该要提供一套 ucontext 了。从可移植性的角度来讲,连 ucontext 都很难站得住脚,外加还容易用错,所以 POSIX 都给 deprecated 了,即便在异常以外这实际上也算是接近刚需的特性(模拟实现会慢到离谱)。

我同意这里保留一个“除数不得为 0 的整数除法”的 narrow contract 从性能和其它方面来讲是合理的。问题是截至目前标准 C++ 里仍然没有自动检查 contract 的基础设施。 在没有这样的基础设施的情况下,我认为把“除数传了 0 ”这个还算常见的错误 foolproof 成至少抛个异常而不是 UB 是更好的。 虽然以“程序员皆无能 问题不可能良好解决”为前提设计接口和编程规范是坏的,但也不该以用户和/或实现者自己绝对无错为前提。我认为适量使用 assert 这类设施来检测并立即报错某些易错的地方是好的。 其实话说这些年来的各种 ABI 攻击缓解特性也是这个道理啊。若不是满大街随手写出缓冲区漏洞的 C 程序员,哪有那些缓解特性的用武之地。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

再者,还有不少严重的灾难性错误(比如硬件失败)是根本无法检查的,对系统状态一致性的维护天然地就依赖其它的手段。

这个。我的理解是,一个系统不可能从内部检查并纠正任意的错误(因为无法分开“检查错误的”和“可能出错的”),真有那么重要的情况下必须从它以外检查它的错误。

其实可以有专用于错误检查的相对隔离的子系统,从不同角度上看能算是内部的,也能算是外部的。如 IA32 的 machine check 。不过,只要有隔离性,使用起来多少是上下文受限的,更适合当做备份系统容灾而非日用。内部原生支持的错误处理才是日常应该依赖的。

这里我倒认为,倒不妨说翻译单元(对象文件 .o )的存在,或者至少它的用法上的惯例,就是历史包袱。 编译-链接的范式意味着,程序的不同部分(主要以功能逻辑划分)之间的组合方式,受限于 ABI 的非常单一且固定的组合方式。函数内联只是这种组合方式局限性的一小部分。 语言及编译器,而不是有着简单的 ABI 规则的链接器,懂得如何有效地对程序的不同部分进行组合。

翻译单元确实是有历史包袱的成分,不过按现在 C++ 的定义(源代码中的声明序列)其实原则上不存在这里的问题,因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路;翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节(一部分理由是跨工具链的互操作,不过这和强调 ABI 的必要性多少有点同义反复了)。

剩下真正地问题,概括起来就是粒度不够细。从设计简单性和性价比讲,我是历来想要干掉(包括声明在内得)“语句”这种冗余设计,并无视 top-level 构造的(因为光是高级语言的语义上问题就一箩筐),所以默认翻译单元就是表达式,能确保允许用户怎么 AOT/JIT 颠三倒四翻译过来过去(以及是不是有持久格式)都无所谓。但这就是另一回事了。

在一个理想世界里,语言、编译器、加载器、操作系统被放在一起设计,就算动态链接也能像 同一个源文件里内联并优化 那样高效,而不是让这些接口只能用这种极不 flexible 也不 feature-rich 的方式描述及实现…… 而在实际当中,不同层级的组件互相抽象对方的当前情况来进行设计,把整个系统的抽象结构搞得非常糟糕……

一起设计是不可能的,总有不同的工具链厂商,系统厂商更不可能是同一个(特别是考虑硬件适配工作)。真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。CIL 这种还是太 LIR 了。Wasm 的 AST 原本可以是候选,被他们自己放弃了。所以现在还是万古如长夜的状况。大有可为、大有可为……

@yangbowen
Copy link

yangbowen commented Jul 26, 2023

真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路

我认为这些问题实际上是密切相关的。
现有大部分 组合程序 方案(包括.o上的链接或者动态链接——把 native ISA 看作一种 IR 的话)所使用的 IR 都是基于“语句”的。这些 IR 围绕着描述“做哪些运算步骤”而不是“运算值之间满足哪些不变量约束”。后者恰恰是优化、查错等需要的信息,而 contract 也恰恰是描述“约束”的语法。
因此我认为你说的这个 HIR 规范必须具备的一个特性是“高效地描述约束,而不是运算步骤”。然后语言层面的 contract 、优化器需要的“证明程序满足何种性质”都能统一到这个上面。


翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节

Clang-LLVM 的 LTO 选项会自动编译到 LLVM bitcode .bc 而不是 .o ,算是一个值得一提的例子吧。

@FrankHB
Copy link
Author

FrankHB commented Aug 15, 2023

真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路

我认为这些问题实际上是密切相关的。 现有大部分 组合程序 方案(包括.o上的链接或者动态链接——把 native ISA 看作一种 IR 的话)所使用的 IR 都是基于“语句”的。这些 IR 围绕着描述“做哪些运算步骤”而不是“运算值之间满足哪些不变量约束”。后者恰恰是优化、查错等需要的信息,而 contract 也恰恰是描述“约束”的语法。 因此我认为你说的这个 HIR 规范必须具备的一个特性是“高效地描述约束,而不是运算步骤”。然后语言层面的 contract 、优化器需要的“证明程序满足何种性质”都能统一到这个上面。

是相关的,但不同的人关注的方面不同。

硬件厂商自动会关心“语句”,因为那是硬件自身 IR(比如 microcode 和某些 RTL 支持的局部编码构造)基于实现可行性和复杂性的自然选择。对软件厂商,他们中一部分会关心的是 toolchain 这样 infrustructure 支持的可实现性,大多数非理论计算机科学出身的会往硬件方向(后端编码)如何省事这方向去靠。而目标用户和理论研究者的则不一定有这样的思想包袱,因为理论上语句表达的顺序构造不是什么基本的东西,即便广泛存在也只是实现上的简化,并且不需要是一种形态(比如 ISA 的程序计数器,语言运行时的顶层 trampoline ,应用软件框架中的顶层事件循环,其实都是一种东西——“默认”控制)。

尽管不同层次的 IR 都可以描述“做哪些运算步骤”,每引入一层明确不便被下游应用复用的要求都会导致似是而非的冗余,所以在抽象能力上,能描述清楚所有功能需求(不论效率)的 HIR 根本上是必须的。但为了包含最大的涉众(而不至于发现不够用整个推翻重来),这种构造又不能是传统意义上的静态的(对硬件和软件开发者,什么叫“静态”都不能有统一的定义——manufacture-time/translation time/runtime 不可能全部预设)。所以即便这种抽象可以是 contract ,也比现在 C++ 意义上的 contract 外延大得多,至少要能描述用户定义的 phase ,这不可能是一个具体 IR 设计中就能简单原生支持的特性。即便排除这个显式定义 phase 的需求,Racket 的 contract 也更接近这里的目的,注意这里的 idiomatic usage 大到已经能代替整个(静态)类型检查了。所以能满足这里的需求的 contract system 是个历史上前所未有地大的设计,至少能涵盖 gradual typing 对应的 typecheck 。从工程可实现性上,我不觉得这是能直接做到一套 spec 里就可能完工的东西。

翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节

Clang-LLVM 的 LTO 选项会自动编译到 LLVM bitcode .bc 而不是 .o ,算是一个值得一提的例子吧。

这是正常的思路,不过需要指出 .o 在 image format 层次上就跟 .bc 并列:a.out、PE/COFF+、ELF、Mach-O ……除了可移植性的现实差别,都是和 LLVM bitcode 能并列的。(考虑到 LLVM 自身就是个 target ,技术上加上 target ISA 这个维度在另一方面也可以是并列的。)由于 LTO 的 opt-in 特色,引入不同 image format 的时机又有更大的变数。于是无论哪方面看,这些都不是基本的东西了,需要在需求上做从用户侧更明确地的拆分才好理清其中具体特性的必要性(所以对应用来说自然就有很多是实现细节)——这也要求引入外部 HIR 才能提供的 high-level 抽象描述能力。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment