From 6dc8716c70eee49ada83ad68bd034eef6be611e5 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 15 Oct 2026 01:16:19 +0800 Subject: [PATCH 1/5] fix: align call arity and native arg-presence semantics with Lua 5.4 Four Lua-semantics bugs found by differential testing against Lua 5.4.9 across all three backends (TCC / GCC / interp): 1. Native functions used as values (local t = type, pcall(table.concat, t)) failed at runtime with a misleading message. semantic_analysis now reports it at compile time; math.pi / string.charpattern stay valid. 2. Optional arguments passed an explicit nil were rejected, though 5.4's luaL_optinteger/optlstring treat nil as absent. Fixed per function in table.concat/unpack, string.byte/sub/rep/find/match and utf8.codepoint/len/offset, keeping checkinteger params strict. 3. Argument count: Lua allows passing fewer or more arguments than the declared parameters (missing ones are nil, extras are discarded). Three gates rejected it: the compile-time check in CompileFunctioncall, the math-specialized direct call path, and the runtime check in CallByNameImpl. All three now pad with nil / discard extras, matching FlCallClosure which already behaved this way. The specialized direct call falls back to the normal path when the count differs - extra arguments previously leaked a raw TCC error into the user. 4. The interp backend treated every file-level local binding as a file-level function, so `local gv = f; gv(1)` failed with "function 'gv' not found". Only LocalFunction definitions may emit CALLNAME. Removing the runtime gate exposed that natives conflated "missing argument" with "explicit nil": 25 required-argument guards now raise bad argument (type/tostring/tonumber and the string/table/math/utf8 families). Contract change: FakeluaCallByName no longer throws on argument-count mismatch; CallByNameImpl fills every dispatched slot before the call, which is what the old Bug M guard was protecting against. Tests: new test/lua/basic/test_basic_call_arity.lua (expected 92, three backends) and the table/string/utf8 *_optnil suites; updated the three exception tests that asserted "too few arguments is a compile error" and the two runtime tests that asserted FakeluaCallByName throws. Full suite: 1584/1589 pass; the 5 failures are the known baseline (4 redis integration tests need a real server, test_websocket_echo_lua is flaky under load and passes standalone). --- ITERATION_PLAN.md | 357 +++++++++++++++++++++++ src/compile/c_gen.cpp | 46 ++- src/compile/semantic_analysis.cpp | 48 ++- src/interp/codegen.cpp | 7 +- src/jit/vm.cpp | 5 +- src/native/basic/native_basic.cpp | 6 + src/native/math/native_math.cpp | 4 +- src/native/string/native_string.cpp | 85 ++++-- src/native/table/native_table.cpp | 37 ++- src/native/utf8/native_utf8.cpp | 39 +-- test/lua/basic/test_basic_call_arity.lua | 78 +++++ test/lua/string/test_string_optnil.lua | 60 ++++ test/lua/table/test_table_optnil.lua | 31 ++ test/lua/utf8/test_utf8_optnil.lua | 45 +++ test/test_basic.cpp | 17 ++ test/test_exception.cpp | 58 ++-- test/test_runtime.cpp | 28 +- test/test_string.cpp | 15 + test/test_syntax_tree.cpp | 67 +++++ test/test_table.cpp | 18 ++ test/test_utf8.cpp | 15 + 21 files changed, 960 insertions(+), 106 deletions(-) create mode 100644 ITERATION_PLAN.md create mode 100644 test/lua/basic/test_basic_call_arity.lua create mode 100644 test/lua/string/test_string_optnil.lua create mode 100644 test/lua/table/test_table_optnil.lua create mode 100644 test/lua/utf8/test_utf8_optnil.lua diff --git a/ITERATION_PLAN.md b/ITERATION_PLAN.md new file mode 100644 index 0000000..5d7b1de --- /dev/null +++ b/ITERATION_PLAN.md @@ -0,0 +1,357 @@ +# fakelua 下一轮迭代清单 + +> 基于 master @ `a55a4bf`(已与 origin/master 同步)的代码审计与实测。 +> 每项都标注了代码位置、验证方式和建议执行顺序。 + +--- + +## P0″ · 本轮 bug 排查修复(2026-10-14,未提交) + +以真实 Lua 5.4.9 作 oracle 做三后端差分测试,修复 4 组语义 bug。 +差分脚本与用例在 `/tmp/flaudiff/`(`cmp_local.py` + `cases*.lua`), +oracle 是本地编译的 `/tmp/lua-5.4.9/src/lua`。 + +### Bug 1 · 原生函数当值使用(编译期报错缺失) + +`local t = type; t(123)`、`pcall(table.concat, t)`、`{cb = string.format}` 在 +运行时才报误导性错误("attempt to call a non-function value")。 +原生函数由编译期按名派发,不是一等值。 + +**修复**:`semantic_analysis.cpp` 的 `check_simple_var` 与 `case Var:` 的 kDot 链 +检查里,命中 `FindNativeFunction` 时直接给编译期明确报错;`math.pi`、 +`string.charpattern` 等常量不受影响。回归测试见 `test_syntax_tree.cpp` 的 +`semantic_native_function_as_value_*`。 + +### Bug 2 · 可选参数显式 nil 被拒 + +`table.concat(t, "-", nil, nil)` 报 "bad argument #3 (number expected)", +而 5.4 的 `tconcat` 用 `luaL_optinteger`,显式 nil 与省略等价。 +逐函数对照 5.4.9 源码修正 10 处(`checkinteger` 的保持报错): +`table.concat/unpack`、`string.byte/sub-j/rep-sep/find-init/match-init`、 +`utf8.codepoint/len/offset-i`。回归测试:`test_*_optnil.lua` 三份。 + +### Bug 3 · 函数调用实参个数语义(三道 arity 门,全部与 Lua 不符) + +Lua 允许实参个数与形参不一致:少传的形参为 nil,多传的实参求值后丢弃。 +fakelua 有三道门分别拒绝,动态调用路径(`FlCallClosure`)却已是 Lua 语义 —— 同一个 +调用经变量转发能过、直接调用报错,语言内部不自洽。 + +| 门 | 位置 | 现象 | 修复 | +|---|---|---|---| +| 编译期直接调用检查 | `c_gen.cpp:CompileFunctioncall` | `f(1)` 报 "wrong number of arguments" | 删除检查 | +| 数学特化直调 | `c_gen.cpp:TryCompileSpecDirectCall` | `add(1,2,3)` 泄漏 TCC 原始错误 `:N: error: too many arguments to function` | 参数个数不符时回退普通路径 | +| 按名分发运行时检查 | `vm.cpp:CallByNameImpl` | `FakeluaCallByName` 报 "expects N argument(s)" | 删除检查(紧跟其后的循环本就补 nil/截断) | + +配合修改 `BuildLocalFunctionCall` 非 vararg 分支:少传补 `kNil`、多传截断, +展开位置超出形参个数时丢弃展开值;`FlUnboxMulti` 越界本来就是 nil。 + +**契约变化**:`FakeluaCallByName` 不再对参数个数不符抛异常(Bug M 担心的读未初始化 +栈内存,由派发前填满 `temp_arg_arr` 保证)。`test_runtime.cpp` 的 +`vm_call_by_name_error_cases` 与 `vm_cvar_call.multi_return_expansion` 已相应改为 +断言补齐/截断语义(含"补的是 nil"的断言)。 + +三个原断言"少传参数=编译错误"的异常测试(`spec_call_arg_count_error`、 +`math_spec_too_few_args`、`no_arg_call`)已改为 Lua 语义:编译通过 + +`Call` 时抛 "attempt to perform arithmetic on non-numeric value"。 + +### Bug 4 · interp 后端 CALLNAME 误判文件级局部变量 + +`local gv = f; gv(1)` 在 interp 下报 `FakeluaCallByName: function 'gv' not found`。 +`codegen.cpp` 的 `file_level_func` 只看 `defining_func == nullptr`,把文件级 +local **变量** 也当成文件级 local **函数**,而 `named_protos_` 只登记函数定义。 +改为同时要求 `def_node->Type() == LocalFunction`。C 后端此处按闭包处理,本就正确。 + +### 连带修复 · 原生函数必填参数缺失静默通过 + +上面 Bug 3 移除运行时检查后,缺参调用真正走进原生函数了,暴露出既有写法 +`if (n < 1) return nil;` 把「缺失」与「显式 nil」混为一谈(Lua 下 `type()`、 +`string.byte()`、`table.concat()` 都报错)。已按 5.4.9 把 25 处必填参数的守卫 +改为 `ThrowBadArgument`:`type/tostring/tonumber`、`string.{len,rep,reverse, +lower,upper,byte,format,find,match,gmatch,gsub,dump,pack,packsize,unpack,load}`、 +`table.{insert,remove,concat,unpack,move,sort}`、`math.{max,min}`、 +`utf8.{codepoint,codes,len,offset}`。`table.create`/`loadfile`/`dofile` 的 +缺参默认值是 fakelua 自有语义,保持不动。 + +### 验证 + +- 新增 `test/lua/basic/test_basic_call_arity.lua` + `test_basic.call_arity` + (期望 92,三后端 × 直接调用/闭包变量/按名分发/多值展开/vararg/原生函数)。 +- 差分 `cases9_arity.lua` 35 例、`cases10_arity2.lua` 12 例、 + `cases11_missing_args.lua` 35 例:三后端 0 分歧;旧 `cases7` 41 例、 + `cases8` 23 例回归无变化(cases7 的 2 例仅报错文案前缀差异)。 + +### 已发现未修(下轮候选) + +1. **零值返回值在原生快路径被塌缩为 nil**:`tostring(select(2, 9))` 在 Lua 下 + 是把 0 个值传给 `tostring` → 报 "bad argument #1 to 'tostring' (value expected)"; + fakelua 的 `tostring`/`tonumber` 快路径用 `CompileExp(arg)` 不保留 multi, + 零值 multi 变成 nil → 返回 "nil"。修法是快路径保留 multi 并在 + `type_ == VAR_MULTI` 时回退 `FakeluaCallByName`(由它摊平)。 + `#{pick(...)}` 这类表构造路径已正确(0)。 +2. `function obj:m() ... end` 方法定义在 fakelua 不支持 + (`SemanticAnalysis: Unsupported function name with method definition`); + 文件级 `local t = {}` 是常量表,不能改字段。 + +--- + +## P0′ · 数学特化函数非数值参数去优化 ✅ 已完成(2026-10-11) + +### 问题(master `9fd9be6` 实测) + +文件级数学特化函数收到**非数值 math 参数**(string/bool/nil/table,或少传参 +补的 nil)时,TCC/GCC 的入口分发器直接 `FakeluaThrowError`。该异常逃逸生成 +代码后无人捕获 → `terminate due to uncaught exception`,**整个进程 abort**; +INTERP 后端却正常执行函数体。三后端不一致,且 Lua 层 `pcall` 无法拦截。 + +```lua +function f(x) + if type(x) ~= "number" then return -1 end -- 合法的 type 守卫 + return x + 1 +end +f("abc") -- interp: -1;TCC/GCC(master): 进程崩溃 +``` + +### 修复 + +- 每个特化函数额外生成一份 `__dyn` 全动态变体(`CompileFuncBody(..., -1)`, + 与非特化函数同一路径),入口分发器检测到任一 math 参数非 int/float 时 + 分派到它,不再抛错;函数体内真正的算术错误仍走可 pcall 的 Lua 错误路径。 +- 新增 `FakeluaSpecMissException`(`exception.h`):试探性原生数值编译失败 + (`CompileNumericExp` 等 7 处 `ThrowError` 改用 `ThrowSpecMiss`)不再刷 + ERROR 日志——这是「试不了原生就回退动态」的常规控制流;逃逸出 `Build` + 顶层时仍转换为与历史文本一致的硬编译错误。 +- `GenerateDecls` 补 `__dyn` 前向声明。 + +### 验证 + +- 新增 `test/lua/infer/test_spec_deopt.lua` + `infer.test_spec_deopt` + (三后端 × debug 开关;含 type 守卫、多参数、异类返回、pcall、递归、 + C++ 边界 catch)。 +- 差分 `.workbuddy/difftest/cases6_deopt.lua`:32 例 vs Lua 5.4.9,0 分歧; + 旧 5 集 223 例回归,21 分歧全为已知范围外项(字符串比较刻意不做、 + find/next nil 尾参 bug1 暂缓、%q/%c 版本差异)。 +- 形态断言污染(第二轮回归发现,共 14 个):`__dyn` 全动态体天然含 + IsTrue/CVar/flua_rbt_ 等,使「特化变体中不应出现 X」的负向断言失效。 + 修复方式:测试 helper `StripDynVariantBodies` 下沉进 `InferGetCCode`, + **默认剥离 `__dyn` 定义体**(前向声明与入口分发器保留),仅 + `test_spec_deopt` 以 `keep_dyn_variants=true` 取原始代码;既有 100+ 处 + 形态断言零改动。 + - 教训:全量回归若经 `tail` 管道会截断 gtest 失败汇总(曾看到 37 失败 + 却只显示 5 个),**必须重定向到文件再 grep**。 +- infer 套件 220/220 全过(TCC/GCC/interp × debug on/off); + 全量 1500+ 测试:除 mysql integration(需真实数据库,环境基线)外全过。 + +--- + +## P0 · 修复 macOS 上测试套件的可用性 ⚠️ 实现在 PR #25,**已被用户关闭,勿重做** + +> 状态澄清(2026-10-11 核实):以下方案曾完整实现于远端分支 +> `fix/jit-compile-cache-and-reproducible-codegen`(PR #25,`76caa37`), +> 但 PR 被**用户故意关闭**,代码从未合入 master(master 至今仍硬编码 +> `static void * _S = (void*)`,无 gcc_cache)。本地全量 GCC 测试 +> 每个用例冷 dlopen ~4.5–5.4s 是常态,全量约 40 分钟。**不要重新提交该 PR。** + + + +### 问题 + +在 macOS 上单个 gtest 用例耗时 **30.5 秒**(本应是毫秒级),全量测试实际无法在本地跑完。 + +### 根因(已隔离复现,非推测) + +`GccJitter::Compile` 的流程是 `fork()` → `execvp("gcc")` → `waitpid()` → `dlopen()`, +每次编译生成一个**全新路径的临时 dylib**。实测数据: + +| 场景 | 耗时 | +|---|---| +| 裸 `gcc -O3 -dynamiclib` | 0.06 s | +| 首次 `dlopen` 某新路径 dylib(4 个路径实测) | **4521 / 4504 / 5437 / 4608 ms** | +| 后续 `dlopen` 同一路径 | 0.2 ms | + +**每一个新 dylib 路径的首次加载都要付约 4.5–5.4 秒**,与文件内容无关, +纯粹是 macOS 代码签名验证(amfid / syspolicyd IPC)的固定开销。差值达 25000 倍。 + +排查中排除的两个假设(避免后来者重走): +- **ad-hoc 签名无效**:实测 `codesign -s -` 为 6312/5106/5222 ms,`-Wl,-adhoc_codesign` + 为 4852/4753/4672 ms,**都不比基线好**。这条路不通。 +- **fork+dlopen 组合本身有问题**:写隔离复现程序跑 15 轮全部秒过,假设不成立。 + +真正起作用的验证是「同路径 vs 新路径」的对照测量。 + +### 顺带发现的两个真实 bug + +1. **生成的 C 代码硬编码了 State 指针**(`c_gen.cpp:451`): + ```c + static void * _S = (void *) 0x1014b2aa0; // ← 地址受 ASLR 影响,每次不同 + ``` + 这导致同一输入编译出不同字节 —— 构建不可复现,且**产物一旦被跨 State 复用, + `_S` 会指向别的 State**。已改为初值 0 + 加载后注入(见下)。 + +2. **`FAKELUA_BUILD_TESTS` 默认为 OFF**,导致 `ctest` 找不到任何测试 + (`ctest --test-dir build` 输出 `No tests were found!!!`)。本地 build 目录里 + `unit_tests` 二进制可能停留在几天前的版本,造成"改了代码却看不到效果"的假象。 + **这条坑了我很久**:连续多轮"验证失败"其实都在跑 3 天前的旧二进制。 + +### 最终实现 + +- **State 指针改为运行时注入**:`c_gen.cpp` 生成 + `static void * _S = 0;` + `void __fakelua_set_state(void*)`, + GCC 与 TCC 两条路径在加载产物后、调用 `__fakelua_init` 之前注入。 + (注意 TCC 以 C89 模式编译,**不能用 `nullptr`**。) +- **按内容缓存编译产物**:`src/jit/gcc_cache.{h,cpp}` 以 + 「生成的 C 代码 + 全部编译参数」的 xxhash-64 摘要为键,命中即复用同一路径, + 从而命中 dyld 的路径缓存。写入用「临时文件 + rename」保证原子性。 +- **可关闭**:`StateGCCConfig::enable_compile_cache = false` 恢复旧行为。 + +实测效果(`closure` 套件,11 个测试): + +| | 耗时 | +|---|---| +| 修复前 | 487 s | +| 冷缓存首次 | 96 s(`_S` 修复带来的编译去重已生效) | +| 热缓存 | **13 s** | + +单个用例:35 s → **0.106 s**。 +`test_basic/test_math/test_string/test_table/syntax_tree/vm_cvar_call/runtime/state/var/util` +共 387 个测试热缓存下 106 s,全部通过。 + +### 遗留 + +- 缓存目录 `$TMPDIR/fakelua-jit-cache` **不会自动清理**,需要手动删除。 + 刻意不放在 `$TMPDIR/fakelua/` 下 —— 那个目录会被 + `test_runtime.cpp` 的 `runtime.generate_tmp_filename_creates_dir` 用 `remove_all` 清掉。 +- CI 三个 workflow 仍无超时保护,建议补 `--gtest_timeout` 或外部 timeout。 + +--- + +## P1 · VarTable 增加数组部分(性能收益最大) + +### 问题 + +`src/var/var_table.h:11-43` 的 `VarTable` 只有 `quick_data_[8]` + 哈希桶, +**没有 Lua 那样的连续数组部分**。因此整数键读写每次都要: +类型检查 → `spec_get` 间接调用判断 → `count_ == 0` 检查 → 算 hash → 判断 `bucket_count_` → **遍历桶链比对**(`c_runtime_header.h:944-976`)。 + +### 为什么这是主要瓶颈 + +实测 Sieve 生成的 C 代码里,**算术已经 100% 原生化** +(`FL_INT_MUL` / `FL_INT_ADD` / int64_t 循环变量),GCC 版 Sieve/5000 = 213μs +甚至比 C++ 参考实现快 2.68 倍。**唯一剩余瓶颈就是表访问。** +Lua 5.4 正是靠数组部分赢的。 + +「数值特化」这条路已经走到尽头,下一轮收益在表访问而非算术。 + +### 方案 + +给 `VarTable` 增加 `CVar* arr_; uint32_t arr_len_;`: + +- `FlSetTableIntImpl` 检测 `k == arr_len_ + 1` 时 append +- `FlGetTableInt` 命中 `1 <= k <= arr_len_` 时直接 `arr_[k-1]` +- 删除时保守退回哈希(只增不删最简单) + +### 代价与风险 + +`var_table.h:8` 明确注释「layout must match exactly」—— +`sizeof(VarTable)` 变化必须同步 `c_runtime_header.h` 里的镜像定义。 +**布局不同步会导致难以排查的内存问题。** + +### 预期收益 + +Sieve / BubbleSort / BinarySearch / Vector3 的**共同**瓶颈, +`for i=2,n do is_prime[i]=true end` 是教科书式顺序填充,能立刻吃满。预计 **2–4x**。 + +--- + +## P2 · spec table 字段按推断类型降为原生标量 + +### 问题 —— 「已经算出来却丢弃」的信息 + +`type_inferencer` 确实算出了 spec 字段的类型,但随后被显式丢弃: + +- `type_inferencer.cpp:1771` — `f.type = T_DYNAMIC; // 类型由 CGen 在 emit 时重新推导` +- `c_gen.cpp:411` — `EmitSpecTypeBoilerplate` 无条件写 `CVar` +- `c_gen.cpp:838` — `GetSpecFieldType()` **全仓库唯一定义、零调用**(死代码,坐实了类型被丢弃) + +### 实测后果 + +```c +// local p = {x=i, y=i*2, z=i*3}; total = total + p.x + p.y + p.z +CVar total = (CVar){.type_ = VAR_INT, .data_.i = 0}; // 本该是 int64_t +for (...) { + OpAdd((total), (FL_SPEC(spec_x, p, _s_x)), flua_op_20); // 每次重新 dispatch + ... +} +``` + +对比**同一文件**中未被 spec 化的 `s = s + i`:`s = FL_INT_ADD((s), (i));` —— 纯原生、无装箱。 +`OpAdd`(`c_runtime_header.h:1154-1158`)每次执行 2 次 `CheckNum` + 1 个分支 + 一次 16 字节 CVar 写回。 + +### 方案 + +1. `BuildCtorFields` 改为读 `main_eval_types` 的真实推断类型,替换 1771 行的硬编码 +2. `EmitSpecTypeBoilerplate` 按 `f.type` 发 `int64_t` / `double` / `CVar` +3. `FL_SPEC` 读取点据 `GetSpecFieldType` 决定返回原生表达式还是 CVar + +### 关键难点 + +**双写一致性**:`FL_SET_SPEC`(`c_runtime_header.h:686-691`)与 spec_set 回调 +(`c_gen.cpp:287`)必须做「原生字段 ↔ spec_vals[]」双写, +否则 `pairs` / 动态 key 读到的会是脏值。这是本项唯一的技术风险, +必须配合 optional 字段的 nil 语义仔细处理。 + +### 预期收益 + +table 密集的数值场景预计 **1.3–1.8x**。因为是「已算出来却丢弃」的信息,ROI 最高。 + +--- + +## P3 · interp 与 GCC 后端的行为一致性 + +`src/interp/` 与 `JIT_GCC` 的**语言特性覆盖基本等价**: +32 个 AST 节点、47 个 opcode、20 个二元 + 4 个一元运算符全部对齐; +标准库**完全共享** `src/native/`,无后端差异。 +真正的 interp 独有缺口只有 4 个,严重度都不高,但都会导致 +「只在 interp 下跑通的脚本在 GCC 下失败」这类问题: + +| # | 问题 | 位置 | 严重度 | 状态(2026-10-11 复测) | +|---|---|---|---|---| +| 1 | `local` 与全局常量名冲突未检查 —— GCC 编译报错,interp 静默通过 | 已移入共享阶段 `semantic_analysis.cpp:1047`(`CheckLocalVar`) | 中 | ✅ 已修复,三后端同文报错 | +| 2 | `GETTABLE` 读路径漏 NaN 检查 —— `t[0/0]` 读不报错但写报错,读写不对称 | 三后端现已收敛一致 | 中低 | ⚠️ 后端间已一致(都报 `table index is NaN`,pcall 可拦),但**读路径比 Lua 5.4 严格**:Lua 读 `t[NaN]` 合法返回 nil,仅写报错;若要严格对齐需放开 GETTABLE 的 NaN 读 | +| 3 | 寄存器宽度硬上限 256(CGen 无此限制) | `codegen.cpp:34-36`、`opcode.h:66-69` | 中低 | 未复测 | +| 4 | 深递归栈溢出错误类型不一致(init 内干净报错 / init 外变 `bad_alloc`) | `interpreter.cpp:75-80` | 低 | 未复测 | + +### 测试盲区 + +- **全部 46 个测试文件中 `JIT_INTERP` 出现次数为 0** —— 覆盖全靠 `AllJitTypes()` 间接循环 +- `test_state.cpp` / `test_runtime.cpp` 把后端写死(TCC 15 次 / GCC 7 次),**排除了 interp**, + 而 interp 恰是唯一依赖 `IsInterpClosure` 魔数判定的后端 +- `test/lua/` 下 **978 个 `.lua` 语料中 724 个从未被任何测试驱动** + (`test_jitter.cpp` 只引用 254 个),这些文件在任何后端下都没被执行过 + +### 建议 + +为缺口 1 和 2 补**定向断言**(明确断言 interp「应该」和 GCC「应该」的行为一致), +并把 `test_state.cpp` / `test_runtime.cpp` 改用 `AllJitTypes()`。 + +--- + +## 明确不要做的方向 + +| 方向 | 原因 | +|---|---| +| **优化 TCC 后端** | 实测 TCC 在 BubbleSort/Sieve/InsertionSort 上**快于 Lua 5.4 达 2.3–2.8x**,只是比 GCC 慢 4–7x。同一份 C 代码 GCC 快 3–30x —— 这是 TinyCC 优化器能力不足(无向量化/跨函数内联),不是 c_gen 的问题 | +| **给 `FL_INT_*` 加溢出检查** | `c_runtime_header.h:1144-1146` 用无符号算术**故意**实现回绕,这正是 Lua 5.4 整数语义。加检查会同时破坏性能和语义 | +| **给 string/bool 加类型维度** | 是刻意的设计边界而非遗漏。string 变长 + 引用计数 + 共享,原生化引入所有权问题;bool 无法用 C `bool` 表达(Lua 中 `0` 和 `""` 都是真值) | +| **为 `and`/`or` 生成 C 的 `\|\|` / `?:`** | 语义不等价 —— Lua 中 `0` 和 `""` 都是真值。必须走完整三值判定 | +| **字符串关系运算符(`<`/`>`/`<=`/`>=`)** | **刻意不做(用户 2026-10-06 拍板)**,非遗漏。fakelua 是静态类型子集,为性能不做任何运行时隐式转换,`"a" < "b"` 报错与 `"10" + 1` 报错是同一条边界。字符串 `==`/`~=` 仍支持(不涉及转换)。差分测试曾把它记为 bug 2,结论作废 | + +--- + +## 其他可清理项 + +- **README 文档过时**:`README.zh.md:114` 仍在展示 `NativeAdd(...)`, + 但全仓库 grep 无此符号 —— 已被 `FL_INT_ADD` 宏(`c_runtime_header.h:1144`)取代。 +- **残留分支**:`fix/open-bugs`(`e2e730b`)内容与 master 的 `a55a4bf` 相同,可删除。 +- **低风险优化**:`c_gen.cpp:4592/4602/4611` 生成的 `SET_NIL(flua_call_N)` 是死存储 + (`t[i]=v` 被预处理重写成函数调用,必然 `return tmp` 一个永不被读取的 nil)。 + Sieve 每轮循环一次。消除它零风险且能改善寄存器压力。 diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index 150a45c..ecdb7b4 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -3390,20 +3390,9 @@ std::string CGen::CompileFunctioncall(const SyntaxTreeInterfacePtr &functioncall std::string call_expr; bool is_local_callee = BindsLocalValue(var_ptr); - if (!is_local_callee && local_func_names_.contains(func_name)) { - const auto &info = local_func_names_.at(func_name); - if (!info.is_vararg && !has_expansion) { - if (static_cast(compiled_args.size()) != info.params_count) { - ThrowError(std::format("wrong number of arguments to '{}': expected {}, got {}", func_name, info.params_count, compiled_args.size()), functioncall); - } - } - if (!info.is_vararg && has_expansion) { - int expanded_count = static_cast(compiled_args.size()) + (info.params_count - expansion_start_idx); - if (expanded_count != info.params_count) { - ThrowError(std::format("wrong number of arguments to '{}': expected {}, got {}", func_name, info.params_count, expanded_count), functioncall); - } - } - } + // Lua 语义:被调函数形参个数与实参个数无须一致——少传的形参在函数体内为 nil, + // 多传的实参求值后丢弃。直接调用路径统一在 BuildLocalFunctionCall 里做 nil 补齐/ + // 截断(与 FlCallClosure 运行时分发路径的行为一致),此处不再做编译期精确个数检查。 if (local_func_names_.contains(func_name) && !is_local_callee) { call_expr = BuildLocalFunctionCall(func_name, compiled_args, has_expansion, expansion_tmp, expansion_start_idx); @@ -4131,12 +4120,22 @@ std::string CGen::TryCompileSpecDirectCall(const std::shared_ptrsecond; const auto explist_arg = args_ptr->Explist(); DEBUG_ASSERT(explist_arg->Type() == SyntaxTreeType::ExpList); const auto explist_arg_ptr = std::dynamic_pointer_cast(explist_arg); const auto &raw_args = explist_arg_ptr->Exps(); + if (static_cast(raw_args.size()) != fn_it->second.params_count) { + return ""; + } if (int bitmask = 0; TryInferMathCallBitmask(callee_name, raw_args, bitmask)) { std::unordered_map native_exprs; @@ -4764,9 +4763,24 @@ std::string CGen::BuildLocalFunctionCall(const std::string &func_name, const std } else { const int expected_params = info.params_count; if (has_expansion) { - for (int i = expansion_start_idx; i < expected_params; ++i) { - args.push_back(std::format("FlUnboxMulti({}, {})", expansion_tmp, i - expansion_start_idx)); + if (expansion_start_idx < expected_params) { + for (int i = expansion_start_idx; i < expected_params; ++i) { + args.push_back(std::format("FlUnboxMulti({}, {})", expansion_tmp, i - expansion_start_idx)); + } + } else { + // 展开位置已超出形参个数:前面的实参已填满所有形参, + // 多出的实参与展开值按 Lua 语义求值后丢弃。 + args.resize(expected_params); + } + } + // Lua 语义:少传的实参以 nil 补齐(FlUnboxMulti 越界同样取 nil), + // 多传的实参截断丢弃;两侧均与 FlCallClosure 运行时路径保持一致。 + if (static_cast(args.size()) < expected_params) { + while (static_cast(args.size()) < expected_params) { + args.push_back("kNil"); } + } else if (static_cast(args.size()) > expected_params) { + args.resize(expected_params); } } std::string call_expr = (info.c_symbol_name.empty() ? func_name : info.c_symbol_name) + "(NULL"; diff --git a/src/compile/semantic_analysis.cpp b/src/compile/semantic_analysis.cpp index 2eb5878..f8e5cdb 100644 --- a/src/compile/semantic_analysis.cpp +++ b/src/compile/semantic_analysis.cpp @@ -1269,10 +1269,10 @@ bool SemanticAnalysis::IsDeclaredSimpleName(const std::string &name, const std:: if (file_level_names.contains(name)) { return true; } - // 宿主用 RegisterNativeFunction 声明的全局原生函数(print/type/pairs 及用户回调名)。 - if (s_->GetVM().FindNativeFunction(name) != nullptr) { - return true; - } + // 宿主用 RegisterNativeFunction 注册的全局原生函数不在此列:裸原生名只有出现在 + // 直接调用位(f(...) / a.b.f(...),整条链已在 exempt_vars 豁免)才合法;出现在 + // 值位置由 check_simple_var 给出「native function cannot be used as a value」 + // 的明确编译期报错(fakelua 按名字派发原生调用,原生函数不是一等值)。 return false; } @@ -1294,6 +1294,16 @@ void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::v if (IsDeclaredSimpleName(name, scopes, file_level_names)) { return; } + // 原生函数出现在值位置(local t = type、pcall(print, ...)、return next 等): + // fakelua 在编译期按名字派发原生调用,原生函数不是一等值。此前这里放行, + // 到运行时才以 "attempt to call/index a non-function/table value" 失败, + // 报错位置与原因都有误导性;改为编译期明确报错。调用位已在 exempt_vars 豁免。 + if (s_->GetVM().FindNativeFunction(name) != nullptr) { + ThrowError(std::format("native function '{}' cannot be used as a value; fakelua dispatches native calls " + "by name at compile time, call it directly or wrap it in a Lua function", + name), + vnode); + } if (lvalue_vars.contains(var.get())) { ThrowError(std::format("undeclared variable '{}' on the left side of assignment; fakelua has no implicit " "globals, declare it with 'local' before assigning", @@ -1380,6 +1390,36 @@ void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::v } case SyntaxTreeType::Var: { const auto var = std::dynamic_pointer_cast(node); + // 点号原生库函数出现在值位置(local f = table.concat、pcall(table.concat, t)、 + // { cb = string.format } 等):调用位的整条点号链已在 exempt_vars 豁免,能走到 + // 这里的 kDot 链都是值位置。全名命中原生函数注册表时给出与裸原生名一致的明确 + // 编译期报错。常量访问(math.pi、string.charpattern)不在注册表里;根名是局部 + // 变量时(local t = {} 之后的 t.concat)是普通字段访问,两者都不报错。 + if (var->GetVarKind() == VarKind::kDot && !exempt_vars.contains(var.get())) { + std::string full; + std::shared_ptr root; + for (auto cur = var; cur && cur->GetVarKind() == VarKind::kDot;) { + full = cur->GetName() + (full.empty() ? "" : "." + full); + const auto pe = std::dynamic_pointer_cast(cur->GetPrefixexp()); + root = (pe && pe->GetPrefixKind() == PrefixExpKind::kVar) + ? std::dynamic_pointer_cast(pe->GetValue()) + : nullptr; + cur = root; + } + if (root && root->GetVarKind() == VarKind::kSimple && !full.empty()) { + const std::string full_name = root->GetName() + "." + full; + bool root_declared = file_level_names.contains(root->GetName()); + for (auto it = scopes.rbegin(); it != scopes.rend() && !root_declared; ++it) { + root_declared = it->contains(root->GetName()); + } + if (!root_declared && s_->GetVM().FindNativeFunction(full_name) != nullptr) { + ThrowError(std::format("native function '{}' cannot be used as a value; fakelua dispatches native " + "calls by name at compile time, call it directly or wrap it in a Lua function", + full_name), + node); + } + } + } check_simple_var(node); CheckVarScopes(var->GetPrefixexp(), scopes, file_level_names, exempt_vars, lvalue_vars); CheckVarScopes(var->GetExp(), scopes, file_level_names, exempt_vars, lvalue_vars); diff --git a/src/interp/codegen.cpp b/src/interp/codegen.cpp index 00cae67..04bcd76 100644 --- a/src/interp/codegen.cpp +++ b/src/interp/codegen.cpp @@ -1612,7 +1612,12 @@ int InterpCodegen::CompileFunctioncall(const SyntaxTreeInterfacePtr &functioncal var_ptr = var.get(); if (const auto it = var_to_def_map_.find(var_ptr); it != var_to_def_map_.end()) { is_local_callee = true; - file_level_func = it->second->defining_func == nullptr; + // 只有文件级 local function 才能走 CALLNAME:named_protos_ 只登记 + // 函数定义。文件级 local 变量(如 local gv = f)即使 defining_func + // 为 nullptr 也不在 named_protos_ 里,必须按闭包调用,否则运行时报 + // "function 'gv' not found"(C 后端此处按闭包处理,行为一致)。 + file_level_func = it->second->defining_func == nullptr && it->second->def_node != nullptr && + it->second->def_node->Type() == SyntaxTreeType::LocalFunction; } } else if (var && var->GetVarKind() == VarKind::kDot) { const auto base_pe = var->GetPrefixexp(); diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index a621781..29f54e9 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -165,9 +165,8 @@ static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg vararg_cvar.data_.m = m; temp_arg_arr[fixed_arg_count] = vararg_cvar; } else { - if (UNLIKELY(!last_is_multi && flat_count != expected_arg_count)) { - ThrowFakeluaException(std::format("FakeluaCallByName: function '{}' expects {} argument(s), got {}", name, expected_arg_count, flat_count)); - } + // Lua 语义:实参个数与形参无须一致——少传的形参补 nil,多传的实参 + // 丢弃(下方循环只取前 expected_arg_count 个)。与 FlCallClosure 一致。 for (int i = 0; i < expected_arg_count; ++i) { temp_arg_arr[i] = i < flat_count ? flat_args_buf[i] : (CVar){static_cast(VarType::Nil)}; } diff --git a/src/native/basic/native_basic.cpp b/src/native/basic/native_basic.cpp index 4164d30..474a7c7 100644 --- a/src/native/basic/native_basic.cpp +++ b/src/native/basic/native_basic.cpp @@ -238,7 +238,10 @@ void RegisterBasicLibraryApi(State *s) { }); // type(v) + // 与 Lua 5.4 的 luaL_checkany 对齐:实参缺失必须与显式传 nil 区分, + // type() 报 "bad argument #1 to 'type' (value expected)"。 RegisterNativeFunction(s, "type", 1, false, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "type", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); switch (static_cast(a0.type_)) { case VarType::Nil: @@ -262,6 +265,7 @@ void RegisterBasicLibraryApi(State *s) { // tostring(v) RegisterNativeFunction(s, "tostring", 1, false, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "tostring", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); const auto &v = AsVar(a0); if (v.Type() == VarType::String || v.Type() == VarType::StringId) { @@ -272,7 +276,9 @@ void RegisterBasicLibraryApi(State *s) { }); // tonumber(e [, base]) + // e 必填:tonumber() 与 tonumber(nil) 语义不同(前者报错,后者返回 nil)。 RegisterNativeFunction(s, "tonumber", 1, true, [](State *state, CVar *args, int n) -> CVar { + if (n < 1) ThrowBadArgument(1, "tonumber", "value expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); const auto &v = AsVar(a0); diff --git a/src/native/math/native_math.cpp b/src/native/math/native_math.cpp index 8954e8c..b542401 100644 --- a/src/native/math/native_math.cpp +++ b/src/native/math/native_math.cpp @@ -131,7 +131,7 @@ void RegisterMathLibraryApi(State *s) { }); RegisterNativeFunction(s, "math.max", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "math.max", "number expected"); CVar max_cvar = inter::GetNativeArg(state, args, n, 0); double max_v = CheckMathNumberArg(max_cvar, 1, "math.max"); for (int i = 1; i < n; ++i) { @@ -153,7 +153,7 @@ void RegisterMathLibraryApi(State *s) { }); RegisterNativeFunction(s, "math.min", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "math.min", "number expected"); CVar min_cvar = inter::GetNativeArg(state, args, n, 0); double min_v = CheckMathNumberArg(min_cvar, 1, "math.min"); for (int i = 1; i < n; ++i) { diff --git a/src/native/string/native_string.cpp b/src/native/string/native_string.cpp index b7e867a..6634de0 100644 --- a/src/native/string/native_string.cpp +++ b/src/native/string/native_string.cpp @@ -646,7 +646,7 @@ void RegisterStringLibraryApi(State *s) { if (!s) return; RegisterNativeFunction(s, "string.len", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaInt(state, 0); + if (n < 1) ThrowBadArgument(1, "string.len", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.len"); std::string temp; @@ -665,9 +665,14 @@ void RegisterStringLibraryApi(State *s) { int64_t len = static_cast(sv.size()); int64_t start_pos = CheckIntegerArg(inter::GetNativeArg(state, args, n, 1), 2, "string.sub"); + // Lua 5.4:j 是 luaL_optinteger(getendpos),显式 nil 与省略等价,默认 -1(末尾)。 + // i 是 luaL_checkinteger,nil 必须报错——与上面 n<2 的缺参报错一致。 int64_t end_pos = len; if (n >= 3) { - end_pos = CheckIntegerArg(inter::GetNativeArg(state, args, n, 2), 3, "string.sub"); + CVar j_var = inter::GetNativeArg(state, args, n, 2); + if (j_var.type_ != static_cast(VarType::Nil)) { + end_pos = CheckIntegerArg(j_var, 3, "string.sub"); + } } start_pos = NormalizePos(start_pos, len); @@ -685,7 +690,9 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.rep", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaStringView(state, ""); + // Lua 5.4:实参缺失与显式 nil 不同,缺失必报 bad argument(rep 的 s/n 都必填)。 + if (n < 1) ThrowBadArgument(1, "string.rep", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.rep", "number expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.rep"); std::string temp; @@ -696,12 +703,15 @@ void RegisterStringLibraryApi(State *s) { int64_t rep_cnt = CheckIntegerArg(rep_var, 2, "string.rep"); if (rep_cnt <= 0) return inter::NativeToFakeluaStringView(state, ""); + // Lua 5.4:sep 是 luaL_optlstring,显式 nil 与省略等价,默认 ""。 std::string sep = ""; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckStringArg(a2, 3, "string.rep"); - std::string temp_sep; - sep = std::string(GetStringArgView(a2, temp_sep)); + if (a2.type_ != static_cast(VarType::Nil)) { + CheckStringArg(a2, 3, "string.rep"); + std::string temp_sep; + sep = std::string(GetStringArgView(a2, temp_sep)); + } } if (sv.empty() && sep.empty()) { @@ -723,7 +733,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.reverse", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.reverse", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.reverse"); std::string temp; @@ -733,7 +743,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.lower", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.lower", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.lower"); std::string temp; @@ -751,7 +761,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.upper", 1, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.upper", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.upper"); std::string temp; @@ -969,7 +979,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.byte", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.byte", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "string.byte"); std::string temp; @@ -977,17 +987,23 @@ void RegisterStringLibraryApi(State *s) { int64_t len = static_cast(sv.size()); if (len == 0) return inter::NativeToFakeluaNil(state); + // Lua 5.4 str_byte 的 i/j 是 luaL_optinteger:显式 nil 与省略等价(i 默认 1, + // j 默认 i)。包装函数转发的显式 nil 不能误报 number expected。 int64_t start_pos = 1; if (n >= 2) { CVar a1 = inter::GetNativeArg(state, args, n, 1); - // 与 Lua 5.4 对齐:超大 float 报 "number has no integer representation" - start_pos = CheckIntegerArg(a1, 2, "string.byte"); + if (a1.type_ != static_cast(VarType::Nil)) { + // 与 Lua 5.4 对齐:超大 float 报 "number has no integer representation" + start_pos = CheckIntegerArg(a1, 2, "string.byte"); + } } int64_t end_pos = start_pos; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - end_pos = CheckIntegerArg(a2, 3, "string.byte"); + if (a2.type_ != static_cast(VarType::Nil)) { + end_pos = CheckIntegerArg(a2, 3, "string.byte"); + } } start_pos = NormalizePos(start_pos, len); @@ -1059,7 +1075,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.format", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.format", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); std::string temp_fmt; std::string_view fmt = GetStringArgView(fmt_var, temp_fmt); @@ -1254,7 +1270,9 @@ void RegisterStringLibraryApi(State *s) { // 若 plain 为 true,则退化为纯子串查找(忽略模式元字符)。 // 找不到时返回 nil。 RegisterNativeFunction(s, "string.find", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + // s 与 pattern 都必填:实参缺失报 bad argument(与显式 nil 区分)。 + if (n < 1) ThrowBadArgument(1, "string.find", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.find", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.find"); @@ -1267,8 +1285,11 @@ void RegisterStringLibraryApi(State *s) { int64_t init_pos = 1; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckNumberArg(a2, 3, "string.find"); - init_pos = inter::CVarToInteger(a2, 1); + // Lua 5.4:init 是 luaL_optinteger,显式 nil 与省略等价,默认 1。 + if (a2.type_ != static_cast(VarType::Nil)) { + CheckNumberArg(a2, 3, "string.find"); + init_pos = inter::CVarToInteger(a2, 1); + } } init_pos = NormalizePos(init_pos, len); if (init_pos < 1) init_pos = 1; @@ -1310,7 +1331,8 @@ void RegisterStringLibraryApi(State *s) { // 与 find 一样从 init 起搜索,只是不返回位置;前导 '^' 锚定到 init 点。 // 有捕获返回所有捕获(位置捕获为整数),无捕获返回整个匹配。 RegisterNativeFunction(s, "string.match", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.match", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.match", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.match"); @@ -1324,8 +1346,11 @@ void RegisterStringLibraryApi(State *s) { int64_t init_pos = 1; if (n >= 3) { CVar a2 = inter::GetNativeArg(state, args, n, 2); - CheckNumberArg(a2, 3, "string.match"); - init_pos = inter::CVarToInteger(a2, 1); + // Lua 5.4:init 是 luaL_optinteger,显式 nil 与省略等价,默认 1。 + if (a2.type_ != static_cast(VarType::Nil)) { + CheckNumberArg(a2, 3, "string.match"); + init_pos = inter::CVarToInteger(a2, 1); + } } init_pos = NormalizePos(init_pos, len); if (init_pos < 1) init_pos = 1; @@ -1354,7 +1379,8 @@ void RegisterStringLibraryApi(State *s) { // 返回一个迭代器闭包;每次调用返回下一个匹配(有捕获时返回捕获值)。 // 模式惰性解析,非法模式在首次迭代时抛错(与 Lua 一致)。 RegisterNativeFunction(s, "string.gmatch", 2, false, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.gmatch", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.gmatch", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.gmatch"); @@ -1379,7 +1405,10 @@ void RegisterStringLibraryApi(State *s) { // 匹配;返回 nil/false 保留原文)/ table(以首个捕获或整个匹配为键查询,nil/false // 保留原文)。前导 '^' 表示只在起点尝试一次。 RegisterNativeFunction(s, "string.gsub", 3, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 3) return inter::NativeToFakeluaNil(state); + // s / pattern / repl 三个都必填 + if (n < 1) ThrowBadArgument(1, "string.gsub", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.gsub", "string expected"); + if (n < 3) ThrowBadArgument(3, "string.gsub", "string/function/table expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CVar a1 = inter::GetNativeArg(state, args, n, 1); CheckStringArg(a0, 1, "string.gsub"); @@ -1566,7 +1595,7 @@ void RegisterStringLibraryApi(State *s) { }); RegisterNativeFunction(s, "string.dump", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "string.dump", "function expected"); CVar fn_var = inter::GetNativeArg(state, args, n, 0); if (fn_var.type_ != static_cast(VarType::Closure) || !fn_var.data_.cl) { ThrowFakeluaException("bad argument #1 to 'string.dump' (function expected)"); @@ -1612,7 +1641,7 @@ void RegisterStringLibraryApi(State *s) { }); auto load_impl = [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "load", "string expected"); CVar code_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(code_var, 1, "load"); if (code_var.type_ != static_cast(VarType::String) && code_var.type_ != static_cast(VarType::StringId) && code_var.type_ != static_cast(VarType::Int) && @@ -1768,7 +1797,7 @@ void RegisterStringLibraryApi(State *s) { // 注册的签名是 (fmt, ...) 即 arg_count=1, is_vararg=true // 调用时:args[0]=fmt, args[1]=Multi(剩余参数) RegisterNativeFunction(s, "string.pack", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "string.pack", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(fmt_var, 1, "string.pack"); std::string temp_fmt; @@ -1993,7 +2022,7 @@ void RegisterStringLibraryApi(State *s) { // string.packsize // 签名: (fmt, ...) packsize 主要需要 fmt,但 z 格式需要字符串长度 RegisterNativeFunction(s, "string.packsize", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaInt(state, 0); + if (n < 1) ThrowBadArgument(1, "string.packsize", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CheckStringArg(fmt_var, 1, "string.packsize"); std::string temp_fmt; @@ -2123,7 +2152,9 @@ void RegisterStringLibraryApi(State *s) { // string.unpack RegisterNativeFunction(s, "string.unpack", 2, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 2) return inter::NativeToFakeluaNil(state); + // fmt 与 s 都必填 + if (n < 1) ThrowBadArgument(1, "string.unpack", "string expected"); + if (n < 2) ThrowBadArgument(2, "string.unpack", "string expected"); CVar fmt_var = inter::GetNativeArg(state, args, n, 0); CVar str_var = inter::GetNativeArg(state, args, n, 1); CheckStringArg(fmt_var, 1, "string.unpack"); diff --git a/src/native/table/native_table.cpp b/src/native/table/native_table.cpp index 863fde1..88ce303 100644 --- a/src/native/table/native_table.cpp +++ b/src/native/table/native_table.cpp @@ -871,7 +871,7 @@ void RegisterTableLibraryApi(State *s) { if (!s) return; RegisterNativeFunction(s, "table.insert", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.insert", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.insert' (table expected)"); @@ -901,7 +901,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.remove", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.remove", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.remove' (table expected)"); @@ -937,7 +937,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.concat", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaStringView(state, ""); + if (n < 1) ThrowBadArgument(1, "table.concat", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); // 标准 Lua:table.concat 第一个参数必须是 table,否则抛出异常 if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { @@ -951,15 +951,22 @@ void RegisterTableLibraryApi(State *s) { std::string temp_sep; sep = std::string(GetStringArgView(sep_var, temp_sep)); } + // Lua 5.4 tconcat 的 i/j 是 luaL_optinteger:显式 nil 与省略等价,取默认值 + // (i 默认 1,j 默认 #t)。包装函数转发(f(t, sep, i, j) 少传实参)会传入显式 + // nil,此处必须与 table.remove 的 pos 同等对待,否则误报 number expected。 int64_t start_i = 1; if (n >= 3) { CVar start_var = inter::GetNativeArg(state, args, n, 2); - start_i = CheckIntegerArg(start_var, 3, "table.concat"); + if (start_var.type_ != static_cast(VarType::Nil)) { + start_i = CheckIntegerArg(start_var, 3, "table.concat"); + } } int64_t end_j = TableHelper::GetTableLen(tbl); if (n >= 4) { CVar end_var = inter::GetNativeArg(state, args, n, 3); - end_j = CheckIntegerArg(end_var, 4, "table.concat"); + if (end_var.type_ != static_cast(VarType::Nil)) { + end_j = CheckIntegerArg(end_var, 4, "table.concat"); + } } if (end_j < start_i) { @@ -1017,20 +1024,26 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.unpack", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.unpack", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'unpack' (table expected)"); } + // Lua 5.4 tunpack 的 i/j 是 luaL_optinteger:显式 nil 与省略等价,取默认值 + // (i 默认 1,j 默认 #t)。与 table.concat / table.remove 的 nil 处理一致。 int64_t start_i = 1; if (n >= 2) { CVar start_var = inter::GetNativeArg(state, args, n, 1); - start_i = CheckIntegerArg(start_var, 2, "table.unpack"); + if (start_var.type_ != static_cast(VarType::Nil)) { + start_i = CheckIntegerArg(start_var, 2, "table.unpack"); + } } int64_t end_j = TableHelper::GetTableLen(tbl); if (n >= 3) { CVar end_var = inter::GetNativeArg(state, args, n, 2); - end_j = CheckIntegerArg(end_var, 3, "table.unpack"); + if (end_var.type_ != static_cast(VarType::Nil)) { + end_j = CheckIntegerArg(end_var, 3, "table.unpack"); + } } if (start_i > end_j) return inter::AllocMultiCVar(state, 0); @@ -1058,7 +1071,11 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.move", 4, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 4) return inter::NativeToFakeluaNil(state); + // a1/f/e/t 四个都必填 + if (n < 1) ThrowBadArgument(1, "table.move", "table expected"); + if (n < 2) ThrowBadArgument(2, "table.move", "number expected"); + if (n < 3) ThrowBadArgument(3, "table.move", "number expected"); + if (n < 4) ThrowBadArgument(4, "table.move", "number expected"); CVar a1 = inter::GetNativeArg(state, args, n, 0); CVar f_var = inter::GetNativeArg(state, args, n, 1); CVar e_var = inter::GetNativeArg(state, args, n, 2); @@ -1099,7 +1116,7 @@ void RegisterTableLibraryApi(State *s) { }); RegisterNativeFunction(s, "table.sort", 1, true, [](State *state, CVar *args, int n) -> CVar { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "table.sort", "table expected"); CVar tbl = inter::GetNativeArg(state, args, n, 0); if (tbl.type_ != static_cast(VarType::Table) || !tbl.data_.t) { ThrowFakeluaException("bad argument #1 to 'table.sort' (table expected)"); diff --git a/src/native/utf8/native_utf8.cpp b/src/native/utf8/native_utf8.cpp index c919479..56ca705 100644 --- a/src/native/utf8/native_utf8.cpp +++ b/src/native/utf8/native_utf8.cpp @@ -132,7 +132,7 @@ static CVar Utf8Char(State *state, CVar *args, int n) { // utf8.codepoint(s [, i [, j]]) // Returns code points for all characters between positions i and j (inclusive). static CVar Utf8Codepoint(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.codepoint", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.codepoint"); @@ -142,14 +142,14 @@ static CVar Utf8Codepoint(State *state, CVar *args, int n) { int64_t len = static_cast(sv.size()); - // Parse i (default 1) + // Parse i (default 1)。Lua 5.4 codepoint 的 i/j 是 luaL_optinteger:显式 nil 与省略等价。 CVar i_var = (n >= 2) ? inter::GetNativeArg(state, args, n, 1) : CVar{static_cast(VarType::Nil)}; - if (n >= 2) CheckNumberArg(i_var, 2, "utf8.codepoint"); - int64_t i = (n >= 2) ? inter::CVarToInteger(i_var, 1) : 1; + if (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 2, "utf8.codepoint"); + int64_t i = (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(i_var, 1) : 1; // Parse j (default i) CVar j_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(j_var, 3, "utf8.codepoint"); - int64_t j = (n >= 3) ? inter::CVarToInteger(j_var, i) : i; + if (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(j_var, 3, "utf8.codepoint"); + int64_t j = (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(j_var, i) : i; // Handle negative indices (relative to end). Index 0 is not from-end. if (i < 0) i = len + i + 1; @@ -264,7 +264,7 @@ extern "C" CVar Utf8CodesIterator(VarClosure *cl, CVar /*s*/, CVar /*var*/) { } static CVar Utf8Codes(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.codes", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.codes"); @@ -280,7 +280,7 @@ static CVar Utf8Codes(State *state, CVar *args, int n) { // Returns the number of UTF-8 characters in s between positions i and j. // If it finds an invalid byte, returns nil + position of the invalid byte. static CVar Utf8Len(State *state, CVar *args, int n) { - if (n < 1) return inter::NativeToFakeluaNil(state); + if (n < 1) ThrowBadArgument(1, "utf8.len", "string expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.len"); @@ -290,14 +290,14 @@ static CVar Utf8Len(State *state, CVar *args, int n) { int64_t byte_len = static_cast(sv.size()); - // Parse i (default 1) + // Parse i (default 1)。Lua 5.4 utflen 的 i/j 是 luaL_optinteger:显式 nil 与省略等价。 CVar i_var = (n >= 2) ? inter::GetNativeArg(state, args, n, 1) : CVar{static_cast(VarType::Nil)}; - if (n >= 2) CheckNumberArg(i_var, 2, "utf8.len"); - int64_t i = (n >= 2) ? inter::CVarToInteger(i_var, 1) : 1; + if (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 2, "utf8.len"); + int64_t i = (n >= 2 && i_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(i_var, 1) : 1; // Parse j (default -1, meaning end of string) CVar j_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(j_var, 3, "utf8.len"); - int64_t j = (n >= 3) ? inter::CVarToInteger(j_var, -1) : -1; + if (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(j_var, 3, "utf8.len"); + int64_t j = (n >= 3 && j_var.type_ != static_cast(VarType::Nil)) ? inter::CVarToInteger(j_var, -1) : -1; // Handle negative indices (relative to end). Index 0 is not from-end (Lua u_posrelat). if (i < 0) i = byte_len + i + 1; @@ -337,7 +337,9 @@ static CVar Utf8Len(State *state, CVar *args, int n) { // Returns the byte position of the n-th character in s, starting at position i. // n can be negative (count backward from i, matching Lua 5.4). static CVar Utf8Offset(State *state, CVar *args, int n) { - if (n < 2) return inter::NativeToFakeluaNil(state); + // s 与 n 都必填(i 可选,默认 1) + if (n < 1) ThrowBadArgument(1, "utf8.offset", "string expected"); + if (n < 2) ThrowBadArgument(2, "utf8.offset", "number expected"); CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckStringArg(a0, 1, "utf8.offset"); @@ -351,10 +353,13 @@ static CVar Utf8Offset(State *state, CVar *args, int n) { CheckNumberArg(target_n_var, 2, "utf8.offset"); int64_t target_n = inter::CVarToInteger(target_n_var, 0); - // Parse i (starting byte position, default 1 if n>=0, len+1 if n<0) + // Parse i (starting byte position, default 1 if n>=0, len+1 if n<0)。 + // Lua 5.4 byteoffset 的 i 是 luaL_optinteger:显式 nil 与省略等价;n 是 checkinteger。 CVar i_var = (n >= 3) ? inter::GetNativeArg(state, args, n, 2) : CVar{static_cast(VarType::Nil)}; - if (n >= 3) CheckNumberArg(i_var, 3, "utf8.offset"); - int64_t start_i = (n >= 3) ? inter::CVarToInteger(i_var, 1) : ((target_n >= 0) ? 1 : byte_len + 1); + if (n >= 3 && i_var.type_ != static_cast(VarType::Nil)) CheckNumberArg(i_var, 3, "utf8.offset"); + int64_t start_i = (n >= 3 && i_var.type_ != static_cast(VarType::Nil)) + ? inter::CVarToInteger(i_var, 1) + : ((target_n >= 0) ? 1 : byte_len + 1); // Lua: 1 <= posi <= len+1 (relative negative i counts from the end; 0 is not from-end) if (start_i < 0) start_i = byte_len + start_i + 1; diff --git a/test/lua/basic/test_basic_call_arity.lua b/test/lua/basic/test_basic_call_arity.lua new file mode 100644 index 0000000..042d7c7 --- /dev/null +++ b/test/lua/basic/test_basic_call_arity.lua @@ -0,0 +1,78 @@ +-- 函数调用实参个数语义(对齐 Lua 5.4):实参少于形参时缺失形参为 nil, +-- 实参多于形参时多出的实参求值后丢弃。覆盖各条调用路径: +-- 1. 同文件 local function 直接调用(CGen 直接调用 / interp CALLNAME) +-- 2. 闭包变量调用(FlCallClosure / interp CALL) +-- 3. 文件级 function(数学特化直调、FakeluaCallByName 按名分发) +-- 4. 多值展开(返回值个数少于/多于形参) +-- 5. vararg(固定参数补 nil,多余实参进入 ...) +-- 6. 原生函数(多传丢弃) +local function two(a, b) + return (a or 0) + (b or 0) +end + +local function one(a) + return a or 0 +end + +local function va(a, ...) + return (a or 0) * 10 + select("#", ...) +end + +local closure_var = two + +local function ret1() + return 7 +end + +local function ret3() + return 1, 2, 3 +end + +-- 文件级函数,会被数学特化 +function double_arity(n) + return n * 2 +end + +function add2_arity(x, y) + return x + y +end + +function test_basic_call_arity() + local total = 0 + + -- 1. local function 直接调用:少传补 nil,多传丢弃 + total = total + two(1) -- 1 + total = total + two(1, 2) -- 3 + total = total + two(1, 2, 100) -- 3 + total = total + one(5, 6, 7) -- 5 + + -- 2. 闭包变量调用 + total = total + closure_var(4) -- 4 + total = total + closure_var(4, 5) -- 9 + total = total + closure_var(4, 5, 100) -- 9 + + -- 3. 文件级函数:数学特化路径多传/精确/少传 + total = total + double_arity(3) -- 6 + total = total + add2_arity(1, 2) -- 3 + total = total + add2_arity(1, 2, 100)-- 3(多传丢弃,不能生成参数个数不符的 C) + total = total + add2_arity(1, 2, 100, 200) -- 3 + + -- 4. 多值展开:返回值个数少于/多于形参 + total = total + two(ret1()) -- 7 + 0 = 7 + total = total + two(ret3()) -- 1 + 2 = 3 + total = total + one(ret3()) -- 1 + total = total + two(1, ret3()) -- 1 + 1 = 2(ret3 其余返回值丢弃) + total = total + one(1, ret3()) -- 1(形参已填满,展开值丢弃) + + -- 5. vararg:固定参数补 nil,多余实参进入 ... + total = total + va(1) -- 10 + 0 = 10 + total = total + va(1, 2, 3) -- 10 + 2 = 12 + total = total + va() -- 0 + 0 = 0 + total = total + va(nil, 1) -- 0 + 1 = 1 + + -- 6. 原生函数:多传丢弃 + total = total + math.floor(1.9, 99) -- 1 + total = total + #string.rep("ab", 2, "-", "x") -- 5 + + return total +end diff --git a/test/lua/string/test_string_optnil.lua b/test/lua/string/test_string_optnil.lua new file mode 100644 index 0000000..265f053 --- /dev/null +++ b/test/lua/string/test_string_optnil.lua @@ -0,0 +1,60 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger/luaL_optlstring 语义)。 +-- 注意逐函数差异(以 5.4.9 源码为准): +-- string.sub i 是 checkinteger(nil 报错),j 是 optinteger(nil 取默认 -1) +-- string.byte i/j 均 optinteger;string.rep sep 是 optlstring(nil 取 "") +-- string.find/match init 是 optinteger;string.gsub n 缺省/nil 不限次数 +local function sub_wrap(s, i, j) + return #string.sub(s, i, j) +end + +local function byte_wrap(s, i, j) + local a, b = string.byte(s, i, j) + return (a or 0) + (b or 0) +end + +local function rep_wrap(s, n, sep) + return #string.rep(s, n, sep) +end + +local function find_wrap(s, pat, init) + local a, b = string.find(s, pat, init) + if a == nil then return 0 end + return a * 100 + b +end + +local function match_wrap(s, pat, init) + local m = string.match(s, pat, init) + if m == nil then return 0 end + return #m +end + +function test_string_optnil() + local total = 0 + + -- string.sub:j 传 nil 取默认(末尾);i 传 nil 必须报错(与 Lua 一致) + total = total + sub_wrap("hello", 2, nil) -- "ello" = 4 + total = total + sub_wrap("hello", 2, 4) -- "ell" = 3 + local ok = pcall(function() string.sub("hello", nil) end) + if ok then total = total + 1000 end -- 不应发生:i 为 nil 必须报错 + + -- string.byte:i 默认 1,j 默认 i(j 给定时返回两个字节) + total = total + byte_wrap("hello", nil) -- 104 + total = total + byte_wrap("hello", 2, nil) -- 101 + total = total + byte_wrap("hello", nil, 2) -- 104+101 = 205 + + -- string.rep:sep 传 nil 取默认 "" + total = total + rep_wrap("ab", 3, nil) -- "ababab" = 6 + total = total + rep_wrap("ab", 3, "-") -- "ab-ab-ab" = 8 + + -- string.find/match:init 默认 1 + total = total + find_wrap("hello", "l", nil) -- 3,3 -> 303 + total = total + find_wrap("hello", "l", 4) -- 4,4 -> 404 + total = total + match_wrap("hello", "l+", nil)-- "ll" = 2 + total = total + match_wrap("hello", "xyz", nil)-- 0 + + -- string.gsub:n 缺省/nil 不限次数 + local s, cnt = string.gsub("aaa", "a", "b", nil) + total = total + #s + cnt -- "bbb" = 3 + 3 + + return total +end diff --git a/test/lua/table/test_table_optnil.lua b/test/lua/table/test_table_optnil.lua new file mode 100644 index 0000000..0c2fe3f --- /dev/null +++ b/test/lua/table/test_table_optnil.lua @@ -0,0 +1,31 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger/luaL_optlstring 语义)。 +-- 覆盖包装函数少传实参时形参以显式 nil 进入被调用库函数的常见转发写法。 +local function concat_wrap(t, sep, i, j) + return table.concat(t, sep, i, j) +end + +local function unpack_wrap(t, i, j) + local a, b, c = table.unpack(t, i, j) + return a + b + c +end + +function test_table_optnil() + local total = 0 + local t = { 1, 2, 3, 4, 5 } + + -- table.concat:i 默认 1,j 默认 #t + total = total + #concat_wrap(t, "-") -- "1-2-3-4-5" = 9 + total = total + #concat_wrap(t, "-", 2) -- "2-3-4-5" = 7 + total = total + #concat_wrap(t, "-", nil, 3) -- "1-2-3" = 5 + total = total + #table.concat(t, "-", nil, nil) -- 同上直接调用 = 9 + + -- table.unpack:i 默认 1,j 默认 #t(取前三个值相加) + total = total + unpack_wrap(t, nil, nil) -- 1+2+3 = 6 + total = total + unpack_wrap(t, 2) -- 2+3+4 = 9 + + -- table.remove:pos 省略/nil 取 #t + local r = { 10, 20, 30 } + total = total + table.remove(r, nil) -- 30 + + return total +end diff --git a/test/lua/utf8/test_utf8_optnil.lua b/test/lua/utf8/test_utf8_optnil.lua new file mode 100644 index 0000000..5854aad --- /dev/null +++ b/test/lua/utf8/test_utf8_optnil.lua @@ -0,0 +1,45 @@ +-- 可选参数显式传 nil 必须与省略等价(Lua 5.4 luaL_optinteger 语义)。 +-- utf8.codepoint/len 的 i/j、utf8.offset 的 i 均为 optinteger; +-- utf8.offset 的 n 是 checkinteger(nil 报错,与 Lua 一致)。 +-- "héllo" 的字节布局:h(1) é(2-3) l(4) l(5) o(6),共 6 字节 5 字符。 +local function codepoint_wrap(s, i, j) + local a, b = utf8.codepoint(s, i, j) + return (a or 0) + (b or 0) +end + +local function len_wrap(s, i, j) + local n = utf8.len(s, i, j) + if n == nil then return -1 end + return n +end + +local function offset_wrap(s, n, i) + return utf8.offset(s, n, i) +end + +function test_utf8_optnil() + local total = 0 + local s = "héllo" + + -- utf8.len:i 默认 1,j 默认 -1(末尾) + total = total + len_wrap(s, nil, nil) -- 5 + total = total + len_wrap(s, 2, nil) -- 从第 2 字节起:é,l,l,o = 4 + total = total + len_wrap(s, nil, 6) -- 到第 6 字节 = 5 + total = total + len_wrap(s, 3, 6) -- 第 3 字节是 é 的续字节(非法)-> -1 + + -- utf8.codepoint:i 默认 1,j 默认 i(j>i 时返回多个码点,取前两个相加) + total = total + codepoint_wrap(s, nil, nil) -- 104 (h) + total = total + codepoint_wrap(s, 2, nil) -- 233 (é) + total = total + codepoint_wrap(s, nil, 3) -- 104+233 = 337 + + -- utf8.offset:n 必填;i 默认 1(n>=0) + total = total + offset_wrap(s, 2, nil) -- 第 2 字符起始字节 -> 2 + total = total + offset_wrap(s, 1) -- 1 + total = total + offset_wrap(s, 0, nil) -- 1 + + -- n 传 nil 必须报错(checkinteger),不能静默取默认 + local ok = pcall(function() utf8.offset(s, nil) end) + if ok then total = total + 1000 end -- 不应发生 + + return total +end diff --git a/test/test_basic.cpp b/test/test_basic.cpp index dcc631b..466b8b8 100644 --- a/test/test_basic.cpp +++ b/test/test_basic.cpp @@ -1347,3 +1347,20 @@ TEST(test_basic, xpcall_coverage) { EXPECT_EQ(ret, 1); FakeluaDeleteState(s); } + +// 函数调用实参个数语义:少传补 nil、多传丢弃。三条调用路径(直接调用 / 闭包变量 / +// 按名分发)与三个后端(TCC/GCC/interp)都必须与 Lua 5.4 一致。 +TEST(test_basic, call_arity) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./basic/test_basic_call_arity.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_basic_call_arity", res); + EXPECT_EQ(res, 92LL); + } + + FakeluaDeleteState(s); +} diff --git a/test/test_exception.cpp b/test/test_exception.cpp index bcbe66f..bf017f8 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1343,18 +1343,25 @@ TEST(exception, for_loop_zero_step_float) { } } -TEST(exception, spec_call_arg_count_error) { +TEST(exception, spec_call_fewer_args_runtime_throw) { FakeluaStateGuard sg; auto s = sg.GetState(); ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - try { - CompileFile(s, "./exception/test_spec_call_arg_count_error.lua", {}); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); + // Lua 语义:实参少于形参合法(缺失形参为 nil),编译期不报错; + // 函数体内对 nil 形参做算术才在运行时报错。 + CompileFile(s, "./exception/test_spec_call_arg_count_error.lua", {}); + + for (auto jit_type: AllJitTypes()) { + try { + int ret = 0; + Call(s, jit_type, "test_fewer_args", ret, 1); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); + } } } @@ -1480,12 +1487,19 @@ TEST(exception, math_spec_too_few_args) { ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - try { - CompileFile(s, "./exception/test_math_spec_too_few_args.lua", {}); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); + // 同 spec_call_fewer_args_runtime_throw:少传参数编译通过, + // 数学特化函数回退到通用 CVar 入口后,对 nil 形参做算术才报错。 + CompileFile(s, "./exception/test_math_spec_too_few_args.lua", {}); + + for (auto jit_type: AllJitTypes()) { + try { + int ret = 0; + Call(s, jit_type, "test_fewer_args", ret, 1); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); + } } } @@ -1555,12 +1569,18 @@ TEST(exception, no_arg_call) { ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - try { - CompileFile(s, "./exception/test_no_arg_call.lua", {}); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); + // compute() 不传实参:编译通过,compute 内 n 为 nil,n * 2 在运行时报错。 + CompileFile(s, "./exception/test_no_arg_call.lua", {}); + + for (auto jit_type: AllJitTypes()) { + try { + int ret = 0; + Call(s, jit_type, "test", ret, 1); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); + } } } diff --git a/test/test_runtime.cpp b/test/test_runtime.cpp index 1a1521a..eab6de1 100644 --- a/test/test_runtime.cpp +++ b/test/test_runtime.cpp @@ -195,12 +195,22 @@ TEST(runtime, vm_call_by_name_error_cases) { EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "nulladdr", 0), std::exception); s.GetVM().RegisterFunction(VmFunction("fnv", 8, TEST_JIT_TYPE, reinterpret_cast(&VmFnEcho8), {})); - EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 9, a0, a1, a2, a3, a4, a5, a6, a7, a8), std::exception); - // Bug M: 参数个数与函数签名不匹配时必须抛异常(否则会读取未初始化栈内存)。 - // 这里 fnv 要求 8 个参数,分别传 3 个 / 0 个都应抛异常。 - EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 3, a0, a1, a2), std::exception); - EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 0), std::exception); + // 参数个数与形参不一致沿 Lua 语义处理:多传的丢弃、少传的补 nil, + // 不再抛异常。Bug M 关注的「读取未初始化栈内存」由补齐保证: + // CallByNameImpl 在派发前把 temp_arg_arr 的前 expected_arg_count 个槽全部写满 + // (实参不足的部分写 kNil),因此下面两次调用返回的都是确定的值。 + const CVar r9 = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 9, a0, a1, a2, a3, a4, a5, a6, a7, a8); + ASSERT_EQ(r9.type_, static_cast(VarType::Int)); + ASSERT_EQ(r9.data_.i, 0);// 多传的第 9 个实参被丢弃,返回第 1 个形参 + + const CVar r3 = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 3, a0, a1, a2); + ASSERT_EQ(r3.type_, static_cast(VarType::Int)); + ASSERT_EQ(r3.data_.i, 0); + + // 0 个实参:8 个形参全部补 nil,返回第 1 个形参即 nil(不是未初始化内存) + const CVar r0_args = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 0); + ASSERT_EQ(r0_args.type_, static_cast(VarType::Nil)); } TEST(runtime, vm_helper_functions_throw_and_alloc) { @@ -519,8 +529,12 @@ TEST(vm_cvar_call, multi_return_expansion) { multi_one.type_ = static_cast(VarType::Multi); multi_one.data_.m = m_one; - // 1. 分支 A: has_any_multi 为 true,但最后一个参数不是 Multi 并且 arg_num != expected_arg_count -> 抛出异常 - EXPECT_THROW(FakeluaCallByName(&s, TEST_JIT_TYPE, "echo2", 3, multi_two_a, int1, int20), std::exception); + // 1. 分支 A: 含 Multi 且末尾实参不是 Multi,实参个数(3) 多于形参(2) —— 按 Lua 语义 + // 截断丢弃,不再抛异常。multi_two_a 非末尾只取 vars[0]=20,故实参为 (20, 1, 20), + // 截断后 (20, 1),echo2 返回第 2 个形参 = 1。 + const CVar r0 = FakeluaCallByName(&s, TEST_JIT_TYPE, "echo2", 3, multi_two_a, int1, int20); + ASSERT_EQ(r0.type_, static_cast(VarType::Int)); + ASSERT_EQ(r0.data_.i, 1); // 2. 正常的多参数解包覆盖: // expected_arg_count = 4, 我们传 3 个参数:int1, multi_two_a (非末尾,只取 vars[0]), multi_two_b (末尾,全部展开) diff --git a/test/test_string.cpp b/test/test_string.cpp index 0977171..2b70a08 100644 --- a/test/test_string.cpp +++ b/test/test_string.cpp @@ -817,3 +817,18 @@ TEST(test_string, replace_empty_from) { CallThrow(s, "test_replace_empty_from", ret); FakeluaDeleteState(s); } + +TEST(test_string, test_string_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./string/test_string_optnil.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_string_optnil", res); + EXPECT_EQ(res, 1146LL); + } + + FakeluaDeleteState(s); +} diff --git a/test/test_syntax_tree.cpp b/test/test_syntax_tree.cpp index 680c49b..bfa610c 100644 --- a/test/test_syntax_tree.cpp +++ b/test/test_syntax_tree.cpp @@ -1907,6 +1907,73 @@ TEST(syntax_tree, preprocess_multipart_funcname_unsupported) { EXPECT_THROW(c.CompileString("function a.b.c() end", {}), std::exception); } +// 原生函数出现在值位置(非直接调用位)必须在编译期明确报错。 +// fakelua 按名字派发原生调用,原生函数不是一等值;此前这类代码放行到运行时才以 +// "attempt to call/index a non-function/table value" 失败,位置与原因都有误导性。 +namespace { + +void ExpectNativeValueError(Compiler &c, const std::string &script, const std::string &expect_substr) { + try { + c.CompileString(script, {}); + FAIL() << "expected CompileString to throw for: " << script; + } catch (const std::exception &e) { + const std::string msg = e.what(); + EXPECT_NE(msg.find(expect_substr), std::string::npos) << msg; + EXPECT_NE(msg.find("cannot be used as a value"), std::string::npos) << msg; + } +} + +}// namespace + +TEST(syntax_tree, semantic_native_function_as_value_simple_name) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 裸原生名(type/print/pairs/next ...) + ExpectNativeValueError(c, "function f()\n local t = type\n return t(1)\nend\n", "native function 'type'"); + ExpectNativeValueError(c, "function f()\n return next\nend\n", "native function 'next'"); + // 传给 pcall 的常见写法 + ExpectNativeValueError(c, "function f()\n return pcall(print, \"x\")\nend\n", "native function 'print'"); +} + +TEST(syntax_tree, semantic_native_function_as_value_dotted) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 点号原生库函数:local f = table.concat / pcall(table.concat, t) / { cb = string.format } + ExpectNativeValueError(c, "function f()\n local g = table.concat\n return g\nend\n", "native function 'table.concat'"); + ExpectNativeValueError(c, "function f()\n local t = { 1, 2 }\n return pcall(table.concat, t, \"-\")\nend\n", + "native function 'table.concat'"); + ExpectNativeValueError(c, "function f()\n return { cb = string.format }\nend\n", "native function 'string.format'"); + ExpectNativeValueError(c, "function f()\n return math.floor\nend\n", "native function 'math.floor'"); +} + +// 值位置检查不能误伤合法写法:局部表字段、局部遮蔽原生名、模块常量、直接调用、冒号调用。 +TEST(syntax_tree, semantic_native_function_value_position_allows_legit_uses) { + const FakeluaStateGuard guard; + const auto s = guard.GetState(); + ASSERT_NE(s, nullptr); + + Compiler c(s); + // 局部表的字段函数(根名是局部变量,走普通字段访问) + EXPECT_NO_THROW(c.CompileString("function f()\n local t = {}\n t.concat = function(a, b) return a .. b end\n" + " local g = t.concat\n return g(\"x\", \"y\")\nend\n", + {})); + // local 遮蔽原生函数名 + EXPECT_NO_THROW(c.CompileString("function f()\n local type = 5\n return type + 1\nend\n", {})); + // 模块常量(math.pi / string.charpattern 等不在原生函数注册表里) + EXPECT_NO_THROW(c.CompileString("function f()\n return math.pi\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n return string.charpattern\nend\n", {})); + // 直接调用位(含点号链与字符串冒号方法) + EXPECT_NO_THROW(c.CompileString("function f()\n return table.concat({ 1, 2 }, \"-\")\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n local s = \"hi\"\n return s:upper()\nend\n", {})); + EXPECT_NO_THROW(c.CompileString("function f()\n print(\"x\")\n return 0\nend\n", {})); +} + // Covers SyntaxTreeTypeToString for all enum values (syntax_tree.h lines 55-128). TEST(syntax_tree, SyntaxTreeTypeToString) { ASSERT_EQ(SyntaxTreeTypeToString(SyntaxTreeType::None), "None"); diff --git a/test/test_table.cpp b/test/test_table.cpp index 1d60de6..c1ca623 100644 --- a/test/test_table.cpp +++ b/test/test_table.cpp @@ -37,6 +37,24 @@ TEST(test_table, test_table_array_part) { FakeluaDeleteState(s); } +// 可选参数显式传 nil 与省略等价(Lua 5.4 luaL_optinteger 语义): +// table.concat 的 i/j、table.unpack 的 i/j、table.remove 的 pos。 +// 期望值由 Lua 5.4.9 参考实现校准。 +TEST(test_table, test_table_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./table/test_table_optnil.lua", config); + int64_t res = 0; + Call(s, jit_type, "test_table_optnil", res); + EXPECT_EQ(res, 75LL); + } + + FakeluaDeleteState(s); +} + TEST(test_table, test_table_native_array_part) { State *s = FakeluaNewState(); ASSERT_NE(s, nullptr); diff --git a/test/test_utf8.cpp b/test/test_utf8.cpp index 781bcb6..0d6911d 100644 --- a/test/test_utf8.cpp +++ b/test/test_utf8.cpp @@ -186,3 +186,18 @@ TEST(test_utf8, test_utf8_boundary_error) { FakeluaDeleteState(s); } + +TEST(test_utf8, test_utf8_optnil) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + + for (auto jit_type: AllJitTypes()) { + CompileFile(s, "./utf8/test_utf8_optnil.lua", config); + double res = 0; + Call(s, jit_type, "test_utf8_optnil", res); + EXPECT_NEAR(res, 691, 0.5); + } + + FakeluaDeleteState(s); +} From 28bbe1cbe2c4290d88a85722b74b0d838858b99a Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 15 Oct 2026 08:45:04 +0800 Subject: [PATCH 2/5] fix: bound declared parameter count before padding call args Removing the argument-count gate in CallByNameImpl left nothing between "the callee declares N parameters" and "write N slots into temp_arg_arr", which is a fixed 32-entry stack array. A callee declaring 33 parameters overflowed it: ASAN caught a 16-byte stack-buffer-overflow at vm.cpp:171 while running vm_cvar_call.parameter_limit_exceptions. Both marshalling sites now reject a declared parameter count above kMaxFunctionInputParams before touching the array, matching the guard FlCallClosure already had in src/state/fakelua.cpp:549. The CGen runtime header had the same latent write (c_runtime_header.h:452) and gets the same guard. Found by the PR #32 asan job. --- src/compile/c_runtime_header.h | 5 +++++ src/jit/vm.cpp | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/src/compile/c_runtime_header.h b/src/compile/c_runtime_header.h index e8ebeec..66ed557 100644 --- a/src/compile/c_runtime_header.h +++ b/src/compile/c_runtime_header.h @@ -392,6 +392,11 @@ static inline CVar FlCallClosure(State *state, CVar cl_var, int arg_num, ...) { const bool is_vararg = cl->is_vararg; const int expected_arg_count = cl->expected_arg_count; const int fixed_arg_count = is_vararg ? expected_arg_count - 1 : expected_arg_count; + // 形参个数也要限:下面按 expected_arg_count 往 temp_arg_arr(定长 32)里写, + // 声明了超过上限的形参会栈缓冲溢出(与 vm.cpp 的 CallByNameImpl 一致)。 + if (UNLIKELY(expected_arg_count < 0 || expected_arg_count > (int)kMaxFunctionInputParams)) { + FakeluaThrowError(state, "too many arguments"); + } CVar raw_arg_arr[kMaxFunctionInputParams]; va_list args_list; diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index 29f54e9..f2a206e 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -115,6 +115,12 @@ static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg ThrowFakeluaException(std::format("FakeluaCallByName: too many arguments ({}) passed for function '{}', max is {}", arg_num, name, kMaxFunctionInputParams)); } + // 形参个数也要限:下面按 expected_arg_count 往 temp_arg_arr(定长 32)里写, + // 声明了超过上限的形参会越界(FlCallClosure 里同样有这层保护)。 + if (UNLIKELY(expected_arg_count > static_cast(kMaxFunctionInputParams))) { + ThrowFakeluaException(std::format("FakeluaCallByName: function '{}' declares too many parameters ({}), max is {}", name, expected_arg_count, kMaxFunctionInputParams)); + } + // 展开 Multi / 补齐参数 const CVar *arg_arr = nullptr; const bool last_is_multi = (arg_num > 0 && raw_arg_arr[arg_num - 1].type_ == static_cast(VarType::Multi)); From 884893aa7069af5a07a4920658a36b07934005ba Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 15 Oct 2026 11:46:40 +0800 Subject: [PATCH 3/5] fix: remove accidental iteration plan, support nil sep in table.concat, and guard negative expected args --- ITERATION_PLAN.md | 357 --------------------------- src/jit/vm.cpp | 2 +- src/native/table/native_table.cpp | 11 +- test/lua/table/test_table_optnil.lua | 4 + test/test_table.cpp | 2 +- 5 files changed, 13 insertions(+), 363 deletions(-) delete mode 100644 ITERATION_PLAN.md diff --git a/ITERATION_PLAN.md b/ITERATION_PLAN.md deleted file mode 100644 index 5d7b1de..0000000 --- a/ITERATION_PLAN.md +++ /dev/null @@ -1,357 +0,0 @@ -# fakelua 下一轮迭代清单 - -> 基于 master @ `a55a4bf`(已与 origin/master 同步)的代码审计与实测。 -> 每项都标注了代码位置、验证方式和建议执行顺序。 - ---- - -## P0″ · 本轮 bug 排查修复(2026-10-14,未提交) - -以真实 Lua 5.4.9 作 oracle 做三后端差分测试,修复 4 组语义 bug。 -差分脚本与用例在 `/tmp/flaudiff/`(`cmp_local.py` + `cases*.lua`), -oracle 是本地编译的 `/tmp/lua-5.4.9/src/lua`。 - -### Bug 1 · 原生函数当值使用(编译期报错缺失) - -`local t = type; t(123)`、`pcall(table.concat, t)`、`{cb = string.format}` 在 -运行时才报误导性错误("attempt to call a non-function value")。 -原生函数由编译期按名派发,不是一等值。 - -**修复**:`semantic_analysis.cpp` 的 `check_simple_var` 与 `case Var:` 的 kDot 链 -检查里,命中 `FindNativeFunction` 时直接给编译期明确报错;`math.pi`、 -`string.charpattern` 等常量不受影响。回归测试见 `test_syntax_tree.cpp` 的 -`semantic_native_function_as_value_*`。 - -### Bug 2 · 可选参数显式 nil 被拒 - -`table.concat(t, "-", nil, nil)` 报 "bad argument #3 (number expected)", -而 5.4 的 `tconcat` 用 `luaL_optinteger`,显式 nil 与省略等价。 -逐函数对照 5.4.9 源码修正 10 处(`checkinteger` 的保持报错): -`table.concat/unpack`、`string.byte/sub-j/rep-sep/find-init/match-init`、 -`utf8.codepoint/len/offset-i`。回归测试:`test_*_optnil.lua` 三份。 - -### Bug 3 · 函数调用实参个数语义(三道 arity 门,全部与 Lua 不符) - -Lua 允许实参个数与形参不一致:少传的形参为 nil,多传的实参求值后丢弃。 -fakelua 有三道门分别拒绝,动态调用路径(`FlCallClosure`)却已是 Lua 语义 —— 同一个 -调用经变量转发能过、直接调用报错,语言内部不自洽。 - -| 门 | 位置 | 现象 | 修复 | -|---|---|---|---| -| 编译期直接调用检查 | `c_gen.cpp:CompileFunctioncall` | `f(1)` 报 "wrong number of arguments" | 删除检查 | -| 数学特化直调 | `c_gen.cpp:TryCompileSpecDirectCall` | `add(1,2,3)` 泄漏 TCC 原始错误 `:N: error: too many arguments to function` | 参数个数不符时回退普通路径 | -| 按名分发运行时检查 | `vm.cpp:CallByNameImpl` | `FakeluaCallByName` 报 "expects N argument(s)" | 删除检查(紧跟其后的循环本就补 nil/截断) | - -配合修改 `BuildLocalFunctionCall` 非 vararg 分支:少传补 `kNil`、多传截断, -展开位置超出形参个数时丢弃展开值;`FlUnboxMulti` 越界本来就是 nil。 - -**契约变化**:`FakeluaCallByName` 不再对参数个数不符抛异常(Bug M 担心的读未初始化 -栈内存,由派发前填满 `temp_arg_arr` 保证)。`test_runtime.cpp` 的 -`vm_call_by_name_error_cases` 与 `vm_cvar_call.multi_return_expansion` 已相应改为 -断言补齐/截断语义(含"补的是 nil"的断言)。 - -三个原断言"少传参数=编译错误"的异常测试(`spec_call_arg_count_error`、 -`math_spec_too_few_args`、`no_arg_call`)已改为 Lua 语义:编译通过 + -`Call` 时抛 "attempt to perform arithmetic on non-numeric value"。 - -### Bug 4 · interp 后端 CALLNAME 误判文件级局部变量 - -`local gv = f; gv(1)` 在 interp 下报 `FakeluaCallByName: function 'gv' not found`。 -`codegen.cpp` 的 `file_level_func` 只看 `defining_func == nullptr`,把文件级 -local **变量** 也当成文件级 local **函数**,而 `named_protos_` 只登记函数定义。 -改为同时要求 `def_node->Type() == LocalFunction`。C 后端此处按闭包处理,本就正确。 - -### 连带修复 · 原生函数必填参数缺失静默通过 - -上面 Bug 3 移除运行时检查后,缺参调用真正走进原生函数了,暴露出既有写法 -`if (n < 1) return nil;` 把「缺失」与「显式 nil」混为一谈(Lua 下 `type()`、 -`string.byte()`、`table.concat()` 都报错)。已按 5.4.9 把 25 处必填参数的守卫 -改为 `ThrowBadArgument`:`type/tostring/tonumber`、`string.{len,rep,reverse, -lower,upper,byte,format,find,match,gmatch,gsub,dump,pack,packsize,unpack,load}`、 -`table.{insert,remove,concat,unpack,move,sort}`、`math.{max,min}`、 -`utf8.{codepoint,codes,len,offset}`。`table.create`/`loadfile`/`dofile` 的 -缺参默认值是 fakelua 自有语义,保持不动。 - -### 验证 - -- 新增 `test/lua/basic/test_basic_call_arity.lua` + `test_basic.call_arity` - (期望 92,三后端 × 直接调用/闭包变量/按名分发/多值展开/vararg/原生函数)。 -- 差分 `cases9_arity.lua` 35 例、`cases10_arity2.lua` 12 例、 - `cases11_missing_args.lua` 35 例:三后端 0 分歧;旧 `cases7` 41 例、 - `cases8` 23 例回归无变化(cases7 的 2 例仅报错文案前缀差异)。 - -### 已发现未修(下轮候选) - -1. **零值返回值在原生快路径被塌缩为 nil**:`tostring(select(2, 9))` 在 Lua 下 - 是把 0 个值传给 `tostring` → 报 "bad argument #1 to 'tostring' (value expected)"; - fakelua 的 `tostring`/`tonumber` 快路径用 `CompileExp(arg)` 不保留 multi, - 零值 multi 变成 nil → 返回 "nil"。修法是快路径保留 multi 并在 - `type_ == VAR_MULTI` 时回退 `FakeluaCallByName`(由它摊平)。 - `#{pick(...)}` 这类表构造路径已正确(0)。 -2. `function obj:m() ... end` 方法定义在 fakelua 不支持 - (`SemanticAnalysis: Unsupported function name with method definition`); - 文件级 `local t = {}` 是常量表,不能改字段。 - ---- - -## P0′ · 数学特化函数非数值参数去优化 ✅ 已完成(2026-10-11) - -### 问题(master `9fd9be6` 实测) - -文件级数学特化函数收到**非数值 math 参数**(string/bool/nil/table,或少传参 -补的 nil)时,TCC/GCC 的入口分发器直接 `FakeluaThrowError`。该异常逃逸生成 -代码后无人捕获 → `terminate due to uncaught exception`,**整个进程 abort**; -INTERP 后端却正常执行函数体。三后端不一致,且 Lua 层 `pcall` 无法拦截。 - -```lua -function f(x) - if type(x) ~= "number" then return -1 end -- 合法的 type 守卫 - return x + 1 -end -f("abc") -- interp: -1;TCC/GCC(master): 进程崩溃 -``` - -### 修复 - -- 每个特化函数额外生成一份 `__dyn` 全动态变体(`CompileFuncBody(..., -1)`, - 与非特化函数同一路径),入口分发器检测到任一 math 参数非 int/float 时 - 分派到它,不再抛错;函数体内真正的算术错误仍走可 pcall 的 Lua 错误路径。 -- 新增 `FakeluaSpecMissException`(`exception.h`):试探性原生数值编译失败 - (`CompileNumericExp` 等 7 处 `ThrowError` 改用 `ThrowSpecMiss`)不再刷 - ERROR 日志——这是「试不了原生就回退动态」的常规控制流;逃逸出 `Build` - 顶层时仍转换为与历史文本一致的硬编译错误。 -- `GenerateDecls` 补 `__dyn` 前向声明。 - -### 验证 - -- 新增 `test/lua/infer/test_spec_deopt.lua` + `infer.test_spec_deopt` - (三后端 × debug 开关;含 type 守卫、多参数、异类返回、pcall、递归、 - C++ 边界 catch)。 -- 差分 `.workbuddy/difftest/cases6_deopt.lua`:32 例 vs Lua 5.4.9,0 分歧; - 旧 5 集 223 例回归,21 分歧全为已知范围外项(字符串比较刻意不做、 - find/next nil 尾参 bug1 暂缓、%q/%c 版本差异)。 -- 形态断言污染(第二轮回归发现,共 14 个):`__dyn` 全动态体天然含 - IsTrue/CVar/flua_rbt_ 等,使「特化变体中不应出现 X」的负向断言失效。 - 修复方式:测试 helper `StripDynVariantBodies` 下沉进 `InferGetCCode`, - **默认剥离 `__dyn` 定义体**(前向声明与入口分发器保留),仅 - `test_spec_deopt` 以 `keep_dyn_variants=true` 取原始代码;既有 100+ 处 - 形态断言零改动。 - - 教训:全量回归若经 `tail` 管道会截断 gtest 失败汇总(曾看到 37 失败 - 却只显示 5 个),**必须重定向到文件再 grep**。 -- infer 套件 220/220 全过(TCC/GCC/interp × debug on/off); - 全量 1500+ 测试:除 mysql integration(需真实数据库,环境基线)外全过。 - ---- - -## P0 · 修复 macOS 上测试套件的可用性 ⚠️ 实现在 PR #25,**已被用户关闭,勿重做** - -> 状态澄清(2026-10-11 核实):以下方案曾完整实现于远端分支 -> `fix/jit-compile-cache-and-reproducible-codegen`(PR #25,`76caa37`), -> 但 PR 被**用户故意关闭**,代码从未合入 master(master 至今仍硬编码 -> `static void * _S = (void*)`,无 gcc_cache)。本地全量 GCC 测试 -> 每个用例冷 dlopen ~4.5–5.4s 是常态,全量约 40 分钟。**不要重新提交该 PR。** - - - -### 问题 - -在 macOS 上单个 gtest 用例耗时 **30.5 秒**(本应是毫秒级),全量测试实际无法在本地跑完。 - -### 根因(已隔离复现,非推测) - -`GccJitter::Compile` 的流程是 `fork()` → `execvp("gcc")` → `waitpid()` → `dlopen()`, -每次编译生成一个**全新路径的临时 dylib**。实测数据: - -| 场景 | 耗时 | -|---|---| -| 裸 `gcc -O3 -dynamiclib` | 0.06 s | -| 首次 `dlopen` 某新路径 dylib(4 个路径实测) | **4521 / 4504 / 5437 / 4608 ms** | -| 后续 `dlopen` 同一路径 | 0.2 ms | - -**每一个新 dylib 路径的首次加载都要付约 4.5–5.4 秒**,与文件内容无关, -纯粹是 macOS 代码签名验证(amfid / syspolicyd IPC)的固定开销。差值达 25000 倍。 - -排查中排除的两个假设(避免后来者重走): -- **ad-hoc 签名无效**:实测 `codesign -s -` 为 6312/5106/5222 ms,`-Wl,-adhoc_codesign` - 为 4852/4753/4672 ms,**都不比基线好**。这条路不通。 -- **fork+dlopen 组合本身有问题**:写隔离复现程序跑 15 轮全部秒过,假设不成立。 - -真正起作用的验证是「同路径 vs 新路径」的对照测量。 - -### 顺带发现的两个真实 bug - -1. **生成的 C 代码硬编码了 State 指针**(`c_gen.cpp:451`): - ```c - static void * _S = (void *) 0x1014b2aa0; // ← 地址受 ASLR 影响,每次不同 - ``` - 这导致同一输入编译出不同字节 —— 构建不可复现,且**产物一旦被跨 State 复用, - `_S` 会指向别的 State**。已改为初值 0 + 加载后注入(见下)。 - -2. **`FAKELUA_BUILD_TESTS` 默认为 OFF**,导致 `ctest` 找不到任何测试 - (`ctest --test-dir build` 输出 `No tests were found!!!`)。本地 build 目录里 - `unit_tests` 二进制可能停留在几天前的版本,造成"改了代码却看不到效果"的假象。 - **这条坑了我很久**:连续多轮"验证失败"其实都在跑 3 天前的旧二进制。 - -### 最终实现 - -- **State 指针改为运行时注入**:`c_gen.cpp` 生成 - `static void * _S = 0;` + `void __fakelua_set_state(void*)`, - GCC 与 TCC 两条路径在加载产物后、调用 `__fakelua_init` 之前注入。 - (注意 TCC 以 C89 模式编译,**不能用 `nullptr`**。) -- **按内容缓存编译产物**:`src/jit/gcc_cache.{h,cpp}` 以 - 「生成的 C 代码 + 全部编译参数」的 xxhash-64 摘要为键,命中即复用同一路径, - 从而命中 dyld 的路径缓存。写入用「临时文件 + rename」保证原子性。 -- **可关闭**:`StateGCCConfig::enable_compile_cache = false` 恢复旧行为。 - -实测效果(`closure` 套件,11 个测试): - -| | 耗时 | -|---|---| -| 修复前 | 487 s | -| 冷缓存首次 | 96 s(`_S` 修复带来的编译去重已生效) | -| 热缓存 | **13 s** | - -单个用例:35 s → **0.106 s**。 -`test_basic/test_math/test_string/test_table/syntax_tree/vm_cvar_call/runtime/state/var/util` -共 387 个测试热缓存下 106 s,全部通过。 - -### 遗留 - -- 缓存目录 `$TMPDIR/fakelua-jit-cache` **不会自动清理**,需要手动删除。 - 刻意不放在 `$TMPDIR/fakelua/` 下 —— 那个目录会被 - `test_runtime.cpp` 的 `runtime.generate_tmp_filename_creates_dir` 用 `remove_all` 清掉。 -- CI 三个 workflow 仍无超时保护,建议补 `--gtest_timeout` 或外部 timeout。 - ---- - -## P1 · VarTable 增加数组部分(性能收益最大) - -### 问题 - -`src/var/var_table.h:11-43` 的 `VarTable` 只有 `quick_data_[8]` + 哈希桶, -**没有 Lua 那样的连续数组部分**。因此整数键读写每次都要: -类型检查 → `spec_get` 间接调用判断 → `count_ == 0` 检查 → 算 hash → 判断 `bucket_count_` → **遍历桶链比对**(`c_runtime_header.h:944-976`)。 - -### 为什么这是主要瓶颈 - -实测 Sieve 生成的 C 代码里,**算术已经 100% 原生化** -(`FL_INT_MUL` / `FL_INT_ADD` / int64_t 循环变量),GCC 版 Sieve/5000 = 213μs -甚至比 C++ 参考实现快 2.68 倍。**唯一剩余瓶颈就是表访问。** -Lua 5.4 正是靠数组部分赢的。 - -「数值特化」这条路已经走到尽头,下一轮收益在表访问而非算术。 - -### 方案 - -给 `VarTable` 增加 `CVar* arr_; uint32_t arr_len_;`: - -- `FlSetTableIntImpl` 检测 `k == arr_len_ + 1` 时 append -- `FlGetTableInt` 命中 `1 <= k <= arr_len_` 时直接 `arr_[k-1]` -- 删除时保守退回哈希(只增不删最简单) - -### 代价与风险 - -`var_table.h:8` 明确注释「layout must match exactly」—— -`sizeof(VarTable)` 变化必须同步 `c_runtime_header.h` 里的镜像定义。 -**布局不同步会导致难以排查的内存问题。** - -### 预期收益 - -Sieve / BubbleSort / BinarySearch / Vector3 的**共同**瓶颈, -`for i=2,n do is_prime[i]=true end` 是教科书式顺序填充,能立刻吃满。预计 **2–4x**。 - ---- - -## P2 · spec table 字段按推断类型降为原生标量 - -### 问题 —— 「已经算出来却丢弃」的信息 - -`type_inferencer` 确实算出了 spec 字段的类型,但随后被显式丢弃: - -- `type_inferencer.cpp:1771` — `f.type = T_DYNAMIC; // 类型由 CGen 在 emit 时重新推导` -- `c_gen.cpp:411` — `EmitSpecTypeBoilerplate` 无条件写 `CVar` -- `c_gen.cpp:838` — `GetSpecFieldType()` **全仓库唯一定义、零调用**(死代码,坐实了类型被丢弃) - -### 实测后果 - -```c -// local p = {x=i, y=i*2, z=i*3}; total = total + p.x + p.y + p.z -CVar total = (CVar){.type_ = VAR_INT, .data_.i = 0}; // 本该是 int64_t -for (...) { - OpAdd((total), (FL_SPEC(spec_x, p, _s_x)), flua_op_20); // 每次重新 dispatch - ... -} -``` - -对比**同一文件**中未被 spec 化的 `s = s + i`:`s = FL_INT_ADD((s), (i));` —— 纯原生、无装箱。 -`OpAdd`(`c_runtime_header.h:1154-1158`)每次执行 2 次 `CheckNum` + 1 个分支 + 一次 16 字节 CVar 写回。 - -### 方案 - -1. `BuildCtorFields` 改为读 `main_eval_types` 的真实推断类型,替换 1771 行的硬编码 -2. `EmitSpecTypeBoilerplate` 按 `f.type` 发 `int64_t` / `double` / `CVar` -3. `FL_SPEC` 读取点据 `GetSpecFieldType` 决定返回原生表达式还是 CVar - -### 关键难点 - -**双写一致性**:`FL_SET_SPEC`(`c_runtime_header.h:686-691`)与 spec_set 回调 -(`c_gen.cpp:287`)必须做「原生字段 ↔ spec_vals[]」双写, -否则 `pairs` / 动态 key 读到的会是脏值。这是本项唯一的技术风险, -必须配合 optional 字段的 nil 语义仔细处理。 - -### 预期收益 - -table 密集的数值场景预计 **1.3–1.8x**。因为是「已算出来却丢弃」的信息,ROI 最高。 - ---- - -## P3 · interp 与 GCC 后端的行为一致性 - -`src/interp/` 与 `JIT_GCC` 的**语言特性覆盖基本等价**: -32 个 AST 节点、47 个 opcode、20 个二元 + 4 个一元运算符全部对齐; -标准库**完全共享** `src/native/`,无后端差异。 -真正的 interp 独有缺口只有 4 个,严重度都不高,但都会导致 -「只在 interp 下跑通的脚本在 GCC 下失败」这类问题: - -| # | 问题 | 位置 | 严重度 | 状态(2026-10-11 复测) | -|---|---|---|---|---| -| 1 | `local` 与全局常量名冲突未检查 —— GCC 编译报错,interp 静默通过 | 已移入共享阶段 `semantic_analysis.cpp:1047`(`CheckLocalVar`) | 中 | ✅ 已修复,三后端同文报错 | -| 2 | `GETTABLE` 读路径漏 NaN 检查 —— `t[0/0]` 读不报错但写报错,读写不对称 | 三后端现已收敛一致 | 中低 | ⚠️ 后端间已一致(都报 `table index is NaN`,pcall 可拦),但**读路径比 Lua 5.4 严格**:Lua 读 `t[NaN]` 合法返回 nil,仅写报错;若要严格对齐需放开 GETTABLE 的 NaN 读 | -| 3 | 寄存器宽度硬上限 256(CGen 无此限制) | `codegen.cpp:34-36`、`opcode.h:66-69` | 中低 | 未复测 | -| 4 | 深递归栈溢出错误类型不一致(init 内干净报错 / init 外变 `bad_alloc`) | `interpreter.cpp:75-80` | 低 | 未复测 | - -### 测试盲区 - -- **全部 46 个测试文件中 `JIT_INTERP` 出现次数为 0** —— 覆盖全靠 `AllJitTypes()` 间接循环 -- `test_state.cpp` / `test_runtime.cpp` 把后端写死(TCC 15 次 / GCC 7 次),**排除了 interp**, - 而 interp 恰是唯一依赖 `IsInterpClosure` 魔数判定的后端 -- `test/lua/` 下 **978 个 `.lua` 语料中 724 个从未被任何测试驱动** - (`test_jitter.cpp` 只引用 254 个),这些文件在任何后端下都没被执行过 - -### 建议 - -为缺口 1 和 2 补**定向断言**(明确断言 interp「应该」和 GCC「应该」的行为一致), -并把 `test_state.cpp` / `test_runtime.cpp` 改用 `AllJitTypes()`。 - ---- - -## 明确不要做的方向 - -| 方向 | 原因 | -|---|---| -| **优化 TCC 后端** | 实测 TCC 在 BubbleSort/Sieve/InsertionSort 上**快于 Lua 5.4 达 2.3–2.8x**,只是比 GCC 慢 4–7x。同一份 C 代码 GCC 快 3–30x —— 这是 TinyCC 优化器能力不足(无向量化/跨函数内联),不是 c_gen 的问题 | -| **给 `FL_INT_*` 加溢出检查** | `c_runtime_header.h:1144-1146` 用无符号算术**故意**实现回绕,这正是 Lua 5.4 整数语义。加检查会同时破坏性能和语义 | -| **给 string/bool 加类型维度** | 是刻意的设计边界而非遗漏。string 变长 + 引用计数 + 共享,原生化引入所有权问题;bool 无法用 C `bool` 表达(Lua 中 `0` 和 `""` 都是真值) | -| **为 `and`/`or` 生成 C 的 `\|\|` / `?:`** | 语义不等价 —— Lua 中 `0` 和 `""` 都是真值。必须走完整三值判定 | -| **字符串关系运算符(`<`/`>`/`<=`/`>=`)** | **刻意不做(用户 2026-10-06 拍板)**,非遗漏。fakelua 是静态类型子集,为性能不做任何运行时隐式转换,`"a" < "b"` 报错与 `"10" + 1` 报错是同一条边界。字符串 `==`/`~=` 仍支持(不涉及转换)。差分测试曾把它记为 bug 2,结论作废 | - ---- - -## 其他可清理项 - -- **README 文档过时**:`README.zh.md:114` 仍在展示 `NativeAdd(...)`, - 但全仓库 grep 无此符号 —— 已被 `FL_INT_ADD` 宏(`c_runtime_header.h:1144`)取代。 -- **残留分支**:`fix/open-bugs`(`e2e730b`)内容与 master 的 `a55a4bf` 相同,可删除。 -- **低风险优化**:`c_gen.cpp:4592/4602/4611` 生成的 `SET_NIL(flua_call_N)` 是死存储 - (`t[i]=v` 被预处理重写成函数调用,必然 `return tmp` 一个永不被读取的 nil)。 - Sieve 每轮循环一次。消除它零风险且能改善寄存器压力。 diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index f2a206e..88d966d 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -117,7 +117,7 @@ static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg // 形参个数也要限:下面按 expected_arg_count 往 temp_arg_arr(定长 32)里写, // 声明了超过上限的形参会越界(FlCallClosure 里同样有这层保护)。 - if (UNLIKELY(expected_arg_count > static_cast(kMaxFunctionInputParams))) { + if (UNLIKELY(expected_arg_count < 0 || expected_arg_count > static_cast(kMaxFunctionInputParams))) { ThrowFakeluaException(std::format("FakeluaCallByName: function '{}' declares too many parameters ({}), max is {}", name, expected_arg_count, kMaxFunctionInputParams)); } diff --git a/src/native/table/native_table.cpp b/src/native/table/native_table.cpp index 88ce303..e7cd5d6 100644 --- a/src/native/table/native_table.cpp +++ b/src/native/table/native_table.cpp @@ -946,10 +946,13 @@ void RegisterTableLibraryApi(State *s) { std::string sep = ""; if (n >= 2) { CVar sep_var = inter::GetNativeArg(state, args, n, 1); - // 标准 Lua:table.concat 的 sep 必须是 string(数字会被转换为字符串,这是标准行为) - CheckStringArg(sep_var, 2, "table.concat"); - std::string temp_sep; - sep = std::string(GetStringArgView(sep_var, temp_sep)); + // Lua 5.4 tconcat:sep 是 luaL_optlstring,显式 nil 与省略等价,默认 "" + if (sep_var.type_ != static_cast(VarType::Nil)) { + // 标准 Lua:table.concat 的 sep 必须是 string(数字会被转换为字符串,这是标准行为) + CheckStringArg(sep_var, 2, "table.concat"); + std::string temp_sep; + sep = std::string(GetStringArgView(sep_var, temp_sep)); + } } // Lua 5.4 tconcat 的 i/j 是 luaL_optinteger:显式 nil 与省略等价,取默认值 // (i 默认 1,j 默认 #t)。包装函数转发(f(t, sep, i, j) 少传实参)会传入显式 diff --git a/test/lua/table/test_table_optnil.lua b/test/lua/table/test_table_optnil.lua index 0c2fe3f..a2705aa 100644 --- a/test/lua/table/test_table_optnil.lua +++ b/test/lua/table/test_table_optnil.lua @@ -19,6 +19,10 @@ function test_table_optnil() total = total + #concat_wrap(t, "-", nil, 3) -- "1-2-3" = 5 total = total + #table.concat(t, "-", nil, nil) -- 同上直接调用 = 9 + -- table.concat:sep 显式 nil 与省略等价,默认 ""(luaL_optlstring 语义) + total = total + #concat_wrap(t, nil) -- "12345" = 5 + total = total + #table.concat(t, nil, 2, 4) -- "234" = 3 + -- table.unpack:i 默认 1,j 默认 #t(取前三个值相加) total = total + unpack_wrap(t, nil, nil) -- 1+2+3 = 6 total = total + unpack_wrap(t, 2) -- 2+3+4 = 9 diff --git a/test/test_table.cpp b/test/test_table.cpp index c1ca623..b1861c3 100644 --- a/test/test_table.cpp +++ b/test/test_table.cpp @@ -49,7 +49,7 @@ TEST(test_table, test_table_optnil) { CompileFile(s, "./table/test_table_optnil.lua", config); int64_t res = 0; Call(s, jit_type, "test_table_optnil", res); - EXPECT_EQ(res, 75LL); + EXPECT_EQ(res, 83LL); } FakeluaDeleteState(s); From 0fedf95253d516f01773d8067e168e67ff5f02a8 Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 15 Oct 2026 12:36:07 +0800 Subject: [PATCH 4/5] docs: clarify FakeLua engineering design philosophy and strict function arity --- README.md | 16 +++++++++++++++- README.zh.md | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0530fb1..8c49398 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,18 @@ To support this positioning, FakeLua **does not implement a complex dynamic garb This design allows FakeLua to fully eliminate GC pause impact on frame rates while maintaining JIT native execution speed, keeping memory overhead at a completely predictable, extremely low level. +### 3. Engineering & Language Philosophy: Deliberate Trade-offs, Not Blind Cloning +FakeLua is not a blind clone of standard Lua's overly permissive dynamic features. Instead, it makes conscious, intentional trade-offs for **execution performance**, **large-scale engineering collaboration**, and **long-term maintainability**: + +* **Strict Function Arity Enforcement**: + In standard Lua, calling a fixed-parameter function with too few arguments silently pads `nil`, and extra arguments are silently discarded. In large-scale, multi-developer projects, this looseness easily conceals bugs caused by signature changes, omitted arguments, or parameter mismatches. FakeLua deliberately tightens this rule: + - Direct calls are strictly validated at **compile time**, rejecting mismatched argument counts with a compile error; + - Dynamic / named calls are strictly intercepted at **runtime**, throwing an exception if argument count differs; + - Functions legitimately expecting variable arguments must explicitly declare varargs (`...`). Strict arity keeps interfaces explicit, prevents silent runtime nil pollution, and enables the JIT compiler to emit direct, zero-overhead native C calls without redundant runtime nil padding or truncation boilerplate. +* **No Implicit Globals**: Variables must be explicitly declared with `local`; undeclared assignments trigger compile errors, preventing accidental global pollution from typos. +* **No Implicit Type Coercion**: Arithmetic operators do not implicitly convert strings to numbers (`"10" + 1` errors), keeping types predictable and enabling aggressive native numeric specialization. +* **Static Native Dispatch**: Built-in C++ functions are statically dispatched by name at compile time rather than wrapped as first-class closure values, maximizing call performance. + ## Core Features ### Three Execution Backends @@ -130,14 +142,16 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Lua 5.4 pattern matching**: `string.find`/`match`/`gmatch`/`gsub` use a self-contained Lua-pattern engine (`%d`/`%w` classes, custom sets, lazy `-`, captures, frontier `%f[set]`, balanced `%bxy`), fully compatible with standard Lua. - **String algorithms**: `string.trim`/`trim_left`/`trim_right`/`split`/`join`/`replace`/`starts_with`/`ends_with`/`contains`/`iequals`/`icontains`/`istarts_with`/`iends_with` via Boost.Algorithm. -### Not Supported +### Not Supported / Differences from Standard Lua +- **Strict function arity**: Fixed-parameter functions require exact argument-to-parameter count matching. Calling with too few or too many arguments triggers compile-time or runtime errors instead of silently padding nil or discarding extras (use `...` for variadic arguments). - **Coroutines**: No `coroutine.create`/`resume`/`yield` support. - **Metatables**: No `__index`, `__newindex`, metamethods, or operator overloading. - **`require`/`module`**: No standard module system (replaced by `package "Name"` mechanism). - **`rawequal`/`rawget`/`rawset`/`rawlen`**: Meaningless without metatables. - **Debug library**: No `debug.*` standard library. - **Implicit type coercion**: No string→number conversion in arithmetic (`"10" + 1` errors). +- **Implicit globals**: Local variables must be declared with `local`; implicit global declarations are rejected. ## Built-in Standard Libraries diff --git a/README.zh.md b/README.zh.md index ee1d9ca..795fa4d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -26,6 +26,18 @@ FakeLua 的设计初衷是为了在**高性能游戏服务器**或类似的实 这种设计使得 FakeLua 在保持 JIT 原生执行速度的同时,能够彻底消除垃圾回收停顿(GC Pause)对帧率的影响,让内存开销保持在一条完全可预测的、极低的水平线上。 +### 3. 工程化与语言设计哲学:严谨取舍,拒绝无脑照搬 +FakeLua 并非无脑照搬标准 Lua 的所有动态弱约束特性,而是在**执行性能**、**大规模工程协同**和**代码易维护性**上做出了有意识的取舍: + +* **定参函数实参个数严格匹配(Strict Arity Enforcement)**: + 标准 Lua 在调用定参函数时,少传参数会自动补 `nil`,多传参数会被默默丢弃。在大规模多人协作项目中,这种过度宽松的特性极易掩盖接口签名变更、遗漏必要传参或参数错位等隐蔽 bug。FakeLua 刻意严格收紧该语义: + - 直接调用在**编译期**严格校验参数个数,不一致直接报编译错误; + - 动态/按名分发在**运行期**严格拦截,实参个数不匹配直接抛出异常; + - 需要变长实参时必须显式声明可变参数(`...`)。严格匹配不仅让接口契约清晰透明、杜绝隐蔽缺陷,也让 JIT 能够生成最纯粹高效的原生 C 调用,省去运行时动态补 nil 和截断的冗余指令。 +* **杜绝隐式全局变量**:变量必须显式使用 `local` 声明,未声明直接赋值会在编译期拦截,彻底消除拼写错误带来的全局污染。 +* **杜绝隐式类型转换**:算术运算不做字符串到数字的隐式转换(如 `"10" + 1` 明确报错),确保计算意图可预测,支撑编译器激进的原生数值特化。 +* **原生函数静态分发**:C++ 原生函数作为高效宿主接口,在编译期按名静态分发而非作为第一类值闭包,大幅提升热点调用性能。 + ## 核心特性 ### 三种执行后端 @@ -130,14 +142,16 @@ FL_SPEC(Table_Spec_1, point, x) = NativeAdd(FL_SPEC(Table_Spec_1, point, x), (CV - **Lua 5.4 模式匹配**:`string.find`/`match`/`gmatch`/`gsub` 使用自包含的 Lua 模式引擎(`%d`/`%w` 字符类、自定义集合、惰性 `-`、捕获、前沿 `%f[set]`、平衡 `%bxy`),与标准 Lua 完全兼容。 - **字符串算法**:`string.trim`/`trim_left`/`trim_right`/`split`/`join`/`replace`/`starts_with`/`ends_with`/`contains`/`iequals`/`icontains`/`istarts_with`/`iends_with` 底层使用 Boost.Algorithm。 -### 未支持 +### 未支持 / 与标准 Lua 的设计差异 +- **函数实参个数严格匹配**:定参函数的实参与形参个数必须严格一致,少传或多传参数会在编译期/运行时直接报错,杜绝标准 Lua 隐式补 nil 或静默截断掩盖的接口 bug(变长参数请显式声明 `...`)。 - **协程(Coroutine)**:不支持 `coroutine.create`/`resume`/`yield`。 - **元表(Metatable)**:不支持 `__index`、`__newindex` 等元方法。 - **`require`/`module`**:无标准模块系统(由 `package "Name"` 机制替代)。 - **`rawequal`/`rawget`/`rawset`/`rawlen`**:因无元表,这些函数无意义。 - **debug 标准库**:不支持 `debug.*`。 - **隐式类型转换**:算术中不做字符串→数字的隐式转换(`"10" + 1` 会报错)。 +- **隐式全局变量**:局部变量必须使用 `local` 声明,不支持隐式全局赋值。 ## 标准内置扩展库 From dd19597b9f7c7a0904bd60f732bab6b2ec5d55ac Mon Sep 17 00:00:00 2001 From: esrrhs Date: Thu, 15 Oct 2026 12:57:23 +0800 Subject: [PATCH 5/5] fix: revert loose arity and enforce strict argument count matching --- src/compile/c_gen.cpp | 42 ++++++------- src/jit/vm.cpp | 5 +- test/lua/basic/test_basic_call_arity.lua | 78 ------------------------ test/lua/string/test_string_optnil.lua | 2 +- test/lua/table/test_table_optnil.lua | 12 ++-- test/lua/utf8/test_utf8_optnil.lua | 2 +- test/test_basic.cpp | 16 ----- test/test_exception.cpp | 58 ++++++------------ test/test_runtime.cpp | 28 +++------ 9 files changed, 56 insertions(+), 187 deletions(-) delete mode 100644 test/lua/basic/test_basic_call_arity.lua diff --git a/src/compile/c_gen.cpp b/src/compile/c_gen.cpp index ecdb7b4..8ca89f1 100644 --- a/src/compile/c_gen.cpp +++ b/src/compile/c_gen.cpp @@ -3390,9 +3390,20 @@ std::string CGen::CompileFunctioncall(const SyntaxTreeInterfacePtr &functioncall std::string call_expr; bool is_local_callee = BindsLocalValue(var_ptr); - // Lua 语义:被调函数形参个数与实参个数无须一致——少传的形参在函数体内为 nil, - // 多传的实参求值后丢弃。直接调用路径统一在 BuildLocalFunctionCall 里做 nil 补齐/ - // 截断(与 FlCallClosure 运行时分发路径的行为一致),此处不再做编译期精确个数检查。 + if (!is_local_callee && local_func_names_.contains(func_name)) { + const auto &info = local_func_names_.at(func_name); + if (!info.is_vararg && !has_expansion) { + if (static_cast(compiled_args.size()) != info.params_count) { + ThrowError(std::format("wrong number of arguments to '{}': expected {}, got {}", func_name, info.params_count, compiled_args.size()), functioncall); + } + } + if (!info.is_vararg && has_expansion) { + int expanded_count = static_cast(compiled_args.size()) + (info.params_count - expansion_start_idx); + if (expanded_count != info.params_count) { + ThrowError(std::format("wrong number of arguments to '{}': expected {}, got {}", func_name, info.params_count, expanded_count), functioncall); + } + } + } if (local_func_names_.contains(func_name) && !is_local_callee) { call_expr = BuildLocalFunctionCall(func_name, compiled_args, has_expansion, expansion_tmp, expansion_start_idx); @@ -4120,9 +4131,9 @@ std::string CGen::TryCompileSpecDirectCall(const std::shared_ptr(args.size()) < expected_params) { - while (static_cast(args.size()) < expected_params) { - args.push_back("kNil"); + for (int i = expansion_start_idx; i < expected_params; ++i) { + args.push_back(std::format("FlUnboxMulti({}, {})", expansion_tmp, i - expansion_start_idx)); } - } else if (static_cast(args.size()) > expected_params) { - args.resize(expected_params); } } std::string call_expr = (info.c_symbol_name.empty() ? func_name : info.c_symbol_name) + "(NULL"; diff --git a/src/jit/vm.cpp b/src/jit/vm.cpp index 88d966d..bdf1b69 100644 --- a/src/jit/vm.cpp +++ b/src/jit/vm.cpp @@ -171,8 +171,9 @@ static CVar CallByNameImpl(State *state, int jit_type, const char *name, int arg vararg_cvar.data_.m = m; temp_arg_arr[fixed_arg_count] = vararg_cvar; } else { - // Lua 语义:实参个数与形参无须一致——少传的形参补 nil,多传的实参 - // 丢弃(下方循环只取前 expected_arg_count 个)。与 FlCallClosure 一致。 + if (UNLIKELY(!last_is_multi && flat_count != expected_arg_count)) { + ThrowFakeluaException(std::format("FakeluaCallByName: function '{}' expects {} argument(s), got {}", name, expected_arg_count, flat_count)); + } for (int i = 0; i < expected_arg_count; ++i) { temp_arg_arr[i] = i < flat_count ? flat_args_buf[i] : (CVar){static_cast(VarType::Nil)}; } diff --git a/test/lua/basic/test_basic_call_arity.lua b/test/lua/basic/test_basic_call_arity.lua deleted file mode 100644 index 042d7c7..0000000 --- a/test/lua/basic/test_basic_call_arity.lua +++ /dev/null @@ -1,78 +0,0 @@ --- 函数调用实参个数语义(对齐 Lua 5.4):实参少于形参时缺失形参为 nil, --- 实参多于形参时多出的实参求值后丢弃。覆盖各条调用路径: --- 1. 同文件 local function 直接调用(CGen 直接调用 / interp CALLNAME) --- 2. 闭包变量调用(FlCallClosure / interp CALL) --- 3. 文件级 function(数学特化直调、FakeluaCallByName 按名分发) --- 4. 多值展开(返回值个数少于/多于形参) --- 5. vararg(固定参数补 nil,多余实参进入 ...) --- 6. 原生函数(多传丢弃) -local function two(a, b) - return (a or 0) + (b or 0) -end - -local function one(a) - return a or 0 -end - -local function va(a, ...) - return (a or 0) * 10 + select("#", ...) -end - -local closure_var = two - -local function ret1() - return 7 -end - -local function ret3() - return 1, 2, 3 -end - --- 文件级函数,会被数学特化 -function double_arity(n) - return n * 2 -end - -function add2_arity(x, y) - return x + y -end - -function test_basic_call_arity() - local total = 0 - - -- 1. local function 直接调用:少传补 nil,多传丢弃 - total = total + two(1) -- 1 - total = total + two(1, 2) -- 3 - total = total + two(1, 2, 100) -- 3 - total = total + one(5, 6, 7) -- 5 - - -- 2. 闭包变量调用 - total = total + closure_var(4) -- 4 - total = total + closure_var(4, 5) -- 9 - total = total + closure_var(4, 5, 100) -- 9 - - -- 3. 文件级函数:数学特化路径多传/精确/少传 - total = total + double_arity(3) -- 6 - total = total + add2_arity(1, 2) -- 3 - total = total + add2_arity(1, 2, 100)-- 3(多传丢弃,不能生成参数个数不符的 C) - total = total + add2_arity(1, 2, 100, 200) -- 3 - - -- 4. 多值展开:返回值个数少于/多于形参 - total = total + two(ret1()) -- 7 + 0 = 7 - total = total + two(ret3()) -- 1 + 2 = 3 - total = total + one(ret3()) -- 1 - total = total + two(1, ret3()) -- 1 + 1 = 2(ret3 其余返回值丢弃) - total = total + one(1, ret3()) -- 1(形参已填满,展开值丢弃) - - -- 5. vararg:固定参数补 nil,多余实参进入 ... - total = total + va(1) -- 10 + 0 = 10 - total = total + va(1, 2, 3) -- 10 + 2 = 12 - total = total + va() -- 0 + 0 = 0 - total = total + va(nil, 1) -- 0 + 1 = 1 - - -- 6. 原生函数:多传丢弃 - total = total + math.floor(1.9, 99) -- 1 - total = total + #string.rep("ab", 2, "-", "x") -- 5 - - return total -end diff --git a/test/lua/string/test_string_optnil.lua b/test/lua/string/test_string_optnil.lua index 265f053..82418e2 100644 --- a/test/lua/string/test_string_optnil.lua +++ b/test/lua/string/test_string_optnil.lua @@ -38,7 +38,7 @@ function test_string_optnil() if ok then total = total + 1000 end -- 不应发生:i 为 nil 必须报错 -- string.byte:i 默认 1,j 默认 i(j 给定时返回两个字节) - total = total + byte_wrap("hello", nil) -- 104 + total = total + byte_wrap("hello", nil, nil) -- 104 total = total + byte_wrap("hello", 2, nil) -- 101 total = total + byte_wrap("hello", nil, 2) -- 104+101 = 205 diff --git a/test/lua/table/test_table_optnil.lua b/test/lua/table/test_table_optnil.lua index a2705aa..59d6ebc 100644 --- a/test/lua/table/test_table_optnil.lua +++ b/test/lua/table/test_table_optnil.lua @@ -14,18 +14,18 @@ function test_table_optnil() local t = { 1, 2, 3, 4, 5 } -- table.concat:i 默认 1,j 默认 #t - total = total + #concat_wrap(t, "-") -- "1-2-3-4-5" = 9 - total = total + #concat_wrap(t, "-", 2) -- "2-3-4-5" = 7 - total = total + #concat_wrap(t, "-", nil, 3) -- "1-2-3" = 5 + total = total + #concat_wrap(t, "-", nil, nil) -- "1-2-3-4-5" = 9 + total = total + #concat_wrap(t, "-", 2, nil) -- "2-3-4-5" = 7 + total = total + #concat_wrap(t, "-", nil, 3) -- "1-2-3" = 5 total = total + #table.concat(t, "-", nil, nil) -- 同上直接调用 = 9 -- table.concat:sep 显式 nil 与省略等价,默认 ""(luaL_optlstring 语义) - total = total + #concat_wrap(t, nil) -- "12345" = 5 + total = total + #concat_wrap(t, nil, nil, nil) -- "12345" = 5 total = total + #table.concat(t, nil, 2, 4) -- "234" = 3 -- table.unpack:i 默认 1,j 默认 #t(取前三个值相加) - total = total + unpack_wrap(t, nil, nil) -- 1+2+3 = 6 - total = total + unpack_wrap(t, 2) -- 2+3+4 = 9 + total = total + unpack_wrap(t, nil, nil) -- 1+2+3 = 6 + total = total + unpack_wrap(t, 2, nil) -- 2+3+4 = 9 -- table.remove:pos 省略/nil 取 #t local r = { 10, 20, 30 } diff --git a/test/lua/utf8/test_utf8_optnil.lua b/test/lua/utf8/test_utf8_optnil.lua index 5854aad..becf6dc 100644 --- a/test/lua/utf8/test_utf8_optnil.lua +++ b/test/lua/utf8/test_utf8_optnil.lua @@ -34,7 +34,7 @@ function test_utf8_optnil() -- utf8.offset:n 必填;i 默认 1(n>=0) total = total + offset_wrap(s, 2, nil) -- 第 2 字符起始字节 -> 2 - total = total + offset_wrap(s, 1) -- 1 + total = total + offset_wrap(s, 1, nil) -- 1 total = total + offset_wrap(s, 0, nil) -- 1 -- n 传 nil 必须报错(checkinteger),不能静默取默认 diff --git a/test/test_basic.cpp b/test/test_basic.cpp index 466b8b8..ed03507 100644 --- a/test/test_basic.cpp +++ b/test/test_basic.cpp @@ -1348,19 +1348,3 @@ TEST(test_basic, xpcall_coverage) { FakeluaDeleteState(s); } -// 函数调用实参个数语义:少传补 nil、多传丢弃。三条调用路径(直接调用 / 闭包变量 / -// 按名分发)与三个后端(TCC/GCC/interp)都必须与 Lua 5.4 一致。 -TEST(test_basic, call_arity) { - State *s = FakeluaNewState(); - ASSERT_NE(s, nullptr); - CompileConfig config; - - for (auto jit_type: AllJitTypes()) { - CompileFile(s, "./basic/test_basic_call_arity.lua", config); - int64_t res = 0; - Call(s, jit_type, "test_basic_call_arity", res); - EXPECT_EQ(res, 92LL); - } - - FakeluaDeleteState(s); -} diff --git a/test/test_exception.cpp b/test/test_exception.cpp index bf017f8..bcbe66f 100644 --- a/test/test_exception.cpp +++ b/test/test_exception.cpp @@ -1343,25 +1343,18 @@ TEST(exception, for_loop_zero_step_float) { } } -TEST(exception, spec_call_fewer_args_runtime_throw) { +TEST(exception, spec_call_arg_count_error) { FakeluaStateGuard sg; auto s = sg.GetState(); ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - // Lua 语义:实参少于形参合法(缺失形参为 nil),编译期不报错; - // 函数体内对 nil 形参做算术才在运行时报错。 - CompileFile(s, "./exception/test_spec_call_arg_count_error.lua", {}); - - for (auto jit_type: AllJitTypes()) { - try { - int ret = 0; - Call(s, jit_type, "test_fewer_args", ret, 1); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); - } + try { + CompileFile(s, "./exception/test_spec_call_arg_count_error.lua", {}); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); } } @@ -1487,19 +1480,12 @@ TEST(exception, math_spec_too_few_args) { ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - // 同 spec_call_fewer_args_runtime_throw:少传参数编译通过, - // 数学特化函数回退到通用 CVar 入口后,对 nil 形参做算术才报错。 - CompileFile(s, "./exception/test_math_spec_too_few_args.lua", {}); - - for (auto jit_type: AllJitTypes()) { - try { - int ret = 0; - Call(s, jit_type, "test_fewer_args", ret, 1); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); - } + try { + CompileFile(s, "./exception/test_math_spec_too_few_args.lua", {}); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); } } @@ -1569,18 +1555,12 @@ TEST(exception, no_arg_call) { ASSERT_NE(s, nullptr); SetDebugLogLevel(s, 0); - // compute() 不传实参:编译通过,compute 内 n 为 nil,n * 2 在运行时报错。 - CompileFile(s, "./exception/test_no_arg_call.lua", {}); - - for (auto jit_type: AllJitTypes()) { - try { - int ret = 0; - Call(s, jit_type, "test", ret, 1); - ASSERT_TRUE(false); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - ASSERT_TRUE(std::string(e.what()).find("attempt to perform arithmetic on non-numeric value") != std::string::npos); - } + try { + CompileFile(s, "./exception/test_no_arg_call.lua", {}); + ASSERT_TRUE(false); + } catch (const std::exception &e) { + std::cout << e.what() << std::endl; + ASSERT_TRUE(std::string(e.what()).find("wrong number of arguments") != std::string::npos); } } diff --git a/test/test_runtime.cpp b/test/test_runtime.cpp index eab6de1..1a1521a 100644 --- a/test/test_runtime.cpp +++ b/test/test_runtime.cpp @@ -195,22 +195,12 @@ TEST(runtime, vm_call_by_name_error_cases) { EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "nulladdr", 0), std::exception); s.GetVM().RegisterFunction(VmFunction("fnv", 8, TEST_JIT_TYPE, reinterpret_cast(&VmFnEcho8), {})); + EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 9, a0, a1, a2, a3, a4, a5, a6, a7, a8), std::exception); - // 参数个数与形参不一致沿 Lua 语义处理:多传的丢弃、少传的补 nil, - // 不再抛异常。Bug M 关注的「读取未初始化栈内存」由补齐保证: - // CallByNameImpl 在派发前把 temp_arg_arr 的前 expected_arg_count 个槽全部写满 - // (实参不足的部分写 kNil),因此下面两次调用返回的都是确定的值。 - const CVar r9 = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 9, a0, a1, a2, a3, a4, a5, a6, a7, a8); - ASSERT_EQ(r9.type_, static_cast(VarType::Int)); - ASSERT_EQ(r9.data_.i, 0);// 多传的第 9 个实参被丢弃,返回第 1 个形参 - - const CVar r3 = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 3, a0, a1, a2); - ASSERT_EQ(r3.type_, static_cast(VarType::Int)); - ASSERT_EQ(r3.data_.i, 0); - - // 0 个实参:8 个形参全部补 nil,返回第 1 个形参即 nil(不是未初始化内存) - const CVar r0_args = FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 0); - ASSERT_EQ(r0_args.type_, static_cast(VarType::Nil)); + // Bug M: 参数个数与函数签名不匹配时必须抛异常(否则会读取未初始化栈内存)。 + // 这里 fnv 要求 8 个参数,分别传 3 个 / 0 个都应抛异常。 + EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 3, a0, a1, a2), std::exception); + EXPECT_THROW((void) FakeluaCallByName(&s, TEST_JIT_TYPE, "fnv", 0), std::exception); } TEST(runtime, vm_helper_functions_throw_and_alloc) { @@ -529,12 +519,8 @@ TEST(vm_cvar_call, multi_return_expansion) { multi_one.type_ = static_cast(VarType::Multi); multi_one.data_.m = m_one; - // 1. 分支 A: 含 Multi 且末尾实参不是 Multi,实参个数(3) 多于形参(2) —— 按 Lua 语义 - // 截断丢弃,不再抛异常。multi_two_a 非末尾只取 vars[0]=20,故实参为 (20, 1, 20), - // 截断后 (20, 1),echo2 返回第 2 个形参 = 1。 - const CVar r0 = FakeluaCallByName(&s, TEST_JIT_TYPE, "echo2", 3, multi_two_a, int1, int20); - ASSERT_EQ(r0.type_, static_cast(VarType::Int)); - ASSERT_EQ(r0.data_.i, 1); + // 1. 分支 A: has_any_multi 为 true,但最后一个参数不是 Multi 并且 arg_num != expected_arg_count -> 抛出异常 + EXPECT_THROW(FakeluaCallByName(&s, TEST_JIT_TYPE, "echo2", 3, multi_two_a, int1, int20), std::exception); // 2. 正常的多参数解包覆盖: // expected_arg_count = 4, 我们传 3 个参数:int1, multi_two_a (非末尾,只取 vars[0]), multi_two_b (末尾,全部展开)