diff --git a/.editorconfig b/.editorconfig index 7e2158e..0c08aae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,5 +7,4 @@ insert_final_newline = true [{*.lua,*.rockspec,.luacov}] indent_style = space indent_size = 3 -call_parentheses = nosingletable -max_line_length = 98 +max_line_length = 105 diff --git a/ckiwi/ckiwi.cpp b/ckiwi/ckiwi.cpp index 4c36f43..f7695a0 100644 --- a/ckiwi/ckiwi.cpp +++ b/ckiwi/ckiwi.cpp @@ -117,15 +117,18 @@ const KiwiErr* new_error(const KiwiErr* base, const std::exception& ex) { static const constexpr KiwiErr kKiwiErrUnhandledCxxException { KiwiErrUnknown, - "An unhandled C++ exception occurred."}; + "An unhandled C++ exception occurred." +}; static const constexpr KiwiErr kKiwiErrNullObjectArg0 { KiwiErrNullObject, - "null object passed as argument #0 (self)"}; + "null object passed as argument #0 (self)" +}; static const constexpr KiwiErr kKiwiErrNullObjectArg1 { KiwiErrNullObject, - "null object passed as argument #1"}; + "null object passed as argument #1" +}; template inline const KiwiErr* wrap_err(F&& f) { @@ -134,42 +137,49 @@ inline const KiwiErr* wrap_err(F&& f) { } catch (const UnsatisfiableConstraint& ex) { static const constexpr KiwiErr err { KiwiErrUnsatisfiableConstraint, - "The constraint cannot be satisfied."}; + "The constraint cannot be satisfied." + }; return &err; } catch (const UnknownConstraint& ex) { static const constexpr KiwiErr err { KiwiErrUnknownConstraint, - "The constraint has not been added to the solver."}; + "The constraint has not been added to the solver." + }; return &err; } catch (const DuplicateConstraint& ex) { static const constexpr KiwiErr err { KiwiErrDuplicateConstraint, - "The constraint has already been added to the solver."}; + "The constraint has already been added to the solver." + }; return &err; } catch (const UnknownEditVariable& ex) { static const constexpr KiwiErr err { KiwiErrUnknownEditVariable, - "The edit variable has not been added to the solver."}; + "The edit variable has not been added to the solver." + }; return &err; } catch (const DuplicateEditVariable& ex) { static const constexpr KiwiErr err { KiwiErrDuplicateEditVariable, - "The edit variable has already been added to the solver."}; + "The edit variable has already been added to the solver." + }; return &err; } catch (const BadRequiredStrength& ex) { static const constexpr KiwiErr err { KiwiErrBadRequiredStrength, - "A required strength cannot be used in this context."}; + "A required strength cannot be used in this context." + }; return &err; } catch (const InternalSolverError& ex) { static const constexpr KiwiErr base { KiwiErrInternalSolverError, - "An internal solver error occurred."}; + "An internal solver error occurred." + }; return new_error(&base, ex); } catch (std::bad_alloc&) { static const constexpr KiwiErr err {KiwiErrAlloc, "A memory allocation failed."}; diff --git a/ckiwi/ckiwi.h b/ckiwi/ckiwi.h index 4982cf3..28526bc 100644 --- a/ckiwi/ckiwi.h +++ b/ckiwi/ckiwi.h @@ -1,10 +1,10 @@ -#ifndef CKIWI_H_ -#define CKIWI_H_ - -#include +#ifndef KIWI_CKIWI_H_ +#define KIWI_CKIWI_H_ #ifdef __cplusplus extern "C" { +#else + #include #endif #define KIWI_REF_ISNULL(ref) ((ref).impl_ == NULL) @@ -98,4 +98,4 @@ char* kiwi_solver_dumps(const KiwiSolver* sp); // Local Variables: // mode: c++ // End: -#endif // CKIWI_H_ +#endif // KIWI_CKIWI_H_ diff --git a/kiwi.lua b/kiwi.lua index 4f07c0e..39e768c 100644 --- a/kiwi.lua +++ b/kiwi.lua @@ -95,7 +95,8 @@ void free(void *); ]]) local strformat = string.format -local ffi_gc, ffi_istype, ffi_new, ffi_string = ffi.gc, ffi.istype, ffi.new, ffi.string +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") @@ -125,26 +126,29 @@ kiwi.ErrKind = ffi.typeof("enum KiwiErrKind") --[[@as kiwi.ErrKind]] ---| '"EQ"' # == (equal) kiwi.RelOp = ffi.typeof("enum KiwiRelOp") -kiwi.Strength = { +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) +do 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) + + --- 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]] @@ -183,8 +187,8 @@ end ---@param var kiwi.Var ---@param coeff number? ---@nodiscard -local function new_expr_one_temp(constant, var, coeff) - local ret = ffi_new(Expression, 1) --[[@as kiwi.Expression]] +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 @@ -193,14 +197,6 @@ local function new_expr_one_temp(constant, var, coeff) return ret end ----@param constant number ----@param var kiwi.Var ----@param coeff number? ----@nodiscard -local function new_expr_one(constant, var, coeff) - return ffi_gc(new_expr_one_temp(constant, var, coeff), ckiwi.kiwi_expression_del_vars) --[[@as kiwi.Expression]] -end - ---@param constant number ---@param var1 kiwi.Var ---@param var2 kiwi.Var @@ -220,45 +216,78 @@ local function new_expr_pair(constant, var1, var2, coeff1, coeff2) return ret end -local Strength = kiwi.Strength +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 function to_expr(o) - if ffi_istype(Expression, o) then - return o --[[@as kiwi.Expression]] - elseif type(o) == "number" then - if o == 0 then - return nil - end - local ret = ffi_new(Expression, 0) --[[@as kiwi.Expression]] - ret.constant = o - ret.term_count = 0 - return ret - end - local var - local coeff = 1.0 - - if ffi_istype(Var, o) then - var = o --[[@as kiwi.Var]] - elseif ffi_istype(Term, o) then - var = o.var - coeff = o.coefficient - else - error("Expected Expression|Term|Var|number, got " .. type(o) .. " instead") - end - return new_expr_one_temp(0.0, var, coeff) + 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(to_expr(lhs), to_expr(rhs), op, strength or REQUIRED), - ckiwi.kiwi_constraint_del - ) --[[@as kiwi.Constraint]] + 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`. @@ -353,11 +382,13 @@ do elseif type(b) == "number" then return Term(a, b) end - error("Invalid var *") + op_error(a, b, "*") end function Var_mt.__div(a, b) - assert(type(b) == "number", "Invalid var /") + if type(b) ~= "number" then + op_error(a, b, "/") + end return Term(a, 1.0 / b) end @@ -379,7 +410,7 @@ do elseif type(b) == "number" then return new_expr_one(b, a) end - error("Invalid var +") + op_error(a, b, "+") end function Var_mt.__sub(a, b) @@ -426,10 +457,12 @@ do 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), function(term) - ckiwi.kiwi_var_del(term.var) - end) + return ffi_gc(ffi_new(T, ckiwi.kiwi_var_clone(var), coefficient or 1.0), term_gc) end function Term_mt.__mul(a, b) @@ -438,11 +471,13 @@ do elseif type(a) == "number" then return Term(b.var, b.coefficient * a) end - error("Invalid term *") + op_error(a, b, "*") end function Term_mt.__div(a, b) - assert(type(b) == "number", "Invalid term /") + if type(b) ~= "number" then + op_error(a, b, "/") + end return Term(a.var, a.coefficient / b) end @@ -464,7 +499,7 @@ do elseif type(b) == "number" then return new_expr_one(b, a.var, a.coefficient) end - error("Invalid term + op") + op_error(a, b, "+") end function Term_mt.__sub(a, b) @@ -603,11 +638,13 @@ do elseif type(b) == "number" then return mul_expr_constant(a, b) end - error("Invalid expr *") + op_error(a, b, "*") end function Expression_mt.__div(a, b) - assert(type(b) == "number", "Invalid expr /") + if type(b) ~= "number" then + op_error(a, b, "/") + end return mul_expr_constant(a, 1.0 / b) end @@ -629,7 +666,7 @@ do elseif type(b) == "number" then return new_expr_constant(a, a.constant + b) end - error("Invalid expr +") + op_error(a, b, "+") end function Expression_mt.__sub(a, b) @@ -639,7 +676,8 @@ do function Expression_mt:__tostring() local tab = new_tab(self.term_count + 1, 0) for i = 0, self.term_count - 1 do - tab[i + 1] = tostring(self.terms_[i]) + 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, " + ") @@ -693,17 +731,18 @@ do ) end + local OPS = { [0] = "<=", ">=", "==" } + local STRENGTH_NAMES = { + [Strength.REQUIRED] = "required", + [Strength.STRONG] = "strong", + [Strength.MEDIUM] = "medium", + [Strength.WEAK] = "weak", + } + function Constraint_mt:__tostring() - local ops = { [0] = "<=", ">=", "==" } - local strengths = { - [Strength.REQUIRED] = "required", - [Strength.STRONG] = "strong", - [Strength.MEDIUM] = "medium", - [Strength.WEAK] = "weak", - } local strength = self:strength() - local strength_str = strengths[strength] or tostring(strength) - local op = ops[tonumber(self:op())] + 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 @@ -726,18 +765,17 @@ do ---@nodiscard function constraints.pair_ratio(left, coeff, right, constant, op, strength) assert(ffi_istype(Var, left) and ffi_istype(Var, right)) - local lhs = ffi_new(Expression, 2) --[[@as kiwi.Expression]] - local dt = lhs.terms_[0] + local dt = tmpexpr.terms_[0] dt.var = left dt.coefficient = 1.0 - dt = lhs.terms_[1] + dt = tmpexpr.terms_[1] dt.var = right dt.coefficient = -coeff - lhs.constant = -(constant or 0.0) - lhs.term_count = 2 + tmpexpr.constant = constant ~= nil and constant or 0 + tmpexpr.term_count = 2 return ffi_gc( - ckiwi.kiwi_constraint_new(lhs, nil, op or "EQ", strength or REQUIRED), + ckiwi.kiwi_constraint_new(tmpexpr, nil, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) --[[@as kiwi.Constraint]] end @@ -767,13 +805,13 @@ do ---@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( - new_expr_one_temp(-(constant or 0.0), var, 1.0), - nil, - op or "EQ", - strength or REQUIRED - ), + ckiwi.kiwi_constraint_new(tmpexpr, nil, op or "EQ", strength or REQUIRED), ckiwi.kiwi_constraint_del ) --[[@as kiwi.Constraint]] end