From 604e3df41fc5b28cc60d4421980742fcf5c71509 Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Tue, 20 Feb 2024 12:07:06 -0600 Subject: [PATCH] just starting --- .luarc.json | 2 +- Makefile | 27 +- ckiwi/luacompat.h | 138 ++++ ckiwi/luakiwi-int.h | 84 ++ ckiwi/luakiwi.c | 1699 +++++++++++++++++++++++++++++++++++++++++ kiwi.lua | 14 +- ljkiwi-scm-1.rockspec | 1 + spec/solver_spec.lua | 58 +- spec/var_spec.lua | 4 +- 9 files changed, 1968 insertions(+), 59 deletions(-) create mode 100644 ckiwi/luacompat.h create mode 100644 ckiwi/luakiwi-int.h create mode 100644 ckiwi/luakiwi.c diff --git a/.luarc.json b/.luarc.json index 73dc269..86b45b7 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", - "runtime.version": "LuaJIT", + "runtime.version": "Lua 5.4", "runtime.path": [ "./?/init.lua", "./?.lua", diff --git a/Makefile b/Makefile index fa8c069..8734bb4 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ SRCDIR := . CC := $(CROSS)gcc -CFLAGS := -fPIC -O2 -CFLAGS += -Wall -I$(SRCDIR)/kiwi +CFLAGS := -fPIC -Os +CXXFLAGS := -I$(SRCDIR)/kiwi -fno-rtti +F_LTO := -flto=auto +CXXFLAGS_EXTRA := -pedantic -std=c++14 -Wall $(F_LTO) +CFLAGS_EXTRA := -pedantic -std=c99 -Wall $(F_LTO) LIBFLAG := -shared LIB_EXT := so +LUA_INCDIR := /usr/include ifeq ($(findstring gcc,$(CC)),gcc) CXX := $(subst gcc,g++,$(CC)) @@ -23,6 +27,9 @@ else endif endif +OBJS := ckiwi.o luakiwi.o + +VPATH = $(SRCDIR)/ckiwi all: ckiwi.$(LIB_EXT) @@ -31,9 +38,19 @@ install: cp -f kiwi.lua $(INST_LUADIR)/kiwi.lua clean: - rm -f ckiwi.$(LIB_EXT) + rm -f ckiwi.$(LIB_EXT) $(OBJS) -ckiwi.$(LIB_EXT): $(SRCDIR)/ckiwi/ckiwi.cpp $(SRCDIR)/ckiwi/ckiwi.h - $(CXX) $(CXXFLAGS) $(CFLAGS) -fPIC -Wall -I$(SRCDIR)/kiwi $(LIBFLAG) -o $@ $< + +ckiwi.$(LIB_EXT): $(OBJS) + $(CXX) $(CXXFLAGS) $(CFLAGS) $(CFLAGS_EXTRA) -I$(SRCDIR)/kiwi $(LIBFLAG) -o $@ $^ + +ckiwi.o: ckiwi.h +luakiwi.o: ckiwi.h luakiwi-int.h luacompat.h + +%.o: %.c + $(CC) $(CFLAGS) $(CFLAGS_EXTRA) -I$(LUA_INCDIR) -c -o $@ $< + +%.o: %.cpp + $(CXX) $(CXXFLAGS) $(CFLAGS) $(CXXFLAGS_EXTRA) -c -o $@ $< .PHONY: all install clean diff --git a/ckiwi/luacompat.h b/ckiwi/luacompat.h new file mode 100644 index 0000000..4615994 --- /dev/null +++ b/ckiwi/luacompat.h @@ -0,0 +1,138 @@ +#ifndef LKIWI_LUACOMPAT_H_ +#define LKIWI_LUACOMPAT_H_ + +#include +#include +#include + +#if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM == 501 + + #define LUA_OPADD 0 + #define LUA_OPSUB 1 + #define LUA_OPMUL 2 + #define LUA_OPDIV 3 + #define LUA_OPMOD 4 + #define LUA_OPPOW 5 + #define LUA_OPUNM 6 + +static int lua_absindex(lua_State* L, int i) { + if (i < 0 && i > LUA_REGISTRYINDEX) + i += lua_gettop(L) + 1; + return i; +} + +static lua_Number lua_tonumberx(lua_State* L, int i, int* isnum) { + lua_Number n = lua_tonumber(L, i); + if (isnum != NULL) { + *isnum = (n != 0 || lua_isnumber(L, i)); + } + return n; +} + +static int luaL_typeerror(lua_State* L, int arg, const char* tname) { + const char* msg; + const char* typearg; /* name for the type of the actual argument */ + if (luaL_getmetafield(L, arg, "__name") == LUA_TSTRING) + typearg = lua_tostring(L, -1); /* use the given type name */ + else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA) + typearg = "light userdata"; /* special name for messages */ + else + typearg = luaL_typename(L, arg); /* standard name */ + msg = lua_pushfstring(L, "%s expected, got %s", tname, typearg); + return luaL_argerror(L, arg, msg); +} + +#endif /* LUA_VERSION_NUM == 501 */ + +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 502 + +static lua_Integer lua_tointegerx(lua_State* L, int i, int* isnum) { + int ok = 0; + lua_Number n = lua_tonumberx(L, i, &ok); + if (ok) { + if (n == (lua_Integer)n) { + if (isnum) + *isnum = 1; + return (lua_Integer)n; + } + } + if (isnum) + *isnum = 0; + return 0; +} + +static void compat_reverse(lua_State* L, int a, int b) { + for (; a < b; ++a, --b) { + lua_pushvalue(L, a); + lua_pushvalue(L, b); + lua_replace(L, a); + lua_replace(L, b); + } +} + +static void lua_rotate(lua_State* L, int idx, int n) { + int n_elems = 0; + idx = lua_absindex(L, idx); + n_elems = lua_gettop(L) - idx + 1; + if (n < 0) + n += n_elems; + if (n > 0 && n < n_elems) { + luaL_checkstack(L, 2, "not enough stack slots available"); + n = n_elems - n; + compat_reverse(L, idx, idx + n - 1); + compat_reverse(L, idx + n, idx + n_elems - 1); + compat_reverse(L, idx, idx + n_elems - 1); + } +} + +static int lua_geti(lua_State* L, int index, lua_Integer i) { + index = lua_absindex(L, index); + lua_pushinteger(L, i); + lua_gettable(L, index); + return lua_type(L, -1); +} + +static const char* luaL_tolstring(lua_State* L, int idx, size_t* len) { + if (!luaL_callmeta(L, idx, "__tostring")) { + int t = lua_type(L, idx), tt = 0; + char const* name = NULL; + switch (t) { + case LUA_TNIL: + lua_pushliteral(L, "nil"); + break; + case LUA_TSTRING: + case LUA_TNUMBER: + lua_pushvalue(L, idx); + break; + case LUA_TBOOLEAN: + if (lua_toboolean(L, idx)) + lua_pushliteral(L, "true"); + else + lua_pushliteral(L, "false"); + break; + default: + tt = luaL_getmetafield(L, idx, "__name"); + name = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : lua_typename(L, t); + lua_pushfstring(L, "%s: %p", name, lua_topointer(L, idx)); + if (tt != LUA_TNIL) + lua_replace(L, -2); + break; + } + } else { + if (!lua_isstring(L, -1)) + luaL_error(L, "'__tostring' must return a string"); + } + return lua_tolstring(L, -1, len); +} + +#endif + +#if !defined(luaL_newlibtable) + #define luaL_newlibtable(L, l) lua_createtable(L, 0, sizeof(l) / sizeof((l)[0]) - 1) +#endif + +#if !defined(luaL_checkversion) + #define luaL_checkversion(L) ((void)0) +#endif + +#endif // LKIWI_LUACOMPAT_H_ diff --git a/ckiwi/luakiwi-int.h b/ckiwi/luakiwi-int.h new file mode 100644 index 0000000..bd7a49f --- /dev/null +++ b/ckiwi/luakiwi-int.h @@ -0,0 +1,84 @@ +#ifndef LUAKIWI_INT_H_ +#define LUAKIWI_INT_H_ + +#include "luacompat.h" + +#define ARR_COUNT(x) (sizeof(x) / sizeof((x)[0])) + +#if defined(__GNUC__) && !defined(LKIWI_NO_BUILTIN) + #define l_likely(x) (__builtin_expect(((x) != 0), 1)) + #define l_unlikely(x) (__builtin_expect(((x) != 0), 0)) +#else + #define l_likely(x) (x) + #define l_unlikely(x) (x) +#endif + +#ifndef LKIWI_NO_STALLOC + #if defined(_MSC_VER) + #include + +static inline void* stalloc(size_t sz) { + void* p = _malloca(sz); + if (!p) { + luaL_error(L, "out of memory"); + } + return p; +} + + #define stfree(ptr) _freea(ptr) + #else + #include + #define stalloc alloca + #define stfree(ptr) + #endif +#else + #include + +static inline void* stalloc(size_t sz) { + void* p = malloc(sz); + if (l_unlikely(!p)) { + luaL_error(L, "out of memory"); + } + return p; +} + + #define stfree(ptr) free(ptr) +#endif + +// Lua 5.1 compatibility for missing lua_arith. +static void compat_arith_unm(lua_State* L) { +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 + lua_Number n = lua_tonumber(L, -1); + if (n != 0 || lua_isnumber(L, -1)) { + lua_pop(L, 1); + lua_pushnumber(L, -n); + } else { + if (!luaL_callmeta(L, -1, "__unm")) + luaL_error(L, "attempt to perform arithmetic on a %s value", luaL_typename(L, -1)); + lua_replace(L, -2); + } +#else + lua_arith(L, LUA_OPUNM); +#endif +} + +// This version supports placeholders. +static void setfuncs(lua_State* L, const luaL_Reg* l, int nup) { + luaL_checkstack(L, nup, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + if (l->func == NULL) /* place holder? */ + lua_pushboolean(L, 0); + else { + int i; + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -nup); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + } + lua_setfield(L, -(nup + 2), l->name); + } + lua_pop(L, nup); /* remove upvalues */ +} + +#define newlib(L, l) (lua_newtable((L)), setfuncs((L), (l), 0)) + +#endif // LUAKIWI_INT_H_ diff --git a/ckiwi/luakiwi.c b/ckiwi/luakiwi.c new file mode 100644 index 0000000..a8240d5 --- /dev/null +++ b/ckiwi/luakiwi.c @@ -0,0 +1,1699 @@ +#include +#include +#include + +#include "ckiwi.h" +#include "luacompat.h" +#include "luakiwi-int.h" + +// Note some of the internal functions do not bother cleaning up the stack, they +// are marked with accordingly. + +enum TypeId { NOTYPE, VAR = 1, TERM, EXPR, CONSTRAINT, SOLVER, ERROR, NUMBER }; + +static const int ERR_KIND_TAB = NUMBER + 1; +static const int VAR_SUB_FN = ERR_KIND_TAB + 1; +static const int CONTEXT_TAB_MAX = VAR_SUB_FN + 1; + +static const char* const lkiwi_error_kinds[] = { + "KiwiErrNone", + "KiwiErrUnsatisfiableConstraint", + "KiwiErrUnknownConstraint", + "KiwiErrDuplicateConstraint", + "KiwiErrUnknownEditVariable", + "KiwiErrDuplicateEditVariable", + "KiwiErrBadRequiredStrength", + "KiwiErrInternalSolverError", + "KiwiErrAlloc", + "KiwiErrNullObject", + "KiwiErrUnknown", +}; + +static const double STRENGTH_REQUIRED = 1001001000.0; +static const double STRENGTH_STRONG = 1000000.0; +static const double STRENGTH_MEDIUM = 1000.0; +static const double STRENGTH_WEAK = 1.0; + +static enum KiwiRelOp get_op_opt(lua_State* L, int idx) { + size_t opn; + const char* op = luaL_optlstring(L, idx, "EQ", &opn); + + if (opn == 2) { + if (op[0] == 'E' && op[1] == 'Q') { + return KIWI_OP_EQ; + } else if (op[0] == 'L' && op[1] == 'E') { + return KIWI_OP_LE; + } else if (op[0] == 'G' && op[1] == 'E') { + return KIWI_OP_GE; + } + } + luaL_argerror(L, idx, "invalid operator"); + return KIWI_OP_EQ; +} + +static inline void push_type(lua_State* L, int type_id) { + lua_rawgeti(L, lua_upvalueindex(1), type_id); +} + +// stack disposition: dirty +static inline int is_udata_obj(lua_State* L, int type_id) { + int result = 0; + if (lua_isuserdata(L, 1) && lua_getmetatable(L, 1)) { + push_type(L, type_id); + result = lua_rawequal(L, -1, -2); + } + lua_pushboolean(L, result); + return 1; +} + +// get typename, copy the stack string to tidx, helpful when using +// with buffers. +static const char* typename(lua_State* L, int idx, int tidx) { + const char* ret = 0; + if (lua_getmetatable(L, idx)) { + lua_getfield(L, -1, "__name"); + ret = lua_tolstring(L, -1, 0); + lua_replace(L, tidx); + lua_pop(L, 1); + } + + return ret ? ret : luaL_typename(L, idx); +} + +// never returns +static int op_error(lua_State* L, const char* op, int lidx, int ridx) { + luaL_Buffer buf; + size_t len; + const char* str; + + // scratch space for strings + lua_pushnil(L); + int stridx = lua_gettop(L); + + luaL_buffinit(L, &buf); + lua_pushfstring(L, "invalid operand type for '%s' %s('", op, typename(L, lidx, stridx)); + luaL_addvalue(&buf); + + str = luaL_tolstring(L, lidx, &len); + lua_replace(L, stridx); + luaL_addlstring(&buf, str, len < 100 ? len : 100); + + lua_pushfstring(L, "') and %s('", typename(L, ridx, stridx)); + luaL_addvalue(&buf); + + str = luaL_tolstring(L, ridx, &len); + lua_replace(L, stridx); + luaL_addlstring(&buf, str, len < 100 ? len : 100); + + luaL_addstring(&buf, "')"); + luaL_pushresult(&buf); + lua_error(L); + return 0; +} + +static void check_arg_error(lua_State* L, int idx, int have_mt) { + lua_pushstring(L, "__name"); + lua_rawget(L, -2); + // TODO: simplify this. This is a bit of a hack to deal with missing args. + // Also these error messages are funky when idx is negative. + int top = lua_gettop(L); + if (idx > 0 && top <= 2 + have_mt) { + lua_pushnil(L); + lua_replace(L, idx); + } + luaL_typeerror(L, idx < 0 ? top + idx - have_mt - 2 + 1 : idx, lua_tostring(L, -1)); +} + +static inline void* check_arg(lua_State* L, int idx, int type_id) { + void* udp = lua_touserdata(L, idx); + int have_mt = lua_getmetatable(L, idx); + push_type(L, type_id); + + if (l_unlikely(!udp || !have_mt || !lua_rawequal(L, -1, -2))) + check_arg_error(L, idx, have_mt); + + lua_pop(L, 2); + return udp; +} + +static inline void* try_type(lua_State* L, int idx, enum TypeId type_id) { + void* p = lua_touserdata(L, idx); + if (!p || !lua_getmetatable(L, idx)) + return 0; + push_type(L, type_id); + return lua_rawequal(L, -1, -2) ? p : 0; +} + +static inline KiwiVarRef try_var(lua_State* L, int idx) { + void* p = try_type(L, idx, VAR); + return p ? *(KiwiVarRef*)p : 0; +} + +static inline KiwiTerm* try_term(lua_State* L, int idx) { + return try_type(L, idx, TERM); +} + +static inline KiwiExpression* try_expr(lua_State* L, int idx) { + return try_type(L, idx, EXPR); +} + +// method to test types for expression functions +// stack disposition: dirty +static inline void* try_arg(lua_State* L, int idx, enum TypeId* type_id, double* num) { + void* p = lua_touserdata(L, idx); + if (!p || !lua_getmetatable(L, idx)) { + int isnum; + *num = lua_tonumberx(L, idx, &isnum); + if (isnum) { + *type_id = NUMBER; + } else + *type_id = NOTYPE; + return 0; + } + + push_type(L, EXPR); + if (lua_rawequal(L, -1, -2)) { + *type_id = EXPR; + return p; + } + push_type(L, VAR); + if (lua_rawequal(L, -1, -3)) { + *type_id = VAR; + return p; + } + push_type(L, TERM); + if (lua_rawequal(L, -1, -4)) { + *type_id = TERM; + return p; + } + *type_id = NOTYPE; + return 0; +} + +static inline KiwiVarRef get_var(lua_State* L, int idx) { + return *((KiwiVarRef*)check_arg(L, idx, VAR)); +} + +static inline KiwiTerm* get_term(lua_State* L, int idx) { + return check_arg(L, idx, TERM); +} + +static inline KiwiExpression* get_expr(lua_State* L, int idx) { + return check_arg(L, idx, EXPR); +} + +static inline KiwiExpression* get_expr_opt(lua_State* L, int idx) { + if (lua_isnoneornil(L, idx)) { + return 0; + } + return check_arg(L, idx, EXPR); +} + +static inline KiwiConstraintRef get_constraint(lua_State* L, int idx) { + return *((KiwiConstraintRef*)check_arg(L, idx, CONSTRAINT)); +} + +static inline KiwiSolver* get_solver(lua_State* L, int idx) { + return *(KiwiSolver**)check_arg(L, idx, SOLVER); +} + +// note this expects the 2nd upvalue to have the variable weak table +static inline KiwiVarRef var_new(lua_State* L, KiwiVarRef var) { + KiwiVarRef* varp = lua_newuserdata(L, sizeof(KiwiVarRef)); + *varp = var; + + push_type(L, VAR); + lua_setmetatable(L, -2); + +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 + // a true compatibility shim has performance implications here + lua_pushlightuserdata(L, var); + lua_pushvalue(L, -2); + lua_rawset(L, lua_upvalueindex(2)); +#else + lua_pushvalue(L, -1); + lua_rawsetp(L, lua_upvalueindex(2), var); +#endif + return var; +} + +static inline KiwiTerm* term_new(lua_State* L) { + KiwiTerm* ret = lua_newuserdata(L, sizeof(KiwiTerm)); + push_type(L, TERM); + lua_setmetatable(L, -2); + return ret; +} + +static inline KiwiExpression* expr_new(lua_State* L, int nterms) { + KiwiExpression* ret = lua_newuserdata( + L, + sizeof(KiwiExpression) + sizeof(KiwiTerm) * (nterms > 1 ? nterms - 1 : 0) + ); + push_type(L, EXPR); + lua_setmetatable(L, -2); + return ret; +} + +static inline KiwiConstraintRef constraint_new( + lua_State* L, + const KiwiExpression* lhs, + const KiwiExpression* rhs, + enum KiwiRelOp op, + double strength +) { + KiwiConstraintRef* ref = lua_newuserdata(L, sizeof(KiwiConstraintRef)); + *ref = kiwi_constraint_new(lhs, rhs, op, strength); + + push_type(L, CONSTRAINT); + lua_setmetatable(L, -2); + return *ref; +} + +// stack disposition: dirty +static KiwiExpression* toexpr(lua_State* L, int idx, KiwiExpression* temp) { + void* ud = lua_touserdata(L, idx); + + if (!ud) { + int isnum; + temp->constant = lua_tonumberx(L, idx, &isnum); + temp->term_count = 0; + return isnum ? temp : 0; + } + if (!lua_getmetatable(L, idx)) + return 0; + + push_type(L, EXPR); + if (lua_rawequal(L, -1, -2)) { + return ud; + } + + temp->constant = 0; + temp->term_count = 1; + push_type(L, VAR); + if (lua_rawequal(L, -1, -3)) { + temp->terms[0].var = *(KiwiVarRef*)ud; + temp->terms[0].coefficient = 1.0; + return temp; + } + push_type(L, TERM); + if (lua_rawequal(L, -1, -4)) { + temp->terms[0] = *(KiwiTerm*)ud; + return temp; + } + return 0; +} + +static int relop(lua_State* L, enum KiwiRelOp op, const char opdisp[2]) { + KiwiExpression tmpl, tmpr; + double strength = luaL_optnumber(L, 3, STRENGTH_REQUIRED); + const KiwiExpression* lhs = toexpr(L, 1, &tmpl); + const KiwiExpression* rhs = toexpr(L, 2, &tmpr); + + if (!lhs || !rhs) { + op_error(L, opdisp, 1, 2); + } + + constraint_new(L, lhs, rhs, op, strength); + return 1; +} + +static int lkiwi_eq(lua_State* L) { + return relop(L, KIWI_OP_EQ, "=="); +} + +static int lkiwi_le(lua_State* L) { + return relop(L, KIWI_OP_LE, "<="); +} + +static int lkiwi_ge(lua_State* L) { + return relop(L, KIWI_OP_GE, ">="); +} + +static inline int push_expr_one(lua_State* L, double constant, const KiwiTerm* term) { + KiwiExpression* expr = expr_new(L, 1); + expr->constant = constant; + expr->term_count = 1; + expr->terms[0].coefficient = term->coefficient; + expr->terms[0].var = kiwi_var_clone(term->var); + return 1; +} + +static inline int +push_expr_pair(lua_State* L, double constant, const KiwiTerm* ta, const KiwiTerm* tb) { + KiwiExpression* e = expr_new(L, 2); + e->constant = constant; + e->term_count = 2; + e->terms[0].coefficient = ta->coefficient; + e->terms[0].var = kiwi_var_clone(ta->var); + e->terms[1].coefficient = tb->coefficient; + e->terms[1].var = kiwi_var_clone(tb->var); + return 1; +} + +static inline int +push_expr_var_term(lua_State* L, double constant, KiwiVarRef var, const KiwiTerm* t) { + KiwiExpression* e = expr_new(L, 2); + e->constant = constant; + e->term_count = 2; + e->terms[0].coefficient = 1.0; + e->terms[0].var = kiwi_var_clone(var); + e->terms[1].coefficient = t->coefficient; + e->terms[1].var = kiwi_var_clone(t->var); + return 1; +} + +static int push_add_expr_term(lua_State* L, const KiwiExpression* expr, const KiwiTerm* t) { + KiwiExpression* e = expr_new(L, expr->term_count + 1); + e->constant = expr->constant; + e->term_count = expr->term_count + 1; + int i = 0; + for (; i < expr->term_count; ++i) { + e->terms[i].coefficient = expr->terms[i].coefficient; + e->terms[i].var = kiwi_var_clone(expr->terms[i].var); + } + e->terms[i].coefficient = t->coefficient; + e->terms[i].var = kiwi_var_clone(t->var); + return 1; +} + +static int lkiwi_var_m_add(lua_State* L) { + enum TypeId type_id_b; + double num; + const void* arg_b = try_arg(L, 2, &type_id_b, &num); + + if (type_id_b == VAR) { + int isnum_a; + num = lua_tonumberx(L, 1, &isnum_a); + if (isnum_a) { + const KiwiTerm t = {*(KiwiVarRef*)arg_b, 1.0}; + return push_expr_one(L, num, &t); + } + } + + KiwiVarRef var_a = try_var(L, 1); + if (var_a) { + switch (type_id_b) { + case VAR: { + const KiwiTerm ta = {var_a, 1.0}, tb = {*(KiwiVarRef*)arg_b, 1.0}; + return push_expr_pair(L, 0.0, &ta, &tb); + } + case TERM: + return push_expr_var_term(L, 0.0, var_a, arg_b); + case EXPR: { + const KiwiTerm t = {var_a, 1.0}; + return push_add_expr_term(L, arg_b, &t); + } + case NUMBER: { + const KiwiTerm t = {var_a, 1.0}; + return push_expr_one(L, num, &t); + } + default: + break; + } + } + return op_error(L, "+", 1, 2); +} + +static int lkiwi_var_m_sub(lua_State* L) { + lua_settop(L, 2); +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 + lua_rawgeti(L, lua_upvalueindex(1), VAR_SUB_FN); + lua_insert(L, 1); + lua_call(L, 2, 1); +#else + lua_arith(L, LUA_OPUNM); + lua_arith(L, LUA_OPADD); +#endif + return 1; +} + +static int lkiwi_var_m_mul(lua_State* L) { + int isnum, varidx = 2; + double num = lua_tonumberx(L, 1, &isnum); + + if (!isnum) { + varidx = 1; + num = lua_tonumberx(L, 2, &isnum); + } + + if (isnum) { + KiwiVarRef var = try_var(L, varidx); + if (var) { + KiwiTerm* term = term_new(L); + term->var = kiwi_var_clone(var); + term->coefficient = num; + return 1; + } + } + return op_error(L, "*", 1, 2); +} + +static int lkiwi_var_m_div(lua_State* L) { + KiwiVarRef var = try_var(L, 1); + int isnum; + double num = lua_tonumberx(L, 2, &isnum); + if (!var || !isnum) { + return op_error(L, "/", 1, 2); + } + KiwiTerm* term = term_new(L); + term->var = kiwi_var_clone(var); + term->coefficient = 1.0 / num; + return 1; +} + +static int lkiwi_var_m_unm(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + KiwiTerm* term = term_new(L); + term->var = kiwi_var_clone(var); + term->coefficient = -1.0; + return 1; +} + +static int lkiwi_var_m_eq(lua_State* L) { + lua_pushboolean(L, get_var(L, 1) == get_var(L, 2)); + return 1; +} + +static int lkiwi_var_m_tostring(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + lua_pushfstring(L, "%s(%f)", kiwi_var_name(var), kiwi_var_value(var)); + return 1; +} + +static int lkiwi_var_m_gc(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + kiwi_var_del(var); + return 0; +} + +static int lkiwi_var_set_name(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + const char* name = luaL_checkstring(L, 2); + kiwi_var_set_name(var, name); + return 0; +} + +static int lkiwi_var_name(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + lua_pushstring(L, kiwi_var_name(var)); + return 1; +} + +static int lkiwi_var_set(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + const double value = luaL_checknumber(L, 2); + kiwi_var_set_value(var, value); + return 0; +} + +static int lkiwi_var_value(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + lua_pushnumber(L, kiwi_var_value(var)); + return 1; +} + +static int lkiwi_var_toterm(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + double coefficient = luaL_optnumber(L, 2, 1.0); + + KiwiTerm* term = term_new(L); + term->var = kiwi_var_clone(var); + term->coefficient = coefficient; + + return 1; +} + +static int lkiwi_var_toexpr(lua_State* L) { + const KiwiTerm t = {get_var(L, 1), 1.0}; + return push_expr_one(L, 0.0, &t); +} + +static const struct luaL_Reg kiwi_var_m[] = { + {"__add", lkiwi_var_m_add}, + {"__sub", lkiwi_var_m_sub}, + {"__mul", lkiwi_var_m_mul}, + {"__div", lkiwi_var_m_div}, + {"__unm", lkiwi_var_m_unm}, + {"__eq", lkiwi_var_m_eq}, + {"__tostring", lkiwi_var_m_tostring}, + {"__gc", lkiwi_var_m_gc}, + {"name", lkiwi_var_name}, + {"set_name", lkiwi_var_set_name}, + {"value", lkiwi_var_value}, + {"set", lkiwi_var_set}, + {"toterm", lkiwi_var_toterm}, + {"toexpr", lkiwi_var_toexpr}, + {"eq", lkiwi_eq}, + {"le", lkiwi_le}, + {"ge", lkiwi_ge}, + {0, 0}}; + +static int lkiwi_var_new(lua_State* L) { + const char* name = luaL_optstring(L, 1, ""); + var_new(L, kiwi_var_new(name)); + return 1; +} + +static int lkiwi_term_m_mul(lua_State* L) { + int isnum, termidx = 2; + double num = lua_tonumberx(L, 1, &isnum); + + if (!isnum) { + termidx = 1; + num = lua_tonumberx(L, 2, &isnum); + } + + if (isnum) { + const KiwiTerm* term = try_term(L, termidx); + if (term) { + KiwiTerm* ret = term_new(L); + ret->var = kiwi_var_clone(term->var); + ret->coefficient = term->coefficient * num; + return 1; + } + } + return op_error(L, "*", 1, 2); +} + +static int lkiwi_term_m_div(lua_State* L) { + const KiwiTerm* term = try_term(L, 1); + int isnum; + double num = lua_tonumberx(L, 2, &isnum); + if (!term || !isnum) { + return op_error(L, "/", 1, 2); + } + KiwiTerm* ret = term_new(L); + ret->var = kiwi_var_clone(term->var); + ret->coefficient = term->coefficient / num; + return 1; +} + +static int lkiwi_term_m_unm(lua_State* L) { + const KiwiTerm* term = get_term(L, 1); + KiwiTerm* ret = term_new(L); + ret->var = kiwi_var_clone(term->var); + ret->coefficient = -term->coefficient; + return 1; +} + +static int lkiwi_term_m_add(lua_State* L) { + enum TypeId type_id_b; + double num; + const void* arg_b = try_arg(L, 2, &type_id_b, &num); + + if (type_id_b == TERM) { + int isnum_a; + num = lua_tonumberx(L, 1, &isnum_a); + if (isnum_a) { + return push_expr_one(L, num, arg_b); + } + } + + const KiwiTerm* term_a = try_term(L, 1); + if (term_a) { + switch (type_id_b) { + case TERM: + return push_expr_pair(L, 0.0, term_a, arg_b); + case VAR: { + const KiwiTerm term_b = {*(KiwiVarRef*)arg_b, 1.0}; + return push_expr_pair(L, 0.0, term_a, &term_b); + } + case EXPR: + return push_add_expr_term(L, arg_b, term_a); + case NUMBER: + return push_expr_one(L, num, term_a); + default: + break; + } + } + return op_error(L, "+", 1, 2); +} + +static int lkiwi_term_m_sub(lua_State* L) { + lua_settop(L, 2); + compat_arith_unm(L); + lkiwi_term_m_add(L); + return 1; +} + +static int lkiwi_term_toexpr(lua_State* L) { + return push_expr_one(L, 0.0, get_term(L, 1)); +} + +static int lkiwi_term_value(lua_State* L) { + KiwiTerm* term = get_term(L, 1); + lua_pushnumber(L, kiwi_var_value(term->var) * term->coefficient); + return 1; +} + +static int lkiwi_term_m_tostring(lua_State* L) { + KiwiTerm* term = get_term(L, 1); + lua_pushfstring(L, "%f %s", term->coefficient, kiwi_var_name(term->var)); + return 1; +} + +static int lkiwi_term_m_gc(lua_State* L) { + KiwiTerm* term = get_term(L, 1); + kiwi_var_del(term->var); + return 0; +} + +static int lkiwi_term_m_index(lua_State* L) { + KiwiTerm* term = get_term(L, 1); + size_t len; + const char* k = lua_tolstring(L, 2, &len); + if (len == 3 && memcmp("var", k, len) == 0) { +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 + lua_pushlightuserdata(L, term->var); + lua_rawget(L, lua_upvalueindex(2)); +#else + lua_rawgetp(L, lua_upvalueindex(2), term->var); +#endif + if (lua_isnil(L, -1)) + var_new(L, kiwi_var_clone(term->var)); + return 1; + } else if (len == 11 && memcmp("coefficient", k, len) == 0) { + lua_pushnumber(L, term->coefficient); + return 1; + } + lua_getmetatable(L, 1); + lua_pushvalue(L, 2); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) { + luaL_error(L, "kiwi.Term has no member named '%s'", k); + } + return 1; +} + +static const struct luaL_Reg kiwi_term_m[] = { + {"__add", lkiwi_term_m_add}, + {"__sub", lkiwi_term_m_sub}, + {"__mul", lkiwi_term_m_mul}, + {"__div", lkiwi_term_m_div}, + {"__unm", lkiwi_term_m_unm}, + {"__tostring", lkiwi_term_m_tostring}, + {"__gc", lkiwi_term_m_gc}, + {"__index", 0}, + {"toexpr", lkiwi_term_toexpr}, + {"value", lkiwi_term_value}, + {"eq", lkiwi_eq}, + {"le", lkiwi_le}, + {"ge", lkiwi_ge}, + {0, 0}}; + +static int lkiwi_term_new(lua_State* L) { + KiwiVarRef var = get_var(L, 1); + double coefficient = luaL_optnumber(L, 2, 1.0); + KiwiTerm* term = term_new(L); + term->var = kiwi_var_clone(var); + term->coefficient = coefficient; + return 1; +} + +static int push_expr_constant(lua_State* L, const KiwiExpression* expr, double constant) { + KiwiExpression* ne = expr_new(L, expr->term_count); + for (int i = 0; i < expr->term_count; i++) { + ne->terms[i].var = kiwi_var_clone(expr->terms[i].var); + ne->terms[i].coefficient = expr->terms[i].coefficient; + } + ne->constant = constant; + ne->term_count = expr->term_count; + return 1; +} + +static int push_mul_expr_coeff(lua_State* L, const KiwiExpression* expr, double coeff) { + KiwiExpression* ne = expr_new(L, expr->term_count); + ne->constant = expr->constant * coeff; + ne->term_count = expr->term_count; + for (int i = 0; i < expr->term_count; i++) { + ne->terms[i].var = kiwi_var_clone(expr->terms[i].var); + ne->terms[i].coefficient = expr->terms[i].coefficient * coeff; + } + return 1; +} + +static int push_add_expr_expr(lua_State* L, const KiwiExpression* a, const KiwiExpression* b) { + int na = a->term_count, nb = b->term_count; + + KiwiExpression* ne = expr_new(L, na + nb); + ne->constant = a->constant + b->constant; + ne->term_count = na + nb; + + for (int i = 0; i < na; i++) { + ne->terms[i].var = kiwi_var_clone(a->terms[i].var); + ne->terms[i].coefficient = a->terms[i].coefficient; + } + for (int i = 0; i < nb; i++) { + ne->terms[i + na].var = kiwi_var_clone(b->terms[i].var); + ne->terms[i + na].coefficient = b->terms[i].coefficient; + } + return 1; +} + +static int lkiwi_expr_m_mul(lua_State* L) { + int isnum, expridx = 2; + double num = lua_tonumberx(L, 1, &isnum); + + if (!isnum) { + expridx = 1; + num = lua_tonumberx(L, 2, &isnum); + } + + if (isnum) { + const KiwiExpression* expr = try_expr(L, expridx); + if (expr) + return push_mul_expr_coeff(L, expr, num); + } + return op_error(L, "*", 1, 2); +} + +static int lkiwi_expr_m_div(lua_State* L) { + const KiwiExpression* expr = try_expr(L, 1); + int isnum; + double num = lua_tonumberx(L, 2, &isnum); + if (!expr || !isnum) { + return op_error(L, "/", 1, 2); + } + return push_mul_expr_coeff(L, expr, 1.0 / num); +} + +static int lkiwi_expr_m_unm(lua_State* L) { + const KiwiExpression* expr = get_expr(L, 1); + return push_mul_expr_coeff(L, expr, -1.0); +} + +static int lkiwi_expr_m_add(lua_State* L) { + enum TypeId type_id_b; + double num; + const void* arg_b = try_arg(L, 2, &type_id_b, &num); + + if (type_id_b == EXPR) { + int isnum_a; + num = lua_tonumberx(L, 1, &isnum_a); + if (isnum_a) { + const KiwiExpression* expr_b = arg_b; + return push_expr_constant(L, expr_b, num + expr_b->constant); + } + } + + const KiwiExpression* expr_a = try_expr(L, 1); + if (expr_a) { + switch (type_id_b) { + case EXPR: + return push_add_expr_expr(L, expr_a, arg_b); + case TERM: + return push_add_expr_term(L, expr_a, arg_b); + case VAR: { + const KiwiTerm term_b = {*(KiwiVarRef*)arg_b, 1.0}; + return push_add_expr_term(L, expr_a, &term_b); + } + case NUMBER: + return push_expr_constant(L, expr_a, num + expr_a->constant); + default: + break; + } + } + return op_error(L, "+", 1, 2); +} + +static int lkiwi_expr_m_sub(lua_State* L) { + lua_settop(L, 2); + compat_arith_unm(L); + lkiwi_expr_m_add(L); + return 1; +} + +static int lkiwi_expr_value(lua_State* L) { + KiwiExpression* expr = get_expr(L, 1); + double sum = expr->constant; + for (int i = 0; i < expr->term_count; i++) { + const KiwiTerm* t = &expr->terms[i]; + sum += kiwi_var_value(t->var) * t->coefficient; + } + lua_pushnumber(L, sum); + return 1; +} + +static int lkiwi_expr_terms(lua_State* L) { + KiwiExpression* expr = get_expr(L, 1); + lua_createtable(L, expr->term_count, 0); + for (int i = 0; i < expr->term_count; i++) { + KiwiTerm* t = &expr->terms[i]; + KiwiTerm* new_term = term_new(L); + new_term->var = kiwi_var_clone(t->var); + new_term->coefficient = t->coefficient; + lua_rawseti(L, -2, i + 1); + } + return 1; +} + +static int lkiwi_expr_copy(lua_State* L) { + KiwiExpression* expr = get_expr(L, 1); + return push_expr_constant(L, expr, expr->constant); +} + +static void push_expr_string(lua_State* L, KiwiExpression* expr) { + luaL_Buffer buf; + luaL_buffinit(L, &buf); + + for (int i = 0; i < expr->term_count; i++) { + KiwiTerm* t = &expr->terms[i]; + lua_pushfstring(L, "%f %s", t->coefficient, kiwi_var_name(t->var)); + luaL_addvalue(&buf); + luaL_addstring(&buf, " + "); + } + + lua_pushfstring(L, "%f", expr->constant); + luaL_addvalue(&buf); + luaL_pushresult(&buf); +} + +static int lkiwi_expr_m_tostring(lua_State* L) { + push_expr_string(L, get_expr(L, 1)); + return 1; +} + +static int lkiwi_expr_m_gc(lua_State* L) { + KiwiExpression* expr = get_expr(L, 1); + for (int i = 0; i < expr->term_count; i++) { + kiwi_var_del(expr->terms[i].var); + } + return 0; +} + +static int lkiwi_expr_m_index(lua_State* L) { + KiwiExpression* expr = get_expr(L, 1); + size_t len; + const char* k = lua_tolstring(L, 2, &len); + if (len == 8 && memcmp("constant", k, len) == 0) { + lua_pushnumber(L, expr->constant); + return 1; + } + lua_getmetatable(L, 1); + lua_pushvalue(L, 2); + lua_rawget(L, -2); + if (lua_isnil(L, -1)) { + luaL_error(L, "kiwi.Expression has no member named '%s'", k); + } + return 1; +} + +static const struct luaL_Reg kiwi_expr_m[] = { + {"__add", lkiwi_expr_m_add}, + {"__sub", lkiwi_expr_m_sub}, + {"__mul", lkiwi_expr_m_mul}, + {"__div", lkiwi_expr_m_div}, + {"__unm", lkiwi_expr_m_unm}, + {"__tostring", lkiwi_expr_m_tostring}, + {"__gc", lkiwi_expr_m_gc}, + {"__index", lkiwi_expr_m_index}, + {"value", lkiwi_expr_value}, + {"terms", lkiwi_expr_terms}, + {"copy", lkiwi_expr_copy}, + {"eq", lkiwi_eq}, + {"le", lkiwi_le}, + {"ge", lkiwi_ge}, + {0, 0}}; + +static int lkiwi_expr_new(lua_State* L) { + int nterms = lua_gettop(L) - 1; + lua_Number constant = luaL_checknumber(L, 1); + + KiwiExpression* expr = expr_new(L, nterms); + expr->constant = constant; + expr->term_count = nterms; + + for (int i = 0; i < nterms; i++) { + const KiwiTerm* term = get_term(L, i + 2); + expr->terms[i].var = kiwi_var_clone(term->var); + expr->terms[i].coefficient = term->coefficient; + } + return 1; +} + +static int lkiwi_constraint_strength(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + lua_pushnumber(L, kiwi_constraint_strength(c)); + return 1; +} + +static int lkiwi_constraint_op(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + enum KiwiRelOp op = kiwi_constraint_op(c); + const char* opstr = "??"; + switch (op) { + case KIWI_OP_EQ: + opstr = "EQ"; + break; + case KIWI_OP_LE: + opstr = "LE"; + break; + case KIWI_OP_GE: + opstr = "GE"; + break; + } + lua_pushlstring(L, opstr, 2); + return 1; +} + +static int lkiwi_constraint_violated(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + lua_pushboolean(L, kiwi_constraint_violated(c)); + return 1; +} + +static int lkiwi_constraint_expression(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + const static int SZ = 12; + KiwiExpression* expr = expr_new(L, SZ); + int n = kiwi_constraint_expression(c, expr, SZ); + + if (l_unlikely(n > SZ)) { + lua_pop(L, 1); + expr = expr_new(L, n); + kiwi_constraint_expression(c, expr, n); + } + return 1; +} + +static int lkiwi_constraint_m_tostring(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + + const static int SZ = 16; + KiwiExpression* expr = stalloc(sizeof(KiwiExpression) + sizeof(KiwiTerm) * (SZ - 1)); + int n = kiwi_constraint_expression(c, expr, SZ); + + if (l_unlikely(n > SZ)) { + expr = malloc(sizeof(KiwiExpression) + sizeof(KiwiTerm) * (n - 1)); + if (!expr) { + luaL_error(L, "out of memory"); + } + kiwi_constraint_expression(c, expr, n); + } + + luaL_Buffer buf; + luaL_buffinit(L, &buf); + const char* oppart = " ?? 0 | "; + switch (kiwi_constraint_op(c)) { + case KIWI_OP_EQ: + oppart = " == 0 | "; + break; + case KIWI_OP_LE: + oppart = " <= 0 | "; + break; + case KIWI_OP_GE: + oppart = " >= 0 | "; + break; + } + + push_expr_string(L, expr); + luaL_addvalue(&buf); + luaL_addlstring(&buf, oppart, 8); + const char* strength_name = 0; + const double strength = kiwi_constraint_strength(c); + + if (strength == STRENGTH_REQUIRED) { + strength_name = "required"; + } else if (strength == STRENGTH_STRONG) { + strength_name = "strong"; + } else if (strength == STRENGTH_MEDIUM) { + strength_name = "medium"; + } else if (strength == STRENGTH_WEAK) { + strength_name = "weak"; + } + + if (strength_name) { + luaL_addstring(&buf, strength_name); + } else { + lua_pushfstring(L, "%f", strength); + luaL_addvalue(&buf); + } + luaL_pushresult(&buf); + + return 1; +} + +static int lkiwi_constraint_m_gc(lua_State* L) { + KiwiConstraintRef c = get_constraint(L, 1); + kiwi_constraint_del(c); + return 0; +} + +static int lkiwi_solver_add_constraint(lua_State* L); +static int lkiwi_solver_remove_constraint(lua_State* L); + +static int lkiwi_constraint_add_to(lua_State* L) { + lua_settop(L, 2); + lua_rotate(L, 1, 1); + lkiwi_solver_add_constraint(L); + lua_settop(L, 2); + return 1; +} + +static int lkiwi_constraint_remove_from(lua_State* L) { + lua_settop(L, 2); + lua_rotate(L, 1, 1); + lkiwi_solver_remove_constraint(L); + lua_settop(L, 2); + return 1; +} + +static const struct luaL_Reg kiwi_constraint_m[] = { + {"__tostring", lkiwi_constraint_m_tostring}, + {"__gc", lkiwi_constraint_m_gc}, + {"strength", lkiwi_constraint_strength}, + {"op", lkiwi_constraint_op}, + {"violated", lkiwi_constraint_violated}, + {"expression", lkiwi_constraint_expression}, + {"add_to", lkiwi_constraint_add_to}, + {"remove_from", lkiwi_constraint_remove_from}, + {0, 0}}; + +static int lkiwi_constraint_new(lua_State* L) { + KiwiExpression* lhs = get_expr_opt(L, 1); + KiwiExpression* rhs = get_expr_opt(L, 2); + enum KiwiRelOp op = get_op_opt(L, 3); + double strength = luaL_optnumber(L, 4, STRENGTH_REQUIRED); + + constraint_new(L, lhs, rhs, op, strength); + return 1; +} + +static int push_pair_constraint( + lua_State* L, + KiwiVarRef left, + double coeff, + KiwiVarRef right, + double constant, + enum KiwiRelOp op, + double strength +) { + KiwiExpression* expr = stalloc(sizeof(KiwiExpression) + sizeof(KiwiTerm)); + expr->constant = constant; + expr->term_count = 2; + expr->terms[0].var = left; + expr->terms[0].coefficient = 1.0; + expr->terms[1].var = right; + expr->terms[1].coefficient = -coeff; + constraint_new(L, expr, 0, op, strength); + return 1; +} + +static int lkiwi_constraints_pair_ratio(lua_State* L) { + return push_pair_constraint( + L, + get_var(L, 1), + luaL_checknumber(L, 2), + get_var(L, 3), + luaL_optnumber(L, 4, 0.0), + get_op_opt(L, 5), + luaL_optnumber(L, 6, STRENGTH_REQUIRED) + ); +} + +static int lkiwi_constraints_pair(lua_State* L) { + return push_pair_constraint( + L, + get_var(L, 1), + 1.0, + get_var(L, 2), + luaL_optnumber(L, 3, 0.0), + get_op_opt(L, 4), + luaL_optnumber(L, 4, STRENGTH_REQUIRED) + ); +} + +static int lkiwi_constraints_single(lua_State* L) { + KiwiExpression expr; + expr.term_count = 1; + expr.terms[0].var = get_var(L, 1); + expr.terms[0].coefficient = 1.0; + expr.constant = luaL_optnumber(L, 2, 0.0); + + constraint_new(L, &expr, 0, get_op_opt(L, 3), luaL_optnumber(L, 4, STRENGTH_REQUIRED)); + return 1; +} + +static const struct luaL_Reg lkiwi_constraints[] = { + {"pair_ratio", lkiwi_constraints_pair_ratio}, + {"pair", lkiwi_constraints_pair}, + {"single", lkiwi_constraints_single}, + {0, 0}}; + +static void lkiwi_mod_constraints_new(lua_State* L, int ctx_i) { + luaL_newlibtable(L, lkiwi_constraints); + lua_pushvalue(L, ctx_i); + setfuncs(L, lkiwi_constraints, 1); +} + +/* kiwi.Error */ + +static void error_new(lua_State* L, const KiwiErr* err, int solver_absi, int item_absi) { + lua_createtable(L, 0, 4); + push_type(L, ERROR); + lua_setmetatable(L, -2); + + lua_pushstring(L, lkiwi_error_kinds[err->kind < KiwiErrUnknown ? err->kind : KiwiErrUnknown]); + lua_setfield(L, -2, "kind"); + + lua_pushstring(L, err->message); + lua_setfield(L, -2, "message"); + + if (solver_absi) { + lua_pushvalue(L, solver_absi); + lua_setfield(L, -2, "solver"); + } + if (item_absi) { + lua_pushvalue(L, item_absi); + lua_setfield(L, -2, "item"); + } + + if (err->must_free) { + free((void*)err); + } +} + +static int lkiwi_error_m_tostring(lua_State* L) { + luaL_Buffer buf; + luaL_buffinit(L, &buf); + + lua_getfield(L, 1, "message"); + luaL_addvalue(&buf); + + lua_getfield(L, 1, "solver"); + lua_pushfstring(L, ": (kiwi.Solver(%p), ", get_solver(L, -1)); + lua_remove(L, -2); // remove solver + luaL_addvalue(&buf); + + lua_getfield(L, 1, "item"); + luaL_tolstring(L, -1, 0); + lua_remove(L, -2); // remove item + luaL_addvalue(&buf); + luaL_addchar(&buf, ')'); + + luaL_pushresult(&buf); + return 1; +} + +static const struct luaL_Reg lkiwi_error_m[] = {{"__tostring", lkiwi_error_m_tostring}, {0, 0}}; + +static int lkiwi_error_mask(lua_State* L) { + int invert = lua_toboolean(L, 2); + + if (lua_type(L, 1) == LUA_TSTRING) { + luaL_typeerror(L, 1, "indexable"); + } + + lua_rawgeti(L, lua_upvalueindex(1), ERR_KIND_TAB); + + unsigned mask = 0; + for (int n = 1;; ++n) { + if (lua_geti(L, 1, n) == LUA_TNIL) { + break; + } + int isnum; + int shift = lua_tointegerx(L, -1, &isnum); + if (!isnum) { + lua_rawget(L, -2 /* err_kind table */); + shift = lua_tointegerx(L, -1, &isnum); + if (!isnum) { + luaL_error(L, "unknown error kind at index %d: %s", n, luaL_tolstring(L, -2, 0)); + } + } + mask |= 1 << shift; + lua_pop(L, 1); + } + lua_pushinteger(L, invert ? ~mask : mask); + return 1; +} + +static int lkiwi_solver_handle_err(lua_State* L, const KiwiErr* err, const KiwiSolver* solver) { + /* This assumes solver is at index 1 */ + lua_settop(L, 2); + if (err) { + error_new(L, err, 1, 2); + unsigned error_mask; + memcpy(&error_mask, solver, sizeof(error_mask)); + if (error_mask & (1 << err->kind)) { + return 2; + } else { + lua_error(L); + } + } + return 1; +} + +static int lkiwi_solver_add_constraint(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + KiwiConstraintRef c = get_constraint(L, 2); + const KiwiErr* err = kiwi_solver_add_constraint(solver, c); + return lkiwi_solver_handle_err(L, err, solver); +} + +static int lkiwi_solver_remove_constraint(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + KiwiConstraintRef c = get_constraint(L, 2); + const KiwiErr* err = kiwi_solver_remove_constraint(solver, c); + return lkiwi_solver_handle_err(L, err, solver); +} + +static int lkiwi_solver_add_edit_var(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + KiwiVarRef var = get_var(L, 2); + double strength = luaL_checknumber(L, 3); + const KiwiErr* err = kiwi_solver_add_edit_var(solver, var, strength); + return lkiwi_solver_handle_err(L, err, solver); +} + +static int lkiwi_solver_remove_edit_var(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + KiwiVarRef var = get_var(L, 2); + const KiwiErr* err = kiwi_solver_remove_edit_var(solver, var); + return lkiwi_solver_handle_err(L, err, solver); +} + +static int lkiwi_solver_suggest_value(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + KiwiVarRef var = get_var(L, 2); + double value = luaL_checknumber(L, 3); + const KiwiErr* err = kiwi_solver_suggest_value(solver, var, value); + return lkiwi_solver_handle_err(L, err, solver); +} + +static int lkiwi_solver_update_vars(lua_State* L) { + kiwi_solver_update_vars(get_solver(L, 1)); + return 0; +} + +static int lkiwi_solver_reset(lua_State* L) { + kiwi_solver_reset(get_solver(L, 1)); + return 0; +} + +static int lkiwi_solver_has_constraint(lua_State* L) { + lua_pushboolean(L, kiwi_solver_has_constraint(get_solver(L, 1), get_constraint(L, 2))); + return 1; +} + +static int lkiwi_solver_has_edit_var(lua_State* L) { + lua_pushboolean(L, kiwi_solver_has_edit_var(get_solver(L, 1), get_var(L, 2))); + return 1; +} + +static int lkiwi_solver_dump(lua_State* L) { + kiwi_solver_dump(get_solver(L, 1)); + return 0; +} + +static int lkiwi_solver_dumps(lua_State* L) { + lua_pushstring(L, kiwi_solver_dumps(get_solver(L, 1))); + return 1; +} + +typedef const KiwiErr* (*add_remove_fn_t)(lua_State*, KiwiSolver*, void*); + +static int lkiwi_add_remove_tab(lua_State* L, add_remove_fn_t fn, void* d) { + KiwiSolver* solver = get_solver(L, 1); + int narg = lua_gettop(L); + + // block this particularly obnoxious case which is always a bug + if (lua_type(L, 2) == LUA_TSTRING) { + luaL_typeerror(L, 2, "indexable"); + } + for (int i = 1;; ++i) { + if (lua_geti(L, 2, i) == LUA_TNIL) { + break; + } + const KiwiErr* err = fn(L, solver, d); + if (err) { + error_new(L, err, 1, narg + 1 /* item_absi */); + unsigned error_mask; + memcpy(&error_mask, solver, sizeof(error_mask)); + if (error_mask & (1 << err->kind)) { + lua_replace(L, 3); + lua_settop(L, 3); + return 2; + } else { + lua_error(L); + } + } + lua_pop(L, 1); + } + lua_settop(L, 2); + return 1; +} + +static const KiwiErr* add_constraint_fn(lua_State* L, KiwiSolver* s, void* d) { + KiwiConstraintRef c = get_constraint(L, -1); + return kiwi_solver_add_constraint(s, c); +} + +static int lkiwi_solver_add_constraints(lua_State* L) { + return lkiwi_add_remove_tab(L, add_constraint_fn, 0); +} + +static const KiwiErr* remove_constraint_fn(lua_State* L, KiwiSolver* s, void* d) { + KiwiConstraintRef c = get_constraint(L, -1); + return kiwi_solver_remove_constraint(s, c); +} + +static int lkiwi_solver_remove_constraints(lua_State* L) { + return lkiwi_add_remove_tab(L, remove_constraint_fn, 0); +} + +static const KiwiErr* add_edit_var_fn(lua_State* L, KiwiSolver* s, void* d) { + KiwiVarRef v = get_var(L, -1); + return kiwi_solver_add_edit_var(s, v, *(double*)d); +} + +static int lkiwi_solver_add_edit_vars(lua_State* L) { + double strength = luaL_checknumber(L, 3); + return lkiwi_add_remove_tab(L, add_edit_var_fn, &strength); +} + +static const KiwiErr* remove_edit_var_fn(lua_State* L, KiwiSolver* s, void* d) { + KiwiVarRef v = get_var(L, -1); + return kiwi_solver_remove_edit_var(s, v); +} + +static int lkiwi_solver_remove_edit_vars(lua_State* L) { + return lkiwi_add_remove_tab(L, remove_edit_var_fn, 0); +} + +static int lkiwi_solver_suggest_values(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + int narg = lua_gettop(L); + + // block this particularly obnoxious case which is always a bug + if (lua_type(L, 2) == LUA_TSTRING) { + luaL_typeerror(L, 2, "indexable"); + } + // block this particularly obnoxious case which is always a bug + if (lua_type(L, 3) == LUA_TSTRING) { + luaL_typeerror(L, 3, "indexable"); + } + + for (int i = 1;; ++i) { + if (lua_geti(L, 2, i) == LUA_TNIL) { + break; + } + KiwiVarRef var = get_var(L, -1); + lua_geti(L, 3, i); + double value = luaL_checknumber(L, -1); + + const KiwiErr* err = kiwi_solver_suggest_value(solver, var, value); + if (err) { + error_new(L, err, 1, narg + 1 /* item_absi */); + unsigned error_mask; + memcpy(&error_mask, solver, sizeof(error_mask)); + if (error_mask & (1 << err->kind)) { + lua_replace(L, 4); + lua_settop(L, 4); + return 3; + } else { + lua_error(L); + } + } + lua_pop(L, 2); + } + lua_settop(L, 3); + return 2; +} + +static int lkiwi_solver_set_error_mask(lua_State* L) { + KiwiSolver* solver = get_solver(L, 1); + + unsigned error_mask; + if (lua_istable(L, 2)) { + lua_settop(L, 3); + lua_rotate(L, 1, -1); + lkiwi_error_mask(L); + error_mask = lua_tointeger(L, -1); + } else { + error_mask = luaL_checkinteger(L, 2); + } + + memcpy(solver, &error_mask, sizeof(error_mask)); + return 0; +} + +static int lkiwi_solver_tostring(lua_State* L) { + lua_pushfstring(L, "kiwi.Solver(%p)", get_solver(L, 1)); + return 1; +} + +static int lkiwi_solver_del(lua_State* L) { + kiwi_solver_del(get_solver(L, 1)); + return 0; +} + +static const struct luaL_Reg kiwi_solver_m[] = { + {"add_constraint", lkiwi_solver_add_constraint}, + {"add_constraints", lkiwi_solver_add_constraints}, + {"remove_constraint", lkiwi_solver_remove_constraint}, + {"remove_constraints", lkiwi_solver_remove_constraints}, + {"add_edit_var", lkiwi_solver_add_edit_var}, + {"add_edit_vars", lkiwi_solver_add_edit_vars}, + {"remove_edit_var", lkiwi_solver_remove_edit_var}, + {"remove_edit_vars", lkiwi_solver_remove_edit_vars}, + {"suggest_value", lkiwi_solver_suggest_value}, + {"suggest_values", lkiwi_solver_suggest_values}, + {"update_vars", lkiwi_solver_update_vars}, + {"reset", lkiwi_solver_reset}, + {"has_constraint", lkiwi_solver_has_constraint}, + {"has_edit_var", lkiwi_solver_has_edit_var}, + {"dump", lkiwi_solver_dump}, + {"dumps", lkiwi_solver_dumps}, + {"set_error_mask", lkiwi_solver_set_error_mask}, + {"__tostring", lkiwi_solver_tostring}, + {"__gc", lkiwi_solver_del}, + {0, 0}}; + +static int lkiwi_solver_new_meth(lua_State* L) { + unsigned error_mask; + if (lua_istable(L, 1)) { + lkiwi_error_mask(L); + error_mask = lua_tointeger(L, -1); + } else { + error_mask = luaL_optinteger(L, 1, 0); + } + + KiwiSolver** sp = lua_newuserdata(L, sizeof(KiwiSolver*)); + *sp = kiwi_solver_new(error_mask); + + push_type(L, SOLVER); + lua_setmetatable(L, -2); + return 1; +} + +static inline double clamp(double n) { + return fmax(0.0, fmin(1000, n)); +} + +static int lkiwi_strength_create(lua_State* L) { + const double a = luaL_checknumber(L, 1); + const double b = luaL_checknumber(L, 2); + const double c = luaL_checknumber(L, 3); + const double w = luaL_optnumber(L, 4, 1.0); + + const double result = clamp(a * w) * 1000000.0 + clamp(b * w) * 1000.0 + clamp(c * w); + lua_pushnumber(L, result); + return 1; +} + +static const struct luaL_Reg lkiwi_strength[] = {{"create", lkiwi_strength_create}, {0, 0}}; + +static void lkiwi_mod_strength_new(lua_State* L) { + newlib(L, lkiwi_strength); + + lua_pushnumber(L, STRENGTH_REQUIRED); + lua_setfield(L, -2, "REQUIRED"); + + lua_pushnumber(L, STRENGTH_STRONG); + lua_setfield(L, -2, "STRONG"); + + lua_pushnumber(L, STRENGTH_MEDIUM); + lua_setfield(L, -2, "MEDIUM"); + + lua_pushnumber(L, STRENGTH_WEAK); + lua_setfield(L, -2, "WEAK"); +} + +static int lkiwi_is_var(lua_State* L) { + return is_udata_obj(L, VAR); +} + +static int lkiwi_is_term(lua_State* L) { + return is_udata_obj(L, TERM); +} + +static int lkiwi_is_expression(lua_State* L) { + return is_udata_obj(L, EXPR); +} + +static int lkiwi_is_constraint(lua_State* L) { + return is_udata_obj(L, CONSTRAINT); +} + +static int lkiwi_is_solver(lua_State* L) { + return is_udata_obj(L, SOLVER); +} + +static int lkiwi_is_error(lua_State* L) { + int result = 0; + if (lua_getmetatable(L, 1)) { + push_type(L, ERROR); + result = lua_rawequal(L, -1, -2); + lua_pop(L, 2); + } + lua_pushboolean(L, result); + return 1; +} + +static const struct luaL_Reg lkiwi[] = { + {"Var", 0}, + {"is_var", lkiwi_is_var}, + {"Term", lkiwi_term_new}, + {"is_term", lkiwi_is_term}, + {"Expression", lkiwi_expr_new}, + {"is_expression", lkiwi_is_expression}, + {"Constraint", lkiwi_constraint_new}, + {"is_constraint", lkiwi_is_constraint}, + {"Solver", lkiwi_solver_new_meth}, + {"is_solver", lkiwi_is_solver}, + {"error_mask", lkiwi_error_mask}, + {"is_error", lkiwi_is_error}, + {"eq", lkiwi_eq}, + {"le", lkiwi_le}, + {"ge", lkiwi_ge}, + {0, 0}}; + +static int no_member_mt_index(lua_State* L) { + luaL_error(L, "attempt to access non-existent member '%s'", lua_tostring(L, 2)); + return 0; +} + +static void no_member_mt_new(lua_State* L) { + lua_createtable(L, 0, 1); + lua_pushcfunction(L, no_member_mt_index); + lua_setfield(L, -2, "__index"); +} + +static void register_type_( + lua_State* L, + const char* name, + int context_absi, + int type_id, + const luaL_Reg* m, + size_t mcnt +) { + lua_createtable(L, 0, mcnt + 2); + lua_pushvalue(L, -2); // no_member_mt + lua_setmetatable(L, -2); + lua_pushstring(L, name); + lua_setfield(L, -2, "__name"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + + /* set type_tab udata as upvalue */ + lua_pushvalue(L, context_absi); + setfuncs(L, m, 1); + + lua_rawseti(L, context_absi, type_id); +} + +#define register_type(L, name, context_absi, ref, m) \ + register_type_(L, name, context_absi, ref, m, ARR_COUNT(m)) + +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 +static void compat_init(lua_State* L, int context_absi) { + static const char var_sub_code[] = + "local a,b=...\n" + "return a + -b"; + + if (luaL_loadbuffer(L, var_sub_code, sizeof(var_sub_code) - 1, "=kiwi internal")) + lua_error(L); + + lua_rawseti(L, context_absi, VAR_SUB_FN); +} +#else +static void compat_init(lua_State* L, int i) {} +#endif /* Lua 5.1 */ + +extern int luaopen_ckiwi(lua_State* L) { + luaL_checkversion(L); + + /* context table */ + lua_createtable(L, 0, CONTEXT_TAB_MAX); + int ctx_i = lua_gettop(L); + + compat_init(L, ctx_i); + + no_member_mt_new(L); + register_type(L, "kiwi.Var", ctx_i, VAR, kiwi_var_m); + register_type(L, "kiwi.Term", ctx_i, TERM, kiwi_term_m); + register_type(L, "kiwi.Expression", ctx_i, EXPR, kiwi_expr_m); + register_type(L, "kiwi.Constraint", ctx_i, CONSTRAINT, kiwi_constraint_m); + register_type(L, "kiwi.Solver", ctx_i, SOLVER, kiwi_solver_m); + register_type(L, "kiwi.Error", ctx_i, ERROR, lkiwi_error_m); + + lua_createtable(L, 0, ARR_COUNT(lkiwi) + 6); + lua_pushvalue(L, ctx_i); + setfuncs(L, lkiwi, 1); + + /* var weak table */ + /* set as upvalue for selected functions */ + lua_createtable(L, 0, 0); + lua_createtable(L, 0, 1); + lua_pushstring(L, "v"); + lua_setfield(L, -2, "__mode"); + lua_setmetatable(L, -2); + + lua_pushvalue(L, ctx_i); + lua_pushvalue(L, -2); + lua_pushcclosure(L, lkiwi_var_new, 2); + lua_setfield(L, -3, "Var"); + + lua_rawgeti(L, ctx_i, TERM); + lua_pushvalue(L, ctx_i); + lua_pushvalue(L, -3); + lua_pushcclosure(L, lkiwi_term_m_index, 2); + lua_setfield(L, -2, "__index"); + lua_pop(L, 2); // TERM mt and var weak table + + /* ErrKind table */ + /* TODO: implement __call metamethod for these */ + lua_createtable(L, ARR_COUNT(lkiwi_error_kinds) + 1, ARR_COUNT(lkiwi_error_kinds)); + for (int i = 0; i < ARR_COUNT(lkiwi_error_kinds); i++) { + lua_pushstring(L, lkiwi_error_kinds[i]); + lua_pushvalue(L, -1); + lua_rawseti(L, -3, i); + lua_pushinteger(L, i); + lua_rawset(L, -3); + } + + lua_pushvalue(L, -1); + lua_rawseti(L, ctx_i, ERR_KIND_TAB); + lua_setfield(L, -2, "ErrKind"); + + lua_rawgeti(L, ctx_i, ERROR); + lua_setfield(L, -2, "Error"); + + lua_pushinteger(L, 0xFFFF); + lua_setfield(L, -2, "ERROR_MASK_ALL"); + + lua_pushinteger( + L, + ~((1 << KiwiErrInternalSolverError) | (1 << KiwiErrAlloc) | (1 << KiwiErrNullObject) + | (1 << KiwiErrUnknown)) + ); + lua_setfield(L, -2, "ERROR_MASK_NON_FATAL"); + + lkiwi_mod_strength_new(L); + lua_setfield(L, -2, "strength"); + + lkiwi_mod_constraints_new(L, ctx_i); + lua_setfield(L, -2, "constraints"); + + return 1; +} diff --git a/kiwi.lua b/kiwi.lua index 3a9e674..b30b41a 100644 --- a/kiwi.lua +++ b/kiwi.lua @@ -345,7 +345,7 @@ end do --- Variables are the values the constraint solver calculates. ---@class kiwi.Var: ffi.cdata* - ---@overload fun(name: string): kiwi.Var + ---@overload fun(name: string?): kiwi.Var ---@operator mul(number): kiwi.Term ---@operator div(number): kiwi.Term ---@operator unm: kiwi.Term @@ -559,7 +559,7 @@ do ---@param expr kiwi.Expression ---@param constant number ---@nodiscard - local function mul_expr_constant(expr, constant) + local function mul_expr_coeff(expr, constant) local ret = ffi_gc(ffi_new(Expression, expr.term_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] for i = 0, expr.term_count - 1 do local st = expr.terms_[i] --[[@as kiwi.Term]] @@ -662,9 +662,9 @@ do function Expression_mt.__mul(a, b) if type(a) == "number" then - return mul_expr_constant(b, a) + return mul_expr_coeff(b, a) elseif type(b) == "number" then - return mul_expr_constant(a, b) + return mul_expr_coeff(a, b) end op_error(a, b, "*") end @@ -673,11 +673,11 @@ do if type(b) ~= "number" then op_error(a, b, "/") end - return mul_expr_constant(a, 1.0 / b) + return mul_expr_coeff(a, 1.0 / b) end function Expression_mt:__unm() - return mul_expr_constant(self, -1.0) + return mul_expr_coeff(self, -1.0) end function Expression_mt.__add(a, b) @@ -719,7 +719,7 @@ do --- Constraints can be built with arbitrary left and right hand expressions. But --- ultimately they all have the form `expression [op] 0`. ---@class kiwi.Constraint: ffi.cdata* - ---@overload fun(lhs: kiwi.Expression, rhs: kiwi.Expression?, op: kiwi.RelOp?, strength: number?): kiwi.Constraint + ---@overload fun(lhs: kiwi.Expression?, rhs: kiwi.Expression?, op: kiwi.RelOp?, strength: number?): kiwi.Constraint local Constraint_cls = { --- The strength of the constraint. ---@type fun(self: kiwi.Constraint): number diff --git a/ljkiwi-scm-1.rockspec b/ljkiwi-scm-1.rockspec index afe04d3..a3ef787 100644 --- a/ljkiwi-scm-1.rockspec +++ b/ljkiwi-scm-1.rockspec @@ -22,6 +22,7 @@ build = { type = "make", build_variables = { CFLAGS = "$(CFLAGS)", + LUA_INCDIR = "$(LUA_INCDIR)", LIBFLAG = "$(LIBFLAG)", LIB_EXT = "$(LIB_EXTENSION)", OBJ_EXT = "$(OBJ_EXTENSION)", diff --git a/spec/solver_spec.lua b/spec/solver_spec.lua index 578535a..8b68847 100644 --- a/spec/solver_spec.lua +++ b/spec/solver_spec.lua @@ -13,7 +13,7 @@ describe("solver", function() it("should create a solver", function() assert.True(kiwi.is_solver(solver)) - assert.False(kiwi.is_solver(kiwi.Term())) + assert.False(kiwi.is_solver(kiwi.Term(kiwi.Var("v1")))) end) describe("edit variables", function() @@ -104,25 +104,15 @@ describe("solver", function() end) it("tolerates a nil self", function() - local _, err = pcall(function() - return kiwi.Solver.add_edit_var(nil, v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch + assert.error(function() + kiwi.Solver.add_edit_var(nil, v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.Nil(err.solver) - assert.equal(v1, err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #0 (self)", err.message) end) it("tolerates a nil var", function() - local _, err = pcall(function() - return solver:add_edit_var(nil, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch + assert.error(function() + solver:add_edit_var(nil, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.True(kiwi.is_solver(err.solver)) - assert.Nil(err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #1", err.message) end) end) @@ -154,7 +144,7 @@ describe("solver", function() it("should require a strength argument", function() assert.error(function() solver:add_edit_vars({ v1, v2 }) ---@diagnostic disable-line: missing-parameter - end, "bad argument #3 to 'f' (cannot convert 'nil' to 'double')") + end) end) it("should error on duplicate variable", function() @@ -211,14 +201,9 @@ describe("solver", function() end) it("tolerates a nil self", function() - local _, err = pcall(function() - return kiwi.Solver.add_edit_vars(nil, { v1, v2 }, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch + assert.has_error(function() + kiwi.Solver.add_edit_vars(nil, { v1, v2 }, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.Nil(err.solver) - assert.equal(v1, err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #0 (self)", err.message) end) end) @@ -274,25 +259,15 @@ describe("solver", function() end) it("tolerates a nil self", function() - local _, err = pcall(function() - return kiwi.Solver.remove_edit_var(nil, v1) ---@diagnostic disable-line: param-type-mismatch + assert.has_error(function() + kiwi.Solver.remove_edit_var(nil, v1) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.Nil(err.solver) - assert.equal(v1, err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #0 (self)", err.message) end) it("tolerates a nil var", function() - local _, err = pcall(function() - return solver:remove_edit_var(nil) ---@diagnostic disable-line: param-type-mismatch + assert.has_error(function() + solver:remove_edit_var(nil) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.True(kiwi.is_solver(err.solver)) - assert.Nil(err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #1", err.message) end) end) @@ -351,14 +326,9 @@ describe("solver", function() end) it("tolerates a nil self", function() - local _, err = pcall(function() - return kiwi.Solver.remove_edit_vars(nil, { v1, v2 }) ---@diagnostic disable-line: param-type-mismatch + assert.has_error(function() + kiwi.Solver.remove_edit_vars(nil, { v1, v2 }) ---@diagnostic disable-line: param-type-mismatch end) - assert.True(kiwi.is_error(err)) - assert.Nil(err.solver) - assert.equal(v1, err.item) - assert.equal("KiwiErrNullObject", err.kind) - assert.equal("null object passed as argument #0 (self)", err.message) end) end) end) diff --git a/spec/var_spec.lua b/spec/var_spec.lua index 4747be4..3bd693a 100644 --- a/spec/var_spec.lua +++ b/spec/var_spec.lua @@ -10,7 +10,7 @@ describe("Var", function() assert.False(kiwi.is_var(kiwi.Constraint())) assert.error(function() - kiwi.Var(1) + kiwi.Var({}) end) end) @@ -26,7 +26,7 @@ describe("Var", function() v:set_name("Δ") assert.equal("Δ", v:name()) assert.error(function() - v:set_name(1) + v:set_name({}) end) end)