px普贤教程PuXian
普贤教程 › AI 专用

踩坑记录

这里记录的全是真事:每一条都在生产或生产等价的验证里发生过,有具体文件、具体报错、具体时刻。写给 AI 的用意很直接 —— 你推测出的行为,八成已经被这些案例否定过一次。

怎么读先扫下面的索引表找你的现象;对上了再往下读细节。每条都是「现象 → 根因 → 修法」三段,根因那一行往往就是你要改的那个认知。

索引:24 条故障

#现象(一句话)类别
01条件永假、分支从不执行、无任何报错语义
02三个文件同时编译失败,报错指向莫名的一行语法
03本该返回 400 的接口变成 500错误处理
04HTTP 响应体开头多出一行日志,客户端解析失败Web
05逐像素画图要几秒性能
06同一段循环,别人跑 25 ms,你跑 6 秒性能
07写 17 MB 二进制时进程直接挂死IO
08改了 bytes 却全是 0,签名对不上IO
09字节查找「没找到」被当成「找到 0 号位」IO
10日志时间比实际早 8 小时时间
11定义一个同名函数后程序段错误语义
12列表拼接出来多了一个分隔符语义
13大整数运算结果变成 0语义
14读不存在的键,进程报 R1008 终止语义
15--fast 编译失败不回落,解释轨却照跑工具链
16import 的文件里定义的常量全是 null语义
17每个请求首次连库慢 0.4 秒数据库
18Web 接口偶发 500,日志只有一行「未定义变量」部署
19响应头只回来 Content-Type,其它全丢Web
20systemd 托管的服务,日志里只有 Started部署
21数组下标算出来是 0.99 导致取错元素语义
22actor 的状态更新处报语法错并发
23读上传的 zip,内容莫名被截断IO
24SQL 转义函数一调用就报参数错误数据库

语义篇

01 · 条件永假,分支从不执行

现象:Mahesvara 的 md 站点把所有 public/ 下的静态资源返回 404,/css/style.css 请求失败,页面无样式;没有任何错误日志,服务一切「正常」。排查耗时约 40 分钟,最终靠逐条打印条件才定位。

根因:判断路径是否含 .. 的那行写成了 contains(p, "..") == 0。contains() 返回 bool,而 PuXian 里 false == 0 是 false(严格类型比较,不做隐式转换)⇒ 这个复合条件恒为假,整条分支成死代码。

修法:if spf != null and (not contains(p, "..")) and spf["is_dir"] == false:

教训「条件恒假」是 PuXian 里最难查的一类 bug:不报错、不告警、行为只是「少做了一件事」。凡是 == 0 / == 1 出现在条件里,先确认被比较的函数返回 bool 还是 int。

02 · 一个保留字让三个文件同时编译失败

现象:send-code.px / phone-send-code.px / reset.px 同时报 E2001 语法错误:期望变量名,实际得到 from,报错位置在它们 import 的公共库第 478 行。

根因:公共库里写了 var from = ...(发信人地址)。from 是保留字。

修法:改名 sender。in 同样是保留字,不能当参数名。

11 · 自定义函数覆盖内建 → 段错误

现象:定义了 def env(name): 之后,程序在某些路径上直接段错误退出,无堆栈、无错误码。

根因:env 是内建。自定义同名函数遮蔽它之后,内部实现调用到自身形成无限递归,运行时不报错而是直接崩。

修法:不要用内建名做函数名/变量名。写之前花 3 秒确认:这个名词不在内置清单里。

12 · join 的参数顺序反了

现象:拼接列表得到 a,b,c, 或者直接报「join 分隔符需要 string」。

根因:PuXian 是 join(分隔符, 列表);Python 是 分隔符.join(列表)。参数顺序与直觉相反。

修法:join(",", parts)。

13 · 大整数静默回绕

现象:2 ** 64 输出 0;哈希/位运算拼出来的数偶尔对不上。

根因:整数是 64 位定长,溢出回绕不报错。

修法:超过 2^53 的中间结果改用字符串或分块处理;需要定长语义时显式取模。

14 · 缺键是运行时错误,不是 null

现象:d["missing"] 直接抛 运行时错误 [i_index]: R1008 键不存在,当前协程终止。

根因:[] 取值不做缺键兜底(旧文档里「返回 null」的说法已不成立)。

修法:if d.has(k): 或 if contains(d, k): 之后再取(没有 keys() 这个内建;要键列表用 d.keys())。写遍历外部数据的代码时,这条必须当成默认动作。

16 · import 文件的顶层赋值不执行

现象:在 cfg.px 顶层写了 let MAX = 100,主文件里用 MAX 得 null。

根因:被 import 的文件只贡献函数定义,顶层赋值语句不执行。

修法:常量声明放在主文件的全局区,或改成函数 def max_size(): return 100。

21 · 除法返回浮点,下标取整出错

现象:list[i / 2] 报类型错误,或者 to_lower(int(a / b)) 这类嵌套里出现奇怪行为。

根因:/ 返回 float;>>、&、int_to_bytes、下标都要求 int。

修法:一律 int(a / b);位运算前先确认两边都是 int。

错误处理篇

03 · 本该 400 的请求变成 500

现象:客户端传坏 JSON,接口返回 500,服务端日志只有一行 运行时错误 [i_call_builtin]: json: 无法解析。

根因:json_parse() 抛不可捕获错误,0.2.382 仍没有 try/catch,整个脚本死掉。

修法:json_parse_opt(x) + .is_err() / .ok()。同一模式适用于所有 *_opt 内建。

IO 篇

07 · write_all 超 16 MiB 挂死

现象:导出 42 MB 文件时进程卡死,CPU 不高、无输出、不退出。

根因:write_all 每次切「剩余全量」的大切片,超过 16 MiB 直接挂住。

修法:open("/dev/stdout","w") 后按 1 MB 分块 write() 循环。

08 · bytes 不可变,改了没生效

现象:拼 zip 的签名头,写完读出全是 0。

根因:bytes_set(b, i, v) 返回新对象,不改 b。忽略返回值等于什么都没做。

修法:b = bytes_set(b, i, v)。

09 · bytes_find 未命中返回 null

现象:判断「是否包含子串」时,未命中的分支走进了「找到」逻辑。

根因:未命中返回 null,不是 -1;且只接受 2 个参数(没有起点偏移)。

修法:var i = bytes_find(b, sub); if i != null:。

23 · read_file 遇 NUL 截断

现象:读上传的 zip / 图片,长度只有预期的一小段。

根因:read_file() 是文本语义,遇到 NUL 字节截断。

修法:二进制走 read_file_opt("/dev/stdin").ok() 或分块 read;写回用 write_bytes。

时间篇

10 · 日志时间早 8 小时

现象:time_format() 格式化出来的时间比本地时间早 8 小时;更隐蔽的一种:数据库队列的 next_at 永远在未来,队列一条也取不出来,且不报错。

根因:time_format() 输出 UTC;MySQL 连接若带 time_zone='+00:00',连接内 NOW() 是 UTC,而表默认值走服务器时区(CST),两者混用就差 8 小时。

修法:写库的时间统一显式 UTC_TIMESTAMP(),不做表默认值;展示层再做时区转换。

工具链与部署篇

15 · --fast 编译失败不回落,解释轨却照跑

现象(0.2.373–0.2.378):同一个脚本 px run 正常,px run --fast 报 R1001 未定义变量。

根因:那几版的编译缓存路径不解析 import(本地库与 stdlib 同样受影响)。0.2.379 已修,两条轨现在逐字节一致。

现在的新形态:--fast 解析 import 了,但严格。写错的 import(例如把 import std.strings 写成 import "strings.px")会直接 E3005 找不到模块 并不回落解释轨;同一脚本用 px run 只打一行警告、继续跑(因为该错 import 对应的函数恰好是内建)。

修法:把 --fast 当严格检查器用——它报错就是真有 import 写错,别去翻自己的变量名。要强制看解释轨行为:px run --interp x.px。

18 · Web 接口偶发 500,日志只有一行

现象:Ma 托管的 px 接口大多 38–76 ms,偶发 1.1 秒并伴零散 500;日志一行「找不到模块」。

根因:Ma 编译时只把「脚本所在目录」打包,import "../other/lib.px" 跨目录、软链跨目录都会缺;旧版 Ma 对缺失 import 只警告不失败,把坏产物缓存成「编译成功」,于是每次请求回落到解释轨。

修法:每个 pxapp 目录自包含(公共库放实体副本);Ma 0.22.4 起编译前预检跨目录 import、失败不落缓存。

19 · 响应头只剩 Content-Type

现象:px 脚本返回的自定义响应头除 Content-Type 外全部消失。

根因:Ma 的 px 响应头协议要求每行以 \n 结尾;用 \r\n 时解析器丢弃除首个头之外的项。

修法:拼响应头一律用 \n。

04 · print 污染 HTTP 响应

现象:接口返回体第一行变成 [send-code] 已发送…,客户端 JSON 解析失败。

根因:在 px_routes 脚本里 print() 写的就是 stdout,而 Ma 把 stdout 原样当 HTTP 响应(Status: 行起头)。

修法:日志写 stderr —— var f = open("/dev/stderr","w"); write(f, msg + "\n"); close(f)(没有 eprint 内建)。

20 · systemd 下日志失踪

现象:手动跑常驻程序日志正常,systemd 托管的只有一行 Started。

根因:封装日志函数用了 open("/dev/stderr","w"),而 systemd 下 fd 是 journal socket,打不开就静默 return。

修法:常驻进程用 print()(stdout 进 journal);Web 侧仍必须写 stderr(stdout 是响应体)。两者规则相反,别混。

数据库篇

17 · 首次连库 0.4 秒

现象:px 版接口每个请求首次连 MySQL 慢 400–450 ms(其中 tcp_connect 0 ms、SELECT 1 25 ms)。

根因:mysql 包(registry,纯 .px 协议驱动)在解释轨下纯解释开销就有 0.4 秒。

修法:走编译轨(px build / Ma mode: fast);或用连接池常驻进程。

24 · mysql_escape 只收一个参数

现象:mysql_escape(conn, s) 报参数数量错误。

根因:签名是 mysql_escape(s),不接收连接。

修法:防注入不要靠转义函数,用 mysql_query_params(conn, sql, [..]) 预处理绑定。

性能与并发篇

05 · 逐像素画图,解释轨要几秒

现象:132×44 的画布写 6400 次 gfx.set_px,解释轨实测 3.85 秒,编译轨 4 毫秒。

根因:gfx.set_px 内部写的是 c[2][y*w+x] = color——嵌套下标赋值每次都先把内层列表复制一份再写回,6400 次写 = 6400 次整画布复制。line/text/fill_rect 都是它的包装。(这与「列表值语义」无关:PuXian 的列表是引用语义,慢在嵌套赋值这条路径上。)

修法:图形优先拼 SVG 字符串(解释轨 91 ms、3 KB),前端 <img src="data:image/svg+xml;base64,…"> 直接吃。

06 · 同一段循环差约 500 倍

现象:10 万次累加,px run 5182–5928 ms,编译产物 10–12 ms(C 轨 4–5 ms)。

根因:解释轨逐条解释字节码;编译轨先生成代码再链接执行——0.2.382 默认是 VM 字节码轨(px build),--c 才走 C 轨。

修法:循环里干活的代码不进解释轨。Web 场景给路由显式 mode: fast。

22 · actor 状态更新报语法错

现象:在 actor 的消息处理里写 state.count = state.count + 1 报语法错。

根因:处理函数需要多行块并在末尾保留逗号;单表达式赋值会被当表达式解析。

修法:写成块形式:def handle(state, msg): 后接多行缩进语句,末尾逗号不要省。

这些坑的共同形状

形状你会怎么误判正确做法
静默失败(不报错、只是少做事)「这段肯定没问题,去看别处」对关键分支逐条件打印,别靠读代码推断
类型严格导致比较失效「0 就是 false」先确认函数返回 bool 还是 int
解释轨 vs 编译轨行为不同「我代码写错了」两轨都跑一遍,差异就是线索
Web 层的隐式协议「响应头没写对」查框架文档的换行/输出约定
缓存把坏结果固化了「改了没生效」清缓存目录,确认产物时间戳
给 AI 的最后一句上面 24 条里,超过一半的共同前提是「我读懂了代码逻辑,所以它应该这样跑」。PuXian 的行为差异密集,读代码得出的结论必须用一次实跑确认。这也是下一章「验证闭环」存在的理由。