Skip to content
Open
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
1 change: 1 addition & 0 deletions src/compile/c_gen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ void CGen::GenerateGlobal(const SyntaxTreeInterfacePtr &chunk) {
InferredType global_type = ir().global_const_vars.at(name);
const auto cname = CIdent(name);
const auto exp_node = std::dynamic_pointer_cast<SyntaxTreeExp>(exp);
// 文件级数值 local 是常量。再赋值在类型推断阶段就已经报错,这里一律 static const。
if (global_type == T_INT) {
if (!exp_node || exp_node->GetExpKind() == ExpKind::kNil) {
Out() << "static const int64_t " << cname << " = 0;\n";
Expand Down
9 changes: 9 additions & 0 deletions src/compile/c_runtime_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ struct VarTable {
需要重算——因此把 VarTable 整体清零的分配路径天然落在安全的重算分支上。 */
uint32_t seq_len_valid_;
int64_t seq_len_;
/* spec 块字节数。0 = 无 spec。与 src/var/var_table.h 同布局。 */
uint32_t spec_bytes;
/* >0:spec 是连续 CVar(表特化)。0:不透明块,按 spec_bytes 整体搬迁。 */
uint32_t spec_cvars;
};

typedef struct State State;
Expand Down Expand Up @@ -647,6 +651,8 @@ static inline uint32_t FlHashString(const char *str, int len) {
__t->spec_count = 0; \
__t->seq_len_valid_ = 1; \
__t->seq_len_ = 0; \
__t->spec_bytes = 0; \
__t->spec_cvars = 0; \
assert(sizeof(__t->quick_data_) == 8 * sizeof(VarEntry)); \
{ int __i; for (__i = 0; __i < 8; ++__i) { \
__t->quick_data_[__i].key.type_ = VAR_NIL; \
Expand All @@ -667,6 +673,9 @@ static inline uint32_t FlHashString(const char *str, int len) {
(v).data_.t->spec_keys = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \
(v).data_.t->spec_vals = (CVar *)FakeluaAlloc(_S, sizeof(CVar) * (field_count), !__fakelua_init_flag__); \
(v).data_.t->spec_count = (field_count); \
(v).data_.t->spec_bytes = (uint32_t)sizeof(SpecType); \
(v).data_.t->spec_cvars = (uint32_t)(field_count); \
assert(sizeof(SpecType) == (field_count) * sizeof(CVar)); \
} while(0)

#define FL_SPEC(SpecType, v, field) (((SpecType *)(v).data_.t->spec)->field)
Expand Down
38 changes: 37 additions & 1 deletion src/compile/type_inferencer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ std::string SpecStringFieldCName(const std::string &key) {
return out;
}

// 预处理生成的文件级初始化函数。它的函数体里对 local x = func() 降级出来的
// 绑定做一次赋值,不能当成用户再赋值。
bool IsFakeluaInitFunction(const std::shared_ptr<SyntaxTreeFunction> &func) {
if (!func) {
return false;
}
const auto fn = std::dynamic_pointer_cast<SyntaxTreeFuncname>(func->Funcname());
if (!fn) {
return false;
}
const auto fnl = std::dynamic_pointer_cast<SyntaxTreeFuncnamelist>(fn->FuncNameList());
if (!fnl) {
return false;
}
const auto &names = fnl->Funcnames();
return names.size() == 1 && names[0] == kInitFunctionName;
}

void UniquifySpecFieldCNames(std::vector<TableFieldInfo> &fields) {
std::unordered_set<std::string> used;
for (auto &f: fields) {
Expand Down Expand Up @@ -383,7 +401,9 @@ InferredType TypeInferencer::TypeEnvironment::MergeType(const InferredType old_t

InferResult TypeInferencer::InferTypes(const ParseResult &pr, const CompileConfig &cfg) {
LOG_DEBUG(s_, "engine", "InferTypes: start for {}", pr.file_name);
file_name_ = pr.file_name;
file_level_types_.clear();
file_level_init_exps_.clear();
InferResult ir;
EvalTypeSnapshot current_map;
TypeEnvironment env;
Expand Down Expand Up @@ -467,7 +487,12 @@ InferredType TypeInferencer::InferNode(const SyntaxTreeInterfacePtr &node, Trave
}
case SyntaxTreeType::Function: {
const auto func = std::dynamic_pointer_cast<SyntaxTreeFunction>(node);
const bool prev_init = in_init_function_;
if (IsFakeluaInitFunction(func)) {
in_init_function_ = true;
}
InferNode(func->Funcbody(), tctx);
in_init_function_ = prev_init;
return RecordType(current_map, node.get(), T_UNKNOWN);
}
case SyntaxTreeType::LocalFunction: {
Expand Down Expand Up @@ -610,6 +635,11 @@ InferredType TypeInferencer::InferLocalVar(const std::shared_ptr<SyntaxTreeLocal
// 在重置 env_ 后重新注入,使函数特化试推断能看到正确类型。
if (!tctx.IsTrialInference() && tctx.env.IsAtFileScope() && IsNumericInferredType(type)) {
file_level_types_[names[i]] = type;
// 同时记录 initializer 节点:文件级数值字面量是常量。
// local x = func() 在预处理里已经改成 local x = nil,不会进这张表。
if (init_node) {
file_level_init_exps_.insert(init_node);
}
}
}

Expand Down Expand Up @@ -646,6 +676,12 @@ InferredType TypeInferencer::InferAssign(const std::shared_ptr<SyntaxTreeAssign>

if (const auto *init = tctx.env.LookupInitNode(name)) {
tctx.var_define_nodes[var.get()] = init;
// 文件级数值字面量是 static const,用户函数里再赋值直接报编译期错误。
// __fakelua_init 除外:local x = func() 不能做 C 静态初值,预处理把它改成
// local x = nil,并只在 __fakelua_init 里写一次 x = func()。那是初始化,不是再赋值。
if (!in_init_function_ && file_level_init_exps_.contains(init)) {
ThrowFakeluaException(std::format("cannot reassign file-level constant '{}' at {}", name, SyntaxTreeLocationStr(file_name_, assign)));
}
}

current_map[var.get()] = current;
Expand Down Expand Up @@ -1092,7 +1128,7 @@ EvalTypeSnapshot TypeInferencer::RunTrialInference(const SyntaxTreeInterfacePtr

// 运行函数体类型推断(不新开作用域,参数已在当前作用域中定义)。
// Trial 推断不消费 shadow 信息(shadow 只与 AST 结构相关,主推断已覆盖),
// 但 TraversalContext 需要一个引用,因此用一个丢弃式的 set 占位。
// 但 TraversalContext 需要一个引用,因此用丢弃式的容器占位。
std::set<std::pair<const SyntaxTreeInterface *, std::string>> dummy_shadowed_decls;
TraversalContext tctx{current_map, env, &ctx, var_define_nodes, dummy_shadowed_decls};
InferBlock(std::dynamic_pointer_cast<SyntaxTreeBlock>(func_block), false, tctx);
Expand Down
6 changes: 6 additions & 0 deletions src/compile/type_inferencer.h
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,13 @@ class TypeInferencer {

private:
State *s_ = nullptr;
std::string file_name_;
// 正在推断 __fakelua_init。它里面的 x = func() 是文件级复杂初值的唯一赋值点,不是再赋值。
bool in_init_function_ = false;
std::unordered_map<std::string, InferredType> file_level_types_;
// 文件级数值字面量 local 的 initializer 节点。InferAssign 用它判断赋值目标是不是
// 这条常量绑定(对函数内同名遮蔽免疫)。__fakelua_init 里的赋值不查这张表。
std::unordered_set<const SyntaxTreeInterface *> file_level_init_exps_;

// 不动点迭代轮次上限(实际通常 2 轮即可收敛)。
static constexpr int kMaxSpecIterations = 16;
Expand Down
91 changes: 84 additions & 7 deletions src/native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ On Windows, `io.open` / `loadfile` / `dofile` / `os.getenv` / `os.tmpname` / Boo
| `net.udp_server(config)` | Bind a UDP socket (`ip`, `port`; `port=0` is ephemeral). `send(data, ip, port)` |
| `net.udp_client(config)` | Connected UDP client (`ip`/`port` are the peer). `send(data)` |
| `obj:dispatch(func_name)` | Register Lua callback function name |
| `obj:send(connid, data)` | Send data (server: specify connid; client: omit) |
| `obj:send(connid, data)` | Send data (server: specify connid; client: omit). From a callback context the send is queued and pumped by the tick after dispatch finishes; failures are logged as WARN |
| `obj:close()` | Close connection/server |
| `obj:close_connection(connid)` | Close single connection (server only) |
| `obj:get_events()` | Event history |
Expand Down Expand Up @@ -402,6 +402,30 @@ Timers go first so callbacks that come due can be picked up by the I/O dispatch
them. Within MySQL, pools are driven before connections so heartbeat and reconnect have
run before a script acquires a connection this round.

**Pump order and callback timing (important):** the order is fixed: timer → net → http → mysql →
redis. Each module dispatches its Lua callbacks synchronously inside the tick, therefore:

- **IO issued from inside a callback produces results at the earliest in the next tick.** A mysql
query issued from a net callback cannot have its result dispatched until the next frame; do not
write code that issues a query from a callback and expects the result synchronously.
- A second `conn:query` on the same connection while one is in flight is **queued** (no error) and
starts automatically once the connection is Ready again; at most one queued query starts per tick.

**Callback context (C++ → Lua) — allowed / forbidden operations:**

net's on_event and the mysql/http/redis result callbacks all run in a restricted dispatch context.
Each entry wraps the call in `IoContext::DispatchScope`, which increments `InDispatch()`
(net `DrainEventsWith`, mysql connect/result dispatch, http `CallNamed`, redis result dispatch).
The flag is an integer depth on the State, so Linux, macOS and Windows behave the same way.
`send` queues while a dispatch is active and the tick pumps the queue after the scope ends.

| Operation | Supported | Notes |
|------|---------|------|
| Mutating fields of a runtime-created table | ✅ | Attaching a table created inside the callback to a long-lived table is a reliable pattern |
| IO such as `send` / mysql `query` | ✅ | sends are pumped by the tick after dispatch finishes; queries queue asynchronously |
| Assigning **fields** of a file-level `local` table | ❌ | file-level tables get CONST_FLAG after init; mutating content throws "attempt to modify a const table" |
| Rebinding a file-level numeric `local` (`x = ...`) | ❌ | a file-level numeric local with a literal initializer is a constant; assigning it later is a compile error that names the source line. `local x = func()` cannot be a C static initializer: the declaration is lowered to `local x = nil` and `__fakelua_init` assigns it once when the shared library is loaded |

---

## Event
Expand Down Expand Up @@ -541,9 +565,18 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv

| Function | Args | Description |
|----------|------|-------------|
| `json.encode(value)` | 1 | Lua value → JSON string; consecutive int keys 1..N → array; floats use `%.17g` |
| `json.encode(value)` | 1 | Lua value → JSON string; consecutive int keys 1..N → array; **an empty table encodes as `[]`**; floats use `%.17g` |
| `json.encode_array(value)` | 1 | Strict array encoding: the top level must be an array-like table (empty table → `[]`; non-empty requires consecutive int keys from 1), otherwise throws |
| `json.decode(str)` | 1 | JSON string → Lua value; `null` → `nil` |

> **Empty-table ambiguity:** Lua cannot distinguish an "empty array" from an "empty object".
> `json.encode` applies the pure-array heuristic and encodes empty tables as `[]`; use
> `json.encode_array` when the client protocol must be strict (array-shaped or error).
>
> **Breaking change:** `json.encode({})` used to produce `{}` and now produces `[]`. Callers that
> need an empty object should encode a table with a string key, or accept both shapes on the client.
> Use `json.encode_array` when the value must be an array.

---

## MySQL
Expand All @@ -556,20 +589,64 @@ Boost.Container-backed structures stored on a NativeObject (C++ heap). They surv

`ssl`: omit/`false`/`"disable"` keeps plaintext (default). `true`/`"require"` demands TLS. `"enable"` uses TLS when the server offers it. Optional `ssl_ca` PEM enables certificate verification.

**Callback arguments:** every async `cb` accepts only an in-package global function name
(string, e.g. `"on_result"` or `"DB.on_result"`); **inline closures are not supported**. FakeLua
has no GC: runtime values live on the temporary arena, and a top-level `Call` resets that arena, so
a raw closure pointer cannot be held safely across ticks. To pass context, append **bound
arguments** (pure data) after the function name: `conn:query(sql, "on_result", tag, ctx_table)`.
Bound arguments are serialized into a self-owned byte blob at registration time (same wire format
as `serialize.encode`), survive any number of `Reset`s, and are deserialized in the dispatch frame
and appended after the fixed callback arguments. The blob is released when the query completes or
the connection is destroyed, so memory usage tracks in-flight queries only; the const arena is not
used.

- Bound arguments may be nil/boolean/number/string and nested tables thereof (no cycles);
non-serializable values (closures, native objects, ...) anywhere in the nesting raise a
"bad argument" error — fields are never silently dropped.
- Bound arguments are a **registration-time snapshot** (copy by value): mutating the original
table after registration is not visible to the callback. Put mutable cross-frame state on the
connection object itself (e.g. `conn.query_done`).
- `pool:with(fn)` invokes fn **synchronously in the same frame** and never stores it across
ticks, so inline closures remain supported there.

**Callback contract:** once `conn:query` is called its callback fires **exactly once** — when the
connection is not ready (handshaking / reconnecting / a previous query still in flight) the query is
queued and starts automatically once the connection is usable; if the connection is closed or in a
terminal error state, the callback receives an error string. All callbacks are driven by
`runtime.tick()`.

| Function/Method | Description |
|----------|-------------|
| `mysql.connect(config, cb)` | Async connect; callback `function cb(err, conn)` |
| `mysql.connect(config, cb, ...)` | Async connect; callback `cb(conn, err, success, ...)`; `...` are bound args appended after the fixed args |
| `mysql_pool.create(config)` | Create connection pool |
| `conn:query(sql, cb)` | Async query; callback `function cb(err, result)` |
| `conn:stmt_prepare(sql, cb)` | Prepare statement |
| `conn:stmt_execute(id, params, cb)` | Execute prepared statement |
| `conn:query(sql, cb, ...)` | Async query; callback `cb(conn, err, result, ...)`; `...` are bound args |
| `conn:stmt_prepare(sql, cb, ...)` | Prepare statement; callback `cb(conn, err, stmt_id, ...)` |
| `conn:stmt_execute(id, params, cb, ...)` | Execute prepared statement; bound args start at the 4th argument |
| `conn:stmt_close(id)` | Close prepared statement |
| `conn:close()` | Close connection |
| `pool:acquire()` | Get connection from pool |
| `pool:acquire()` | Get connection from pool (nil when none available) |
| `pool:release(conn)` | Return connection to pool |
| `pool:with(fn)` | Lease-style usage: acquires a connection and passes it to `fn(conn)`, returning it automatically when fn returns or throws; returns fn's return value. Prevents the "forgot to release / callback never fired so the connection is never returned" leak pattern |
| `pool:close()` | Close pool |
| `pool:stats()` | Returns `{total, healthy}` |

**Result table layout (SELECT):** `result[1] = true`; `result[2]` is the column info table (each item
`{[1]=column name, [2]=MySQL type code}`); `result[3]` is the rows table, each row indexed by column
position (1-based).

**Result table layout (INSERT/UPDATE/DELETE/DDL):** `result[1] = false`; `result[4] = affected_rows`;
`result[5] = last_insert_id`; `result[6] = info`.

**Row value types:** converted by column type — integer columns (TINY/SHORT/LONG/LONGLONG/INT24/YEAR)
return numbers, float/decimal columns (FLOAT/DOUBLE/DECIMAL/NEWDECIMAL) return numbers, all other
columns (strings/dates/BLOBs) return strings, NULL returns nil. Boolean-style `TINYINT(1)` returns a
number (0/1); convert as needed.

> **DECIMAL / NEWDECIMAL precision:** these columns become IEEE 754 doubles, about 15–16 significant
> decimal digits. A `DECIMAL(18,6)` (and similar high-precision definitions) can lose the last digit
> or two, so it is a poor sole representation of money. Cast to a string in SQL
> (`CAST(price AS CHAR)`) and keep it as a string in Lua when the value must be exact.

---

## Redis
Expand Down
Loading
Loading