Files
ljkiwi/kiwi.lua
John K. Luebs d83cc3468c Drop LuaJIT 2.0.5 support
Primary CI target is luajit-openresty. Even 2.1.0-beta3 is ancient.
2024-02-17 22:36:28 -06:00

1148 lines
36 KiB
Lua

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,
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(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,
}
---@param expr kiwi.Expression
---@param constant number
---@nodiscard
local function mul_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 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
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
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, terms, constant)
local term_count = terms and #terms or 0
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 or 0.0
if terms then
for i, t in ipairs(terms) do
local dt = e.terms_[i - 1] --[[@as kiwi.Term]]
dt.var = ckiwi.kiwi_var_clone(t.var)
dt.coefficient = t.coefficient
end
end
return e
end
function Expression_mt.__mul(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
op_error(a, b, "*")
end
function Expression_mt.__div(a, b)
if type(b) ~= "number" then
op_error(a, b, "/")
end
return mul_expr_constant(a, 1.0 / b)
end
function Expression_mt:__unm()
return mul_expr_constant(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 = 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]]
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|number)[]
---@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|number)[] )?): 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|number)[] 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