Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
16 changes: 15 additions & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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++ 原生函数作为高效宿主接口,在编译期按名静态分发而非作为第一类值闭包,大幅提升热点调用性能。

## 核心特性

### 三种执行后端
Expand Down Expand Up @@ -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` 声明,不支持隐式全局赋值。

## 标准内置扩展库

Expand Down
10 changes: 10 additions & 0 deletions src/compile/c_gen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4131,12 +4131,22 @@ std::string CGen::TryCompileSpecDirectCall(const std::shared_ptr<SyntaxTreeFunct
if (BindsLocalValue(callee_var.get())) {
return "";
}
// 特化直调按原函数的精确形参个数发射 C 调用。若实参个数与形参不一致,
// 回退普通直接调用路径,由 CompileFunctioncall 给出明确的编译期
// wrong number of arguments 报错,避免向底层 C 编译器发射参数不符的调用。
const auto fn_it = local_func_names_.find(callee_name);
if (fn_it == local_func_names_.end()) {
return "";
}
if (const auto math_it = ir().math_param_positions.find(callee_name); math_it != ir().math_param_positions.end()) {
const auto &math_params = math_it->second;
const auto explist_arg = args_ptr->Explist();
DEBUG_ASSERT(explist_arg->Type() == SyntaxTreeType::ExpList);
const auto explist_arg_ptr = std::dynamic_pointer_cast<SyntaxTreeExplist>(explist_arg);
const auto &raw_args = explist_arg_ptr->Exps();
if (static_cast<int>(raw_args.size()) != fn_it->second.params_count) {
return "";
}

if (int bitmask = 0; TryInferMathCallBitmask(callee_name, raw_args, bitmask)) {
std::unordered_map<int, std::string> native_exprs;
Expand Down
5 changes: 5 additions & 0 deletions src/compile/c_runtime_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
48 changes: 44 additions & 4 deletions src/compile/semantic_analysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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",
Expand Down Expand Up @@ -1380,6 +1390,36 @@ void SemanticAnalysis::CheckVarScopes(const SyntaxTreeInterfacePtr &node, std::v
}
case SyntaxTreeType::Var: {
const auto var = std::dynamic_pointer_cast<SyntaxTreeVar>(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<SyntaxTreeVar> root;
for (auto cur = var; cur && cur->GetVarKind() == VarKind::kDot;) {
full = cur->GetName() + (full.empty() ? "" : "." + full);
const auto pe = std::dynamic_pointer_cast<SyntaxTreePrefixexp>(cur->GetPrefixexp());
root = (pe && pe->GetPrefixKind() == PrefixExpKind::kVar)
? std::dynamic_pointer_cast<SyntaxTreeVar>(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);
Expand Down
7 changes: 6 additions & 1 deletion src/interp/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/jit/vm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 < 0 || expected_arg_count > static_cast<int>(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<int>(VarType::Multi));
Expand Down
6 changes: 6 additions & 0 deletions src/native/basic/native_basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<VarType>(a0.type_)) {
case VarType::Nil:
Expand All @@ -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) {
Expand All @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions src/native/math/native_math.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Loading
Loading