local kiwi = {} local ffi = require("ffi") 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, KiwiErrUnknown, }; enum KiwiRelOp { LE, GE, EQ }; typedef struct { void* private_; } KiwiVarRef; typedef struct { KiwiVarRef var; double coefficient; } KiwiTerm; typedef struct { double constant; int term_count; KiwiTerm terms_[?]; } KiwiExpression; typedef struct { void* private_; } KiwiConstraintRef; typedef struct { enum KiwiErrKind kind; char message[64]; } KiwiErr; typedef struct { void* impl_; } KiwiSolverRef; KiwiVarRef kiwi_var_new(const char* name); void kiwi_var_del(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); int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other); KiwiConstraintRef kiwi_constraint_new(const KiwiExpression* expression, enum KiwiRelOp op, double strength); void kiwi_constraint_del(KiwiConstraintRef constraint); double kiwi_constraint_strength(KiwiConstraintRef constraint); enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint); int kiwi_constraint_violated(KiwiConstraintRef constraint); int kiwi_constraint_expression( KiwiConstraintRef constraint, KiwiExpression* out, int out_size ); KiwiErr kiwi_solver_add_constraint(KiwiSolverRef s, KiwiConstraintRef constraint); KiwiErr kiwi_solver_remove_constraint(KiwiSolverRef s, KiwiConstraintRef constraint); int kiwi_solver_has_constraint(KiwiSolverRef s, KiwiConstraintRef constraint); KiwiErr kiwi_solver_add_edit_var(KiwiSolverRef s, KiwiVarRef var, double strength); KiwiErr kiwi_solver_remove_edit_var(KiwiSolverRef s, KiwiVarRef var); int kiwi_solver_has_edit_var(KiwiSolverRef s, KiwiVarRef var); KiwiErr kiwi_solver_suggest_value(KiwiSolverRef s, KiwiVarRef var, double value); void kiwi_solver_update_vars(KiwiSolverRef s); void kiwi_solver_reset(KiwiSolverRef s); void kiwi_solver_dump(KiwiSolverRef s); char* kiwi_solver_dumps(KiwiSolverRef s, void* (*alloc)(size_t)); KiwiSolverRef kiwi_solver_new(); void kiwi_solver_del(KiwiSolverRef s); void free(void *); ]]) local strformat = string.format local ffi_cast, ffi_copy, ffi_istype, ffi_new, ffi_sizeof, ffi_string = ffi.cast, ffi.copy, ffi.istype, ffi.new, ffi.sizeof, 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. ---| '"KiwiErrUnknown"' # An unknown error occurred. kiwi.ErrKind = ffi.typeof("enum KiwiErrKind") --[[@as kiwi.ErrKind]] ---@class kiwi.KiwiErr: ffi.ctype* ---@field package kind kiwi.ErrKind ---@field package message ffi.cdata* ---@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, } --- 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) local function clamp(n) return math.max(0, math.min(1000, n)) end w = w or 1.0 return clamp(a * w) * 1000000.0 + clamp(b * w) * 1000.0 + clamp(c * w) end local Var = ffi.typeof("KiwiVarRef") --[[@as kiwi.Var]] kiwi.Var = Var local Term = ffi.typeof("KiwiTerm") --[[@as kiwi.Term]] kiwi.Term = Term local Expression = ffi.typeof("KiwiExpression") --[[@as kiwi.Expression]] kiwi.Expression = Expression local Constraint = ffi.typeof("KiwiConstraintRef") --[[@as kiwi.Constraint]] kiwi.Constraint = Constraint --- Define a constraint with expressions as `a <= b`. ---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@return kiwi.Constraint function kiwi.le(a, b, strength) return Constraint(a - b, "LE", strength) end --- Define a constraint with expressions as `a >= b`. ---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@return kiwi.Constraint function kiwi.ge(a, b, strength) return Constraint(a - b, "GE", strength) end --- Define a constraint with expressions as `a == b`. ---@param a kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param b kiwi.Expression|kiwi.Term|kiwi.Var|number ---@param strength? number ---@return kiwi.Constraint function kiwi.eq(a, b, strength) return Constraint(a - b, "EQ", strength) end ---@param expr kiwi.Expression ---@param term kiwi.Term ---@nodiscard local function add_expr_term(expr, term) local ret = ffi_new(Expression, expr.term_count + 1, expr.constant, expr.term_count + 1) --[[@as kiwi.Expression]] ffi_copy(ret.terms_, expr.terms_, ffi_sizeof(expr.terms_, expr.term_count)) ---@diagnostic disable-line: param-type-mismatch ret.terms_[expr.term_count] = term return ret end ---@param constant number ---@param term kiwi.Term ---@nodiscard local function new_expr_one(constant, term) local ret = ffi_new(Expression, 1, constant, 1) --[[@as kiwi.Expression]] ret.terms_[0] = term return ret end ---@param constant number ---@param term1 kiwi.Term ---@param term2 kiwi.Term ---@nodiscard local function new_expr_pair(constant, term1, term2) local ret = ffi_new(Expression, 2, constant, 2) --[[@as kiwi.Expression]] ret.terms_[0] = term1 ret.terms_[1] = term2 return ret end --- Variables are the values the constraint solver calculates. ---@class kiwi.Var: ffi.ctype* ---@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 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 function Var_cls:toterm(coefficient) return Term(self, coefficient or 1.0) end ffi.metatype(Var, { __index = Var_cls, __gc = ckiwi.kiwi_var_del, __new = function(_, name) return ckiwi.kiwi_var_new(name) end, __mul = function(a, b) if type(a) == "number" then return Term(b, a) elseif type(b) == "number" then return Term(a, b) end error("Invalid var *") end, __div = function(a, b) assert(type(b) == "number", "Invalid var /") return Term(a, 1.0 / b) end, __unm = function(var) return Term(var, -1.0) end, __add = function(a, b) if ffi_istype(Var, b) then local bt = Term(b) if type(a) == "number" then return new_expr_one(a, bt) else return new_expr_pair(0.0, Term(a), bt) end elseif ffi_istype(Term, b) then return new_expr_pair(0.0, b, Term(a)) elseif ffi_istype(Expression, b) then return add_expr_term(b, Term(a)) elseif type(b) == "number" then return new_expr_one(b, Term(a)) end error("Invalid var +") end, __sub = function(a, b) return a + -b end, __tostring = function(var) return var:name() .. "(" .. var:value() .. ")" end, }) --- Terms are the components of an expression. --- Each term is a variable multiplied by a constant coefficient (default 1.0). ---@class kiwi.Term: ffi.ctype* ---@overload fun(var: kiwi.Var, coefficient: number?): kiwi.Term ---@field coefficient number ---@field var 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 Term_cls = { le = kiwi.le, ge = kiwi.ge, eq = kiwi.eq, } ---@return number 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) end ffi.metatype(Term, { __index = Term_cls, __new = function(_, var, coefficient) return ffi_new(Term, var, coefficient or 1.0) end, __mul = function(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 error("Invalid term *") end, __div = function(term, denom) assert(type(denom) == "number", "Invalid term /") return Term(term.var, term.coefficient / denom) end, __unm = function(term) return Term(term.var, -term.coefficient) end, __add = function(a, b) if ffi_istype(Var, b) then return new_expr_pair(0.0, a, Term(b)) elseif ffi_istype(Term, 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(Expression, b) then return add_expr_term(b, a) elseif type(b) == "number" then return new_expr_one(b, a) end error("Invalid term + op") end, __sub = function(a, b) return a + -b end, __tostring = function(term) return tostring(term.coefficient) .. " " .. term.var:name() end, }) do ---@param expr kiwi.Expression ---@param constant number ---@nodiscard local function mul_expr_constant(expr, constant) local ret = ffi_new(Expression, expr.term_count, expr.constant * constant, expr.term_count) --[[@as kiwi.Expression]] for i = 0, expr.term_count - 1 do ret.terms_[i] = ffi_new(Term, expr.terms_[i].var, expr.terms_[i].coefficient * constant) --[[@as kiwi.Term]] end return ret end ---@param a kiwi.Expression ---@param b kiwi.Expression ---@nodiscard local function add_expr_expr(a, b) local ret = ffi_new( Expression, a.term_count + b.term_count, a.constant + b.constant, a.term_count + b.term_count ) --[[@as kiwi.Expression]] ffi_copy(ret.terms_, a.terms_, ffi_sizeof(a.terms_, a.term_count)) ---@diagnostic disable-line: param-type-mismatch ffi_copy(ret.terms_[a.term_count], b.terms_, ffi_sizeof(b.terms_, b.term_count)) ---@diagnostic disable-line: param-type-mismatch return ret end ---@param expr kiwi.Expression ---@param constant number ---@nodiscard local function new_expr_constant(expr, constant) local ret = ffi_new(Expression, expr.term_count, constant, expr.term_count) --[[@as kiwi.Expression]] ffi_copy(ret.terms_, expr.terms_, ffi_sizeof(expr.terms_, expr.term_count)) ---@diagnostic disable-line: param-type-mismatch return ret end --- Expressions are a sum of terms with an added constant. ---@class kiwi.Expression: ffi.ctype* ---@overload fun(terms: kiwi.Term[], constant: number?): 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, } ---@return number ---@nodiscard function Expression_cls:value() local sum = self.constant for i = 0, self.term_count - 1 do sum = sum + self.terms_[i]:value() 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 terms[i + 1] = self.terms_[i] end return terms end ---@return kiwi.Expression ---@nodiscard function Expression_cls:copy() return new_expr_constant(self, self.constant) end ffi.metatype(Expression, { __index = Expression_cls, __new = function(_, terms, constant) return ffi_new(Expression, #terms, constant or 0.0, #terms, terms) end, __mul = function(a, b) if type(a) == "number" then return mul_expr_constant(b, a) elseif type(b) == "number" then return mul_expr_constant(a, b) end error("Invalid expr *") end, __div = function(expr, denom) assert(type(denom) == "number", "Invalid expr /") return mul_expr_constant(expr, 1.0 / denom) end, __unm = function(expr) return mul_expr_constant(expr, -1.0) end, __add = function(a, b) if ffi_istype(Var, b) then return add_expr_term(a, Term(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) elseif type(b) == "number" then return new_expr_constant(a, a.constant + b) end error("Invalid expr +") end, __sub = function(a, b) return a + -b end, __tostring = function(expr) local tab = new_tab(expr.term_count + 1, 0) for i = 0, expr.term_count - 1 do tab[i + 1] = tostring(expr.terms_[i]) end tab[expr.term_count + 1] = expr.constant return concat(tab, " + ") end, }) end --- 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.ctype* ---@overload fun(expr: 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 = 8 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]] ckiwi.kiwi_constraint_expression(self, expr, n) end return expr end --- 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 kiwi.new_pair_ratio_constraint(left, coeff, right, constant, op, strength) return Constraint( new_expr_pair(-(constant or 0.0), Term(left), Term(right, -coeff)), op, strength ) end --- 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 kiwi.new_pair_constraint(left, right, constant, op, strength) return Constraint( new_expr_pair(-(constant or 0.0), Term(left), Term(right, -1.0)), 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 kiwi.new_single_constraint(var, constant, op, strength) return Constraint(new_expr_one(-(constant or 0.0), Term(var)), op, strength) end local Strength = kiwi.Strength local REQUIRED = Strength.REQUIRED ffi.metatype(Constraint, { __index = Constraint_cls, __gc = ckiwi.kiwi_constraint_del, __new = function(_, expr, op, strength) return ckiwi.kiwi_constraint_new(expr, op or "EQ", strength or REQUIRED) end, __tostring = function(self) local ops = { [0] = "<=", ">=", "==", } local strengths = { [Strength.REQUIRED] = "required", [Strength.STRONG] = "strong", [Strength.MEDIUM] = "medium", [Strength.WEAK] = "weak", } local strength = self:strength() return strformat( "%s %s 0 | %s", self:expression(), ops[tonumber(self:op())], strengths[strength] or tostring(strength) ) end, }) local Error_mt = { __tostring = function(self) return strformat("%s: (%s, %s)", self.message, self.solver, self.item) end, } ---@param kind kiwi.ErrKind ---@param message string ---@param solver kiwi.Solver ---@param item any local function new_error(kind, message, solver, item) ---@class kiwi.Error ---@field kind kiwi.ErrKind ---@field message string ---@field solver kiwi.Solver? ---@field item any? return setmetatable({ kind = kind, message = message, solver = solver, item = item, }, Error_mt) end ---@param f fun(solver: kiwi.Solver, item: any, ...): kiwi.KiwiErr ---@param solver kiwi.Solver ---@param item any local function try_solver(f, solver, item, ...) local err = f(solver, item, ...) if err.kind ~= 0 then error(new_error(err.kind, ffi_string(err.message), solver, item)) end end ---@class kiwi.Solver: ffi.ctype* ---@overload fun(): kiwi.Solver local Solver_cls = { --- 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, } --- Adds a 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 constraint kiwi.Constraint function Solver_cls:add_constraint(constraint) try_solver(ckiwi.kiwi_solver_add_constraint, self, constraint) end --- Removes a constraint from the solver. --- Raises --- KiwiErrUnknownConstraint: The given constraint has not been added to the solver. ---@param constraint kiwi.Constraint function Solver_cls:remove_constraint(constraint) try_solver(ckiwi.kiwi_solver_remove_constraint, self, constraint) end --- Test whether a constraint is in the solver. ---@param constraint kiwi.Constraint ---@return boolean ---@nodiscard function Solver_cls:has_constraint(constraint) return ckiwi.kiwi_solver_has_constraint(self, constraint) ~= 0 end --- Adds an edit variable to the solver. --- --- This method should be called before the `suggestValue` method is --- used to supply a suggested value for the given edit variable. --- Raises --- KiwiErrDuplicateEditVariable: The given edit variable has already been added to the solver. --- 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`) function Solver_cls:add_edit_var(var, strength) try_solver(ckiwi.kiwi_solver_add_edit_var, self, var, strength) end --- Remove an edit variable from the solver. --- Raises --- KiwiErrUnknownEditVariable: The given edit variable has not been added to the solver ---@param var kiwi.Var the edit variable to remove function Solver_cls:remove_edit_var(var) try_solver(ckiwi.kiwi_solver_remove_edit_var, self, var) end --- Test whether an edit variable has been added to the solver. ---@param var kiwi.Var the edit variable to check ---@return boolean ---@nodiscard function Solver_cls:has_edit_var(var) return ckiwi.kiwi_solver_has_edit_var(self, var) ~= 0 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: The given edit variable has not been added to the solver. ---@param var kiwi.Var the edit variable to suggest a value for ---@param value number the suggested value function Solver_cls:suggest_value(var, value) try_solver(ckiwi.kiwi_solver_suggest_value, self, var, value) end --- Dump a representation of the solver to a string. ---@return string ---@nodiscard function Solver_cls:dumps() local cs = ckiwi.kiwi_solver_dumps(self, nil) local s = ffi_string(cs) ffi.C.free(cs) return s end kiwi.Solver = ffi.metatype("KiwiSolverRef", { __index = Solver_cls, __new = function(_) return ckiwi.kiwi_solver_new() end, __gc = ckiwi.kiwi_solver_del, __tostring = function(self) return strformat("kiwi.Solver(0x%X)", ffi_cast("intptr_t", ffi_cast("void*", self))) end, }) --[[@as kiwi.Solver]] return kiwi