@datamoc/pyroeis

中文文档
0.9.1发布于 2026/10/17MITTypeScriptCommonJS零依赖构建来源已验证近 30 天下载 1987

Pyroeis: a Lua 5.4.6 VM and Lua-to-JavaScript translator for browsers and Node.js

原始使用文档
文档目录17 节
  1. Features
  2. Performance
  3. Optional numeric compilation
  4. Compilation and execution limits
  5. Offline Lua to JavaScript translation
  6. Installation
  7. Usage
  8. Basic Example
  9. TypeScript
  10. Why Pyroeis has its own JavaScript ↔ Lua exchange API
  11. Extended Auxiliary Library ( lauxlib )
  12. Memory Accounting Hook ( lua_setallocf )
  13. Testing
  14. Documentation
  15. License
  16. Migration from @datamoc/fengari
  17. Interactive demos

Pyroeis

🔴 Pyroeis (Ancient Greek: Πυρόεις) in Greek mythology is the god of the wandering star Areios, identified with the planet Mars.

🔴 Pyroeis (en grec ancien Πυρόεις) est, dans la mythologie grecque, le dieu de l’étoile errante Areios, la planète Mars.

Pyroeis logo

Pyroeis is an independent project derived from Fengari, maintained by datamoc. Install it as @datamoc/pyroeis.

A Lua 5.4 VM written in JavaScript ES6, targeting both Node.js and browser environments. This package implements Lua 5.4.6, including extended auxiliary library (lauxlib) functionality and comprehensive test coverage. Alignment with Lua 5.5.1 is in progress and tracked in docs/lua-5.5.1; none of its milestone lots is complete yet, so the VM reports Lua 5.4.6 through lua.lua_version and _VERSION.

Features

  • Lua 5.4.6 compliance: implements the Lua 5.4 language and standard libraries, plus an extended auxiliary library (lauxlib).
  • Cross-Platform: Runs in Node.js and modern browsers.
  • Extensible: Easy to integrate with existing JavaScript projects.
  • Comprehensive Testing: Includes tests for all core and extended functionality.
  • Lua 5.5.1 alignment in progress: the migration plan, differential oracle and conformance tracking live in docs/lua-5.5.1.
  • Memory accounting hook: lua_setallocf lets an embedder observe, bound or refuse the VM’s internal string and buffer allocations; a refusal behaves like a failing C allocator (LUA_ERRMEM).

Performance

Pyroeis interprets Lua in JavaScript, so it is much slower than native JavaScript on tight numeric loops. docs/benchmark is a self-contained page that runs the same algorithms in Lua (Pyroeis) and in plain JavaScript in your own browser and reports the measured slowdown. Serve the docs/ folder with GitHub Pages to publish it, and run npm run bench:build to regenerate docs/benchmark/pyroeis.bundle.js after changing src/.

Optional numeric compilation

PyroeisVm.compile(pathOrLuaRef) captures a Lua function and returns a reusable JavaScript callable. Eligible pure numeric functions execute as generated JavaScript, including complete nested numeric for loops:

const { PyroeisVm } = require('@datamoc/pyroeis/src/pyroeis-vm.js');
const vm = new PyroeisVm();
vm.run(`
    function sum(n)
        local s = 0
        for i = 1, n do s = s + i end
        return s
    end
`);
const sum = vm.compile('sum');
console.log(sum.compiled, sum(100000)); // true, 5000050000
sum.release(); // optional: release the captured Lua function immediately

This opt-in compiler supports numeric constants, parameters, locals, assignments, +, -, *, /, ^, unary minus, returns and integer numeric for loops. Calls, globals/upvalues, tables, comparisons/branches, varargs and other bytecode use the interpreter. Unsupported input types, integer calculations outside the safe Number range, float loop controls, debug hooks and active execution limits also use the interpreter. Lua integer overflow and float types are preserved; use LuaFloat to explicitly pass integral floats, as with the existing VM API.

The callable returns the first result and captures the function at compilation time; redefining its global does not replace it. compiled reports eligibility, reason explains unsupported code, and source exposes the generated JavaScript body. Eligibility does not guarantee that every invocation takes the fast path. Generated code is cached per Lua prototype. Environments that prohibit new Function (for example browser CSP without unsafe-eval) use the interpreter. No generated code contains Lua source text or identifier names.

Run node tools/benchmark-numeric.js for a checksum-verified comparison of the interpreter, compiled Lua and native JavaScript on your machine.

Compilation and execution limits

compile is deliberately disabled while limits are active: the callable then always runs in the interpreter. Generated JavaScript is not interrupted by the instruction-counting hook, the pattern-matching interrupt points or the allocation hook, so it would have to enforce the budget on its own, and that is a second enforcement mechanism to get right (every loop shape, one budget shared with the interpreter, no fresh budget after a guard falls back to the VM). Skipping it keeps the sandbox guarantees in one place. This is not a gap to work around:

  • run code you do not trust through run/call with limits (and sandbox);
  • if you need the speed, call compile only for functions you wrote or reviewed, on a VM without limits, and keep untrusted scripts on a separate, limited VM.

limits.coroutines, limits.tables and limits.closures count the objects a script creates per top-level call, not the live ones (the garbage collector is not observable). On a VM kept across calls a script can therefore still keep up to that many new objects per call: for untrusted scripts, use a fresh VM per script, or discard the VM when it is done. A call that exceeds any limit fails with a LimitError whose limit names it (instructions, time, memory, coroutines, tables, closures) and whose message is <limit> limit exceeded, on run, call, callRef, Coroutine#resume and protect alike (memory keeps not enough memory).

Offline Lua to JavaScript translation

To leave Lua behind, lua2js turns Lua source into JavaScript a developer can read and keep editing. It is separate from the runtime numeric compiler above: it never runs Lua, it prints idiomatic JavaScript from the source and marks what it cannot translate.

npx lua2js game.lua -o game.js --format esm   # none (default) | esm | cjs
const { transpile } = require('@datamoc/pyroeis/src/ltranspile.js');
const { code, warnings, exports, freeGlobals } = transpile(luaSource, { format: 'cjs' });
function stats(t)
  local sum = 0
  for i, v in ipairs(t) do sum = sum + v end
  return sum, sum / #t
end
function stats(t) {
    let sum = 0;
    for (let i = 1; i <= t.length; i++) {
        const v = t[i - 1];
        sum += v;
    }
    return [sum, sum / t.length];
}

What is translated: locals (const/let, renamed on shadowing or JS reserved words), functions and closures (arrows when a single expression), if/while/repeat/numeric and generic for, goto continue, and/or ternary idioms, string concatenation (template literals), tables as arrays or objects (inferred from use), multiple returns (arrays, inferred per function), varargs, methods, Lua patterns with a literal pattern string (find, match, gmatch, gsub, converted to RegExp), and the common parts of math, string, table, os and io.

An online version lives in docs/lua2js/index.html: paste Lua, read the JavaScript, and benchmark both in your browser.

Things to know before trusting the output:

  • Every construct that cannot be translated faithfully gets a // TODO(lua2js): ... comment and an entry in warnings. This covers metatables, coroutines, Lua patterns that have no RegExp equivalent (%b, %f, position captures) or are not string literals, goto other than continue, bitwise operators (JavaScript uses 32 bits) and iterators that do not return arrays. freeGlobals lists the globals the host must provide. A warning with unsafe: true marks a construct that reaches the JavaScript environment (eval, Function, globalThis, process, fetch...) or a prototype (t.__proto__, t.constructor): translated code is plain JavaScript, not a sandbox, so do not run the translation of untrusted Lua.
  • Lua arrays are 1-based and JavaScript arrays are 0-based: indexes are shifted (t[i] becomes t[i - 1]) when the table or the index is known to be numeric; otherwise the index is left alone and flagged.
  • nil is undefined. if x becomes truthy(x) (Lua treats 0 and "" as true) unless x is known to be a boolean. a or b keeps Lua’s meaning (truthy(a) ? a : b, or a ?? b when a is known never to be false); the nullishOr option (--nullish-or) always emits the shorter a ?? b, which differs from Lua only when a is false.
  • Integers and floats are both JavaScript numbers: 2^53 and beyond, 1.0 printing as 1, and string-to-number coercion ("10" + 1) are not preserved.
  • Small helpers (truthy, mod, format, ...) are emitted at the top of the output only when used.

Installation

npm install @datamoc/pyroeis

Usage

Basic Example

const { lua, lauxlib, lualib } = require('@datamoc/pyroeis');

// Create a new Lua state
const L = lauxlib.luaL_newstate();
lualib.luaL_openlibs(L);  // Load standard libraries

// Execute Lua code
lauxlib.luaL_dostring(L, lua.to_luastring("print('Hello from Lua 5.4!')"));

// Close the Lua state
lua.lua_close(L);

TypeScript

The package ships its declarations (src/pyroeis.d.ts): import { PyroeisVm, LuaToJsOptions } from '@datamoc/pyroeis' is typed without @ts-expect-error. The C-API namespaces (lua, lauxlib, lualib) are typed as any.

Why Pyroeis has its own JavaScript ↔ Lua exchange API

Pyroeis uses Lua for application rules, game strategies and scripts whose host controls the available operations. PyroeisVm therefore converts ordinary JavaScript objects, arrays and maps into Lua tables, reads Lua tables back as JavaScript data, and exposes host operations through defineFunction. A copied table does not provide a live handle to the original JavaScript object.

fengari-interop serves a different integration model: JavaScript objects remain accessible through userdata, Lua values can be wrapped for use from JavaScript, and its js library includes access to the JavaScript global environment. That model is useful for trusted Lua code that needs to work directly with JavaScript APIs. Our default interface keeps scripts working with Lua data and a host-selected set of functions instead of granting that general bridge.

This choice also keeps calls, Lua references, coroutines, error handling and execution budgets within the same VM API. BigInt, binary strings, LuaFloat, Multi and Yield have explicit conversions. Functions, threads and full userdata use VM-owned LuaRef handles; ordinary tables are snapshots. The MWG demo takes this approach further: it sends target descriptions into a worker and accepts a validated target index, while the host retains the map, pathfinder and rendering objects.

The tradeoff is conversion work and the absence of automatic shared-object identity, prototypes or metatable behavior for copied data. JavaScript cycles cannot currently be pushed as tables; Lua table cycles are preserved when read. Callbacks remain trusted host code: execution budgets cannot preempt arbitrary JavaScript or cover all its allocations. This API is not a complete isolation mechanism, and fengari-interop is not inherently unsafe merely because it offers broader access.

The upstream bridge imports fengari and declares it as a peer dependency; it is not a supported drop-in bridge for @datamoc/pyroeis. Using or porting it would require validating the VM and C-API compatibility as well as the authority exposed to scripts. See the detailed design and examples and the upstream implementation and package manifest.

Extended Auxiliary Library (lauxlib)

const { lauxlib } = require('@datamoc/pyroeis');

// Example: Using luaL_addgsub for string substitution
const B = new lauxlib.luaL_Buffer();
lauxlib.luaL_buffinit(L, B);
lauxlib.luaL_addgsub(B, lua.to_luastring("hello world"), lua.to_luastring("world"), lua.to_luastring("Lua"));
lauxlib.luaL_pushresult(B);

Memory Accounting Hook (lua_setallocf)

lua_setallocf(L, allocf, ud) installs a per-state hook called synchronously as allocf(ud, nsize) immediately before each internal Uint8Array allocation of nsize bytes (the full new size, growth reallocations included). Returning normally permits the allocation; throwing refuses it, which aborts the enclosing protected call with status LUA_ERRMEM and the standard "not enough memory" error object, leaving the state usable. lua_getallocf(L) returns [allocf, ud].

const { lua, lauxlib, lualib } = require('@datamoc/pyroeis');

const L = lauxlib.luaL_newstate();
lualib.luaL_openlibs(L);

let budget = 1024;
lua.lua_setallocf(L, (ud, nsize) => {
    if (nsize > ud.budget) throw new Error('over budget');
}, { budget });

lauxlib.luaL_dostring(L, lua.to_luastring('return ("x"):rep(10)'));
console.log(lua.lua_tojsstring(L, -1)); // xxxxxxxxxx

This is the JavaScript shape of C Lua’s lua_Alloc extension point: it is accounting, not ownership (the JavaScript engine owns the memory, and there are no free notifications). Not charged: the stateless to_luastring conversions an embedder drives itself, zero-size allocations, and the few internal temporaries whose site has no lua_State at hand. See src/lmem.js for the exact contract.

Testing

Run the test suite:

npm test

For extended tests (e.g., auxiliary library functions):

npm run test:extended

Documentation

License

MIT © Michel Weinachter (Datamoc), Benoit Giannangeli, Daurnimator, and contributors.

Migration from @datamoc/fengari

Install @datamoc/pyroeis and update your imports. Use PyroeisVm for the high-level VM API. The legacy src/fengari* entry points and the FengariVm alias were removed; use src/pyroeis.js / src/pyroeis-vm.js and PyroeisVm. The browser bundle exposes Pyroeis only. Version 0.6.0 removes the remaining Fengari names: the Lua library fengari is now pyroeis, the constants FENGARI_* are PYROEIS_* (and describe Pyroeis, not Fengari), luaopen_fengari is luaopen_pyroeis, LUA_FENGARILIBNAME is LUA_PYROEISLIBNAME, the libs option takes 'pyroeis', and the configuration is read from the PYROEISCONF environment variable (a JSON object such as {"LUAI_MAXSTACK":100000}) instead of FENGARICONF.

Interactive demos

The Pyroeis website includes execution-limit reproductions, four-mode benchmarks, the Lua-to-JavaScript translator and a Lua-controlled MWG robot.

Run npm run pages:build to generate the browser bundles and retrieve the pinned demo dependencies with SHA-512 verification, then serve docs/ over HTTP (for example, python -m http.server 8765 --directory docs). The GitHub Pages workflow builds and deploys the same folder on pushes to master.

版本与发布记录

@datamoc/pyroeis 共发布 7 个版本,首次发布于 2026/10/12;过去 12 个月发布了 7 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.1latest2026/10/1728 天前
0.9.02026/10/141 个月前
0.8.02026/10/131 个月前
0.7.02026/10/131 个月前
0.6.02026/10/131 个月前
0.5.42026/10/121 个月前

关于 @datamoc/pyroeis 的常见问题

@datamoc/pyroeis 是什么?

Pyroeis: a Lua 5.4.6 VM and Lua-to-JavaScript translator for browsers and Node.js

如何安装 @datamoc/pyroeis?

在项目目录运行 npm install @datamoc/pyroeis,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 lua2js,可以用 npx lua2js 直接运行。

@datamoc/pyroeis 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@datamoc/pyroeis 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@datamoc/pyroeis 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@datamoc/pyroeis 可以商用吗?

@datamoc/pyroeis 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@datamoc/pyroeis 的下载量有多少?

根据 npm 官方下载统计,@datamoc/pyroeis 在 2026/10/12 – 2026/11/10 这 30 天内被下载 1,987 次,日均约 66 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@datamoc/pyroeis 还在维护吗?

最新版本 0.9.1 发布于 2026/10/17(28 天前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/10/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。