Script Engines
Corpus transforms and script generators run in one of two engines built into Mr.Docs: Lua and JavaScript. Both see the same ctx object and the same mrdocs API. The corpus transforms and script generators pages show examples in both languages.
This page is about what differs in practice between the engines: how fast each engine runs, what a symbol costs a script, how each engine uses memory, and what happens when it runs out.
The short version: Lua is faster, 2 to 20 times on the same work, and has no memory limit of its own. JavaScript is reasonable for any real codebase, with a fixed heap of at most 4 GB per engine and the language’s own built-ins but no module system.
| Lua | JavaScript | |
|---|---|---|
Engine |
Lua 5.4.8 |
JerryScript 3.0.0, |
Speed on the corpus |
Better: a loop costs a microsecond or less per symbol |
Good: a loop costs a few microseconds per symbol |
Symbols |
Lazy userdata, under 100 bytes each while alive |
Lazy proxies, about 50 bytes each while alive |
Memory |
The system allocator, as needed |
One fixed block per engine, up to 4 GB |
Standard Library |
The whole Lua standard library |
The ECMAScript built-ins |
Out of memory |
A Lua error a script can catch with |
The engine halts for good and Mr.Docs exits with an error. A script cannot catch it |
Performance
The table below compares six transforms, each written once in JavaScript and once in Lua and run over the Mr.Docs corpus (6414 symbols):
| Task | Lua | JavaScript | Ratio |
|---|---|---|---|
Count symbols |
3 ms |
7 ms |
2× |
Collect names |
4 ms |
15 ms |
4× |
Read a nested property |
4 ms |
32 ms |
8× |
Build text per symbol |
4 ms |
66 ms |
15× |
Keep every symbol in an array |
1 ms |
19 ms |
17× |
Number loop, no symbols involved |
17 ms |
157 ms |
9× |
Each transform timed itself from inside the script, so extraction and rendering are excluded. The figures are the average of 30 runs. The scripts and the runner live in tests/benchmarks/script-engines/, and cmake --build build/<preset> --target mrdocs-benchmark-script-engines prints the table for your machine.
Lua wins every row:
-
The number loop, which touches no symbols, shows the interpreters themselves: Lua’s register machine with native integers runs about nine times faster than JerryScript’s stack machine with 32-bit tagged values.
-
On the symbol tasks, the gap is smaller where a script only reads, and larger where it builds things. Reading a property costs about a microsecond in JavaScript. Every symbol a script touches is a
Proxy, and every read goes through a trap that converts the property name and locks the engine. Lua does the same work through a metatable on a small userdata, at a fraction of the cost. -
Building a string one piece at a time is where JavaScript falls furthest behind: JerryScript copies on every concatenation, while Lua’s
table.concatbuilds the result once. -
For a corpus of 100,000 symbols, a JavaScript transform that reads two properties of every symbol takes well under a second. The same Lua transform takes a tenth of that.
Symbols as values
In both engines ctx.corpus.symbols is lazy. Reading it costs nothing, and each element becomes a script value when the script reaches it. A loop over every symbol of any corpus works, and so does keeping the ones you filtered out. On the Mr.Docs corpus, keeping 102,624 JavaScript symbol proxies alive at once took 129 ms and added 5.4 MB, about 53 bytes each. Lua kept all 6414 symbols in 579 KB.
Each read builds a fresh value, in both engines, so two reads of the same element are not the same object. Compare symbols by id, not by identity. Do not rely on indexOf, includes or a Set to find or remove duplicate symbol objects in JavaScript, or on table keys in Lua.
Memory
Lua
Lua stores native (typically 64-bit) pointers and takes every value from the system allocator, one call at a time, so its values can live anywhere in memory and there is no block to run out of. JerryScript stores 32-bit offsets into one block instead, which is where its 4 GB comes from. Lua frees a value when its garbage collector finds it is no longer used.
Thus, a Lua script is limited by the machine’s memory, like the rest of Mr.Docs. If an allocation does fail, Lua raises a "not enough memory" error that a script can catch with pcall. If the script does not catch it, it ends the transform with that message.
JavaScript
JerryScript stores 32-bit offsets into one block instead of native pointers, so each object costs less, and that block is where its values live. The block grows as a script creates objects and can reach just under 4 GB, the most a 32-bit offset can address. A script that keeps 50 MB alive costs about 50 MB. If a script exhausts the block, the engine stops and Mr.Docs exits with an error that names the size. A script cannot catch it, since there is no JavaScript exception for this case.
Standard libraries
Lua
Mr.Docs opens the whole standard library: string, table, math, io, os, utf8, coroutine, debug and package. require finds those by name. It does not find module files of your own: package.path is Lua’s default (system paths and the current directory), not the addon directory, so require "helper" for a helper.lua next to the script fails. Keep a Lua extension in one file, or set package.path yourself at the top of the script.
JavaScript
The engine implements the language and the ECMAScript built-ins, and nothing beyond that.
-
It provides
Object,Array,String,Number,BigInt,Math,JSON,RegExp,Date,Map,Set,WeakMap,WeakRef,Promise,Symbol,Proxy,Reflect,ArrayBufferwith the typed arrays, generators, andasyncfunctions. -
It does not provide
Intl,setTimeout,queueMicrotask,TextEncoder,fetch,structuredClone, or anything Node or a browser would add (fs,path,process,require).
There is no module system: import and export are syntax errors, so a script is one file.
Mr.Docs adds two host objects: console, with log and error, and mrdocs, with register_transform, register_generator and report. Everything else a script needs comes through the ctx object it is handed.
Choosing a language
Pick Lua when the script runs over a large codebase, keeps many symbols alive, or runs on every build. It has no memory limit of its own, runs faster on the same work, and has the whole Lua standard library, including io and os. You should also use Lua for scripts you’re intenting to reuse across many projects.
Pick JavaScript when its authors already write JavaScript, or when the work is a few lookups per run. You get the language and its built-ins: JSON, regular expressions, Map, Set, Promise and the array methods. It handles any corpus that fits in 4 GB, which is enough for most real codebases.
Whichever you pick, the ctx object, the mrdocs.register_transform and mrdocs.register_generator calls, and the symbol fields at the DOM reference are the same.