feat(ext): expose Lua API for checks and languages

This commit is contained in:
2026-08-28 21:51:59 -04:00
parent 67bfcde386
commit 2e8553116b
4 changed files with 961 additions and 6 deletions
+153 -6
View File
@@ -54,6 +54,14 @@
#include "lua.h"
/* The probe-spec struct (struct st_lua_probe_spec) and its static-inline
* free live in api.h (todo 21). Including it here is deliberate and safe
* under the frozen unit-test link lines: api.h contributes only a struct
* definition + a static inline, so lua.c gains NO reference to any
* api.c/abi.c symbol -- tests/unit/test_lua.c and test_discovery.c, which
* link lua.c WITHOUT api.c/abi.c, keep linking. */
#include "api.h"
/* ------------------------------------------------------------------ */
/* libm-free math shims */
/* ------------------------------------------------------------------ */
@@ -296,7 +304,8 @@ pow(double x, double y)
struct st_lua_reg {
struct st_lua_reg *next;
char *name; /* owned copy */
void *probe_spec; /* checks only; always NULL for languages */
void *probe_spec; /* checks: a struct st_lua_probe_spec * (owned);
languages: always NULL (name only in v1) */
};
struct st_lua_rt {
@@ -346,6 +355,7 @@ regs_free(struct st_lua_reg *head)
while (p != NULL) {
struct st_lua_reg *next = p->next;
free(p->name);
st_lua_probe_spec_free((struct st_lua_probe_spec *)p->probe_spec);
free(p);
p = next;
}
@@ -515,15 +525,143 @@ l_math_open(lua_State *L)
/* registration API (Lua side) */
/* ------------------------------------------------------------------ */
/* The `st` table: st.register_check(name) / st.register_language(name).
* Both write into the runtime's own registries through the same C API
* todo 21 will enumerate when bridging into a struct st_ext_ctx. The
* rt travels as a light-userdata upvalue -- there is no global state. */
/* The `st` table: st.register_check(name[, spec]) and
* st.register_language(name[, spec]). Both write into the runtime's own
* registries through the same C API todo 21 enumerates when bridging into
* a struct st_ext_ctx. The rt travels as a light-userdata upvalue -- there
* is no global state. A present second argument (a probe-spec TABLE) is
* parsed into a heap struct st_lua_probe_spec (api.h) and passed through
* as the registry entry's opaque spec pointer. */
static void
l_links_free(char **link)
{
size_t i;
if (link == NULL) {
return;
}
for (i = 0; link[i] != NULL; i++) {
free(link[i]);
}
free(link);
}
/* Parses the optional probe-spec table (argument 2) into a heap
* struct st_lua_probe_spec. Returns NULL when argument 2 is absent or
* nil (bare registration); raises a Lua error on a malformed table.
* All fields are strdup'd (so the borrowed lua_tostring pointers never
* outlive their stack slot); the caller owns the result. */
static struct st_lua_probe_spec *
l_parse_probe_spec(lua_State *L)
{
struct st_lua_probe_spec *spec;
char *kind_dup = NULL;
char *source_dup = NULL;
char **link = NULL;
size_t nlink = 0;
size_t i;
const char *s;
if (lua_gettop(L) < 2 || lua_isnoneornil(L, 2)) {
return NULL;
}
luaL_checktype(L, 2, LUA_TTABLE);
lua_getfield(L, 2, "kind");
if (lua_type(L, -1) != LUA_TSTRING) {
luaL_error(L, "probe spec 'kind' must be a string");
return NULL;
}
s = lua_tostring(L, -1);
if (strcmp(s, "compile") != 0 && strcmp(s, "link") != 0 &&
strcmp(s, "run") != 0) {
luaL_error(L, "probe spec 'kind' must be \"compile\", \"link\" or "
"\"run\"");
return NULL;
}
kind_dup = strdup(s);
lua_pop(L, 1);
if (kind_dup == NULL) {
luaL_error(L, "out of memory");
return NULL;
}
lua_getfield(L, 2, "source");
if (lua_type(L, -1) != LUA_TSTRING) {
free(kind_dup);
luaL_error(L, "probe spec 'source' must be a string");
return NULL;
}
s = lua_tostring(L, -1);
source_dup = strdup(s);
lua_pop(L, 1);
if (source_dup == NULL) {
free(kind_dup);
luaL_error(L, "out of memory");
return NULL;
}
lua_getfield(L, 2, "link");
if (!lua_isnoneornil(L, -1)) {
luaL_checktype(L, -1, LUA_TTABLE);
nlink = lua_rawlen(L, -1);
link = calloc(nlink + 1, sizeof *link);
if (link == NULL) {
free(kind_dup);
free(source_dup);
luaL_error(L, "out of memory");
return NULL;
}
for (i = 0; i < nlink; i++) {
size_t slen;
lua_geti(L, -1, (lua_Integer)i + 1);
s = lua_tostring(L, -1);
slen = lua_rawlen(L, -1);
if (lua_type(L, -1) != LUA_TSTRING || s == NULL ||
strlen(s) != slen) {
lua_pop(L, 1);
l_links_free(link);
free(kind_dup);
free(source_dup);
luaL_error(L, "probe spec 'link' entries must be strings "
"without NUL bytes");
return NULL;
}
link[i] = strdup(s);
lua_pop(L, 1);
if (link[i] == NULL) {
l_links_free(link);
free(kind_dup);
free(source_dup);
luaL_error(L, "out of memory");
return NULL;
}
}
link[nlink] = NULL;
}
lua_pop(L, 1);
spec = malloc(sizeof *spec);
if (spec == NULL) {
l_links_free(link);
free(kind_dup);
free(source_dup);
luaL_error(L, "out of memory");
return NULL;
}
spec->kind = kind_dup;
spec->source = source_dup;
spec->link = link;
return spec;
}
static int
l_st_register_check(lua_State *L)
{
struct st_lua_rt *rt = lua_touserdata(L, lua_upvalueindex(1));
struct st_lua_probe_spec *spec;
size_t len;
const char *name;
@@ -535,7 +673,9 @@ l_st_register_check(lua_State *L)
if (name == NULL || strlen(name) != len) {
return luaL_error(L, "check name must not contain a NUL byte");
}
if (st_lua_register_check(rt, name, NULL) != 0) {
spec = l_parse_probe_spec(L);
if (st_lua_register_check(rt, name, spec) != 0) {
st_lua_probe_spec_free(spec); /* not stored: free it */
return luaL_error(L, "check '%s' is already registered", name);
}
return 0;
@@ -556,6 +696,13 @@ l_st_register_language(lua_State *L)
if (name == NULL || strlen(name) != len) {
return luaL_error(L, "language name must not contain a NUL byte");
}
/* Optional second arg: accept a probe-spec TABLE for forward
* compatibility, but for v1 register the name only -- the table's
* contents (detection info) are consumed by todo 22. A non-table
* second arg is a clean error. */
if (lua_gettop(L) >= 2 && !lua_isnoneornil(L, 2)) {
luaL_checktype(L, 2, LUA_TTABLE);
}
if (st_lua_register_language(rt, name) != 0) {
return luaL_error(L, "language '%s' is already registered", name);
}