CodeGraph Rust 内核移植实战:Lua 与 Luau 的 bug-for-bug 等价迁移全解
CodeGraph 的 Rust 提取内核要把 TS 侧基于 WebAssembly 的 tree-sitter 提取器逐语言移植成原生 Rust walker,docs/design/lua-luau-kernel-port-checklist.md 是其中 Lua + Luau(R7b batch 4)合并移植的"bug-for-bug 清单":它记录了每个 TS 分支会命中什么、哪些怪癖必须原样保留、哪些机制必须保持静默,以及如何用语法版本校验与全仓扫描验证两臂输出的字节级一致。读完本文,你将理解一个提取器移植清单从语法准备、钩子语义、死机制识别到等价性门禁的完整方法论,并能在当前仓库中看到这份清单已被落地的对应实现。
为什么 Lua 和 Luau 可以合并成一份清单
Lua 和 Luau(Roblox 的渐进式类型化超集)的 TS 侧提取器本身就是共享关系:src/extraction/languages/luau.ts 只有 36 行,通过 { ...luaExtractor, <4 个覆盖项> } 扩展 src/extraction/languages/lua.ts,两种语法的节点名词汇表一致(function_declaration、variable_declaration、function_call、dot_index_expression/method_index_expression 等)。因此移植结论是:内核侧只写一个 walker 模块 codegraph-kernel/src/lua.rs,用 ccpp 先例的语言参数化方式分发("lua" | "luau" => lua::extract(&file_path, &content, &language)),方言差异被压缩到恰好四项:
| 差异项 | lua | luau |
|---|---|---|
typeAliasTypes |
[] |
['type_definition'] |
isExported |
无钩子(wire 上 absent) | 对节点自身前 7 字符做 UTF-16 切片判断是否以 export 开头 |
getSignature |
parameters 字段文本 |
参数文本 + : <parameters 之后的命名子节点>(若该子节点是 block 则不加) |
| 语法句柄 | 内联编译的 v0.4.1 语法 | crate tree-sitter-luau = "=1.2.0" |
其余全部——require 钩子、receiver 方法、变量分支、调用形状、docstring、function-ref 捕获——两方言字节共享。当前仓库中 codegraph-kernel/src/lua.rs 的模块头注释明确写道:"One walker, two dialects (ccpp precedent): the differences are exactly four",并用 is_luau: bool 字段驱动这四处分支;langs.rs 的 LANGUAGES 数组已包含 "lua", "luau"(现为 20 种语言)。文档同时选定 go.rs 作为最近的参照模板(receiver-QN 方法、extract_type_alias → bool、node_ids vec、按 node_ids[from] 去重的 fn-ref flush)。
清单中所有"语法形状"论断都用生产环境 vendored wasm 做了 CST dump 与 childForFieldName 真值表探测,所有"提取行为"论断都对照真实 dist/ 提取器的 ground-truth dump 固定下来——不是靠读代码推断。这是这份清单可信度的来源,也是移植工作的第一原则:先钉住现有行为,再谈重写。
语法准备:内联编译 C 与 crate 钉版两条路线
两个语言的 wasm 早已在 VENDORED_WASM_LANGS 中(grammars.ts 第 291 行附近,文件映射 lua: 'tree-sitter-lua.wasm' / luau: 'tree-sitter-luau.wasm'),且 vendored 版本恰好就是移植目标修订版,因此本次移植完全不需要动 wasm 侧,也没有传统的 bump 门禁。但内核是原生 Rust,语法来源要分两条路线:
lua — vendored-grammar-C 路线(沿用 kotlin 机制)。探测确认 vendored wasm 与 tree-sitter-lua v0.4.1 tag 表格逐位一致(ABI 15,STATE_COUNT 262,SYMBOL_COUNT 137,FIELD_COUNT 22),而 v0.4.1 不在 crates.io 上(只有 0.1/0.2/0.5),所以必须把 tag 中 CHECK-IN 的生成产物(parser.c、scanner.c 及 tree_sitter 头文件,文档逐一列出了各文件 sha)放进 codegraph-kernel/grammars/lua/,由 build.rs 用 cc::Build 编译——include grammars/lua、编译 parser.c 与 scanner.c、参考 tag 自带 build.rs 的 flags(.std("c11")、msvc -utf-8)、cargo:rerun-if-changed=grammars/lua。当前仓库 codegraph-kernel/build.rs 第 32-41 行正是这个块,codegraph-kernel/grammars/lua 目录已存在。langs.rs 侧则声明 extern "C" { fn tree_sitter_lua() -> *const (); } 并用 LanguageFn::from_raw 包装(见 langs.rs 第 21-25 与 80-82 行)。注意 lua 有外部 scanner(注释与长字符串),vendor 时必须带上 scanner.c。
luau — 普通 crate 钉版(csharp 风格)。vendored wasm 与 tree-sitter-luau v1.2.0 ≡ crates.io 1.2.0 三方一致,且 swift 移植中"tag≠crate"的教训在这里被复查并排除:tag 与 crate tarball 的 parser.c(8f25bc17…)与 scanner.c(a157bb52…)sha 逐位相同(ABI 14,STATE_COUNT 585,SYMBOL_COUNT 197,FIELD_COUNT 21)。因此 Cargo.toml 中直接写 tree-sitter-luau = "=1.2.0",crate 依赖只有 tree-sitter-language = "0.1" + cc 构建依赖、tree-sitter 仅 dev-dep(0.26.3),与内核的 tree-sitter 0.25 无版本冲突。
语法等价门禁取代 bump 门禁。由于 wasm 侧零改动,不存在新旧对比 dump,改由 tests/kernel-grammar-parity.test.ts 的 GRAMMAR_LANGUAGES 数组追加 'lua', 'luau'(当前第 39 行已包含两者)来断言:C 编译/ crate 版本与 vendored wasm 是同一修订版(逐 id 对比 kind+field 表、lua 走 ABI 15 / luau 走 ABI 14)。
错误发生率:lua 近乎为零,luau 是语法的固有形状
清单对七个真实仓库做了全文件错误扫描(error-sweep.cjs,文件均 ≤1MiB),这张表决定了移植后的预期延迟(deferral)行为:
| 仓库 | 语言 | 文件数 | hasError | 首个错误类别 |
|---|---|---|---|---|
| Kong/kong | lua | 1,309 | 1(0.08%) | spec/fixtures/invalid-module.lua — 故意写坏 |
| folke/lazy.nvim | lua | 65 | 0 | — |
| openresty/lua-resty-core | lua | 38 | 0 | — |
| lune-org/lune | luau | 221 | 3(1.36%) | .d.luau 类型包/typeof 声明 |
| dphfox/Fusion | luau | 113 | 8(7.08%) | 泛型类型包 (A...) -> T;默认类型参数 type Scope<C = Fusion> |
| luau-lang/luau tests/ | luau | 203 | 39(19.21%) | 仅压测 — 故意测试未来语法 |
| JohnnyMorganz/StyLua tests/ | lua | 419 | 118(28.16%) | 仅压测 — cfxlua 方言(+=、C 注释) |
关键结论有三:全部 2,368 个文件中零 phantom-hasError(有 hasError 但没有 ERROR 节点的文件),所以与 kotlin 不同,lua/luau 的延迟信号永远伴随真实 ERROR 节点出现;但策略上仍只看 has_error() 标志,不去扫 ERROR 节点。lua 侧任何一次延迟都是 walker 自身的 bug 信号(ruby 式预期:0-1 次延迟,出现即可疑);luau 的 1.4–7.1% 是两臂共有(同一语法修订版两边行为一致),由两种语法固有形状驱动——泛型类型包 (A...) -> T 与默认类型参数 <T = D>——扫描阈值定为 --max-deferral 0.1(scripts/kernel-parity.mjs 第 38 行默认值),luau 超过 ~10% 才是 bug 信号。方言交叉污染也会被延迟:luau 拒绝 @native/@checked(1.2.0 语法早于 2024+ 语法)与 lua-only 语法(goto、位运算);lua 拒绝 luau 特性(type X = …、continue、+=、反引号插值)。所有错误文件整体让给 wasm 臂,wasm 错误时也会做 best-effort 部分提取,两臂表格相同因此行为一致。
require 钩子:最大的 bug-for-bug 风险面
Lua 没有 import 语句,模块加载靠调用全局 require,这被实现为一个 visitNode 钩子(lua.ts 第 105-151 行 + requireModule 第 28-60 行)。清单把这套机制列为"单个最大的 bug-for-bug 面",因为它包含多处刻意的不对称:
requireModule(callNode) 的判定顺序(必须逐字移植):
name:字段子节点必须是identifier且文本恰为require(点号/冒号调用者是 dot/method_index 节点,天然不匹配);arguments:字段必须存在。- 字符串优先、广度优先:
findDescendant(args, 'string_content')(BFS 遍历 namedChildren,lua.ts 第 9-17 行)取 trim 后文本。这抓住require("a.b")、单引号、[[...]]长字符串(string_content 位于 string 节点内部),也抓住任何参数里"随便哪里有字符串"的 require——被固定(pinned)的怪行为:require(script:WaitForChild("Kid"))→ import"Kid"(字符串赢过方法索引路径);require("a" .. "b")→ import"a"(BFS 序第一个 string_content——确定性的垃圾,照原样保留)。 - 兜底
findDescendant(args, 'string')加手工剥[[ ]]/引号(只在 string 节点无 content 子节点时到达;require("")→ mod 为假值 → null)。 - Roblox 实例路径:第一个
dot_index_expression(否则method_index_expression)后代的field:(或method:)子节点文本——require(script.Parent.Signal)→"Signal"(末段)。 - 其余情况返回 null(
require(dynName)、require())。
钩子分发(lua.ts 第 129-150 行)的不对称语义:
- 节点为
function_call:是 require →emit(node)+ 返回 true(被认领,绝不会被二次计为 call);不是 → 返回 false → 落到 extractCall 发出calls "require"边(pinned:顶层require(dynamicTop)→ FILE 节点发 calls require)。 - 节点为
variable_declaration:找assignment_statement子 → 其expression_list→ 对每个function_call子emit(val)(多 requirelocal a, b = require("x"), require("y")→ 两个 import——pinned),总是返回 false → extractVariable 照常执行。从此处不发的:更深层嵌套的 require({ mod = require("t") }表值——pinned 无 import)、require("m").field(exprList 子节点是 dot_index——pinned 无 import;但accessed变量照常铸出)。 emit:在调用节点位置创建 import 节点(ctx.createNode('import', mod, callNode, { signature: 调用文本 trim 后截 100 字符 }),qualifiedName = 模块字符串),若栈非空则addUnresolvedReference(fromNodeId = 栈顶,恒为 FILE 节点;referenceKind = 'imports';line = 调用 startRow+1;不带 filePath——真实 dump 中零条 ref 携带过 filePath,所以 wire v2 的 REF_FLAG_FILE_PATH 不需要)。
最关键的一条:钩子从不在函数体内运行(visitFunctionBody 没有钩子调用)。于是顶层 local lazy = require("app.lazy")(包括顶层 if/for/while 块内——pinned extract-condreq:if ok then local m = require("in.if") end → import in.if + variable m,都归属于 FILE)产出 import 节点,而函数体内同一条语句产出 calls "require" 引用且无 import(pinned extract-bodies-lua.txt L8-9——neovim lazy-loading 惯用法就落在这一侧)。把这几条任何一条移植错,扫描第一天就会亮灯。
变量节点:位置在标识符上,初始值子树永不遍历
variable_declaration 分支(tree-sitter.ts 提取器 lua 分支)的行为同样被逐字节固定:
- kind 恒为
variable(无isConst钩子,constant永不出现);docstring 取自声明节点;isExported = 钩子 ?? false→ 两语言都是 false(luau 的切片看到的是local …)。 - 解析:
assign= 第一个assignment_statement命名子节点 ?? 节点本身(覆盖无=的裸local x);varList= assign 的variable_list子;exprList= assign 的expression_list子;names= varList 中仅identifier子(attribute节点<const>/<close>被跳过且不打乱位置);values= exprList 的命名子节点。 - 每个 name 位置配对创建节点,位置在标识符上(pinned:
variable "http" L2 C6-10);signature =`= ${valueText 截 100}${≥100 时加 '...'}`,该索引无值 → 连 signature 键都不存在(pinned:local ok, err = pcall(...)→ok有 signature,err没有)。签名保留原始字节:内嵌\n(= [[long\nstring]])、CRLF\r\n、完整匿名函数文本。docstring 与 isExported 复制到声明的每个 name 上。 - 分支随后
scanFnRefSubtree(node, 0)+ skipChildren → 初始值子树永不被遍历:local fromCall = topFn(3, 4)不发出任何 calls 引用;local anon = function(v) return hidden(v) end不铸函数节点、不发 calls(pinned)。与之对比会遍历的形状:顶层x = topFn(10)赋值(无分支 → 递归)→ 发 calls topFn;M.assigned = function(z) return topFn(z) end→ calls topFn 归属于 FILE 节点(pinned L68)。函数体内则两种声明形式都遍历(局部变量不铸节点)——反转只发生在顶层:顶层 local 声明的调用不可见、顶层全局赋值可见;体内则相反。 - 同 id 重复行是合法输出:
local x = 1; local x = 2一行内 → 两行同 id 的variable:x(pinnedsameLineTwoLocals)。压缩成单行的 lua 包会经常撞上,walker 按node_idsvec 模式原样发两行,store upsert 负责折叠——parity 比较的是入 store 前的输出。
调用提取:原始文本调用者世界与换行粘连陷阱
extractCall 的 lua 路径有一个结构性事实:function 字段在本语法中为 NULL(真值表固定),所以调用者恒取 namedChild(0)(即 name: 子:identifier / dot_index / method_index / parenthesized / 链式中的 function_call)。成员分支(:4364)永不触发——dot_index_expression/method_index_expression 不在其接受的类型列表里,LITERAL_RECEIVER_TYPES、SKIP_RECEIVERS 与所有 re-encode 都不可达。一切落入 ELSE 分支:calleeName = 调用者节点的原始源码文本(UTF-16 子串)。被 pinned 的形状:
- 裸
topFn(5)→topFn;糖调用require 'x'/f {t}(无括号参数)同一路由; - 点号
M.create(2)→M.create;深层core.util.log("x")→core.util.log; - 冒号方法保留冒号:
M:render({})→M:render;M.sub.deep:chained(13)→M.sub.deep:chained;体内self:helperMethod(o)→self:helperMethod、self.field.deep(1)→self.field.deep——self永不被剥掉(此路径无 SKIP_RECEIVERS)。解析层依赖这些精确字节(见"解析侧消费者"一节); - 括号调用原样保留:
M.registrykey→M.registry[key];t2k2→t2[k2](括号逐字); - 调用结果作调用者:
f2()(15)→ 外层引用f2()+ 内层引用f2(无 skipChildren,子节点递归,链上每一环都发引用); - 换行粘连陷阱(lua 语句歧义的固有产物):调用语句后紧跟以
(开头的行被解析为一条粘连链——obj:foo():bar()\n\t(helper)(4)发出四条引用:obj:foo():bar()\n\t(helper)(原始文本含内嵌换行/制表符,字节逐字)、obj:foo():bar()、obj:foo():bar、obj:foo(pinnedextract-bodies-lua.txtL19);string.format("%d", 9)\n("literal"):upper()同理三条。必须逐字节复现,包括"调用者"是整条内层 function_call 文本的粘连中间环; - 括号转换正则(:4529-4532):
/^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/命中时把(handler)(16)(分号分隔或体内首句、未粘连)的调用者文本(handler)改写为handler(两次 pinned);粘连/带引号的括号调用者("x"):upper不命中(保持原文)。移植时正则要用 JS\s语义(会匹配\r)。
Luau 额外形状(全部 pinned):插值调用在体内发出(`point {p.x} of {Config.total()}` → calls Config.total,位置在内层调用处);if 表达式两臂(if p.x > 0 then bump() else drop() 两臂都发);v += grow() → calls grow(update_statement 被递归,且无 fn-ref 捕获——分发里根本没有 update_statement 键)。引用行统一为:fromNodeId = 栈顶、referenceKind = 'calls'、line = 调用 startRow+1、column = UTF-16 startColumn。
Luau 类型别名、函数签名与 docstring 怪癖
类型别名(luau type_definition):简单别名 name 子节点是 identifier;泛型别名的 name 子是 generic_type,节点名即其逐字文本——Generic<T>、Map<K, V>(空格保留,pinned)。export type 的节点从 export 关键字开始(CST 探测确认),isExported 切片钩子给出 true。:2973 的 TYPE_ANNOTATION_LANGUAGES.has('luau') 为 FALSE → 别名值不发任何引用(双重死亡:value 字段在此语法中也是 NULL)。extractTypeAlias 返回 false → 别名子节点被重新访问:typeof(require(...)) 别名内的 function_call 命中钩子 → type_alias 节点 + import 节点 + imports 引用三样齐发(pinned:FromTypeof → type_alias L11 C0-55 + import "Config" L11 C25-54 + FILE 的 imports 引用;别名节点先创建)。体内局部 type X = …(luau 合法)铸出什么都没——visitFunctionBody 没有 typeAlias 分支(pinned)。lua 侧 typeAliasTypes 为空,.lua 文件里混入 type X = 本来就会解析报错而被整体延迟。
函数与方法:function_declaration 覆盖全局、local、表函数 function t.f、方法 function t:m 四种形态——同一种节点类型,靠 name: 子节点区分形态;匿名 function() end(function_definition)不在类型列表中,靠外层变量捕获。getReceiverType(lua.ts 第 92-99 行):name: 子是 dot_index_expression/method_index_expression → 返回其 table: 字段文本(逐字点号源码:function M.sub.deep:chained() → receiver M.sub.deep,pinned),否则 undefined。接收者 QN 覆盖:function M.attached() 即使嵌套在函数体内也是 M::attached 而非 render::attached(pinned);function _G.installed() 铸出 method _G::installed。嵌套裸函数用节点名拼栈 QN:M:render 体内的 local function inner → render::inner(用方法名而非其 QN);体内声明的全局 function leakedGlobal() 仍受栈作用域约束 → render::leakedGlobal(pinned 怪癖,保留)。owner-contains 逻辑(:1799-1813)永不触发——它要求文件内存在 kind ∈ {struct, class, enum, trait} 的节点,lua 从不铸这些 kind,点号 receiver 也匹配不到任何节点名。唯一的包含边就是栈顶 contains 边。extractMethod 不传 isExported——luau 方法 isExported 为 undefined 而 luau 函数为 false(pinned:method "make" 无标志、function "typedTop" isExported=false),这是 lua↔luau 在节点载荷上除签名/类型别名外的唯一分歧。
docstring 怪癖(tree-sitter-helpers.ts 第 95-127 行,两语法各只有一种注释 kind comment,覆盖 -- 行注释、--[[ ]] 块注释、LuaDoc ---、指令 --!strict):
- 块注释剥
--[[ … ]]/--[==[ … ]==]的开闭;行注释逐行剥^--\s?; - LuaDoc
---保留一个前导-(--- Summary→- Summary,两语言 pinned); --!strict变成 docstring 文本!strict且并入注释链——pinned:torture.luau 的coredocstring 是"!strict\nheader comment for torture.luau";- 注释链跨空行延续(兄弟跳过空白),被任何非注释命名兄弟打断——包括第 1 行的
hash_bang_line(shebang + 注释链 → 注释链保留,pinned); - CRLF 字节 pinned:行注释 docstring 与 LF 完全相同(每条注释各自 trim 后以
\n连接);块注释 docstring 保留内部\r\n(只 trim 两端)。签名同样保留原始\r\n。相关剥离规则(lua_open/lua_close/dashes)已存在于内核共享的 docstring.rs(#1329 的多行剥离工具),移植时直接调用共享模块。
Function-as-value 捕获与必须保持静默的"死机制"
fn-ref 捕获(#756):LUA_SPEC 中 idTypes = {identifier}(仅裸标识符——点号值永不命中,{ on_make = M.make } 捕获无物,pinned);dispatch 键为 arguments → 参数、assignment_statement → RHS(无字段,RHS = 末个命名子 = expression_list;参数存储跳过逻辑比较 namedChild(0) 的尾标识符与整个 RHS 文本,M.cb = cb 跳过,pinned)、field → value: 字段值(键控与位置表字段都带 value:,真值表确认)。捕获点在顶层 call 参数 + 赋值 + 每个 ladder 递归经过的 field 节点、体内逐节点、以及 scanFnRefSubtree(其 halt 列表不含 function_definition,所以扫描会深入匿名函数初始值体,深度上限 12)。flush 时 definedHere = 同文件函数/方法名;importedNames 中点号模块路径只贡献末段(app.core → core),Roblox 叶子名(Signal)整体通过——pinned:(helper) 粘连候选因存在 import helper 而被 flush。按 ${fromNodeId}|${name} 去重,首次出现位置胜出(三个 topFn 表值候选 → 一条 function_ref 位于首个位置)。pinned 总量(torture.lua):pcall(topFn, 7, 8) → topFn;表注册(含位置键 [1]、嵌套表、去重、missing 门控、M.cb = cb 跳过)各形态;luau { plain = typedTop } → typedTop;M:update(…, print) → print 门控掉(未定义/未导入)。
死机制——walker 必须把这些复现为静默(清单以专节列出,这是"绝不发明行为"的边界):
- 值引用边:
VALUE_REF_LANGS无 lua/luau → flushValueRefs 丢弃全部收集物;赋值影子修剪的assignmentcase 是 Python 的节点 kind,不是 lua 的assignment_statement,即便将来加入也空转。references边带 valueRef 元数据的:零; - 静态成员引用:STATIC_MEMBER_LANGS 排除 lua/luau,extractStaticMemberRef 入口即返回;
- 类型标注引用:TYPE_ANNOTATION_LANGUAGES 排除 → luau 参数类型与返回类型只活在签名字符串里,returnType 字段永不设置;
- instantiates / 继承:无 lua 节点 kind 命中 INSTANTIATION_KINDS;class/interface/enum 类型列表全空 → extractInheritance 永不运行。
setmetatable元表类模式只发出 calls 引用——无类合成、无 extends(pinned); - decorates:
extractDecoratorsFor对每个函数/方法都会运行,但 decorator/annotation/attribute 节点不会出现在它扫描的位置(lua 的attribute=<const>/<close>在 variable_list 内部,pinned:local x <const> = 99→ variable x、signature= 99、无引用)。decorates引用:零。
等价性机械:字节序、wire 标志与延迟策略
移植验收的"曾经咬过人"清单:
- 发射顺序:file 节点 → 源码序遍历。每个 require 声明:钩子内 import 节点 → imports 引用先于 extractVariable 的变量节点(钩子循环先完成);contains 边随 createNode 交错;源码位置交错的 imports + calls 引用;function_ref 引用最后 flush。store/harness 对 rowid 顺序敏感,必须逐条复现。
- wire 标志(bit pairs):lua 世界唯一会被设置的标志是变量的
isExported=false(present=1, value=0);luau 额外在函数(false)与类型别名(export type为 true)上设置 present 位——但方法不设(extractMethod 不传)。lua 函数 present 位为 0(undefined)。present-vs-value 的区分必须字节精确。visibility 字节处处为 0;returnType/decorators/typeParameters 处处缺失。 - UTF-16 列号/切片:所有位置与 getNodeText(调用者原文、签名、docstring 源、import 签名 + trim)都按 UTF-16 码元计(内核侧对应
textutil::col16)。pinned:local préfixe→ C6-13(7 个 UTF-16 单位)。 - 节点 ID 的 hash 输入行号:变量 hash 标识符的行;import hash 调用的行;函数/方法 hash 声明行;luau type_alias hash type_definition 行(export 时从
export起)。同 id 重复行照发不去重。 - 延迟策略:按文件
has_error()→defer:(消息约定同 ruby.rs)。codegraph-kernel/src/lua.rs 第 96-98 行即此实现:if tree.root_node().has_error() { return Err("defer: parse tree contains errors — wasm recovery is canonical") }。
解析侧消费者:walker 输出的字节被下游固定
移植不需要动解析层,但解析层反向固定了 walker 必须发出的字节:
resolveLuaRequire(import-resolver.ts 第 1688 行起,分发 :1485):把 imports 引用的 referenceName 当点号路径处理(telescope.config→telescope/config.lua)或 Roblox 叶子(Signal→Signal.luau),依次尝试后缀<p>.lua | .luau | /init.lua | /init.luau,以与请求文件共享前缀最长者胜,命中 file 节点、置信度 0.9(代码注释说明 ≥0.9 是为了压过 name-matching 的同名自匹配)。walker 的义务:referenceName = 模块字符串/叶子逐字(点号完整,不做任何归一化)。- Lua 冒号方法解析:提取发出
lg:log形状的调用;name-matcher 的luaColonMatch(name-matcher.ts 第 1758 行,正则/^([\w.]+):(\w+)$/)把它们拆开做局部变量 receiver 类型推断(#1108),其中含 #1124 lookahead——第三个模式用负向前瞻拒绝 PascalCase 方法调用(lg:Log(),Roblox 惯例)被误认为"类型标注",因为 Lua 调用语法恰是同样的receiver:Name形状。预过滤保留单冒号引用在任一侧(或大写 receiver)命名已知符号时存活。walker 的义务:单冒号字节形状(receiver 限[\w.]——括号/粘连垃圾本来就不会被解析)。 - 除此之外没有框架 resolver 或 synthesizer 消费 lua/luau(
CC_LANGUAGES = {swift, kotlin}、NATIVE = {java, kotlin, objc, cpp}均不含),也没有.lua内容嗅探——语言纯按扩展名判定(grammars.ts 的detectLanguage),1MiB MAX_FILE_SIZE 与生成文件跳过是编排器侧共享逻辑。
验证门禁:从语法表到全仓扫描
按迁移计划的分层门禁,lua/luau 的验收链条(当前仓库已全部落地):
- Stage 0(与 walker 同批或之前):内核 C vendor(grammars/lua,sha 见上)+ build.rs 块 + langs.rs 条目 + luau crate 钉版 +
GRAMMAR_LANGUAGES += 'lua', 'luau'—— 证明 C/crate ≡ vendored wasm 修订版(lua ABI 15 / luau ABI 14,kind+field 表逐 id 比对)。无需 dump 对比:wasm 臂原封未动。对应 tests/kernel-grammar-parity.test.ts。 - Torture 夹具:新建的 tests/kernel-lua-parity.test.ts(文件头注释逐条列举了夹具覆盖面)配合 torture.lua 与 torture.luau 夹具,加上内存派生的 CRLF 变体(固定块注释
\r\ndocstring 字节与 CRLF 签名)与粘连链/延迟 pin。torture.lua 清单(99 行、parse-clean):shebang + 注释链 docstring、require 全形态(双引号、Roblox 实例路径、单引号、[[ ]]、script:WaitForChild字符串优先、.field与动态参数的静默、双 require 顺序、pcall-require 无产出)、LuaDoc-怪癖、receiver QN(t.f/t:m/a.b.c:m/_G.f)、体内 require→calls-require、self 冒号/点号调用、括号调用者、嵌套 local/receiver/global 函数的 QN 怪癖、表 fn-ref 注册(位置键 + 嵌套 + 去重 + missing 门控 +M.cb = cb跳过)、全局赋值可见 vs local 声明不可见的反转、(handler)转换重写、裸local x(无 signature 键)、单行多语句、]==]长字符串、<const>属性、非 ASCII 行(UTF-16 列)、goto/label、一行local x = 1; local x = 2同 id。torture.luau(62 行):--!strictdocstring 并入、普通/export type/泛型(Generic<T>逐字名)/typeof(require(...))别名四形态、带返回后缀的类型化签名、类型化方法(无 isExported)、体内 type(无产出)、插值调用、if-表达式两臂、+=RHS 调用、cast、continue。延迟夹具各一:一个含 luau 语法(x += 1)的 lua 文件、一个含默认类型参数(type S<T = U> = {})的 luau 文件——内核defer:,wasm 臂输出原样断言。 - Parity 扫描(
scripts/kernel-parity.mjs <dir>,默认--max-deferral 0.1):kong(大仓 lua,1,309 文件,预期 ≤1 次延迟)、lazy.nvim(65,预期 0)、lua-resty-core(38,LuaJIT/OpenResty 惯用法,预期 0)、lune(luau 221,预期 ~3)、Fusion(luau 113 重度类型化,预期 ~8);luau-lang/luau tests/ 与 StyLua tests/ 仅作可选压测臂(--max-deferral 0.3/0.5,只判非延迟部分的 parity)。随后 kong + lazy.nvim + lune + Fusion 上做全量初始化 dump 字节对比(内核臂 vsCODEGRAPH_KERNEL=0的 scripts/dump-graph.mjs,cmp 逐字节一致)。 - 路由开启:以上全部通过后,把
'lua', 'luau'加入 src/extraction/kernel/index.ts 的DEFAULT_ROUTED(第 95-96 行,注释即引用了上述扫描预期与延迟含义)。当前仓库中这一步已完成,CODEGRAPH_KERNEL_LANGS环境变量可按语言覆盖、CODEGRAPH_KERNEL=0全关。
方法论小结
这份清单示范了一套可复用的"移植前调查"范式:先对生产 wasm 做 CST/字段真值表探测钉住语法形状,再对真实提取器做 ground-truth dump 钉住行为(含刻意保留的"确定性垃圾",如 BFS 字符串优先与换行粘连链),把移植义务分成三类——必须逐字保留的怪癖(require 不对称、raw-text 调用者、isExported wire 分歧)、必须保持静默的死机制(value-refs/static-member/type-annotation refs/instantiates/decorates)、以及用真实仓库错误率表推导出的延迟预期;最后以"语法版本表等价测试 + torture 夹具 + CRLF 变体 + 全仓 dump 字节对比 + 分级路由开启"作为不可跳过的验收阶梯。文档末尾的 Probe artifacts 一节列出了全部可再生的探测脚本与夹具清单(CST dump、字段真值表、31 条 snippet 探针、错误分类文件、门控仓浅克隆),并声明这些临时目录可丢弃——从这份文档本身即可完整重新推导。这也是它作为设计文档的核心价值:移植完成后,它仍是 lua.rs 中每一处"看起来不合理"代码的权威出处——lua.rs 的模块头注释直接写明 "The authoritative quirk list is docs/design/lua-luau-kernel-port-checklist.md"。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00