feat: 实现loadFunction预加载函数能力 + AI辅助编程标识

- 新增 loadFunction/callFunction/unloadFunction 预加载函数 API
- 预加载语义:加载文件→顶层执行一次→返回函数存入Lua Registry→可多次调用
- 与管道模式(load/runScript)存储完全解耦:funcs[]+Registry vs pkgs[]+虚拟栈
- 新增错误码 5023/5024/5025
- 新增单元测试:仓颉侧 8 个 + C++ GTest 侧 11 个
- 本机以 Cangjie 1.1.0 实编译并通过功能运行验证
- README/doc 增加 AI 辅助编程标识及预加载函数模式说明
This commit is contained in:
JianFeeeee
2026-08-23 12:03:04 +08:00
parent 207082a1a0
commit 42e8b7676d
14 changed files with 759 additions and 6 deletions

View File

@ -15,6 +15,12 @@ foreign func run(selfd: CPointer<Unit>, path: CString, arg: CString): Int32
@C
foreign func dostring(selfd: CPointer<Unit>,target: CString): Int32
@C
foreign func loadfunction(selfd: CPointer<Unit>,path: CString, name: CString): Int32
@C
foreign func unloadfunction(selfd: CPointer<Unit>,name: CString): Int32
@C
foreign func callfunction(selfd: CPointer<Unit>,name: CString, arg: CString): Int32
@C
foreign func cleanup(selfd: CPointer<Unit>): Int32
@C
foreign func get_errno(): Int32
@ -94,6 +100,9 @@ public class LuaError <: Exception {
case 5020 => "Error in callback functions"
case 5021 => "change workdir error"
case 5022 => "No callable chunk found"
case 5023 => "Function not found"
case 5024 => "Too many preloaded functions"
case 5025 => "Preloaded file must return a function"
case _ => "Unknown error"
}
}
@ -230,6 +239,82 @@ public class LuaRunner {
return this.cachedResult
}
/// 加载函数到内存(预加载)
///
/// 加载 Lua 文件 → 编译成 chunk → 执行顶层代码(顶层代码只执行一次)→ 捕获返回的 Lua 函数 → 存入独立的 Registry 引用表。
/// 与 load() 的块压栈不同:
/// - load() 把 chunk 留在虚拟栈上(管道模式用),每次调用会重新执行
/// - loadFunction() 执行一次顶层代码,保留返回的函数对象,后续 callFunction 多次调用不重复执行
/// 注意:被加载的 Lua 文件必须在其顶层 return 一个函数。
///
/// @param path 函数所在文件路径
/// @param name 函数名称callFunction/unloadFunction 时使用)
/// @return 当前实例
/// @throws LuaError 当文件不存在/顶层未返回函数/已存在同名函数时抛出
public func loadFunction(path: String, name: String): This {
let pathPtr = toCStr(path)
let namePtr = toCStr(name)
let result = unsafe { loadfunction(this.handle, pathPtr, namePtr) }
unsafe {
if (!pathPtr.isNull()) { LibC.free(pathPtr) }
if (!namePtr.isNull()) { LibC.free(namePtr) }
}
if (result != 0) {
throw LuaError(unsafe { get_errno() })
}
return this
}
/// 卸载预加载的函数(释放 Registry 引用)
///
/// @param name loadFunction 时指定的函数名称
/// @return 当前实例
/// @throws LuaError 当函数未找到时抛出
public func unloadFunction(name: String): This {
let namePtr = toCStr(name)
let result = unsafe { unloadfunction(this.handle, namePtr) }
unsafe {
if (!namePtr.isNull()) { LibC.free(namePtr) }
}
if (result != 0) {
throw LuaError(unsafe { get_errno() })
}
return this
}
/// 调用已预加载的函数
///
/// 通过 name 在预加载函数表中查找,从 Lua Registry 取出函数对象后 pcall 调用。
/// 不消耗该函数,因此可对同一函数重复调用(顶层代码只在 loadFunction 时执行一次)。
/// 注意:与 runScript 管道模式完全独立,互不干扰。
///
/// @param name loadFunction 时指定的函数名称
/// @param arg 传递给函数的单参数;传空字符串表示无参数(底层会转为 NULL
/// @return 函数执行的字符串结果
/// @throws LuaError 当函数不存在或执行出错时抛出
public func callFunction(name: String, arg: String): String {
let namePtr = toCStr(name)
// 空字符串会被 toCStr 转为 NULLC 层据此识别“无参数”
let argPtr = toCStr(arg)
let code = unsafe { callfunction(this.handle, namePtr, argPtr) }
unsafe {
if (!namePtr.isNull()) { LibC.free(namePtr) }
if (!argPtr.isNull()) { LibC.free(argPtr) }
}
this.cachedResult = toString(unsafe { getresult(this.handle) })
if (code != 0) {
throw LuaError(unsafe { get_errno() }, this.cachedResult)
}
return this.cachedResult
}
/// 获取缓存的结果
public func result(): String { this.cachedResult }

View File

@ -323,3 +323,178 @@ func testDefaultIOCallback(): Unit {
let result = unsafe { defaultIO() }
@Expect(result, 0)
}
// ====================
// 8. loadFunction / callFunction 预加载测试
// ====================
@Test
func testLoadFunctionAndCall(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
let res = runner.callFunction("add", "hello")
// 闭包带计数器,第一次调用应返回 call #1
@Expect(res, "result: hello (call #1)")
}
@Test
func testCallFunctionMultipleTimes(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_multi_call.lua"), "multi")
let r1 = runner.callFunction("multi", "")
@Expect(r1, "multi:init-done")
let r2 = runner.callFunction("multi", "")
@Expect(r2, "multi:init-done")
let r3 = runner.callFunction("multi", "")
@Expect(r3, "multi:init-done")
let r4 = runner.callFunction("multi", "")
@Expect(r4, "multi:init-done")
}
@Test
func testCallFunctionCallCount(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "a"), "result: a (call #1)")
@Expect(runner.callFunction("add", "b"), "result: b (call #2)")
@Expect(runner.callFunction("add", "c"), "result: c (call #3)")
}
@Test
func testCallFunctionNotFound(): Unit {
let runner = LuaRunner()
try {
runner.callFunction("nonexistent", "")
fail("Should throw function not found error")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testLoadFunctionFileNotFound(): Unit {
let runner = LuaRunner()
try {
runner.loadFunction("/no/such/file.lua", "bad")
fail("Should throw file not found error")
} catch (e: LuaError) {
@Expect(e.code, 5003)
}
}
@Test
func testLoadFunctionNotAFunction(): Unit {
let runner = LuaRunner()
try {
runner.loadFunction(scriptPath("func_not_a_function.lua"), "bad")
fail("Should throw function not valid error")
} catch (e: LuaError) {
@Expect(e.code, 5025)
}
}
@Test
func testLoadFunctionDuplicate(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "dup")
try {
runner.loadFunction(scriptPath("func_add.lua"), "dup")
fail("Should throw on duplicate name")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testCallFunctionRuntimeError(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_runtime_error.lua"), "bad")
try {
runner.callFunction("bad", "")
fail("Should throw runtime error")
} catch (e: LuaError) {
@Expect(e.code, 5010)
}
}
@Test
func testCallFunctionWithUnloadFunction(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "before"), "result: before (call #1)")
runner.unloadFunction("add")
try {
runner.callFunction("add", "")
fail("Should throw function not found after unloadFunction")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
// 卸载后可重新加载同名函数
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "after"), "result: after (call #1)")
}
@Test
func testLoadFunctionAndPipelineNoInterference(): Unit {
let runner = LuaRunner()
// 预加载函数(用独立 func 脚本)
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "x"), "result: x (call #1)")
// 管道模式(用 pipeline 脚本,走 pkgs 栈)
runner.load(scriptPath("pipeline_add_suffix.lua"), "add_suffix")
.load(scriptPath("pipeline_to_upper.lua"), "to_upper")
.load(scriptPath("pipeline_add_prefix.lua"), "add_prefix")
let initialData = runner.runScript(scriptPath("pipeline_generate_data.lua"), "hello")
@Expect(initialData, "hello")
@Expect(runner.runScript("", ""), "[PREFIX] hello")
@Expect(runner.runScript("", ""), "[PREFIX] HELLO")
@Expect(runner.runScript("", ""), "[PREFIX] HELLO [SUFFIX]")
// 管道结束后,预加载函数仍可调用,且计数器继续
@Expect(runner.callFunction("add", "y"), "result: y (call #2)")
}
@Test
func testLoadFunctionAndCleanCoexists(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "x"), "result: x (call #1)")
runner.clear()
try {
runner.callFunction("add", "")
fail("Should throw after clean")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testLoadFunctionOverflow(): Unit {
ensureGeneratedScriptsDir()
let runner = LuaRunner()
for (i in 0..20) {
let name = "overflow_func_${i}"
let path = generatedScriptPath(name + ".lua")
File.writeTo(path, ("return function() return \"" + name + "\" end").toArray())
try {
runner.loadFunction(path, name)
} catch (e: LuaError) {
@Expect(e.code, 5024)
break
}
}
}