From 59bdeedc1874c3dfbfe458ebe6e59158fdfd75aa Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Mon, 12 Feb 2024 04:21:48 -0600 Subject: [PATCH] Initial actual commit, awaiting CI --- .gitignore | 1 + .luarc.json | 1 - Makefile | 32 ++ README.md | 94 +++++ ckiwi/ckiwi.cpp | 258 ++++++++++++++ ckiwi/ckiwi.h | 118 +++++++ kiwi.lua | 793 ++++++++++++++++++++++++++++++++++++++++++ ljkiwi-scm-1.rockspec | 33 ++ 8 files changed, 1329 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 README.md create mode 100644 ckiwi/ckiwi.cpp create mode 100644 ckiwi/ckiwi.h create mode 100644 kiwi.lua create mode 100644 ljkiwi-scm-1.rockspec diff --git a/.gitignore b/.gitignore index 5b0714b..067b483 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.so *.o .cache/ +compile_commands.json diff --git a/.luarc.json b/.luarc.json index 5e855b1..679e34f 100644 --- a/.luarc.json +++ b/.luarc.json @@ -8,6 +8,5 @@ "lua_modules/share/lua/5.1/?.lua", "lua_modules/share/lua/5.1/?/init.lua" ], - "workspace.library": ["lua_modules/share/lua/5.1"], "workspace.checkThirdParty": false } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00ae1f3 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +SRCDIR ?= . +CC ?= $(CROSS)gcc +CFLAGS ?= -fPIC -O2 +CFLAGS += -Wall -I$(SRCDIR)/kiwi +LIBFLAG ?= -shared +LIB_EXT ?= so + +ifeq ($(findstring gcc, $(CC)), gcc) + CXX := $(subst gcc, g++, $(CC)) + CXXFLAGS += -std=c++11 +else +ifeq ($(CC), clang) + CXX := clang++ + CXXFLAGS += -std=c++11 +else + CXX := $(CC) +endif +endif + +all: ckiwi.$(LIB_EXT) + +install: + cp -f ckiwi.$(LIB_EXT) $(INST_LIBDIR)/ckiwi.$(LIB_EXT) + cp -f kiwi.lua $(INST_LUADIR)/kiwi.lua + +clean: + rm -f ckiwi.$(LIB_EXT) + +ckiwi.$(LIB_EXT): $(SRCDIR)/ckiwi/ckiwi.cpp + $(CXX) $(CXXFLAGS) $(CFLAGS) -fPIC -Wall -I$(SRCDIR)/kiwi $(LIBFLAG) -o $@ $< + +.PHONY: all install clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..890630e --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +ljkiwi - Free LuaJIT FFI kiwi (Cassowary derived) constraint solver. + +[![CI](https://github.com/jkl1337/ljkiwi/actions/workflows/ci.yml/badge.svg)](https://github.com/jkl1337/ljkiwi/actions/workflows/ci.yml) +[![Coverage Status](https://coveralls.io/repos/github/jkl1337/ljkiwi/badge.svg?branch=master)](https://coveralls.io/github/jkl1337/ljkiwi?branch=master) +[![luarocks](https://img.shields.io/luarocks/v/jkl/ljkiwi)](https://luarocks.org/modules/jkl/ljkiwi) + +# Introduction + +Kiwi is a reasonably efficient C++ implementation of the Cassowary constraint solving algorithm. It is an implementation of the algorithm as described in the paper ["The Cassowary Linear Arithmetic Constraint Solving Algorithm"](http://www.cs.washington.edu/research/constraints/cassowary/techreports/cassowaryTR.pdf) by Greg J. Badros and Alan Borning. The Kiwi implementation is not based on the original C++ implementation, but is a ground-up reimplementation with performance 10x to 500x faster in typical use. +Cassowary constraint solving is a technique that is particularly well suited to user interface layout. It is the algorithm Apple uses for iOS and OS X Auto Layout. + +There are a few Lua implementations or attempts. The SILE typesetting system has a pure Lua implementation of the original Cassowary code, which appears to be correct but is quite slow. There are two extant Lua ports of Kiwi, one that is based on a C rewrite of Kiwi. However testing of these was not encouraging with either segfaults or incorrect results. +Since the C++ Kiwi library is well tested and widely used it was simpler to provide a LuaJIT FFI wrapper and use that. +This package has no dependencies other than a C++11 toolchain to compile the included Kiwi library and a small C wrapper. + +The Lua API has a pure Lua expression builder. There is of course some overhead to this, however in most cases expression building is infrequent and the underlying structures can be reused. + +The wrapper is quite close to the Kiwi C++/Python port with a few naming changes. + +## Example + +```lua +local kiwi = require("kiwi") +local Var = kiwi.Var + +local Button = setmetatable({}, { + __call = function(_, identifier) + return setmetatable({ + left = Var(identifier .. " left"), + width = Var(identifier .. " width"), + }, { + __tostring = function(self) + return "Button(" .. self.left:value() .. ", " .. self.width:value() .. ")" + end, + }) + end, +}) + +local b1 = Button("b1") +local b2 = Button("b2") + +local left_edge = Var("left") +local right_edge = Var("width") + +local STRONG = kiwi.Strength.STRONG + +-- stylua: ignore start +local constraints = { + left_edge :eq(0.0), + -- two buttons are the same width + b1.width :eq(b2.width), + -- button1 starts 50 from the left margin + b1.left :eq(left_edge + 50), + -- button2 ends 50 from the right margin + right_edge :eq(b2.left + b2.width + 50), + -- button2 starts at least 100 from the end of button1. This is the "elastic" constraint + b2.left :ge(b1.left + b1.width + 100), + -- button1 has a minimum width of 87 + b1.width :ge(87), + -- button1 has a preferred width of 87 + b1.width :eq(87, STRONG), + -- button2 has minimum width of 113 + b2.width :ge(113), + -- button2 has a preferred width of 113 + b2.width :eq(113, STRONG), +} +-- stylua: ignore end + +local solver = kiwi.Solver() + +for _, c in ipairs(constraints) do + solver:add_constraint(c) +end + +solver:update_vars() + +print(b1) -- Button(50, 113) +print(b2) -- Button(263, 113) +print(left_edge:value()) -- 0 +print(right_edge:value()) -- 426 + +solver:add_edit_var(right_edge, STRONG) +solver:suggest_value(right_edge, 500) +solver:update_vars() +print(b1) -- Button(50, 113) +print(b2) -- Button(337, 113) +print(right_edge:value()) -- 500 + +``` + +In addition to the expression builder there are convenience constructors: `new_pair_ratio_constraint`, `new_pair_constraint`, and `new_single_constraint` to allow efficient construction of the most common simple expression types for GUI layout. + +## Documentation +WIP - However the API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server. diff --git a/ckiwi/ckiwi.cpp b/ckiwi/ckiwi.cpp new file mode 100644 index 0000000..4a6304e --- /dev/null +++ b/ckiwi/ckiwi.cpp @@ -0,0 +1,258 @@ +#include "ckiwi.h" + +#include + +#include + +using namespace kiwi; + +namespace { +template +class alignas(T) SharedRef { + private: + CS ref_; + + public: + T* operator&() { + return reinterpret_cast(&ref_); + } + + T* operator->() { + return reinterpret_cast(&ref_); + } + + operator T&() { + return *reinterpret_cast(&ref_); + } + + operator CS() const { + return ref_; + } + + T& instance() { + return *reinterpret_cast(&ref_); + } + + void destroy() { + instance().~T(); + ref_ = {0}; + } + + SharedRef(CS ref) : ref_(ref) {} + + template + SharedRef(Args&&... args) { + new (&ref_) T(std::forward(args)...); + } + + static_assert(sizeof(CS) >= sizeof(T), "SharedRef cannot wrap T (size)"); +}; + +using ConstraintRef = SharedRef; +using VariableRef = SharedRef; + +KiwiErr make_error(KiwiErrKind kind, const std::exception& ex) { + constexpr auto max_n = sizeof(KiwiErr::message) - 1; + const auto n = std::min(std::strlen(ex.what()), max_n); + + KiwiErr err {kind}; + + std::memcpy(err.message, ex.what(), n); + if (n == max_n) + err.message[max_n] = 0; + return err; +} + +KiwiErr make_error(KiwiErrKind kind) { + return KiwiErr {kind, {0}}; +} + +template +inline KiwiErr wrap_err(F&& f) { + try { + f(); + } catch (const UnsatisfiableConstraint& err) { + return make_error(KiwiErrUnsatisfiableConstraint, err); + } catch (const UnknownConstraint& err) { + return make_error(KiwiErrUnknownConstraint, err); + } catch (const DuplicateConstraint& err) { + return make_error(KiwiErrDuplicateConstraint, err); + } catch (const UnknownEditVariable& err) { + return make_error(KiwiErrUnknownEditVariable, err); + } catch (const DuplicateEditVariable& err) { + return make_error(KiwiErrDuplicateEditVariable, err); + } catch (const BadRequiredStrength& err) { + return make_error(KiwiErrBadRequiredStrength, err); + } catch (const InternalSolverError& err) { + return make_error(KiwiErrInternalSolverError, err); + } catch (std::bad_alloc&) { + return make_error(KiwiErrAlloc); + } catch (const std::exception& err) { + return make_error(KiwiErrUnknown, err); + } catch (...) { + return make_error(KiwiErrUnknown); + } + return make_error(KiwiErrNone); +} +} // namespace + +extern "C" { + +KiwiVarRef kiwi_var_new(const char* name) { + return VariableRef(name); +} + +void kiwi_var_del(KiwiVarRef var) { + VariableRef(var).destroy(); +} + +const char* kiwi_var_name(KiwiVarRef var) { + return VariableRef(var)->name().c_str(); +} + +void kiwi_var_set_name(KiwiVarRef var, const char* name) { + VariableRef(var)->setName(name); +} + +double kiwi_var_value(KiwiVarRef var) { + return VariableRef(var)->value(); +} + +void kiwi_var_set_value(KiwiVarRef var, double value) { + VariableRef(var)->setValue(value); +} + +int kiwi_var_eq(KiwiVarRef var, KiwiVarRef other) { + return VariableRef(var)->equals(VariableRef(other)); +} + +KiwiConstraintRef kiwi_constraint_new( + const KiwiExpression* expression, + enum KiwiRelOp op, + double strength +) { + if (strength < 0.0) { + strength = kiwi::strength::required; + } + std::vector terms; + terms.reserve(expression->term_count); + + for (auto* t = expression->terms; t != expression->terms + expression->term_count; + ++t) { + terms.emplace_back(VariableRef(t->var), t->coefficient); + } + + return ConstraintRef( + Expression(std::move(terms), expression->constant), + static_cast(op), + strength + ); +} + +void kiwi_constraint_del(KiwiConstraintRef constraint) { + ConstraintRef(constraint).destroy(); +} + +double kiwi_constraint_strength(KiwiConstraintRef constraint) { + return ConstraintRef(constraint)->strength(); +} + +enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint) { + return static_cast(ConstraintRef(constraint)->op()); +} + +int kiwi_constraint_violated(KiwiConstraintRef constraint) { + return ConstraintRef(constraint)->violated(); +} + +int kiwi_constraint_expression( + KiwiConstraintRef constraint, + KiwiExpression* out, + int out_size +) { + const auto& expr = ConstraintRef(constraint).instance().expression(); + const auto& terms = expr.terms(); + const int n_terms = terms.size(); + if (!out || out_size < n_terms) + return n_terms; + + auto* p = out->terms; + for (const auto& t : terms) { + *p = KiwiTerm {VariableRef(t.variable()), t.coefficient()}; + ++p; + } + out->term_count = p - out->terms; + out->constant = expr.constant(); + + return n_terms; +} + +KiwiSolverRef kiwi_solver_new() { + return KiwiSolverRef {new (std::nothrow) Solver()}; +} + +void kiwi_solver_del(KiwiSolverRef s) { + delete reinterpret_cast(s.impl_); +} + +KiwiErr kiwi_solver_add_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) { + return wrap_err([=]() { + reinterpret_cast(s.impl_)->addConstraint(ConstraintRef(constraint)); + }); +} + +KiwiErr kiwi_solver_remove_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) { + return wrap_err([=]() { + reinterpret_cast(s.impl_)->removeConstraint(ConstraintRef(constraint)); + }); +} + +int kiwi_solver_has_constraint(KiwiSolverRef s, KiwiConstraintRef constraint) { + return reinterpret_cast(s.impl_)->hasConstraint(ConstraintRef(constraint)); +} + +KiwiErr kiwi_solver_add_edit_var(KiwiSolverRef s, KiwiVarRef var, double strength) { + return wrap_err([=]() { + reinterpret_cast(s.impl_)->addEditVariable(VariableRef(var), strength); + }); +} + +KiwiErr kiwi_solver_remove_edit_var(KiwiSolverRef s, KiwiVarRef var) { + return wrap_err([=]() { + reinterpret_cast(s.impl_)->removeEditVariable(VariableRef(var)); + }); +} + +int kiwi_solver_has_edit_var(KiwiSolverRef s, KiwiVarRef var) { + return reinterpret_cast(s.impl_)->hasEditVariable(VariableRef(var)); +} + +KiwiErr kiwi_solver_suggest_value(KiwiSolverRef s, KiwiVarRef var, double value) { + return wrap_err([=]() { + reinterpret_cast(s.impl_)->suggestValue(VariableRef(var), value); + }); +} + +void kiwi_solver_update_vars(KiwiSolverRef s) { + reinterpret_cast(s.impl_)->updateVariables(); +} + +void kiwi_solver_reset(KiwiSolverRef s) { + reinterpret_cast(s.impl_)->reset(); +} + +void kiwi_solver_dump(KiwiSolverRef s) { + reinterpret_cast(s.impl_)->dump(); +} + +char* kiwi_solver_dumps(KiwiSolverRef s, void* (*alloc)(size_t)) { + const auto val = reinterpret_cast(s.impl_)->dumps(); + const auto buf_size = val.size() + 1; + auto* buf = static_cast(alloc ? alloc(buf_size) : malloc(buf_size)); + if (!buf) + return nullptr; + std::memcpy(buf, val.c_str(), val.size() + 1); + return buf; +} + +} // extern "C" diff --git a/ckiwi/ckiwi.h b/ckiwi/ckiwi.h new file mode 100644 index 0000000..d7f1c31 --- /dev/null +++ b/ckiwi/ckiwi.h @@ -0,0 +1,118 @@ +#ifndef CKIWI_H_ +#define CKIWI_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define KIWI_REF_ISNULL(ref) ((ref).impl_ == NULL) + +// LuaJIT start +enum KiwiErrKind { + KiwiErrNone, + KiwiErrUnsatisfiableConstraint = 1, + KiwiErrUnknownConstraint, + KiwiErrDuplicateConstraint, + KiwiErrUnknownEditVariable, + KiwiErrDuplicateEditVariable, + KiwiErrBadRequiredStrength, + KiwiErrInternalSolverError, + KiwiErrAlloc, + KiwiErrUnknown, +}; + +enum KiwiRelOp { KIWI_OP_LE, KIWI_OP_GE, KIWI_OP_EQ }; + +typedef struct { + void* private_; +} KiwiVarRef; + +typedef struct { + KiwiVarRef var; + double coefficient; +} KiwiTerm; + +typedef struct { + double constant; + int term_count; + KiwiTerm terms[1]; +} 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); +// LuaJIT end + +#ifdef __cplusplus +} +#endif + +// Local Variables: +// mode: c++ +// End: +#endif // CKIWI_H_ diff --git a/kiwi.lua b/kiwi.lua new file mode 100644 index 0000000..31abd4e --- /dev/null +++ b/kiwi.lua @@ -0,0 +1,793 @@ +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 diff --git a/ljkiwi-scm-1.rockspec b/ljkiwi-scm-1.rockspec new file mode 100644 index 0000000..f7547e7 --- /dev/null +++ b/ljkiwi-scm-1.rockspec @@ -0,0 +1,33 @@ +rockspec_format = "3.0" +package = "ljkiwi" +version = "scm-1" +source = { + url = "git+https://github.com/jkl1337/ljkiwi", +} +description = { + summary = "A LuaJIT FFI binding for the Kiwi constraint solver.", + detailed = [[ + ljkiwi is a LuaJIT FFI binding for the Kiwi constraint solver. Kiwi is a fast + implementation of the Cassowary constraint solving algorithm. ljkiwi provides + reasonably efficient bindings using the LuaJIT FFI.]], + license = "MIT", + issues_url = "https://github.com/jkl1337/ljkiwi/issues", + maintainer = "John Luebs", +} +dependencies = { + "lua >= 5.1", +} + +build = { + type = "make", + build_variables = { + CFLAGS = "$(CFLAGS)", + LIBFLAG = "$(LIBFLAG)", + LIB_EXT = "$(LIB_EXTENSION)", + OBJ_EXT = "$(OBJ_EXTENSION)", + }, + install_variables = { + INST_LIBDIR = "$(LIBDIR)", + INST_LUADIR = "$(LUADIR)", + }, +}