2026-02-06 17:34:26 -05:00
|
|
|
### A New Lua Type: Tokens
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
Tokens are a custom Lua data type built on top of Lua's
|
|
|
|
|
lightuserdata. They are mainly intended for use as sentinels
|
|
|
|
|
and special reserved values.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
## Motivation
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
Tokens were invented when we were developing a JSON-to-LUA
|
|
|
|
|
converter. Such a converter is mostly straightforward: json
|
|
|
|
|
tables and lua tables are very similar. However, we did
|
|
|
|
|
encounter a stumbling block. Consider this JSON:
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "foo": null }
|
|
|
|
|
```
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
In Lua, setting a table key to nil deletes the key. There is
|
|
|
|
|
no way to represent "foo is present with value null" in a
|
|
|
|
|
Lua table. You might try `{foo = 0}` or `{foo = "null"}`,
|
|
|
|
|
but both are lossy: you can no longer distinguish JSON null
|
|
|
|
|
from the number 0 or the string "null". Any sentinel value
|
|
|
|
|
drawn from an existing Lua type collides with legitimate
|
|
|
|
|
values of that type.
|
|
|
|
|
|
|
|
|
|
The solution is to use lightuserdata. A lightuserdata is a
|
|
|
|
|
distinct Lua type — it cannot be confused with a string,
|
|
|
|
|
number, boolean, or nil, and unlike nil, it can be stored in
|
|
|
|
|
a table. The Luprex engine does not use lightuserdata for
|
|
|
|
|
any other purpose, so all lightuserdata values are available
|
|
|
|
|
for use as tokens.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
## What a Token Is
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
A token is a short string encoded as a base37 number and
|
|
|
|
|
stored in the 8-byte lightuserdata value. The lightuserdata
|
|
|
|
|
is not actually a pointer to anything — it holds the
|
|
|
|
|
base37-encoded integer directly. Tokens may only contain the
|
|
|
|
|
characters a-z and 0-9, and the null terminator. Since 37^12
|
|
|
|
|
fits in 64 bits but 37^13 does not, the maximum token length
|
|
|
|
|
is 12 characters. That is sufficient for most natural
|
|
|
|
|
identifiers.
|
|
|
|
|
|
|
|
|
|
## The Lua Lexer
|
|
|
|
|
|
|
|
|
|
We have modified the lua lexer/parser to support tokens.
|
|
|
|
|
To write a token in lua, use an @ sign:
|
|
|
|
|
|
|
|
|
|
local x = @hello
|
2026-02-06 17:34:26 -05:00
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
This actually stores a light user data constant in x.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
## The C++ Side: struct LuaToken
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
On the C++ side, tokens are represented by `struct LuaToken`
|
|
|
|
|
(in luastack.hpp). You can construct one from a string:
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
```cpp
|
2026-02-19 00:32:29 -05:00
|
|
|
LuaToken("null")
|
2026-02-06 17:34:26 -05:00
|
|
|
```
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
This constructor is `consteval`, this is as efficient as a
|
|
|
|
|
literal integer. If the string contains invalid characters
|
|
|
|
|
(anything outside a-z, 0-9) or is too long, the error is
|
|
|
|
|
caught at compile time.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
There is also a runtime constructor that accepts
|
|
|
|
|
`std::string_view`, for cases where the token string is not
|
|
|
|
|
known at compile time.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
The LuaStack API provides the usual accessors for tokens:
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
LS.set(slot, LuaToken("null")) // store a token in a LuaSlot
|
|
|
|
|
LuaToken t = LS.cktoken(slot) // extract a token (error if not lightuserdata)
|
|
|
|
|
auto t = LS.trytoken(slot) // extract a token (returns empty optional on mismatch)
|
|
|
|
|
```
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
Named token constants can be auto-registered into the Lua
|
|
|
|
|
environment using the `LuaTokenConstant` macro, which works
|
|
|
|
|
the same way `LuaDefine` auto-registers functions:
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
```cpp
|
2026-02-19 00:32:29 -05:00
|
|
|
LuaTokenConstant(json_null, "null", "Represents JSON null")
|
2026-02-06 17:34:26 -05:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Properties
|
|
|
|
|
|
|
|
|
|
- **Distinct type.** Tokens are lightuserdata, a separate Lua type. They cannot collide with strings, numbers, booleans, tables, or nil.
|
2026-02-19 00:32:29 -05:00
|
|
|
- **Storable in tables.** Tokens can be used as both table keys and table values.
|
2026-02-06 17:34:26 -05:00
|
|
|
- **No allocation.** Tokens are 8 bytes inline. There is no heap allocation and no string interning.
|
|
|
|
|
- **Fast comparison.** Comparing two tokens is just an integer comparison.
|
|
|
|
|
|
|
|
|
|
## Passing Tokens to Unreal
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
Tokens can get passed to Unreal in a variety of ways. For
|
|
|
|
|
example, in animation step key-value pairs, the value can be
|
|
|
|
|
a token. When tokens are passed to Unreal, they are
|
|
|
|
|
converted to FNames. Since both tokens and FNames are short
|
|
|
|
|
identifier-like strings with fast comparison, the mapping is
|
|
|
|
|
natural.
|
2026-02-06 17:34:26 -05:00
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
2026-02-19 00:32:29 -05:00
|
|
|
Tokens are mainly intended as sentinels and special reserved
|
|
|
|
|
values. The JSON null example above is the motivating case,
|
|
|
|
|
but tokens can represent any short reserved constant the
|
|
|
|
|
engine needs.
|
2026-02-18 23:23:59 -05:00
|
|
|
|
|
|
|
|
## Serialization and Difference Transmission
|
|
|
|
|
|
|
|
|
|
I believe that we properly serialize and difference transmit tokens.
|
|
|
|
|
|
|
|
|
|
* the serialize_lua function handles tokens explicitly
|
|
|
|
|
* the difference transmitter has code for tokens
|
|
|
|
|
* eris always saved lightuserdata as 64-bit numbers
|
|
|
|
|
|