/* SPDX-FileCopyrightText: (C) 2026 Vladimir Zorin * SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later * Licensed under OWL v1.0+. See LICENSE. */ #include #include #include #include #include "lauxlib.h" #include "lua.h" #include "lem_piece_table.h" /* ── internal types ────────────────────────────────────────────────── */ typedef enum { SRC_ORIGINAL = 0, SRC_ADD = 1 } piece_src_t; typedef struct { piece_src_t source; size_t offset; size_t length; } piece_t; typedef struct { char *data; size_t len; size_t cap; } add_buf_t; typedef struct undo_entry { piece_t *pieces; size_t piece_count; size_t total_len; struct undo_entry *next; } undo_entry_t; struct lem_piece_table { /* backing buffers */ char *original; size_t original_len; add_buf_t add; /* piece list (dynamic array) */ piece_t *pieces; size_t piece_count; size_t piece_cap; size_t total_len; /* line index: starts[i] = byte offset of line i. starts[line_count] = total_len (sentinel). */ size_t *line_starts; size_t line_count; size_t line_cap; /* undo / redo */ undo_entry_t *undo_stack; undo_entry_t *redo_stack; size_t undo_depth; size_t redo_depth; /* edit grouping */ int group_open; int group_dirty; piece_t *group_snap; size_t group_snap_count; size_t group_snap_len; }; /* ── memory helpers ────────────────────────────────────────────────── */ #define INIT_PIECE_CAP 16 #define INIT_LINE_CAP 16 #define INIT_ADD_CAP 256 #define MAX_UNDO_DEPTH 1000 /* Grow `cap` (doubling, starting from `init`) until it reaches `need`. Returns 0 on success, -1 on overflow. */ static int grow_cap(size_t need, size_t init, size_t *out_cap) { size_t cap = *out_cap ? *out_cap : init; while (cap < need) { if (cap > SIZE_MAX / 2) return -1; cap *= 2; } *out_cap = cap; return 0; } static int ensure_piece_cap(lem_piece_table_t *pt, size_t need) { if (need <= pt->piece_cap) return 0; size_t cap = pt->piece_cap; if (grow_cap(need, INIT_PIECE_CAP, &cap) < 0) return -1; if (cap > SIZE_MAX / sizeof(piece_t)) return -1; piece_t *np = realloc(pt->pieces, cap * sizeof(piece_t)); if (!np) return -1; pt->pieces = np; pt->piece_cap = cap; return 0; } static int ensure_line_cap(lem_piece_table_t *pt, size_t need) { if (need <= pt->line_cap) return 0; size_t cap = pt->line_cap; if (grow_cap(need, INIT_LINE_CAP, &cap) < 0) return -1; if (cap > SIZE_MAX / sizeof(size_t)) return -1; size_t *ns = realloc(pt->line_starts, cap * sizeof(size_t)); if (!ns) return -1; pt->line_starts = ns; pt->line_cap = cap; return 0; } static int add_buf_append(add_buf_t *ab, const char *data, size_t len) { if (len > SIZE_MAX - ab->len) return -1; size_t need = ab->len + len; if (need > ab->cap) { size_t cap = ab->cap; if (grow_cap(need, INIT_ADD_CAP, &cap) < 0) return -1; char *nd = realloc(ab->data, cap); if (!nd) return -1; ab->data = nd; ab->cap = cap; } memcpy(ab->data + ab->len, data, len); ab->len += len; return 0; } static piece_t *copy_pieces(const piece_t *src, size_t count) { if (count == 0) return NULL; if (count > SIZE_MAX / sizeof(piece_t)) return NULL; piece_t *dst = malloc(count * sizeof(piece_t)); if (!dst) return NULL; memcpy(dst, src, count * sizeof(piece_t)); return dst; } static size_t count_newlines(const char *s, size_t n) { size_t c = 0; for (size_t i = 0; i < n; i++) if (s[i] == '\n') c++; return c; } /* ── undo / redo helpers ───────────────────────────────────────────── */ static void free_undo_entry(undo_entry_t *e) { free(e->pieces); free(e); } static void free_undo_stack(undo_entry_t *stack) { while (stack) { undo_entry_t *next = stack->next; free_undo_entry(stack); stack = next; } } /* Push an already-allocated entry onto a stack and apply the depth cap. Cannot fail. */ static void push_entry_node(undo_entry_t **stack, size_t *depth, undo_entry_t *e) { e->next = *stack; *stack = e; (*depth)++; /* Cap history: drop entries past MAX_UNDO_DEPTH from the tail. */ if (*depth > MAX_UNDO_DEPTH) { undo_entry_t *cur = *stack; for (size_t i = 1; i < MAX_UNDO_DEPTH && cur; i++) cur = cur->next; if (cur) { undo_entry_t *tail = cur->next; cur->next = NULL; while (tail) { undo_entry_t *n = tail->next; free_undo_entry(tail); tail = n; (*depth)--; } } } } static void push_entry(undo_entry_t **stack, size_t *depth, piece_t *pieces, size_t count, size_t total_len) { undo_entry_t *e = malloc(sizeof(undo_entry_t)); if (!e) { /* The snapshot is lost; free it rather than leak. It's OK to skip this undo point — worst case the user loses one step of history on extreme OOM. The undo/redo paths use push_entry_node directly so they can pre-allocate and propagate failure instead of silently dropping history. */ free(pieces); return; } e->pieces = pieces; e->piece_count = count; e->total_len = total_len; push_entry_node(stack, depth, e); } static undo_entry_t *pop_entry(undo_entry_t **stack, size_t *depth) { undo_entry_t *e = *stack; *stack = e->next; e->next = NULL; assert(*depth > 0); (*depth)--; return e; } static void clear_redo(lem_piece_table_t *pt) { free_undo_stack(pt->redo_stack); pt->redo_stack = NULL; pt->redo_depth = 0; } static int save_undo_point(lem_piece_table_t *pt) { if (pt->group_open) { if (!pt->group_dirty) { piece_t *snap = copy_pieces(pt->pieces, pt->piece_count); if (!snap && pt->piece_count > 0) return -1; pt->group_snap = snap; pt->group_snap_count = pt->piece_count; pt->group_snap_len = pt->total_len; pt->group_dirty = 1; /* New edit: invalidate redo here too, not only at group_close — otherwise redo() mid-group rewrites over the grouped edits. */ clear_redo(pt); } return 0; } piece_t *snap = copy_pieces(pt->pieces, pt->piece_count); if (!snap && pt->piece_count > 0) return -1; push_entry(&pt->undo_stack, &pt->undo_depth, snap, pt->piece_count, pt->total_len); clear_redo(pt); return 0; } /* ── line index ────────────────────────────────────────────────────── */ static const char *piece_data(const lem_piece_table_t *pt, const piece_t *p) { return (p->source == SRC_ORIGINAL) ? pt->original + p->offset : pt->add.data + p->offset; } static int rebuild_line_index(lem_piece_table_t *pt) { size_t count = 1; /* at least one line */ /* first pass: count newlines */ for (size_t i = 0; i < pt->piece_count; i++) { const char *d = piece_data(pt, &pt->pieces[i]); for (size_t j = 0; j < pt->pieces[i].length; j++) if (d[j] == '\n') count++; } if (ensure_line_cap(pt, count + 1) < 0) /* +1 for sentinel */ return -1; pt->line_count = count; pt->line_starts[0] = 0; /* second pass: record offsets */ size_t li = 1; size_t pos = 0; for (size_t i = 0; i < pt->piece_count; i++) { const char *d = piece_data(pt, &pt->pieces[i]); for (size_t j = 0; j < pt->pieces[i].length; j++) { pos++; if (d[j] == '\n') { pt->line_starts[li++] = pos; } } } pt->line_starts[count] = pt->total_len; /* sentinel */ return 0; } /* ── piece list split ──────────────────────────────────────────────── */ /* Split the piece list so that byte offset `off` falls at a piece boundary. Returns the piece index at which `off` starts, or (size_t)-1 on OOM. Callers must check for the error sentinel. */ static size_t split_at(lem_piece_table_t *pt, size_t off) { if (off == 0) return 0; if (off == pt->total_len) return pt->piece_count; size_t pos = 0; for (size_t i = 0; i < pt->piece_count; i++) { size_t end = pos + pt->pieces[i].length; if (off == end) return i + 1; /* already at boundary */ if (off < end) { /* split piece i at inner offset (off - pos). Reserve capacity BEFORE mutating pt->pieces[i] so a failure leaves state untouched. */ if (ensure_piece_cap(pt, pt->piece_count + 1) < 0) return (size_t)-1; size_t inner = off - pos; piece_t right = {pt->pieces[i].source, pt->pieces[i].offset + inner, pt->pieces[i].length - inner}; pt->pieces[i].length = inner; memmove(&pt->pieces[i + 2], &pt->pieces[i + 1], (pt->piece_count - i - 1) * sizeof(piece_t)); pt->pieces[i + 1] = right; pt->piece_count++; return i + 1; } pos = end; } /* Unreachable: the off == pt->total_len early return above guarantees the loop finds a piece satisfying off < end. */ assert(0 && "split_at invariant violated"); return pt->piece_count; } /* ── public C API ──────────────────────────────────────────────────── */ lem_piece_table_t *lem_pt_new(const char *content, size_t len) { lem_piece_table_t *pt = calloc(1, sizeof(lem_piece_table_t)); if (!pt) return NULL; if (content && len > 0) { pt->original = malloc(len); if (!pt->original) { free(pt); return NULL; } memcpy(pt->original, content, len); pt->original_len = len; if (ensure_piece_cap(pt, 1) < 0) { free(pt->original); free(pt); return NULL; } pt->pieces[0] = (piece_t){SRC_ORIGINAL, 0, len}; pt->piece_count = 1; pt->total_len = len; } if (rebuild_line_index(pt) < 0) { lem_pt_free(pt); return NULL; } return pt; } void lem_pt_free(lem_piece_table_t *pt) { if (!pt) return; free(pt->original); free(pt->add.data); free(pt->pieces); free(pt->line_starts); free_undo_stack(pt->undo_stack); free_undo_stack(pt->redo_stack); free(pt->group_snap); free(pt); } int lem_pt_insert(lem_piece_table_t *pt, size_t offset, const char *text, size_t len) { if (len == 0) return 0; if (offset > pt->total_len) return -1; if (len > SIZE_MAX - pt->total_len) return -1; /* Pre-allocate line index capacity for the post-edit shape so the final rebuild_line_index cannot fail mid-edit. */ size_t added_lines = count_newlines(text, len); if (added_lines > SIZE_MAX - pt->line_count) return -1; size_t new_line_count = pt->line_count + added_lines; if (new_line_count > SIZE_MAX - 1) return -1; if (ensure_line_cap(pt, new_line_count + 1) < 0) return -1; if (save_undo_point(pt) < 0) return -1; size_t add_off = pt->add.len; if (add_buf_append(&pt->add, text, len) < 0) return -1; size_t idx = split_at(pt, offset); if (idx == (size_t)-1) return -1; if (ensure_piece_cap(pt, pt->piece_count + 1) < 0) return -1; piece_t np = {SRC_ADD, add_off, len}; memmove(&pt->pieces[idx + 1], &pt->pieces[idx], (pt->piece_count - idx) * sizeof(piece_t)); pt->pieces[idx] = np; pt->piece_count++; pt->total_len += len; /* Cannot fail: line cap was pre-allocated above. */ int r = rebuild_line_index(pt); assert(r == 0); (void)r; return 0; } int lem_pt_delete(lem_piece_table_t *pt, size_t offset, size_t len) { if (len == 0) return 0; if (offset > pt->total_len || len > pt->total_len - offset) return -1; if (save_undo_point(pt) < 0) return -1; size_t start = split_at(pt, offset); if (start == (size_t)-1) return -1; size_t end = split_at(pt, offset + len); if (end == (size_t)-1) return -1; size_t removed = end - start; if (removed > 0) { memmove(&pt->pieces[start], &pt->pieces[end], (pt->piece_count - end) * sizeof(piece_t)); pt->piece_count -= removed; } pt->total_len -= len; /* Cannot fail: deletion never increases the line count, and line_cap already covers the pre-delete count. */ int r = rebuild_line_index(pt); assert(r == 0); (void)r; return 0; } size_t lem_pt_length(lem_piece_table_t *pt) { return pt->total_len; } size_t lem_pt_get_text(lem_piece_table_t *pt, size_t offset, size_t len, char *buf, size_t buf_size) { if (offset >= pt->total_len) return 0; /* overflow-safe clamp of len to the available bytes */ if (len > pt->total_len - offset) len = pt->total_len - offset; if (len > buf_size) len = buf_size; size_t written = 0; size_t pos = 0; for (size_t i = 0; i < pt->piece_count && written < len; i++) { const piece_t *p = &pt->pieces[i]; size_t pend = pos + p->length; if (pend <= offset) { pos = pend; continue; } const char *d = piece_data(pt, p); size_t skip = (offset > pos) ? offset - pos : 0; size_t avail = p->length - skip; size_t to_copy = len - written; if (to_copy > avail) to_copy = avail; memcpy(buf + written, d + skip, to_copy); written += to_copy; pos = pend; } return written; } size_t lem_pt_line_count(lem_piece_table_t *pt) { return pt->line_count; } size_t lem_pt_line_offset(lem_piece_table_t *pt, size_t line) { if (line >= pt->line_count) return pt->total_len; return pt->line_starts[line]; } size_t lem_pt_line_length(lem_piece_table_t *pt, size_t line) { if (line >= pt->line_count) return 0; return pt->line_starts[line + 1] - pt->line_starts[line]; } size_t lem_pt_offset_to_line(lem_piece_table_t *pt, size_t offset) { /* line_count >= 1 always (rebuild_line_index runs in every constructor) */ if (offset >= pt->total_len) return pt->line_count - 1; /* binary search in line_starts */ size_t lo = 0, hi = pt->line_count; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if (pt->line_starts[mid + 1] <= offset) lo = mid + 1; else hi = mid; } return lo; } /* Count lines (newlines + 1) for a snapshot's piece list, using pt's live original/add buffers (which never shrink). */ static size_t snapshot_line_count(const lem_piece_table_t *pt, const piece_t *pieces, size_t piece_count) { size_t lines = 1; for (size_t i = 0; i < piece_count; i++) { const piece_t *p = &pieces[i]; const char *d = (p->source == SRC_ORIGINAL) ? pt->original + p->offset : pt->add.data + p->offset; for (size_t j = 0; j < p->length; j++) if (d[j] == '\n') lines++; } return lines; } /* Swap current pt->pieces with peek-of-other-stack and pop. All allocations done up front; the destructive transfer cannot fail. */ static int swap_to_snapshot(lem_piece_table_t *pt, undo_entry_t **src_stack, size_t *src_depth, undo_entry_t **dst_stack, size_t *dst_depth) { if (!*src_stack) return -1; /* Step 1 — pre-allocate the destination entry node. */ undo_entry_t *new_dst = malloc(sizeof(undo_entry_t)); if (!new_dst) return -1; /* Step 2 — pre-allocate line index capacity for the snapshot we're about to restore. Peek without popping so we can bail out cleanly. */ undo_entry_t *src_top = *src_stack; size_t snap_lines = snapshot_line_count(pt, src_top->pieces, src_top->piece_count); if (snap_lines > SIZE_MAX - 1) { free(new_dst); return -1; } if (ensure_line_cap(pt, snap_lines + 1) < 0) { free(new_dst); return -1; } /* Step 3 — destructive transfer (no allocation past this point). */ new_dst->pieces = pt->pieces; new_dst->piece_count = pt->piece_count; new_dst->total_len = pt->total_len; push_entry_node(dst_stack, dst_depth, new_dst); undo_entry_t *e = pop_entry(src_stack, src_depth); pt->pieces = e->pieces; pt->piece_count = e->piece_count; pt->piece_cap = e->piece_count; pt->total_len = e->total_len; free(e); /* Cannot fail: line cap was pre-allocated above. */ int r = rebuild_line_index(pt); assert(r == 0); (void)r; return 0; } int lem_pt_undo(lem_piece_table_t *pt) { return swap_to_snapshot(pt, &pt->undo_stack, &pt->undo_depth, &pt->redo_stack, &pt->redo_depth); } int lem_pt_redo(lem_piece_table_t *pt) { return swap_to_snapshot(pt, &pt->redo_stack, &pt->redo_depth, &pt->undo_stack, &pt->undo_depth); } void lem_pt_group_open(lem_piece_table_t *pt) { if (pt->group_open) return; /* already open */ pt->group_open = 1; pt->group_dirty = 0; } void lem_pt_group_close(lem_piece_table_t *pt) { if (!pt->group_open) return; if (pt->group_dirty) { push_entry(&pt->undo_stack, &pt->undo_depth, pt->group_snap, pt->group_snap_count, pt->group_snap_len); clear_redo(pt); pt->group_snap = NULL; } else { free(pt->group_snap); pt->group_snap = NULL; } pt->group_open = 0; pt->group_dirty = 0; } char *lem_pt_snapshot(lem_piece_table_t *pt, size_t *out_len) { size_t len = pt->total_len; if (len > SIZE_MAX - 1) return NULL; char *buf = malloc(len + 1); if (!buf) return NULL; lem_pt_get_text(pt, 0, len, buf, len); buf[len] = '\0'; if (out_len) *out_len = len; return buf; } /* ── Lua bindings ──────────────────────────────────────────────────── */ #define PT_MT "lem.piece_table" typedef struct { lem_piece_table_t *pt; } lua_pt_ud_t; static lua_pt_ud_t *check_pt(lua_State *L) { lua_pt_ud_t *ud = (lua_pt_ud_t *)luaL_checkudata(L, 1, PT_MT); if (!ud->pt) luaL_error(L, "piece table is closed"); return ud; } /* Validate a numeric argument before the double→size_t conversion: negative, NaN, fractional, or huge values would be undefined behavior in the raw cast. Raises a Lua error on invalid input. */ static size_t check_size_arg(lua_State *L, int idx) { lua_Number n = luaL_checknumber(L, idx); if (!(n >= 0 && n <= 9007199254740992.0 /* 2^53 */) || n != (lua_Number)(size_t)n) luaL_argerror(L, idx, "expected a non-negative integer"); return (size_t)n; } /* pt.new([content_string]) */ static int l_pt_new(lua_State *L) { size_t len = 0; const char *content = NULL; if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) content = luaL_checklstring(L, 1, &len); /* Create the userdata first so __gc covers every later failure path (lua_newuserdata can longjmp on OOM, which would leak the table). */ lua_pt_ud_t *ud = lua_newuserdata(L, sizeof(lua_pt_ud_t)); ud->pt = NULL; luaL_getmetatable(L, PT_MT); lua_setmetatable(L, -2); ud->pt = lem_pt_new(content, len); if (!ud->pt) { lua_pushnil(L); lua_pushstring(L, "failed to create piece table"); return 2; } return 1; } /* p:insert(offset, text) — 0-based byte offset */ static int l_pt_insert(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t offset = check_size_arg(L, 2); size_t len; const char *text = luaL_checklstring(L, 3, &len); if (lem_pt_insert(ud->pt, offset, text, len) < 0) { lua_pushnil(L); lua_pushstring(L, "insert failed (out of range or out of memory)"); return 2; } lua_pushboolean(L, 1); return 1; } /* p:delete(offset, length) — 0-based byte offset */ static int l_pt_delete(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t offset = check_size_arg(L, 2); size_t len = check_size_arg(L, 3); if (lem_pt_delete(ud->pt, offset, len) < 0) { lua_pushnil(L); lua_pushstring(L, "delete failed (out of bounds or out of memory)"); return 2; } lua_pushboolean(L, 1); return 1; } /* p:get_text(offset, length) — returns string */ static int l_pt_get_text(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t offset = check_size_arg(L, 2); size_t len = check_size_arg(L, 3); if (offset >= ud->pt->total_len && len > 0) { lua_pushnil(L); lua_pushstring(L, "offset out of range"); return 2; } if (len > ud->pt->total_len - offset) len = ud->pt->total_len - offset; if (len == 0) { lua_pushlstring(L, "", 0); return 1; } /* GC-managed scratch buffer: lua_pushlstring can longjmp on OOM, which would leak a malloc'd buffer. */ char *buf = lua_newuserdata(L, len); size_t got = lem_pt_get_text(ud->pt, offset, len, buf, len); lua_pushlstring(L, buf, got); return 1; } /* p:length() */ static int l_pt_length(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lua_pushnumber(L, (lua_Number)lem_pt_length(ud->pt)); return 1; } /* p:line_count() */ static int l_pt_line_count(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lua_pushnumber(L, (lua_Number)lem_pt_line_count(ud->pt)); return 1; } /* p:line_offset(line) — 1-based line number, returns 0-based byte offset */ static int l_pt_line_offset(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t line = check_size_arg(L, 2); if (line < 1 || line > ud->pt->line_count) { lua_pushnil(L); lua_pushstring(L, "line number out of range"); return 2; } lua_pushnumber(L, (lua_Number)lem_pt_line_offset(ud->pt, line - 1)); return 1; } /* p:line_length(line) — 1-based line number */ static int l_pt_line_length(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t line = check_size_arg(L, 2); if (line < 1 || line > ud->pt->line_count) { lua_pushnil(L); lua_pushstring(L, "line number out of range"); return 2; } lua_pushnumber(L, (lua_Number)lem_pt_line_length(ud->pt, line - 1)); return 1; } /* p:offset_to_line(offset) — returns 1-based line number */ static int l_pt_offset_to_line(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t offset = check_size_arg(L, 2); lua_pushnumber(L, (lua_Number)(lem_pt_offset_to_line(ud->pt, offset) + 1)); return 1; } /* p:undo() — returns true/false */ static int l_pt_undo(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lua_pushboolean(L, lem_pt_undo(ud->pt) == 0); return 1; } /* p:redo() — returns true/false */ static int l_pt_redo(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lua_pushboolean(L, lem_pt_redo(ud->pt) == 0); return 1; } /* p:group_open() */ static int l_pt_group_open(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lem_pt_group_open(ud->pt); return 0; } /* p:group_close() */ static int l_pt_group_close(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); lem_pt_group_close(ud->pt); return 0; } /* p:snapshot() — returns string */ static int l_pt_snapshot(lua_State *L) { lua_pt_ud_t *ud = check_pt(L); size_t len = ud->pt->total_len; if (len == 0) { lua_pushlstring(L, "", 0); return 1; } /* GC-managed scratch buffer (see l_pt_get_text) */ char *buf = lua_newuserdata(L, len); size_t got = lem_pt_get_text(ud->pt, 0, len, buf, len); lua_pushlstring(L, buf, got); return 1; } /* p:close() */ static int l_pt_close(lua_State *L) { lua_pt_ud_t *ud = (lua_pt_ud_t *)luaL_checkudata(L, 1, PT_MT); if (ud->pt) { lem_pt_free(ud->pt); ud->pt = NULL; } return 0; } static int l_pt_gc(lua_State *L) { return l_pt_close(L); } /* ── registration ──────────────────────────────────────────────────── */ static const luaL_Reg pt_methods[] = { {"insert", l_pt_insert }, {"delete", l_pt_delete }, {"get_text", l_pt_get_text }, {"length", l_pt_length }, {"line_count", l_pt_line_count }, {"line_offset", l_pt_line_offset }, {"line_length", l_pt_line_length }, {"offset_to_line", l_pt_offset_to_line}, {"undo", l_pt_undo }, {"redo", l_pt_redo }, {"group_open", l_pt_group_open }, {"group_close", l_pt_group_close }, {"snapshot", l_pt_snapshot }, {"close", l_pt_close }, {NULL, NULL } }; static const luaL_Reg module_funcs[] = { {"new", l_pt_new}, {NULL, NULL } }; int luaopen_lem_piece_table(lua_State *L) { luaL_newmetatable(L, PT_MT); lua_newtable(L); luaL_register(L, NULL, pt_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_pt_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); luaL_newlib(L, module_funcs); return 1; }