Polish redbean serialization

This commit is contained in:
Justine Tunney 2022-04-29 06:06:23 -07:00
parent 7aafa64ab3
commit 2d1731b995
24 changed files with 828 additions and 158 deletions

View file

@ -24,6 +24,8 @@ LOCAL MODIFICATIONS
Integer literals such as `033` will now be interpreted as octal.
Integer literals such as `0b10` will now be interpreted as binary.
The `\e` string literal escape sequence has been added, which is
equivalent to `\27` (the Lua version of `\033`) or the ASCII ESC
character. It may be used for teletypewriter control like having

View file

@ -15,9 +15,9 @@ int LuaParseUrl(lua_State *);
int LuaPushHeader(lua_State *, struct HttpMessage *, char *, int);
int LuaPushHeaders(lua_State *, struct HttpMessage *, const char *);
void EscapeLuaString(char *, size_t, char **);
void LuaPrintStack(lua_State *);
void LuaPushLatin1(lua_State *, const char *, size_t);
void LuaPushUrlParams(lua_State *, struct UrlParams *);
void LuaPrintStack(lua_State *);
COSMOPOLITAN_C_END_
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */

101
third_party/lua/lapi.c vendored
View file

@ -504,26 +504,51 @@ static void *touserdata (const TValue *o) {
}
/**
* lua_touserdata [-0, +0, ]
*
* If the value at the given index is a full userdata, returns its
* memory-block address. If the value is a light userdata, returns its value
* (a pointer). Otherwise, returns NULL.
*/
LUA_API void *lua_touserdata (lua_State *L, int idx) {
const TValue *o = index2value(L, idx);
return touserdata(o);
}
/**
* lua_tothread [-0, +0, ]
*
* Converts the value at the given index to a Lua thread (represented as
* lua_State*). This value must be a thread; otherwise, the function returns
* NULL.
*/
LUA_API lua_State *lua_tothread (lua_State *L, int idx) {
const TValue *o = index2value(L, idx);
return (!ttisthread(o)) ? NULL : thvalue(o);
}
/*
** Returns a pointer to the internal representation of an object.
** Note that ANSI C does not allow the conversion of a pointer to
** function to a 'void*', so the conversion here goes through
** a 'size_t'. (As the returned pointer is only informative, this
** conversion should not be a problem.)
*/
/**
* lua_topointer [-0, +0, ]
*
* Converts the value at the given index to a generic C pointer (void*). The
* value can be a userdata, a table, a thread, a string, or a function;
* otherwise, lua_topointer returns NULL. Different objects will give
* different pointers. There is no way to convert the pointer back to its
* original value.
*
* Typically this function is used only for hashing and debug information.
*/
LUA_API const void *lua_topointer (lua_State *L, int idx) {
/*
** Returns a pointer to the internal representation of an object.
** Note that ANSI C does not allow the conversion of a pointer to
** function to a 'void*', so the conversion here goes through
** a 'size_t'. (As the returned pointer is only informative, this
** conversion should not be a problem.)
*/
const TValue *o = index2value(L, idx);
switch (ttypetag(o)) {
case LUA_VLCF: return cast_voidp(cast_sizet(fvalue(o)));
@ -881,6 +906,12 @@ static Table *gettable (lua_State *L, int idx) {
}
/**
* lua_rawget [-1, +1, ]
*
* Similar to lua_gettable, but does a raw access (i.e., without
* metamethods).
*/
LUA_API int lua_rawget (lua_State *L, int idx) {
Table *t;
const TValue *val;
@ -901,6 +932,15 @@ LUA_API int lua_rawgeti (lua_State *L, int idx, lua_Integer n) {
}
/**
* lua_rawgetp [-0, +1, ]
*
* Pushes onto the stack the value t[k], where t is the table at the given
* index and k is the pointer p represented as a light userdata. The access
* is raw; that is, it does not use the __index metavalue.
*
* Returns the type of the pushed value.
*/
LUA_API int lua_rawgetp (lua_State *L, int idx, const void *p) {
Table *t;
TValue k;
@ -911,6 +951,17 @@ LUA_API int lua_rawgetp (lua_State *L, int idx, const void *p) {
}
/**
* lua_createtable [-0, +1, m]
*
* Creates a new empty table and pushes it onto the stack. Parameter narr is
* a hint for how many elements the table will have as a sequence; parameter
* nrec is a hint for how many other elements the table will have. Lua may
* use these hints to preallocate memory for the new table. This
* preallocation may help performance when you know in advance how many
* elements the table will have. Otherwise you can use the function
* lua_newtable.
*/
LUA_API void lua_createtable (lua_State *L, int narray, int nrec) {
Table *t;
lua_lock(L);
@ -924,6 +975,15 @@ LUA_API void lua_createtable (lua_State *L, int narray, int nrec) {
}
/**
* lua_getmetatable [-0, +(0|1), ]
*
* int lua_getmetatable (lua_State *L, int index);
*
* If the value at the given index has a metatable, the function pushes that
* metatable onto the stack and returns 1. Otherwise, the function returns 0
* and pushes nothing on the stack.
*/
LUA_API int lua_getmetatable (lua_State *L, int objindex) {
const TValue *obj;
Table *mt;
@ -951,6 +1011,17 @@ LUA_API int lua_getmetatable (lua_State *L, int objindex) {
}
/**
* lua_getiuservalue [-0, +1, ]
*
* int lua_getiuservalue (lua_State *L, int index, int n);
*
* Pushes onto the stack the n-th user value associated with the full
* userdata at the given index and returns the type of the pushed value.
*
* If the userdata does not have that value, pushes nil and returns
* LUA_TNONE.
*/
LUA_API int lua_getiuservalue (lua_State *L, int idx, int n) {
TValue *o;
int t;
@ -1116,6 +1187,15 @@ LUA_API void lua_rawseti (lua_State *L, int idx, lua_Integer n) {
}
/**
* lua_setmetatable [-1, +0, ]
*
* Pops a table or nil from the stack and sets that value as the new
* metatable for the value at the given index. (nil means no metatable.)
*
* (For historical reasons, this function returns an int, which now is always
* 1.)
*/
LUA_API int lua_setmetatable (lua_State *L, int objindex) {
TValue *obj;
Table *mt;
@ -1156,6 +1236,13 @@ LUA_API int lua_setmetatable (lua_State *L, int objindex) {
}
/**
* lua_setiuservalue [-1, +0, ]
*
* Pops a value from the stack and sets it as the new n-th user value
* associated to the full userdata at the given index. Returns 0 if the
* userdata does not have that value.
*/
LUA_API int lua_setiuservalue (lua_State *L, int idx, int n) {
TValue *o;
int res;

View file

@ -45,6 +45,12 @@
('a' <= c_ && c_ <= 'f')); \
})
#define lisbdigit(C) \
({ \
unsigned char c_ = (C); \
'0' <= c_&& c_ <= '1'; \
})
#define lisprint(C) \
({ \
unsigned char c_ = (C); \

View file

@ -27,7 +27,6 @@
*/
#define lobject_c
#define LUA_CORE
#include "libc/intrin/kprintf.h"
#include "third_party/lua/lctype.h"
#include "third_party/lua/ldebug.h"
#include "third_party/lua/ldo.h"
@ -285,6 +284,14 @@ static const char *l_str2int (const char *s, lua_Integer *result) {
empty = 0;
}
}
if (s[0] == '0' &&
(s[1] == 'b' || s[1] == 'B')) { /* [jart] binary */
s += 2; /* skip '0b' */
for (; lisbdigit(cast_uchar(*s)); s++) {
a = a * 2 + (*s - '0');
empty = 0;
}
}
else if (s[0] == '0') { /* [jart] octal is the best radix */
for (s += 1; lisdigit(cast_uchar(*s)); s++) {
int d = *s - '0';

View file

@ -30,13 +30,13 @@
#include "libc/calls/calls.h"
#include "libc/calls/sigbits.h"
#include "libc/errno.h"
#include "libc/intrin/kprintf.h"
#include "libc/intrin/nomultics.internal.h"
#include "libc/log/check.h"
#include "libc/macros.internal.h"
#include "libc/mem/mem.h"
#include "libc/runtime/gc.h"
#include "libc/runtime/runtime.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/sa.h"
#include "third_party/linenoise/linenoise.h"
@ -330,7 +330,7 @@ void lua_initrepl(lua_State *L, const char *progname) {
prompt = get_prompt(L, 1);
if ((g_historypath = linenoiseGetHistoryPath(progname))) {
if (linenoiseHistoryLoad(g_historypath) == -1) {
kprintf("%r%s: failed to load history: %m%n", g_historypath);
fprintf(stderr, "%r%s: failed to load history: %m%n", g_historypath);
free(g_historypath);
g_historypath = 0;
}

View file

@ -16,23 +16,37 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/assert.h"
#include "libc/bits/bits.h"
#include "libc/fmt/itoa.h"
#include "libc/mem/mem.h"
#include "libc/runtime/gc.internal.h"
#include "libc/stdio/append.internal.h"
#include "net/http/escape.h"
#include "third_party/lua/cosmo.h"
#include "third_party/lua/lauxlib.h"
#include "third_party/lua/lua.h"
#include "third_party/lua/visitor.h"
static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level,
char *numformat, int idx) {
char *numformat, int idx,
struct LuaVisited *visited) {
char *s;
bool isarray;
size_t tbllen, z;
size_t tbllen, i, z;
char ibuf[21], fmt[] = "%.14g";
if (level > 0) {
switch (lua_type(L, idx)) {
case LUA_TNIL:
appendw(buf, READ32LE("null"));
return 0;
case LUA_TBOOLEAN:
appendw(buf, lua_toboolean(L, idx) ? READ32LE("true")
: READ64LE("false\0\0"));
return 0;
case LUA_TSTRING:
s = lua_tolstring(L, idx, &z);
s = EscapeJsStringLiteral(s, z, &z);
@ -41,21 +55,7 @@ static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level,
appendw(buf, '"');
free(s);
return 0;
case LUA_TNIL:
appendw(buf, READ32LE("null"));
return 0;
case LUA_TFUNCTION:
appendf(buf, "\"func@%p\"", lua_touserdata(L, idx));
return 0;
case LUA_TUSERDATA:
appendf(buf, "\"udata@%p\"", lua_touserdata(L, idx));
return 0;
case LUA_TLIGHTUSERDATA:
appendf(buf, "\"light@%p\"", lua_touserdata(L, idx));
return 0;
case LUA_TTHREAD:
appendf(buf, "\"thread@%p\"", lua_touserdata(L, idx));
return 0;
case LUA_TNUMBER:
if (lua_isinteger(L, idx)) {
appendd(buf, ibuf,
@ -78,58 +78,64 @@ static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level,
appendf(buf, fmt, lua_tonumber(L, idx));
}
return 0;
case LUA_TBOOLEAN:
appends(buf, lua_toboolean(L, idx) ? "true" : "false");
return 0;
case LUA_TTABLE:
tbllen = lua_rawlen(L, idx);
// encode tables with numeric indices and empty tables as arrays
isarray =
tbllen > 0 || // integer keys present
(lua_pushnil(L), lua_next(L, -2) == 0) || // no non-integer keys
(lua_pop(L, 2), false); // pop key/value pushed by lua_next
appendw(buf, isarray ? '[' : '{');
if (isarray) {
for (size_t i = 1; i <= tbllen; i++) {
if (i > 1) appendw(buf, ',');
lua_rawgeti(L, -1, i); // table/-2, value/-1
LuaEncodeJsonDataImpl(L, buf, level - 1, numformat, -1);
lua_pop(L, 1);
if (LuaPushVisit(visited, lua_topointer(L, idx))) {
tbllen = lua_rawlen(L, idx);
// encode tables with numeric indices and empty tables as arrays
isarray =
tbllen > 0 || // integer keys present
(lua_pushnil(L), !lua_next(L, -2)) || // no non-integer keys
(lua_pop(L, 2), false); // pop key/value pushed by lua_next
appendw(buf, isarray ? '[' : '{');
if (isarray) {
for (i = 1; i <= tbllen; i++) {
if (i > 1) appendw(buf, ',');
lua_rawgeti(L, -1, i); // table/-2, value/-1
LuaEncodeJsonDataImpl(L, buf, level - 1, numformat, -1, visited);
lua_pop(L, 1);
}
} else {
i = 1;
lua_pushnil(L); // push the first key
while (lua_next(L, -2)) {
if (!lua_isstring(L, -2)) {
luaL_error(L, "expected number or string as key value");
unreachable;
}
if (i++ > 1) appendw(buf, ',');
appendw(buf, '"');
if (lua_type(L, -2) == LUA_TSTRING) {
s = lua_tolstring(L, -2, &z);
s = EscapeJsStringLiteral(s, z, &z);
appendd(buf, s, z);
free(s);
} else {
// we'd still prefer to use lua_tostring on a numeric index, but
// can't use it in-place, as it breaks lua_next (changes numeric
// key to a string)
lua_pushvalue(L, -2); // table/-4, key/-3, value/-2, key/-1
s = lua_tolstring(L, idx, &z);
appendd(buf, s, z); // use the copy
lua_remove(L, -1); // remove copied key: tab/-3, key/-2, val/-1
}
appendw(buf, '"' | ':' << 010);
LuaEncodeJsonDataImpl(L, buf, level - 1, numformat, -1, visited);
lua_pop(L, 1); // table/-2, key/-1
}
// stack: table/-1, as the key was popped by lua_next
}
appendw(buf, isarray ? ']' : '}');
LuaPopVisit(visited);
return 0;
} else {
int i = 1;
lua_pushnil(L); // push the first key
while (lua_next(L, -2)) {
if (!lua_isstring(L, -2)) {
luaL_error(L, "expected number or string as key value");
unreachable;
}
if (i++ > 1) appendw(buf, ',');
appendw(buf, '"');
if (lua_type(L, -2) == LUA_TSTRING) {
s = lua_tolstring(L, -2, &z);
s = EscapeJsStringLiteral(s, z, &z);
appendd(buf, s, z);
free(s);
} else {
// we'd still prefer to use lua_tostring on a numeric index, but
// can't use it in-place, as it breaks lua_next (changes numeric
// key to a string)
lua_pushvalue(L, -2); // table/-4, key/-3, value/-2, key/-1
s = lua_tolstring(L, idx, &z);
appendd(buf, s, z); // use the copy
lua_remove(L, -1); // remove copied key: tab/-3, key/-2, val/-1
}
appendw(buf, '"' | ':' << 010);
LuaEncodeJsonDataImpl(L, buf, level - 1, numformat, -1);
lua_pop(L, 1); // table/-2, key/-1
}
// stack: table/-1, as the key was popped by lua_next
// TODO(jart): don't leak memory with longjmp
luaL_error(L, "can't serialize cyclic data structure to json");
unreachable;
}
appendw(buf, isarray ? ']' : '}');
return 0;
default:
luaL_error(L, "can't serialize value of this type");
luaL_error(L, "won't serialize %s to json", luaL_typename(L, idx));
unreachable;
}
} else {
@ -140,6 +146,9 @@ static int LuaEncodeJsonDataImpl(lua_State *L, char **buf, int level,
int LuaEncodeJsonData(lua_State *L, char **buf, char *numformat, int idx) {
int rc;
rc = LuaEncodeJsonDataImpl(L, buf, 64, numformat, idx);
struct LuaVisited visited = {0};
rc = LuaEncodeJsonDataImpl(L, buf, 64, numformat, idx, &visited);
assert(!visited.n);
free(visited.p);
return rc;
}

View file

@ -25,39 +25,44 @@
#include "libc/x/x.h"
#include "third_party/lua/cosmo.h"
#include "third_party/lua/lauxlib.h"
#include "third_party/lua/lctype.h"
#include "third_party/lua/lua.h"
#include "third_party/lua/visitor.h"
struct Visited {
int n;
void **p;
};
static bool PushVisit(struct Visited *visited, void *p) {
int i;
for (i = 0; i < visited->n; ++i) {
if (visited->p[i] == p) {
return false;
}
static bool IsLuaIdentifier(lua_State *L, int idx) {
size_t i, n;
const char *p;
p = luaL_checklstring(L, idx, &n);
if (!lislalpha(p[0])) return false;
for (i = 1; i < n; ++i) {
if (!lislalnum(p[i])) return false;
}
visited->p = xrealloc(visited->p, ++visited->n * sizeof(*visited->p));
visited->p[visited->n - 1] = p;
return true;
}
static void PopVisit(struct Visited *visited) {
assert(visited->n > 0);
--visited->n;
// TODO: Can we be smarter with lua_rawlen?
static bool IsLuaArray(lua_State *L) {
int i;
lua_pushnil(L);
for (i = 1; lua_next(L, -2); ++i) {
if (!lua_isinteger(L, -2) || lua_tointeger(L, -2) != i) {
lua_pop(L, 2);
return false;
}
lua_pop(L, 1);
}
return true;
}
static int LuaEncodeLuaDataImpl(lua_State *L, char **buf, int level,
char *numformat, int idx,
struct Visited *visited) {
struct LuaVisited *visited) {
char *s;
bool didcomma;
bool isarray;
lua_Integer i;
int ktype, vtype;
size_t tbllen, buflen, slen;
char ibuf[21], fmt[] = "%.14g";
char ibuf[24], fmt[] = "%.14g";
if (level > 0) {
switch (lua_type(L, idx)) {
@ -71,15 +76,15 @@ static int LuaEncodeLuaDataImpl(lua_State *L, char **buf, int level,
return 0;
case LUA_TFUNCTION:
appendf(buf, "\"func@%p\"", lua_touserdata(L, idx));
appendf(buf, "\"func@%p\"", lua_topointer(L, idx));
return 0;
case LUA_TLIGHTUSERDATA:
appendf(buf, "\"light@%p\"", lua_touserdata(L, idx));
appendf(buf, "\"light@%p\"", lua_topointer(L, idx));
return 0;
case LUA_TTHREAD:
appendf(buf, "\"thread@%p\"", lua_touserdata(L, idx));
appendf(buf, "\"thread@%p\"", lua_topointer(L, idx));
return 0;
case LUA_TUSERDATA:
@ -111,7 +116,7 @@ static int LuaEncodeLuaDataImpl(lua_State *L, char **buf, int level,
case LUA_TNUMBER:
if (lua_isinteger(L, idx)) {
appendd(buf, ibuf,
FormatInt64(ibuf, luaL_checkinteger(L, idx)) - ibuf);
FormatFlex64(ibuf, luaL_checkinteger(L, idx), 2) - ibuf);
} else {
// TODO(jart): replace this api
while (*numformat == '%' || *numformat == '.' ||
@ -133,58 +138,52 @@ static int LuaEncodeLuaDataImpl(lua_State *L, char **buf, int level,
return 0;
case LUA_TBOOLEAN:
if (lua_toboolean(L, idx)) {
appendw(buf, READ32LE("true"));
} else {
appendw(buf, READ64LE("false\0\0"));
}
appendw(buf, lua_toboolean(L, idx) ? READ32LE("true")
: READ64LE("false\0\0"));
return 0;
case LUA_TTABLE:
i = 0;
didcomma = false;
appendw(buf, '{');
lua_pushvalue(L, idx);
lua_pushnil(L); // push the first key
while (lua_next(L, -2)) {
++i;
ktype = lua_type(L, -2);
vtype = lua_type(L, -1);
if (ktype != LUA_TNUMBER || lua_tointeger(L, -2) != i) {
if (PushVisit(visited, lua_touserdata(L, -2))) {
if (i > 1) appendw(buf, ',' | ' ' << 8);
didcomma = true;
if (LuaPushVisit(visited, lua_topointer(L, idx))) {
appendw(buf, '{');
lua_pushvalue(L, idx); // idx becomes invalid once we change stack
isarray = IsLuaArray(L);
lua_pushnil(L); // push the first key
while (lua_next(L, -2)) {
ktype = lua_type(L, -2);
vtype = lua_type(L, -1);
if (i++ > 0) appendw(buf, ',' | ' ' << 8);
if (isarray) {
// use {v₁,v₂,...} for lua-normal integer keys
} else if (ktype == LUA_TSTRING && IsLuaIdentifier(L, -2)) {
// use {𝑘=𝑣} syntax when 𝑘 is legal as a lua identifier
s = lua_tolstring(L, -2, &slen);
appendd(buf, s, slen);
appendw(buf, '=');
} else {
// use {[𝑘]=𝑣} otherwise
appendw(buf, '[');
LuaEncodeLuaDataImpl(L, buf, level - 1, numformat, -2, visited);
appendw(buf, ']' | '=' << 010);
PopVisit(visited);
} else {
// TODO: Strange Lua data structure, do nothing.
lua_pop(L, 1);
continue;
}
}
if (PushVisit(visited, lua_touserdata(L, -1))) {
if (!didcomma && i > 1) appendw(buf, ',' | ' ' << 8);
LuaEncodeLuaDataImpl(L, buf, level - 1, numformat, -1, visited);
PopVisit(visited);
} else {
// TODO: Strange Lua data structure, do nothing.
lua_pop(L, 1);
continue;
lua_pop(L, 1); // table/-2, key/-1
}
lua_pop(L, 1); // table/-2, key/-1
lua_pop(L, 1); // table ref
LuaPopVisit(visited);
appendw(buf, '}');
} else {
appendf(buf, "\"cyclic@%p\"", lua_topointer(L, idx));
}
lua_pop(L, 1); // table
// stack: table/-1, as the key was popped by lua_next
appendw(buf, '}');
return 0;
default:
// TODO(jart): don't leak memory with longjmp
luaL_error(L, "can't serialize value of this type");
unreachable;
}
} else {
// TODO(jart): don't leak memory with longjmp
luaL_error(L, "too many nested tables");
unreachable;
}
@ -192,7 +191,7 @@ static int LuaEncodeLuaDataImpl(lua_State *L, char **buf, int level,
int LuaEncodeLuaData(lua_State *L, char **buf, char *numformat, int idx) {
int rc;
struct Visited visited = {0};
struct LuaVisited visited = {0};
rc = LuaEncodeLuaDataImpl(L, buf, 64, numformat, idx, &visited);
assert(!visited.n);
free(visited.p);

38
third_party/lua/visitor.c vendored Normal file
View file

@ -0,0 +1,38 @@
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi
Copyright 2022 Justine Alexandra Roberts Tunney
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/assert.h"
#include "libc/x/x.h"
#include "third_party/lua/visitor.h"
bool LuaPushVisit(struct LuaVisited *visited, const void *p) {
int i;
for (i = 0; i < visited->n; ++i) {
if (visited->p[i] == p) {
return false;
}
}
visited->p = xrealloc(visited->p, ++visited->n * sizeof(*visited->p));
visited->p[visited->n - 1] = p;
return true;
}
void LuaPopVisit(struct LuaVisited *visited) {
assert(visited->n > 0);
--visited->n;
}

16
third_party/lua/visitor.h vendored Normal file
View file

@ -0,0 +1,16 @@
#ifndef COSMOPOLITAN_THIRD_PARTY_LUA_VISITOR_H_
#define COSMOPOLITAN_THIRD_PARTY_LUA_VISITOR_H_
#if !(__ASSEMBLER__ + __LINKER__ + 0)
COSMOPOLITAN_C_START_
struct LuaVisited {
int n;
const void **p;
};
bool LuaPushVisit(struct LuaVisited *, const void *);
void LuaPopVisit(struct LuaVisited *);
COSMOPOLITAN_C_END_
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
#endif /* COSMOPOLITAN_THIRD_PARTY_LUA_VISITOR_H_ */