-- kiwi.lua - LuaJIT FFI bindings with C API fallback to kiwi constraint solver. local ffi do local ffi_loader = package.preload["ffi"] if ffi_loader == nil then return require("ckiwi") end ffi = ffi_loader() --[[@as ffilib]] end local kiwi = {} local ckiwi do local cpath, err = package.searchpath("ckiwi", package.cpath) if cpath == nil then error("kiwi dynamic library 'ckiwi' not found\n" .. err) end ckiwi = ffi.load(cpath) end ffi.cdef([[ enum KiwiErrKind { KiwiErrNone, KiwiErrUnsatisfiableConstraint = 1, KiwiErrUnknownConstraint, KiwiErrDuplicateConstraint, KiwiErrUnknownEditVariable, KiwiErrDuplicateEditVariable, KiwiErrBadRequiredStrength, KiwiErrInternalSolverError, KiwiErrAlloc, KiwiErrNullObject, KiwiErrUnknown, }; enum KiwiRelOp { LE, GE, EQ }; typedef struct KiwiVarRefType* KiwiVarRef; typedef struct KiwiConstraintRefType* KiwiConstraintRef; typedef struct KiwiTerm { KiwiVarRef var; double coefficient; } KiwiTerm; typedef struct KiwiExpression { double constant; int term_count; KiwiTerm terms_[?]; } KiwiExpression; typedef struct KiwiErr { enum KiwiErrKind kind; const char* message; bool must_free; } KiwiErr; typedef struct KiwiSolver { unsigned error_mask_; } KiwiSolver; KiwiVarRef kiwi_var_new(const char* name); void kiwi_var_del(KiwiVarRef var); KiwiVarRef kiwi_var_clone(KiwiVarRef var); const char* kiwi_var_name(KiwiVarRef var); void kiwi_var_set_name(KiwiVarRef var, const char* name); double kiwi_var_value(KiwiVarRef var); void kiwi_var_set_value(KiwiVarRef var, double value); bool kiwi_var_eq(KiwiVarRef var, KiwiVarRef other); void kiwi_expression_del_vars(KiwiExpression* expr); KiwiConstraintRef kiwi_constraint_new( const KiwiExpression* lhs, const KiwiExpression* rhs, enum KiwiRelOp op, double strength ); void kiwi_constraint_del(KiwiConstraintRef constraint); KiwiConstraintRef kiwi_constraint_clone(KiwiConstraintRef constraint); double kiwi_constraint_strength(KiwiConstraintRef constraint); enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint); bool kiwi_constraint_violated(KiwiConstraintRef constraint); int kiwi_constraint_expression(KiwiConstraintRef constraint, KiwiExpression* out, int out_size); KiwiSolver* kiwi_solver_new(unsigned error_mask); void kiwi_solver_del(KiwiSolver* s); const KiwiErr* kiwi_solver_add_constraint(KiwiSolver* sp, KiwiConstraintRef constraint); const KiwiErr* kiwi_solver_remove_constraint(KiwiSolver* sp, KiwiConstraintRef constraint); bool kiwi_solver_has_constraint(const KiwiSolver* sp, KiwiConstraintRef constraint); const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* sp, KiwiVarRef var, double strength); const KiwiErr* kiwi_solver_remove_edit_var(KiwiSolver* sp, KiwiVarRef var); bool kiwi_solver_has_edit_var(const KiwiSolver* sp, KiwiVarRef var); const KiwiErr* kiwi_solver_suggest_value(KiwiSolver* sp, KiwiVarRef var, double value); void kiwi_solver_update_vars(KiwiSolver* sp); void kiwi_solver_reset(KiwiSolver* sp); void kiwi_solver_dump(const KiwiSolver* sp); char* kiwi_solver_dumps(const KiwiSolver* sp); void free(void *); ]]) local strformat = string.format local ffi_copy, ffi_gc, ffi_istype, ffi_new, ffi_string = ffi.copy, ffi.gc, ffi.istype, ffi.new, ffi.string local concat = table.concat local has_table_new, new_tab = pcall(require, "table.new") if not has_table_new or type(new_tab) ~= "function" then new_tab = function(_, _) return {} end end ---@alias kiwi.ErrKind ---| '"KiwiErrNone"' # No error. ---| '"KiwiErrUnsatisfiableConstraint"' # The given constraint is required and cannot be satisfied. ---| '"KiwiErrUnknownConstraint"' # The given constraint has not been added to the solver. ---| '"KiwiErrDuplicateConstraint"' # The given constraint has already been added to the solver. ---| '"KiwiErrUnknownEditVariable"' # The given edit variable has not been added to the solver. ---| '"KiwiErrDuplicateEditVariable"' # The given edit variable has already been added to the solver. ---| '"KiwiErrBadRequiredStrength"' # The given strength is >= required. ---| '"KiwiErrInternalSolverError"' # An internal solver error occurred. ---| '"KiwiErrAlloc"' # A memory allocation error occurred. ---| '"KiwiErrNullObject"' # A method was invoked on a null or empty object. ---| '"KiwiErrUnknown"' # An unknown error occurred. kiwi.ErrKind = ffi.typeof("enum KiwiErrKind") --[[@as kiwi.ErrKind]] ---@alias kiwi.RelOp ---| '"LE"' # <= (less than or equal) ---| '"GE"' # >= (greater than or equal) ---| '"EQ"' # == (equal) kiwi.RelOp = ffi.typeof("enum KiwiRelOp") kiwi.strength = { REQUIRED = 1001001000.0, STRONG = 1000000.0, MEDIUM = 1000.0, WEAK = 1.0, } do local function clamp(n) return math.max(0, math.min(1000, n)) end --- Create a custom constraint strength. ---@param a number: Scale factor 1e6 ---@param b number: Scale factor 1e3 ---@param c number: Scale factor 1 ---@param w? number: Weight ---@return number ---@nodiscard function kiwi.strength.create(a, b, c, w) w = w or 1.0 return clamp(a * w) * 1000000.0 + clamp(b * w) * 1000.0 + clamp(c * w) end end local Var = ffi.typeof("struct KiwiVarRefType") --[[@as kiwi.Var]] kiwi.Var = Var function kiwi.is_var(o) return ffi_istype(Var, o) end local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]] kiwi.Term = Term function kiwi.is_term(o) return ffi_istype(Term, o) end local Expression = ffi.typeof("struct KiwiExpression") --[[@as kiwi.Expression]] kiwi.Expression = Expression function kiwi.is_expression(o) return ffi_istype(Expression, o) end local Constraint = ffi.typeof("struct KiwiConstraintRefType") --[[@as kiwi.Constraint]] kiwi.Constraint = Constraint function kiwi.is_constraint(o) return ffi_istype(Constraint, o) end ---@param expr kiwi.Expression ---@param var kiwi.Var ---@param coeff number? ---@nodiscard local function add_expr_term(expr, var, coeff) local ret = ffi_gc(ffi_new(Expression, expr.term_count + 1), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] for i = 0, expr.term_count - 1 do local st = expr.terms_[i] --[[@as kiwi.Term]] local dt = ret.terms_[i] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(st.var) dt.coefficient = st.coefficient end local dt = ret.terms_[expr.term_count] dt.var = ckiwi.kiwi_var_clone(var) dt.coefficient = coeff or 1.0 ret.constant = expr.constant ret.term_count = expr.term_count + 1 return ret end ---@param constant number ---@param var kiwi.Var ---@param coeff number? ---@nodiscard local function new_expr_one(constant, var, coeff) local ret = ffi_gc(ffi_new(Expression, 1), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] local dt = ret.terms_[0] dt.var = ckiwi.kiwi_var_clone(var) dt.coefficient = coeff or 1.0 ret.constant = constant ret.term_count = 1 return ret end ---@param constant number ---@param var1 kiwi.Var ---@param var2 kiwi.Var ---@param coeff1 number? ---@param coeff2 number? ---@nodiscard local function new_expr_pair(constant, var1, var2, coeff1, coeff2) local ret = ffi_gc(ffi_new(Expression, 2), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] local dt = ret.terms_[0] dt.var = ckiwi.kiwi_var_clone(var1) dt.coefficient = coeff1 or 1.0 dt = ret.terms_[1] dt.var = ckiwi.kiwi_var_clone(var2) dt.coefficient = coeff2 or 1.0 ret.constant = constant ret.term_count = 2 return ret end local function typename(o) if ffi.istype(Var, o) then return "Var" elseif ffi.istype(Term, o) then return "Term" elseif ffi.istype(Expression, o) then return "Expression" elseif ffi.istype(Constraint, o) then return "Constraint" else return type(o) end end local function op_error(a, b, op) --stylua: ignore -- level 3 works for arithmetic without TCO (no return), and for rel with TCO forced (explicit return) error(strformat( "invalid operand type for '%s' %.40s('%.99s') and %.40s('%.99s')", op, typename(a), tostring(a), typename(b), tostring(b)), 3) end local Strength = kiwi.strength local REQUIRED = Strength.REQUIRED local OP_NAMES = { LE = "<=", GE = ">=", EQ = "==", } local SIZEOF_TERM = ffi.sizeof(Term) --[[@as integer]] local tmpexpr = ffi_new(Expression, 2) --[[@as kiwi.Expression]] local tmpexpr_r = ffi_new(Expression, 1) --[[@as kiwi.Expression]] local function toexpr(o, temp) if ffi_istype(Expression, o) then return o --[[@as kiwi.Expression]] elseif type(o) == "number" then temp.constant = o temp.term_count = 0 return temp end temp.constant = 0 temp.term_count = 1 local t = temp.terms_[0] if ffi_istype(Var, o) then t.var = o --[[@as kiwi.Var]] t.coefficient = 1.0 elseif ffi_istype(Term, o) then ffi_copy(t, o, SIZEOF_TERM) else return nil end return temp end ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param op kiwi.RelOp ---@param strength? number ---@nodiscard local function rel(lhs, rhs, op, strength) local el = toexpr(lhs, tmpexpr) local er = toexpr(rhs, tmpexpr_r) if el == nil or er == nil then op_error(lhs, rhs, OP_NAMES[op]) end return ffi_gc(ckiwi.kiwi_constraint_new(el, er, op, strength or REQUIRED), ckiwi.kiwi_constraint_del) --[[@as kiwi.Constraint]] end --- Define a constraint with expressions as `a <= b`. ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@nodiscard function kiwi.le(lhs, rhs, strength) return rel(lhs, rhs, "LE", strength) end --- Define a constraint with expressions as `a >= b`. ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@nodiscard function kiwi.ge(lhs, rhs, strength) return rel(lhs, rhs, "GE", strength) end --- Define a constraint with expressions as `a == b`. ---@param lhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param rhs kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@nodiscard function kiwi.eq(lhs, rhs, strength) return rel(lhs, rhs, "EQ", strength) end do --- Variables are the values the constraint solver calculates. ---@class kiwi.Var: ffi.cdata* ---@overload fun(name: string?): kiwi.Var ---@operator mul(number): kiwi.Term ---@operator div(number): kiwi.Term ---@operator unm: kiwi.Term ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression local Var_cls = { le = kiwi.le, ge = kiwi.ge, eq = kiwi.eq, --- Change the name of the variable. ---@type fun(self: kiwi.Var, name: string) set_name = ckiwi.kiwi_var_set_name, --- Get the current value of the variable. ---@type fun(self: kiwi.Var): number value = ckiwi.kiwi_var_value, --- Set the value of the variable. ---@type fun(self: kiwi.Var, value: number) set = ckiwi.kiwi_var_set_value, } --- Get the name of the variable. ---@return string ---@nodiscard function Var_cls:name() return ffi_string(ckiwi.kiwi_var_name(self)) end --- Create a term from this variable. ---@param coefficient number? ---@return kiwi.Term ---@nodiscard function Var_cls:toterm(coefficient) return Term(self, coefficient) end --- Create a term from this variable. ---@param coefficient number? ---@param constant number? ---@return kiwi.Expression ---@nodiscard function Var_cls:toexpr(coefficient, constant) return new_expr_one(constant or 0.0, self, coefficient) end local Var_mt = { __index = Var_cls, } function Var_mt:__new(name) return ffi_gc(ckiwi.kiwi_var_new(name), ckiwi.kiwi_var_del) end function Var_mt.__mul(a, b) if type(a) == "number" then return Term(b, a) elseif type(b) == "number" then return Term(a, b) end op_error(a, b, "*") end function Var_mt.__div(a, b) if type(b) ~= "number" then op_error(a, b, "/") end return Term(a, 1.0 / b) end function Var_mt:__unm() return Term(self, -1.0) end function Var_mt.__add(a, b) if ffi_istype(Var, b) then if type(a) == "number" then return new_expr_one(a, b) else return new_expr_pair(0.0, a, b) end elseif ffi_istype(Term, b) then return new_expr_pair(0.0, a, b.var, 1.0, b.coefficient) elseif ffi_istype(Expression, b) then return add_expr_term(b, a) elseif type(b) == "number" then return new_expr_one(b, a) end op_error(a, b, "+") end function Var_mt.__sub(a, b) return a + -b end function Var_mt:__tostring() return self:name() .. "(" .. self:value() .. ")" end ffi.metatype(Var, Var_mt) end do --- Terms are the components of an expression. --- Each term is a variable multiplied by a constant coefficient (default 1.0). ---@class kiwi.Term: ffi.cdata* ---@overload fun(var: kiwi.Var, coefficient: number?): kiwi.Term ---@field var kiwi.Var ---@field coefficient number ---@operator mul(number): kiwi.Term ---@operator div(number): kiwi.Term ---@operator unm: kiwi.Term ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression local Term_cls = { le = kiwi.le, ge = kiwi.ge, eq = kiwi.eq, } ---@return number ---@nodiscard function Term_cls:value() return self.coefficient * self.var:value() end --- Create an expression from this term. ---@param constant number? ---@return kiwi.Expression function Term_cls:toexpr(constant) return new_expr_one(constant or 0.0, self.var, self.coefficient) end local Term_mt = { __index = Term_cls } local function term_gc(term) ckiwi.kiwi_var_del(term.var) end function Term_mt.__new(T, var, coefficient) return ffi_gc(ffi_new(T, ckiwi.kiwi_var_clone(var), coefficient or 1.0), term_gc) end function Term_mt.__mul(a, b) if type(b) == "number" then return Term(a.var, a.coefficient * b) elseif type(a) == "number" then return Term(b.var, b.coefficient * a) end op_error(a, b, "*") end function Term_mt.__div(a, b) if type(b) ~= "number" then op_error(a, b, "/") end return Term(a.var, a.coefficient / b) end function Term_mt:__unm() return Term(self.var, -self.coefficient) end function Term_mt.__add(a, b) if ffi_istype(Var, b) then return new_expr_pair(0.0, a.var, b, a.coefficient) elseif ffi_istype(Term, b) then if type(a) == "number" then return new_expr_one(a, b.var, b.coefficient) else return new_expr_pair(0.0, a.var, b.var, a.coefficient, b.coefficient) end elseif ffi_istype(Expression, b) then return add_expr_term(b, a.var, a.coefficient) elseif type(b) == "number" then return new_expr_one(b, a.var, a.coefficient) end op_error(a, b, "+") end function Term_mt.__sub(a, b) return Term_mt.__add(a, -b) end function Term_mt:__tostring() return tostring(self.coefficient) .. " " .. self.var:name() end ffi.metatype(Term, Term_mt) end do --- Expressions are a sum of terms with an added constant. ---@class kiwi.Expression: ffi.cdata* ---@overload fun(constant: number, ...: kiwi.Term): kiwi.Expression ---@field constant number ---@field package term_count number ---@field package terms_ ffi.cdata* ---@operator mul(number): kiwi.Expression ---@operator div(number): kiwi.Expression ---@operator unm: kiwi.Expression ---@operator add(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression ---@operator sub(kiwi.Expression|kiwi.Term|kiwi.Var|number): kiwi.Expression local Expression_cls = { le = kiwi.le, ge = kiwi.ge, eq = kiwi.eq, } ---@param expr kiwi.Expression ---@param constant number ---@nodiscard 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]] local dt = ret.terms_[i] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(st.var) dt.coefficient = st.coefficient * constant end ret.constant = expr.constant * constant ret.term_count = expr.term_count return ret end ---@param a kiwi.Expression ---@param b kiwi.Expression ---@nodiscard local function add_expr_expr(a, b) local a_count = a.term_count local b_count = b.term_count local ret = ffi_gc(ffi_new(Expression, a_count + b_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] for i = 0, a_count - 1 do local dt = ret.terms_[i] --[[@as kiwi.Term]] local st = a.terms_[i] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(st.var) dt.coefficient = st.coefficient end for i = 0, b_count - 1 do local dt = ret.terms_[a_count + i] --[[@as kiwi.Term]] local st = b.terms_[i] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(st.var) dt.coefficient = st.coefficient end ret.constant = a.constant + b.constant ret.term_count = a_count + b_count return ret end ---@param expr kiwi.Expression ---@param constant number ---@nodiscard local function new_expr_constant(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 dt = ret.terms_[i] --[[@as kiwi.Term]] local st = expr.terms_[i] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(st.var) dt.coefficient = st.coefficient end ret.constant = constant ret.term_count = expr.term_count return ret end ---@return number ---@nodiscard function Expression_cls:value() local sum = self.constant for i = 0, self.term_count - 1 do local t = self.terms_[i] sum = sum + t.var:value() * t.coefficient end return sum end ---@return kiwi.Term[] ---@nodiscard function Expression_cls:terms() local terms = new_tab(self.term_count, 0) for i = 0, self.term_count - 1 do local t = self.terms_[i] --[[@as kiwi.Term]] terms[i + 1] = Term(t.var, t.coefficient) end return terms end ---@return kiwi.Expression ---@nodiscard function Expression_cls:copy() return new_expr_constant(self, self.constant) end local Expression_mt = { __index = Expression_cls, } function Expression_mt.__new(T, constant, ...) local term_count = select("#", ...) local e = ffi_gc(ffi_new(T, term_count), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] e.term_count = term_count e.constant = constant for i = 1, term_count do local t = select(i, ...) local dt = e.terms_[i - 1] --[[@as kiwi.Term]] dt.var = ckiwi.kiwi_var_clone(t.var) dt.coefficient = t.coefficient end return e end function Expression_mt.__mul(a, b) if type(a) == "number" then return mul_expr_coeff(b, a) elseif type(b) == "number" then return mul_expr_coeff(a, b) end op_error(a, b, "*") end function Expression_mt.__div(a, b) if type(b) ~= "number" then op_error(a, b, "/") end return mul_expr_coeff(a, 1.0 / b) end function Expression_mt:__unm() return mul_expr_coeff(self, -1.0) end function Expression_mt.__add(a, b) if ffi_istype(Var, b) then return add_expr_term(a, b) elseif ffi_istype(Expression, b) then if type(a) == "number" then return new_expr_constant(b, a + b.constant) else return add_expr_expr(a, b) end elseif ffi_istype(Term, b) then return add_expr_term(a, b.var, b.coefficient) elseif type(b) == "number" then return new_expr_constant(a, a.constant + b) end op_error(a, b, "+") end function Expression_mt.__sub(a, b) return Expression_mt.__add(a, -b) end function Expression_mt:__tostring() local tab = new_tab(self.term_count + 1, 0) for i = 0, self.term_count - 1 do local t = self.terms_[i] tab[i + 1] = tostring(t.coefficient) .. " " .. t.var:name() end tab[self.term_count + 1] = self.constant return concat(tab, " + ") end ffi.metatype(Expression, Expression_mt) end do --- A constraint is a linear inequality or equality with associated strength. --- 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 local Constraint_cls = { --- The strength of the constraint. ---@type fun(self: kiwi.Constraint): number strength = ckiwi.kiwi_constraint_strength, --- The relational operator of the constraint. ---@type fun(self: kiwi.Constraint): kiwi.RelOp op = ckiwi.kiwi_constraint_op, --- Whether the constraint is violated in the current solution. ---@type fun(self: kiwi.Constraint): boolean violated = ckiwi.kiwi_constraint_violated, } --- The reduced expression defining the constraint. ---@return kiwi.Expression ---@nodiscard function Constraint_cls:expression() local SZ = 7 -- 2**7 bytes on x64 local expr = ffi_new(Expression, SZ) --[[@as kiwi.Expression]] local n = ckiwi.kiwi_constraint_expression(self, expr, SZ) if n > SZ then expr = ffi_new(Expression, n) --[[@as kiwi.Expression]] n = ckiwi.kiwi_constraint_expression(self, expr, n) end return ffi_gc(expr, ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] end --- Add the constraint to the solver. --- Raises: --- KiwiErrDuplicateConstraint: The given constraint has already been added to the solver. --- KiwiErrUnsatisfiableConstraint: The given constraint is required and cannot be satisfied. ---@param solver kiwi.Solver ---@return kiwi.Constraint function Constraint_cls:add_to(solver) solver:add_constraint(self) return self end --- Remove the constraint from the solver. --- Raises: --- KiwiErrUnknownConstraint: The given constraint has not been added to the solver. ---@param solver kiwi.Solver ---@return kiwi.Constraint function Constraint_cls:remove_from(solver) solver:remove_constraint(self) return self end local Constraint_mt = { __index = Constraint_cls, } function Constraint_mt:__new(lhs, rhs, op, strength) return ffi_gc( ckiwi.kiwi_constraint_new(lhs, rhs, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) end local OPS = { [0] = "<=", ">=", "==" } local STRENGTH_NAMES = { [Strength.REQUIRED] = "required", [Strength.STRONG] = "strong", [Strength.MEDIUM] = "medium", [Strength.WEAK] = "weak", } function Constraint_mt:__tostring() local strength = self:strength() local strength_str = STRENGTH_NAMES[strength] or tostring(strength) local op = OPS[tonumber(self:op())] return strformat("%s %s 0 | %s", tostring(self:expression()), op, strength_str) end ffi.metatype(Constraint, Constraint_mt) end do local constraints = {} kiwi.constraints = constraints --- Create a constraint between a pair of variables with ratio. --- The constraint is of the form `left [op|==] coeff right + [constant|0.0]`. ---@param left kiwi.Var ---@param coeff number right side term coefficient ---@param right kiwi.Var ---@param constant number? constant (default 0.0) ---@param op kiwi.RelOp? relational operator (default "EQ") ---@param strength number? strength (default REQUIRED) ---@return kiwi.Constraint ---@nodiscard function constraints.pair_ratio(left, coeff, right, constant, op, strength) assert(ffi_istype(Var, left) and ffi_istype(Var, right)) local dt = tmpexpr.terms_[0] dt.var = left dt.coefficient = 1.0 dt = tmpexpr.terms_[1] dt.var = right dt.coefficient = -coeff tmpexpr.constant = constant ~= nil and constant or 0 tmpexpr.term_count = 2 return ffi_gc( ckiwi.kiwi_constraint_new(tmpexpr, nil, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) --[[@as kiwi.Constraint]] end local pair_ratio = constraints.pair_ratio --- Create a constraint between a pair of variables with ratio. --- The constraint is of the form `left [op|==] right + [constant|0.0]`. ---@param left kiwi.Var ---@param right kiwi.Var ---@param constant number? constant (default 0.0) ---@param op kiwi.RelOp? relational operator (default "EQ") ---@param strength number? strength (default REQUIRED) ---@return kiwi.Constraint ---@nodiscard function constraints.pair(left, right, constant, op, strength) return pair_ratio(left, 1.0, right, constant, op, strength) end --- Create a single term constraint --- The constraint is of the form `var [op|==] [constant|0.0]`. ---@param var kiwi.Var ---@param constant number? constant (default 0.0) ---@param op kiwi.RelOp? relational operator (default "EQ") ---@param strength number? strength (default REQUIRED) ---@return kiwi.Constraint ---@nodiscard function constraints.single(var, constant, op, strength) assert(ffi_istype(Var, var)) tmpexpr.constant = -(constant or 0) tmpexpr.term_count = 1 local t = tmpexpr.terms_[0] t.var = var t.coefficient = 1.0 return ffi_gc( ckiwi.kiwi_constraint_new(tmpexpr, nil, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) --[[@as kiwi.Constraint]] end end do local bit = require("bit") local band, bor, lshift = bit.band, bit.bor, bit.lshift local C = ffi.C --- Produce a custom error raise mask --- Error kinds specified in the mask will not cause a lua --- error to be raised. ---@param kinds (kiwi.ErrKind|integer)[] ---@param invert boolean? ---@return integer function kiwi.error_mask(kinds, invert) local mask = 0 for _, k in ipairs(kinds) do mask = bor(mask, lshift(1, kiwi.ErrKind(k))) end return invert and bit.bnot(mask) or mask end kiwi.ERROR_MASK_ALL = 0xFFFF --- an error mask that raises errors only for fatal conditions kiwi.ERROR_MASK_NON_FATAL = bit.bnot(kiwi.error_mask({ "KiwiErrInternalSolverError", "KiwiErrAlloc", "KiwiErrNullObject", "KiwiErrUnknown", })) ---@class kiwi.KiwiErr: ffi.cdata* ---@field package kind kiwi.ErrKind ---@field package message ffi.cdata* ---@field package must_free boolean ---@overload fun(): kiwi.KiwiErr local KiwiErr = ffi.typeof("struct KiwiErr") --[[@as kiwi.KiwiErr]] local Error_mt = { ---@param self kiwi.Error ---@return string __tostring = function(self) return strformat("%s: (%s, %s)", self.message, tostring(self.solver), tostring(self.item)) end, } ---@class kiwi.Error ---@field kind kiwi.ErrKind ---@field message string ---@field solver kiwi.Solver? ---@field item any? kiwi.Error = Error_mt function kiwi.is_error(o) return type(o) == "table" and getmetatable(o) == Error_mt end ---@param kind kiwi.ErrKind ---@param message string ---@param solver kiwi.Solver ---@param item any ---@return kiwi.Error local function new_error(kind, message, solver, item) return setmetatable({ kind = kind, message = message, solver = solver, item = item, }, Error_mt) end ---@generic T ---@param f fun(solver: kiwi.Solver, item: T, ...): kiwi.KiwiErr? ---@param solver kiwi.Solver ---@param item T ---@return T, kiwi.Error? local function try_solver(f, solver, item, ...) local err = f(solver, item, ...) if err ~= nil then local kind = err.kind local message = err.message ~= nil and ffi_string(err.message) or "" if err.must_free then C.free(err) end local errdata = new_error(kind, message, solver, item) local error_mask = solver and solver.error_mask_ or 0 return item, band(error_mask, lshift(1, kind --[[@as integer]])) == 0 and error(errdata) or errdata end return item end ---@class kiwi.Solver: ffi.cdata* ---@field package error_mask_ integer ---@overload fun(error_mask: (integer|(kiwi.ErrKind|integer)[] )?): kiwi.Solver local Solver_cls = { --- Test whether a constraint is in the solver. ---@type fun(self: kiwi.Solver, constraint: kiwi.Constraint): boolean has_constraint = ckiwi.kiwi_solver_has_constraint, --- Test whether an edit variable has been added to the solver. ---@type fun(self: kiwi.Solver, var: kiwi.Var): boolean has_edit_var = ckiwi.kiwi_solver_has_edit_var, --- Update the values of the external solver variables. ---@type fun(self: kiwi.Solver) update_vars = ckiwi.kiwi_solver_update_vars, --- Reset the solver to the empty starting conditions. --- --- This method resets the internal solver state to the empty starting --- condition, as if no constraints or edit variables have been added. --- This can be faster than deleting the solver and creating a new one --- when the entire system must change, since it can avoid unecessary --- heap (de)allocations. ---@type fun(self: kiwi.Solver) reset = ckiwi.kiwi_solver_reset, --- Dump a representation of the solver to stdout. ---@type fun(self: kiwi.Solver) dump = ckiwi.kiwi_solver_dump, } --- Sets the error mask for the solver. ---@param mask integer|(kiwi.ErrKind|integer)[] the mask value or an array of kinds ---@param invert boolean? whether to invert the mask if an array was passed for mask function Solver_cls:set_error_mask(mask, invert) if type(mask) == "table" then mask = kiwi.error_mask(mask, invert) end self.error_mask_ = mask end ---@generic T ---@param solver kiwi.Solver ---@param items T|T[] ---@param f fun(solver: kiwi.Solver, item: T, ...): kiwi.KiwiErr? ---@return T|T[], kiwi.Error? local function add_remove_items(solver, items, f, ...) for _, item in ipairs(items) do local _, err = try_solver(f, solver, item, ...) if err ~= nil then return items, err end end return items end --- Add a constraint to the solver. --- Errors: --- KiwiErrDuplicateConstraint --- KiwiErrUnsatisfiableConstraint ---@param constraint kiwi.Constraint ---@return kiwi.Constraint constraint, kiwi.Error? function Solver_cls:add_constraint(constraint) return try_solver(ckiwi.kiwi_solver_add_constraint, self, constraint) end --- Add constraints to the solver. --- Errors: --- KiwiErrDuplicateConstraint --- KiwiErrUnsatisfiableConstraint ---@param constraints kiwi.Constraint[] ---@return kiwi.Constraint[] constraints, kiwi.Error? function Solver_cls:add_constraints(constraints) return add_remove_items(self, constraints, ckiwi.kiwi_solver_add_constraint) end --- Remove a constraint from the solver. --- Errors: --- KiwiErrUnknownConstraint ---@param constraint kiwi.Constraint ---@return kiwi.Constraint constraint, kiwi.Error? function Solver_cls:remove_constraint(constraint) return try_solver(ckiwi.kiwi_solver_remove_constraint, self, constraint) end --- Remove constraints from the solver. --- Errors: --- KiwiErrUnknownConstraint ---@param constraints kiwi.Constraint[] ---@return kiwi.Constraint[] constraints, kiwi.Error? function Solver_cls:remove_constraints(constraints) return add_remove_items(self, constraints, ckiwi.kiwi_solver_remove_constraint) end --- Add an edit variables to the solver. --- --- This method should be called before the `suggestValue` method is --- used to supply a suggested value for the given edit variable. --- Errors: --- KiwiErrDuplicateEditVariable --- KiwiErrBadRequiredStrength: The given strength is >= required. ---@param var kiwi.Var the variable to add as an edit variable ---@param strength number the strength of the edit variable (must be less than `Strength.REQUIRED`) ---@return kiwi.Var var, kiwi.Error? function Solver_cls:add_edit_var(var, strength) return try_solver(ckiwi.kiwi_solver_add_edit_var, self, var, strength) end --- Add edit variables to the solver. --- --- This method should be called before the `suggestValue` method is --- used to supply a suggested value for the given edit variable. --- Errors: --- KiwiErrDuplicateEditVariable --- KiwiErrBadRequiredStrength: The given strength is >= required. ---@param vars kiwi.Var[] the variables to add as an edit variable ---@param strength number the strength of the edit variables (must be less than `Strength.REQUIRED`) ---@return kiwi.Var[] vars, kiwi.Error? function Solver_cls:add_edit_vars(vars, strength) return add_remove_items(self, vars, ckiwi.kiwi_solver_add_edit_var, strength) end --- Remove an edit variable from the solver. --- Raises: --- KiwiErrUnknownEditVariable ---@param var kiwi.Var the edit variable to remove ---@return kiwi.Var var, kiwi.Error? function Solver_cls:remove_edit_var(var) return try_solver(ckiwi.kiwi_solver_remove_edit_var, self, var) end --- Removes edit variables from the solver. --- Raises: --- KiwiErrUnknownEditVariable ---@param vars kiwi.Var[] the edit variables to remove ---@return kiwi.Var[] vars, kiwi.Error? function Solver_cls:remove_edit_vars(vars) return add_remove_items(self, vars, ckiwi.kiwi_solver_remove_edit_var) end --- Suggest a value for the given edit variable. --- This method should be used after an edit variable has been added to the solver in order --- to suggest the value for that variable. After all suggestions have been made, --- the `update_vars` methods can be used to update the values of the external solver variables. --- Raises: --- KiwiErrUnknownEditVariable ---@param var kiwi.Var the edit variable to suggest a value for ---@param value number the suggested value ---@return kiwi.Var var, kiwi.Error? function Solver_cls:suggest_value(var, value) return try_solver(ckiwi.kiwi_solver_suggest_value, self, var, value) end --- Suggest values for the given edit variables. --- Convenience wrapper of `suggest_value` that takes tables of `kiwi.Var` and number pairs. --- Raises: --- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver. ---@param vars kiwi.Var[] edit variables to suggest ---@param values number[] suggested values ---@return kiwi.Var[] vars, number[] values, kiwi.Error? function Solver_cls:suggest_values(vars, values) for i, var in ipairs(vars) do local _, err = try_solver(ckiwi.kiwi_solver_suggest_value, self, var, values[i]) if err ~= nil then return vars, values, err end end return vars, values end --- Dump a representation of the solver to a string. ---@return string ---@nodiscard function Solver_cls:dumps() local cs = ckiwi.kiwi_solver_dumps(self) local s = ffi_string(cs) C.free(cs) return s end local Solver_mt = { __index = Solver_cls, } function Solver_mt:__new(error_mask) if type(error_mask) == "table" then error_mask = kiwi.error_mask(error_mask) end return ffi_gc(ckiwi.kiwi_solver_new(error_mask or 0), ckiwi.kiwi_solver_del) end kiwi.Solver = ffi.metatype("struct KiwiSolver", Solver_mt) --[[@as kiwi.Solver]] function kiwi.is_solver(s) return ffi_istype(kiwi.Solver, s) end end return kiwi