24 Commits

Author SHA1 Message Date
00a9fda814 again 2024-02-22 22:57:27 -06:00
a8c1a10cab fix MSVC warnings 2024-02-22 22:40:53 -06:00
2a71914ed8 ok 2024-02-22 20:58:47 -06:00
dea448e46b better 2024-02-22 18:23:33 -06:00
3811d82212 skin this cat 2024-02-22 16:02:31 -06:00
2baee14c5d misc Lua Version difference fixes 2024-02-22 11:15:17 -06:00
198265ee36 get compiling with c++ 2024-02-22 10:15:01 -06:00
604e3df41f just starting 2024-02-22 03:54:51 -06:00
6a99504835 Minor fixes and API change
- fix stupid spaces in Makefile
- small type annotation fix
- Modify Expression constructor API to use varargs
- Update kiwi.lua to support future C API module
2024-02-21 22:14:24 -06:00
9b00e62d43 update github workflow 2024-02-19 21:31:59 -06:00
3631704544 update LICENSE 2024-02-19 09:20:46 -06:00
17f67b8879 use our own lua gh action 2024-02-18 00:20:41 -06:00
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
b3cf6136a4 Convert enum values to number for LuaJIT 2.0.5 compat 2024-02-17 22:07:01 -06:00
2ffc5a333b All kinds of things
- Add modified BSD license
- Initial test runner github action
- Add is_{type} functions
- export kiwi.Error metatable such that it appears like a class.
- Allow Expression.new to take nil for terms
- Initial set of specs
2024-02-17 21:58:13 -06:00
d85796a038 More flexible error handling, convenience methods
Allow getting solver errors returned rather than raising error.
The API allows setting a mask of which error kinds raise vs return.

Also add some convenience methods:
- `add_to` and `remove_from` constraint methods to solver.
- add and remove multiple constraints and edit/suggest variables at once
2024-02-17 01:08:45 -06:00
37833f7b2b Remove now unnecessary const_casts. 2024-02-17 01:08:45 -06:00
59465d3142 Add const to Solver dump methods and Variable equals. 2024-02-17 01:08:29 -06:00
8b57e0c441 Use global scratch space for temporary objects, better errors 2024-02-15 16:23:57 -06:00
bc948d1a61 Restyle and limit scopes for quicker navigation 2024-02-14 16:00:49 -06:00
3311b582a1 Refactors to produce slightly less garbage 2024-02-14 13:53:14 -06:00
84a01179cd Manage GC data better
Make sure that all cdata references for variables is tracked. This is
done by attaching finalizers to Expression and Term objects. Some
effort is made to avoid creating tracked temporary objects (esp Terms)
now.
2024-02-14 01:42:56 -06:00
e43272487f Guard against most egregious mistakes in calling the library
LuaJIT FFI is not inherently memory safe and there is no way
to completely guard against the caller doing something that
will trample over memory, but we can get pretty close. Biggest
issue is that an empty table will stand-in for a ref struct with
a null ref. So check for that in all the calls. In the calls that
raise errors we now have a specific error for it. In the other
functions the "nil" object is handled quietly but without a nullptr
dereference and hopefully no UB.
2024-02-13 16:58:59 -06:00
59bdeedc18 Initial actual commit, awaiting CI 2024-02-12 18:05:41 -06:00
31 changed files with 5332 additions and 9 deletions

View File

@@ -7,5 +7,4 @@ insert_final_newline = true
[{*.lua,*.rockspec,.luacov}] [{*.lua,*.rockspec,.luacov}]
indent_style = space indent_style = space
indent_size = 3 indent_size = 3
call_parentheses = nosingletable max_line_length = 105
max_line_length = 98

36
.github/workflows/busted.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Busted
on: [push, pull_request]
jobs:
busted:
strategy:
fail-fast: false
matrix:
lua_version: ["luajit-openresty", "luajit-2.1.0-beta3", "luajit-git"]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup lua
uses: jkl1337/gh-actions-lua@master
with:
luaVersion: ${{ matrix.lua_version }}
- name: Setup luarocks
uses: jkl1337/gh-actions-luarocks@master
- name: Setup dependencies
run: |
luarocks install busted
luarocks install luacov-coveralls
- name: Build C library
run: make
- name: Run busted tests
run: busted -c -v
- name: Report test coverage
if: success()
continue-on-error: true
run: luacov-coveralls -e .luarocks -e spec
env:
COVERALLS_REPO_TOKEN: ${{ github.token }}

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
*.so *.so
*.o *.o
.cache/ .cache/
compile_commands.json

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "LuaJIT", "runtime.version": "Lua 5.4",
"runtime.path": [ "runtime.path": [
"./?/init.lua", "./?/init.lua",
"./?.lua", "./?.lua",
@@ -8,6 +8,6 @@
"lua_modules/share/lua/5.1/?.lua", "lua_modules/share/lua/5.1/?.lua",
"lua_modules/share/lua/5.1/?/init.lua" "lua_modules/share/lua/5.1/?/init.lua"
], ],
"workspace.library": ["lua_modules/share/lua/5.1"], "workspace.library": ["${3rd}/busted/library", "${3rd}/luassert/library"],
"workspace.checkThirdParty": false "workspace.checkThirdParty": false
} }

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2024 John Luebs
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

56
Makefile Normal file
View File

@@ -0,0 +1,56 @@
SRCDIR := .
CC := $(CROSS)gcc
CFLAGS := -fPIC -O3
CXXFLAGS := -I$(SRCDIR)/kiwi -fno-rtti
F_LTO := -flto=auto
CXXFLAGS_EXTRA := -std=c++14 -Wall $(F_LTO)
CFLAGS_EXTRA := -pedantic -std=c99 -Wall $(F_LTO)
LIBFLAG := -shared
LIB_EXT := so
LUA_INCDIR := /usr/include
ifeq ($(findstring gcc,$(CC)),gcc)
CXX := $(subst gcc,g++,$(CC))
CXXFLAGS += -std=c++14
ifneq ($(SANITIZE),)
CFLAGS += -fsanitize=undefined -fsanitize=address
endif
else
ifeq ($(CC),clang)
CXX := clang++
CXXFLAGS += -std=c++14
ifneq ($(SANITIZE),)
CFLAGS += -fsanitize=undefined -fsanitize=address
endif
else
CXX := $(CC)
endif
endif
OBJS := ckiwi.o luakiwi.o
VPATH = $(SRCDIR)/ckiwi $(SRCDIR)/luakiwi
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) $(OBJS)
ckiwi.$(LIB_EXT): $(OBJS)
$(CXX) $(CXXFLAGS) $(CFLAGS) $(CFLAGS_EXTRA) -I$(SRCDIR)/kiwi $(LIBFLAG) -o $@ $^
ckiwi.o: ckiwi.h
luakiwi.o: ckiwi.h luakiwi-int.h luacompat.h
%.o: %.c
$(CC) -I$(LUA_INCDIR) $(CFLAGS) $(CFLAGS_EXTRA) -c -o $@ $<
%.o: %.cpp
$(CXX) -I$(LUA_INCDIR) $(CXXFLAGS) $(CFLAGS) $(CXXFLAGS_EXTRA) -c -o $@ $<
.PHONY: all install clean

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
ljkiwi - Free LuaJIT FFI kiwi (Cassowary derived) constraint solver.
[![CI](https://github.com/jkl1337/ljkiwi/actions/workflows/busted.yml/badge.svg)](https://github.com/jkl1337/ljkiwi/actions/workflows/busted.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++14 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 is a convenience constraints submodule with: `pair_ratio`, `pair`, and `single` 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.

View File

@@ -27,7 +27,7 @@ BreakBeforeTernaryOperators: true
BreakConstructorInitializers: AfterColon BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterColon BreakInheritanceList: AfterColon
BreakStringLiterals: false BreakStringLiterals: false
ColumnLimit: 90 ColumnLimit: 98
CompactNamespaces: false CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4 ConstructorInitializerIndentWidth: 4

432
ckiwi/ckiwi.cpp Normal file
View File

@@ -0,0 +1,432 @@
#include "ckiwi.h"
#include <kiwi/kiwi.h>
#include <cstdlib>
#include <cstring>
using namespace kiwi;
namespace {
template<typename T, typename R, typename... Args>
inline R make_cref(Args&&... args) {
static_assert(
sizeof(R) >= sizeof(T), //NOLINT(bugprone-sizeof-expression)
"to_cref: R too small for T"
);
static_assert(alignof(R) >= alignof(T), "to_cref: R alignment too small for T");
R cref;
new (&cref) T(std::forward<Args>(args)...);
return cref;
}
template<typename... Args>
inline decltype(auto) make_var_cref(Args&&... args) {
return make_cref<Variable, KiwiVarRef>(std::forward<Args>(args)...);
}
template<typename... Args>
inline decltype(auto) make_constraint_cref(Args&&... args) {
return make_cref<Constraint, KiwiConstraintRef>(std::forward<Args>(args)...);
}
template<class T, class R>
class SharedRef {
private:
R& cref_;
public:
explicit SharedRef<T, R>(R& cref) : cref_(cref) {}
static_assert(
sizeof(R) >= sizeof(T), //NOLINT(bugprone-sizeof-expression)
"SharedRef<T,CS> CS too small for T"
);
R clone() const {
return make_cref<T, R>(cref());
}
void release() {
if (cref_) {
ptr()->~T();
cref_ = nullptr;
}
}
T* ptr() {
T* p;
void* s = &cref_;
std::memcpy(&p, &s, sizeof p); //NOLINT(bugprone-sizeof-expression)
return p;
}
const T* const_ptr() const {
const T* p;
const void* s = &cref_;
std::memcpy(&p, &s, sizeof p); //NOLINT(bugprone-sizeof-expression)
return p;
}
const T& cref() const {
return *const_ptr();
}
T* operator&() const {
return ptr();
}
T* operator->() {
return ptr();
}
const T* operator->() const {
return const_ptr();
}
operator const T&() const {
return cref();
}
explicit operator bool() const {
return cref_;
}
};
using ConstraintRef = SharedRef<Constraint, KiwiConstraintRef>;
using VariableRef = SharedRef<Variable, KiwiVarRef>;
using ConstVariableRef = const SharedRef<const Variable, const KiwiVarRef>;
const KiwiErr* new_error(const KiwiErr* base, const std::exception& ex) {
if (!std::strcmp(ex.what(), base->message))
return base;
const auto msg_n = std::strlen(ex.what()) + 1;
auto* mem = static_cast<char*>(std::malloc(sizeof(KiwiErr) + msg_n));
if (!mem) {
return base;
}
const auto* err = new (mem) KiwiErr {base->kind, mem + sizeof(KiwiErr), true};
std::memcpy(const_cast<char*>(err->message), ex.what(), msg_n);
return err;
}
static const constexpr KiwiErr kKiwiErrUnhandledCxxException {
KiwiErrUnknown,
"An unhandled C++ exception occurred."
};
static const constexpr KiwiErr kKiwiErrNullObjectArg0 {
KiwiErrNullObject,
"null object passed as argument #0 (self)"
};
static const constexpr KiwiErr kKiwiErrNullObjectArg1 {
KiwiErrNullObject,
"null object passed as argument #1"
};
template<typename F>
inline const KiwiErr* wrap_err(F&& f) {
try {
f();
} catch (const UnsatisfiableConstraint& ex) {
static const constexpr KiwiErr err {
KiwiErrUnsatisfiableConstraint,
"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."
};
return &err;
} catch (const DuplicateConstraint& ex) {
static const constexpr KiwiErr err {
KiwiErrDuplicateConstraint,
"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."
};
return &err;
} catch (const DuplicateEditVariable& ex) {
static const constexpr KiwiErr err {
KiwiErrDuplicateEditVariable,
"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."
};
return &err;
} catch (const InternalSolverError& ex) {
static const constexpr KiwiErr base {
KiwiErrInternalSolverError,
"An internal solver error occurred."
};
return new_error(&base, ex);
} catch (std::bad_alloc&) {
static const constexpr KiwiErr err {KiwiErrAlloc, "A memory allocation failed."};
return &err;
} catch (const std::exception& ex) {
return new_error(&kKiwiErrUnhandledCxxException, ex);
} catch (...) {
return &kKiwiErrUnhandledCxxException;
}
return nullptr;
}
template<typename P, typename R, typename F>
inline const KiwiErr* wrap_err(P ptr, F&& f) {
if (!ptr) {
return &kKiwiErrNullObjectArg0;
}
return wrap_err([&]() { f(ptr); });
}
template<typename P, typename R, typename F>
inline const KiwiErr* wrap_err(P ptr, R ref, F&& f) {
if (!ptr) {
return &kKiwiErrNullObjectArg0;
} else if (!ref) {
return &kKiwiErrNullObjectArg1;
}
return wrap_err([&]() { f(ptr, ref); });
}
} // namespace
extern "C" {
KiwiVarRef kiwi_var_new(const char* name) {
return make_var_cref(name ? name : "");
}
void kiwi_var_del(KiwiVarRef var) {
VariableRef(var).release();
}
KiwiVarRef kiwi_var_clone(KiwiVarRef var) {
return VariableRef(var).clone();
}
const char* kiwi_var_name(KiwiVarRef var) {
const VariableRef self(var);
return self ? self->name().c_str() : "(<null>)";
}
void kiwi_var_set_name(KiwiVarRef var, const char* name) {
VariableRef self(var);
if (self)
self->setName(name);
}
double kiwi_var_value(KiwiVarRef var) {
const VariableRef self(var);
return self ? self->value() : std::numeric_limits<double>::quiet_NaN();
}
void kiwi_var_set_value(KiwiVarRef var, double value) {
VariableRef self(var);
if (self)
self->setValue(value);
}
bool kiwi_var_eq(KiwiVarRef var, KiwiVarRef other) {
ConstVariableRef self(var); // const defect in upstream
const VariableRef other_ref(other);
return self && other_ref && self->equals(other_ref);
}
void kiwi_expression_del_vars(KiwiExpression* expr) {
if (!expr)
return;
for (auto* t = expr->terms; t != expr->terms + expr->term_count; ++t) {
VariableRef(t->var).release();
}
}
KiwiConstraintRef kiwi_constraint_new(
const KiwiExpression* lhs,
const KiwiExpression* rhs,
enum KiwiRelOp op,
double strength
) {
if (strength < 0.0) {
strength = kiwi::strength::required;
}
std::vector<Term> terms;
terms.reserve((lhs ? lhs->term_count : 0) + (rhs ? rhs->term_count : 0));
if (lhs) {
for (auto* t = lhs->terms; t != lhs->terms + lhs->term_count; ++t) {
ConstVariableRef var(t->var);
if (var)
terms.emplace_back(var.cref(), t->coefficient);
}
}
if (rhs) {
for (auto* t = rhs->terms; t != rhs->terms + rhs->term_count; ++t) {
ConstVariableRef var(t->var);
if (var)
terms.emplace_back(var.cref(), -t->coefficient);
}
}
return make_constraint_cref(
Expression(std::move(terms), (lhs ? lhs->constant : 0.0) - (rhs ? rhs->constant : 0.0)),
static_cast<RelationalOperator>(op),
strength
);
}
void kiwi_constraint_del(KiwiConstraintRef constraint) {
ConstraintRef(constraint).release();
}
KiwiConstraintRef kiwi_constraint_clone(KiwiConstraintRef constraint) {
return ConstraintRef(constraint).clone();
}
double kiwi_constraint_strength(KiwiConstraintRef constraint) {
const ConstraintRef self(constraint);
return self ? self->strength() : std::numeric_limits<double>::quiet_NaN();
}
enum KiwiRelOp kiwi_constraint_op(KiwiConstraintRef constraint) {
const ConstraintRef self(constraint);
return self ? static_cast<KiwiRelOp>(self->op()) : KiwiRelOp::KIWI_OP_EQ;
}
bool kiwi_constraint_violated(KiwiConstraintRef constraint) {
const ConstraintRef self(constraint);
return self ? self->violated() : 0;
}
int kiwi_constraint_expression(KiwiConstraintRef constraint, KiwiExpression* out, int out_size) {
const ConstraintRef self(constraint);
if (!self)
return 0;
const auto& expr = self->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 {make_var_cref(t.variable()), t.coefficient()};
++p;
}
out->term_count = p - out->terms;
out->constant = expr.constant();
return n_terms;
}
struct KiwiSolver {
unsigned error_mask;
Solver solver;
};
KiwiSolver* kiwi_solver_new(unsigned error_mask) {
return new KiwiSolver {error_mask};
}
void kiwi_solver_del(KiwiSolver* s) {
if (s)
delete s;
}
const KiwiErr* kiwi_solver_add_constraint(KiwiSolver* s, KiwiConstraintRef constraint) {
return wrap_err(s, ConstraintRef(constraint), [](auto* s, const auto c) {
s->solver.addConstraint(c);
});
}
const KiwiErr* kiwi_solver_remove_constraint(KiwiSolver* s, KiwiConstraintRef constraint) {
return wrap_err(s, ConstraintRef(constraint), [](auto* s, const auto c) {
s->solver.removeConstraint(c);
});
}
bool kiwi_solver_has_constraint(const KiwiSolver* s, KiwiConstraintRef constraint) {
ConstraintRef c(constraint);
if (!s || !c)
return 0;
return s->solver.hasConstraint(c);
}
const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* s, KiwiVarRef var, double strength) {
return wrap_err(s, VariableRef(var), [strength](auto* s, const auto v) {
s->solver.addEditVariable(v, strength);
});
}
const KiwiErr* kiwi_solver_remove_edit_var(KiwiSolver* s, KiwiVarRef var) {
return wrap_err(s, VariableRef(var), [](auto* s, const auto v) {
s->solver.removeEditVariable(v);
});
}
bool kiwi_solver_has_edit_var(const KiwiSolver* s, KiwiVarRef var) {
VariableRef v(var);
if (!s || !v)
return 0;
return s->solver.hasEditVariable(v);
}
const KiwiErr* kiwi_solver_suggest_value(KiwiSolver* s, KiwiVarRef var, double value) {
return wrap_err(s, VariableRef(var), [value](auto* s, const auto v) {
s->solver.suggestValue(v, value);
});
}
void kiwi_solver_update_vars(KiwiSolver* s) {
if (s)
s->solver.updateVariables();
}
void kiwi_solver_reset(KiwiSolver* s) {
if (s)
s->solver.reset();
}
void kiwi_solver_dump(const KiwiSolver* s) {
if (s)
s->solver.dump();
}
char* kiwi_solver_dumps(const KiwiSolver* s) {
if (!s)
return nullptr;
const auto str = s->solver.dumps(); // upstream library defect
const auto buf_size = str.size() + 1;
auto* buf = static_cast<char*>(std::malloc(buf_size));
if (!buf)
return nullptr;
std::memcpy(buf, str.c_str(), str.size() + 1);
return buf;
}
} // extern "C"

101
ckiwi/ckiwi.h Normal file
View File

@@ -0,0 +1,101 @@
#ifndef KIWI_CKIWI_H_
#define KIWI_CKIWI_H_
#ifdef __cplusplus
extern "C" {
#else
#include <stdbool.h>
#endif
#define KIWI_REF_ISNULL(ref) ((ref).impl_ == NULL)
// LuaJIT start
enum KiwiErrKind {
KiwiErrNone,
KiwiErrUnsatisfiableConstraint = 1,
KiwiErrUnknownConstraint,
KiwiErrDuplicateConstraint,
KiwiErrUnknownEditVariable,
KiwiErrDuplicateEditVariable,
KiwiErrBadRequiredStrength,
KiwiErrInternalSolverError,
KiwiErrAlloc,
KiwiErrNullObject,
KiwiErrUnknown,
};
enum KiwiRelOp { KIWI_OP_LE, KIWI_OP_GE, KIWI_OP_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[1]; // LuaJIT: struct KiwiTerm terms_[?];
} KiwiExpression;
typedef struct KiwiErr {
enum KiwiErrKind kind;
const char* message;
bool must_free;
} KiwiErr;
typedef struct KiwiSolver
KiwiSolver; // LuaJIT: typedef struct { 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);
// LuaJIT end
#ifdef __cplusplus
}
#endif
// Local Variables:
// mode: c++
// End:
#endif // KIWI_CKIWI_H_

1155
kiwi.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -145,7 +145,7 @@ public:
/* Dump a representation of the solver internals to stdout. /* Dump a representation of the solver internals to stdout.
*/ */
void dump() void dump() const
{ {
debug::dump( m_impl ); debug::dump( m_impl );
} }
@@ -153,7 +153,7 @@ public:
/* Dump a representation of the solver internals to a stream. /* Dump a representation of the solver internals to a stream.
*/ */
void dump( std::ostream& out ) void dump( std::ostream& out ) const
{ {
debug::dump( m_impl, out ); debug::dump( m_impl, out );
} }
@@ -161,7 +161,7 @@ public:
/* Dump a representation of the solver internals to a string. /* Dump a representation of the solver internals to a string.
*/ */
std::string dumps() std::string dumps() const
{ {
return debug::dumps( m_impl ); return debug::dumps( m_impl );
} }

View File

@@ -72,7 +72,7 @@ public:
} }
// operator== is used for symbolics // operator== is used for symbolics
bool equals(const Variable &other) bool equals(const Variable &other) const
{ {
return m_data == other.m_data; return m_data == other.m_data;
} }

35
ljkiwi-scm-1.rockspec Normal file
View File

@@ -0,0 +1,35 @@
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)",
LUA_INCDIR = "$(LUA_INCDIR)",
LIBFLAG = "$(LIBFLAG)",
LIB_EXT = "$(LIB_EXTENSION)",
OBJ_EXT = "$(OBJ_EXTENSION)",
},
install_variables = {
INST_LIBDIR = "$(LIBDIR)",
INST_LUADIR = "$(LUADIR)",
LIB_EXT = "$(LIB_EXTENSION)",
},
}

2
luakiwi.def Normal file
View File

@@ -0,0 +1,2 @@
EXPORTS
luaopen_ckiwi

88
luakiwi/.clang-format Normal file
View File

@@ -0,0 +1,88 @@
AccessModifierOffset: -2
AlignAfterOpenBracket: BlockIndent # New in v14. For earlier clang-format versions, use AlwaysBreak instead.
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: DontAlign
AlignOperands: false
AlignTrailingComments: false
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BreakAfterAttributes: Always
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterColon
BreakStringLiterals: false
ColumnLimit: 98
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
FixNamespaceComments: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentCaseLabels: true
IndentPPDirectives: BeforeHash
IndentWidth: 3
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
NamespaceIndentation: Inner
PointerAlignment: Left
ReferenceAlignment: Left # New in v13. int &name ==> int& name
ReflowComments: false
SeparateDefinitionBlocks: Always # New in v14.
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: false
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 4
UseTab: Never

147
luakiwi/luacompat.h Normal file
View File

@@ -0,0 +1,147 @@
#ifndef LKIWI_LUACOMPAT_H_
#define LKIWI_LUACOMPAT_H_
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#ifdef __cplusplus
}
#endif // __cplusplus
#if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM == 501
#define LUA_OPADD 0
#define LUA_OPSUB 1
#define LUA_OPMUL 2
#define LUA_OPDIV 3
#define LUA_OPMOD 4
#define LUA_OPPOW 5
#define LUA_OPUNM 6
static int lua_absindex(lua_State* L, int i) {
if (i < 0 && i > LUA_REGISTRYINDEX)
i += lua_gettop(L) + 1;
return i;
}
static lua_Number lua_tonumberx(lua_State* L, int i, int* isnum) {
lua_Number n = lua_tonumber(L, i);
if (isnum != NULL) {
*isnum = (n != 0 || lua_isnumber(L, i));
}
return n;
}
static lua_Integer lua_tointegerx(lua_State* L, int i, int* isnum) {
int ok = 0;
lua_Number n = lua_tonumberx(L, i, &ok);
if (ok) {
if (n == (lua_Integer)n) {
if (isnum)
*isnum = 1;
return (lua_Integer)n;
}
}
if (isnum)
*isnum = 0;
return 0;
}
static const char* luaL_tolstring(lua_State* L, int idx, size_t* len) {
if (!luaL_callmeta(L, idx, "__tostring")) {
int t = lua_type(L, idx), tt = 0;
char const* name = NULL;
switch (t) {
case LUA_TNIL:
lua_pushliteral(L, "nil");
break;
case LUA_TSTRING:
case LUA_TNUMBER:
lua_pushvalue(L, idx);
break;
case LUA_TBOOLEAN:
if (lua_toboolean(L, idx))
lua_pushliteral(L, "true");
else
lua_pushliteral(L, "false");
break;
default:
tt = luaL_getmetafield(L, idx, "__name");
name = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : lua_typename(L, t);
lua_pushfstring(L, "%s: %p", name, lua_topointer(L, idx));
if (tt != LUA_TNIL)
lua_replace(L, -2);
break;
}
} else {
if (!lua_isstring(L, -1))
luaL_error(L, "'__tostring' must return a string");
}
return lua_tolstring(L, -1, len);
}
#endif /* LUA_VERSION_NUM == 501 */
#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 502
static void compat_reverse(lua_State* L, int a, int b) {
for (; a < b; ++a, --b) {
lua_pushvalue(L, a);
lua_pushvalue(L, b);
lua_replace(L, a);
lua_replace(L, b);
}
}
static void lua_rotate(lua_State* L, int idx, int n) {
int n_elems = 0;
idx = lua_absindex(L, idx);
n_elems = lua_gettop(L) - idx + 1;
if (n < 0)
n += n_elems;
if (n > 0 && n < n_elems) {
luaL_checkstack(L, 2, "not enough stack slots available");
n = n_elems - n;
compat_reverse(L, idx, idx + n - 1);
compat_reverse(L, idx + n, idx + n_elems - 1);
compat_reverse(L, idx, idx + n_elems - 1);
}
}
static int lua_geti(lua_State* L, int index, lua_Integer i) {
index = lua_absindex(L, index);
lua_pushinteger(L, i);
lua_gettable(L, index);
return lua_type(L, -1);
}
#endif /* LUA_VERSION_NUM <= 502 */
#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM <= 503
static int luaL_typeerror(lua_State* L, int arg, const char* tname) {
const char* msg;
const char* typearg; /* name for the type of the actual argument */
if (luaL_getmetafield(L, arg, "__name") == LUA_TSTRING)
typearg = lua_tostring(L, -1); /* use the given type name */
else if (lua_type(L, arg) == LUA_TLIGHTUSERDATA)
typearg = "light userdata"; /* special name for messages */
else
typearg = luaL_typename(L, arg); /* standard name */
msg = lua_pushfstring(L, "%s expected, got %s", tname, typearg);
return luaL_argerror(L, arg, msg);
}
#endif /* LUA_VERSION_NUM <= 503 */
#if !defined(luaL_newlibtable)
#define luaL_newlibtable(L, l) lua_createtable(L, 0, sizeof(l) / sizeof((l)[0]) - 1)
#endif
#if !defined(luaL_checkversion)
#define luaL_checkversion(L) ((void)0)
#endif
#endif // LKIWI_LUACOMPAT_H_

264
luakiwi/luakiwi-int.h Normal file
View File

@@ -0,0 +1,264 @@
#ifndef LUAKIWI_INT_H_
#define LUAKIWI_INT_H_
#include <kiwi/kiwi.h>
#include <cstring>
#include "luacompat.h"
#define ARR_COUNT(x) ((int)(sizeof(x) / sizeof((x)[0])))
#if defined(__GNUC__) && !defined(LKIWI_NO_BUILTIN)
#define l_likely(x) (__builtin_expect(((x) != 0), 1))
#define l_unlikely(x) (__builtin_expect(((x) != 0), 0))
#else
#define l_likely(x) (x)
#define l_unlikely(x) (x)
#endif
// Lua 5.1 compatibility for missing lua_arith.
static void compat_arith_unm(lua_State* L) {
#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501
lua_Number n = lua_tonumber(L, -1);
if (n != 0 || lua_isnumber(L, -1)) {
lua_pop(L, 1);
lua_pushnumber(L, -n);
} else {
if (!luaL_callmeta(L, -1, "__unm"))
luaL_error(L, "attempt to perform arithmetic on a %s value", luaL_typename(L, -1));
lua_replace(L, -2);
}
#else
lua_arith(L, LUA_OPUNM);
#endif
}
// This version supports placeholders.
static void setfuncs(lua_State* L, const luaL_Reg* l, int nup) {
luaL_checkstack(L, nup, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */
if (l->func == NULL) /* place holder? */
lua_pushboolean(L, 0);
else {
int i;
for (i = 0; i < nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -nup);
lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */
}
lua_setfield(L, -(nup + 2), l->name);
}
lua_pop(L, nup); /* remove upvalues */
}
#define newlib(L, l) (lua_newtable((L)), setfuncs((L), (l), 0))
namespace {
using namespace kiwi;
enum KiwiErrKind {
KiwiErrNone,
KiwiErrUnsatisfiableConstraint = 1,
KiwiErrUnknownConstraint,
KiwiErrDuplicateConstraint,
KiwiErrUnknownEditVariable,
KiwiErrDuplicateEditVariable,
KiwiErrBadRequiredStrength,
KiwiErrInternalSolverError,
KiwiErrAlloc,
KiwiErrNullObject,
KiwiErrUnknown,
};
struct KiwiTerm {
Variable* var;
double coefficient;
};
struct KiwiExpression {
double constant;
int term_count;
Constraint* owner;
// https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
// Although using one-element arrays this way is discouraged, GCC handles accesses to
// trailing one-element array members analogously to zero-length arrays.
// The only reason avoiding FAM here is to support older MSVC.
// Otherwise this is a non-issue.
KiwiTerm terms[1];
};
// This structure was initially designed for LuaJIT FFI. It works OK for C++
// though it certainly isn't idiomatic.
struct KiwiErr {
enum KiwiErrKind kind;
const char* message;
bool must_free;
};
struct KiwiSolver {
unsigned error_mask;
Solver solver;
};
const KiwiErr* new_error(const KiwiErr* base, const std::exception& ex) {
if (!std::strcmp(ex.what(), base->message))
return base;
const auto msg_n = std::strlen(ex.what()) + 1;
auto* mem = static_cast<char*>(std::malloc(sizeof(KiwiErr) + msg_n));
if (!mem) {
return base;
}
const auto* err = new (mem) KiwiErr {base->kind, mem + sizeof(KiwiErr), true};
std::memcpy(const_cast<char*>(err->message), ex.what(), msg_n);
return err;
}
const constexpr KiwiErr kKiwiErrUnhandledCxxException {
KiwiErrUnknown,
"An unhandled C++ exception occurred."};
template<typename F>
inline const KiwiErr* wrap_err(F&& f) {
try {
f();
} catch (const UnsatisfiableConstraint&) {
static const constexpr KiwiErr err {
KiwiErrUnsatisfiableConstraint,
"The constraint cannot be satisfied."};
return &err;
} catch (const UnknownConstraint&) {
static const constexpr KiwiErr err {
KiwiErrUnknownConstraint,
"The constraint has not been added to the solver."};
return &err;
} catch (const DuplicateConstraint&) {
static const constexpr KiwiErr err {
KiwiErrDuplicateConstraint,
"The constraint has already been added to the solver."};
return &err;
} catch (const UnknownEditVariable&) {
static const constexpr KiwiErr err {
KiwiErrUnknownEditVariable,
"The edit variable has not been added to the solver."};
return &err;
} catch (const DuplicateEditVariable&) {
static const constexpr KiwiErr err {
KiwiErrDuplicateEditVariable,
"The edit variable has already been added to the solver."};
return &err;
} catch (const BadRequiredStrength&) {
static const constexpr KiwiErr err {
KiwiErrBadRequiredStrength,
"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."};
return new_error(&base, ex);
} catch (std::bad_alloc&) {
static const constexpr KiwiErr err {KiwiErrAlloc, "A memory allocation failed."};
return &err;
} catch (const std::exception& ex) {
return new_error(&kKiwiErrUnhandledCxxException, ex);
} catch (...) {
return &kKiwiErrUnhandledCxxException;
}
return nullptr;
}
template<typename P, typename R, typename F>
inline const KiwiErr* wrap_err(P& s, F&& f) {
return wrap_err([&]() { f(s); });
}
template<typename P, typename R, typename F>
inline const KiwiErr* wrap_err(P& s, R& ref, F&& f) {
return wrap_err([&]() { f(s, ref); });
}
inline Variable* kiwi_var_retain(Variable* var) {
alignas(Variable) unsigned char buf[sizeof(Variable)];
new (buf) Variable(*var);
return var;
}
inline Constraint* kiwi_constraint_retain(Constraint* c) {
alignas(Constraint) unsigned char buf[sizeof(Constraint)];
new (buf) Constraint(*c);
return c;
}
inline void kiwi_constraint_new(
const KiwiExpression* lhs,
const KiwiExpression* rhs,
RelationalOperator op,
double strength,
Constraint* mem
) {
if (strength < 0.0) {
strength = kiwi::strength::required;
}
std::vector<Term> terms;
terms.reserve((lhs ? lhs->term_count : 0) + (rhs ? rhs->term_count : 0));
if (lhs) {
for (auto* t = lhs->terms; t != lhs->terms + lhs->term_count; ++t) {
terms.emplace_back(*t->var, t->coefficient);
}
}
if (rhs) {
for (auto* t = rhs->terms; t != rhs->terms + rhs->term_count; ++t) {
terms.emplace_back(*t->var, -t->coefficient);
}
}
new (mem) Constraint(
Expression(std::move(terms), (lhs ? lhs->constant : 0.0) - (rhs ? rhs->constant : 0.0)),
static_cast<RelationalOperator>(op),
strength
);
}
inline const KiwiErr* kiwi_solver_add_constraint(Solver& s, const Constraint& constraint) {
return wrap_err(s, constraint, [](auto& solver, const auto& c) { solver.addConstraint(c); });
}
inline const KiwiErr* kiwi_solver_remove_constraint(Solver& s, const Constraint& constraint) {
return wrap_err(s, constraint, [](auto& solver, const auto& c) {
solver.removeConstraint(c);
});
}
inline const KiwiErr* kiwi_solver_add_edit_var(Solver& s, const Variable& var, double strength) {
return wrap_err(s, var, [strength](auto& solver, const auto& v) {
solver.addEditVariable(v, strength);
});
}
inline const KiwiErr* kiwi_solver_remove_edit_var(Solver& s, const Variable& var) {
return wrap_err(s, var, [](auto& solver, const auto& v) { solver.removeEditVariable(v); });
}
inline const KiwiErr* kiwi_solver_suggest_value(Solver& s, const Variable& var, double value) {
return wrap_err(s, var, [value](auto& solver, const auto& v) {
solver.suggestValue(v, value);
});
}
} // namespace
// Local Variables:
// mode: c++
// End:
#endif // LUAKIWI_INT_H_

1689
luakiwi/luakiwi.cpp Normal file

File diff suppressed because it is too large Load Diff

125
spec/constraint_spec.lua Normal file
View File

@@ -0,0 +1,125 @@
expose("module", function()
require("kiwi")
end)
describe("Constraint", function()
local kiwi = require("kiwi")
local LUA_VERSION = tonumber(_VERSION:match("%d+%.%d+"))
describe("construction", function()
local v, lhs
before_each(function()
v = kiwi.Var("foo")
lhs = v + 1
end)
it("has correct type", function()
assert.True(kiwi.is_constraint(kiwi.Constraint()))
assert.False(kiwi.is_constraint(v))
end)
it("default op and strength", function()
local c = kiwi.Constraint(lhs)
assert.equal("EQ", c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end)
it("configure op", function()
local c = kiwi.Constraint(lhs, nil, "LE")
assert.equal("LE", c:op())
end)
it("configure strength", function()
local c = kiwi.Constraint(lhs, nil, "GE", kiwi.strength.STRONG)
assert.equal(kiwi.strength.STRONG, c:strength())
end)
-- TODO: standardize formatting
it("formats well", function()
local c = kiwi.Constraint(lhs)
if LUA_VERSION <= 5.2 then
assert.equal("1 foo + 1 == 0 | required", tostring(c))
else
assert.equal("1.0 foo + 1.0 == 0 | required", tostring(c))
end
c = kiwi.Constraint(lhs * 2, nil, "GE", kiwi.strength.STRONG)
if LUA_VERSION <= 5.2 then
assert.equal("2 foo + 2 >= 0 | strong", tostring(c))
else
assert.equal("2.0 foo + 2.0 >= 0 | strong", tostring(c))
end
c = kiwi.Constraint(lhs / 2, nil, "LE", kiwi.strength.MEDIUM)
assert.equal("0.5 foo + 0.5 <= 0 | medium", tostring(c))
c = kiwi.Constraint(lhs, kiwi.Expression(3), "GE", kiwi.strength.WEAK)
if LUA_VERSION <= 5.2 then
assert.equal("1 foo + -2 >= 0 | weak", tostring(c))
else
assert.equal("1.0 foo + -2.0 >= 0 | weak", tostring(c))
end
end)
it("rejects invalid args", function()
assert.error(function()
local _ = kiwi.Constraint(1)
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, 1)
end)
assert.error(function()
local _ = kiwi.Constraint("")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, "")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, nil, "foo")
end)
assert.error(function()
local _ = kiwi.Constraint(lhs, nil, "LE", "foo")
end)
end)
it("combines lhs and rhs", function()
local v2 = kiwi.Var("bar")
local rhs = kiwi.Expression(3, 5 * v2, 3 * v)
local c = kiwi.Constraint(lhs, rhs)
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(-2.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-5.0, t[2].coefficient)
assert.equal(-2.0, e.constant)
end)
end)
describe("method", function()
local c, v
before_each(function()
v = kiwi.Var("foo")
c = kiwi.Constraint(2 * v + 1)
end)
it("violated", function()
assert.True(c:violated())
v:set(-0.5)
assert.False(c:violated())
end)
it("add/remove constraint", function()
local s = kiwi.Solver()
c:add_to(s)
assert.True(s:has_constraint(c))
c:remove_from(s)
assert.False(s:has_constraint(c))
end)
end)
end)

335
spec/solver_spec.lua Normal file
View File

@@ -0,0 +1,335 @@
expose("module", function()
require("kiwi")
end)
describe("solver", function()
local kiwi = require("kiwi")
---@type kiwi.Solver
local solver
before_each(function()
solver = kiwi.Solver()
end)
it("should create a solver", function()
assert.True(kiwi.is_solver(solver))
assert.False(kiwi.is_solver(kiwi.Term(kiwi.Var("v1"))))
end)
describe("edit variables", function()
local v1, v2, v3
before_each(function()
v1 = kiwi.Var("foo")
v2 = kiwi.Var("bar")
v3 = kiwi.Var("baz")
end)
describe("add_edit_var", function()
it("should add a variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v1))
end)
it("should return the argument", function()
assert.equal(v1, solver:add_edit_var(v1, kiwi.strength.STRONG))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:add_edit_var("", kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_var(v1, "") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should require a strength argument", function()
assert.error(function()
solver:add_edit_var(v1) ---@diagnostic disable-line: missing-parameter
end)
end)
it("should error on duplicate variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
local _, err = pcall(function()
return solver:add_edit_var(v1, kiwi.strength.STRONG)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
end)
it("should error on invalid strength", function()
local _, err = pcall(function()
return solver:add_edit_var(v1, kiwi.strength.REQUIRED)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
end)
it("should return errors for duplicate variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
local ret, err = solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.Nil(err)
ret, err = solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.equal(v1, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("should return errors for invalid strength", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
---@diagnostic disable: need-check-nil
local ret, err = solver:add_edit_var(v2, kiwi.strength.REQUIRED)
assert.equal(v2, ret)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
assert.error(function()
kiwi.Solver.add_edit_var(nil, v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("tolerates a nil var", function()
assert.error(function()
solver:add_edit_var(nil, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
end)
end)
describe("add_edit_vars", function()
it("should add variables", function()
solver:add_edit_vars({ v1, v2 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v1))
assert.True(solver:has_edit_var(v2))
assert.False(solver:has_edit_var(v3))
end)
it("should return the argument", function()
local arg = { v1, v2, v3 }
assert.equal(arg, solver:add_edit_vars(arg, kiwi.strength.STRONG))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:add_edit_vars(v1, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_vars("", kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:add_edit_vars(v1, "") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should require a strength argument", function()
assert.error(function()
solver:add_edit_vars({ v1, v2 }) ---@diagnostic disable-line: missing-parameter
end)
end)
it("should error on duplicate variable", function()
local _, err = pcall(function()
return solver:add_edit_vars({ v1, v2, v3, v2, v3 }, kiwi.strength.STRONG)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
end)
it("should error on invalid strength", function()
local _, err = pcall(function()
return solver:add_edit_vars({ v1, v2 }, kiwi.strength.REQUIRED)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
end)
it("should return errors for duplicate variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
local ret, err = solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.Nil(err)
local arg = { v1, v2, v3 }
ret, err = solver:add_edit_vars(arg, kiwi.strength.STRONG)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrDuplicateEditVariable", err.kind)
assert.equal("The edit variable has already been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("should return errors for invalid strength", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrBadRequiredStrength" })
arg = { v2, v3 }
local ret, err = solver:add_edit_vars(arg, kiwi.strength.REQUIRED)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrBadRequiredStrength", err.kind)
assert.equal("A required strength cannot be used in this context.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
assert.has_error(function()
kiwi.Solver.add_edit_vars(nil, { v1, v2 }, kiwi.strength.STRONG) ---@diagnostic disable-line: param-type-mismatch
end)
end)
end)
describe("remove_edit_var", function()
it("should remove a variable", function()
solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v2))
solver:remove_edit_var(v2)
assert.True(solver:has_edit_var(v1))
assert.False(solver:has_edit_var(v2))
assert.True(solver:has_edit_var(v3))
end)
it("should return the argument", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
assert.equal(v1, solver:remove_edit_var(v1))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:remove_edit_var("") ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:remove_edit_var({ v1 }) ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should error on unknown variable", function()
solver:add_edit_var(v1, kiwi.strength.STRONG)
local _, err = pcall(function()
return solver:remove_edit_var(v2)
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
end)
it("should return errors if requested", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrUnknownEditVariable" })
local ret, err = solver:remove_edit_var(v1)
assert.equal(v1, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v1, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
assert.has_error(function()
kiwi.Solver.remove_edit_var(nil, v1) ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("tolerates a nil var", function()
assert.has_error(function()
solver:remove_edit_var(nil) ---@diagnostic disable-line: param-type-mismatch
end)
end)
end)
describe("remove_edit_vars", function()
it("should remove variables", function()
solver:add_edit_vars({ v1, v2, v3 }, kiwi.strength.STRONG)
assert.True(solver:has_edit_var(v2))
assert.True(solver:has_edit_var(v3))
solver:remove_edit_vars({ v2, v3 })
assert.False(solver:has_edit_var(v2))
assert.False(solver:has_edit_var(v3))
end)
it("should return the argument", function()
local arg = { v1, v2, v3 }
solver:add_edit_vars(arg, kiwi.strength.STRONG)
assert.equal(arg, solver:remove_edit_vars(arg))
end)
it("should error on incorrect type", function()
assert.error(function()
solver:remove_edit_vars(v1) ---@diagnostic disable-line: param-type-mismatch
end)
assert.error(function()
solver:remove_edit_vars("") ---@diagnostic disable-line: param-type-mismatch
end)
end)
it("should error on unknown variables", function()
local _, err = pcall(function()
return solver:remove_edit_vars({ v2, v1 })
end)
assert.True(kiwi.is_error(err))
assert.True(kiwi.is_solver(err.solver))
assert.equal(v2, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
end)
it("should return errors for unknown variables", function()
solver:set_error_mask({ "KiwiErrDuplicateEditVariable", "KiwiErrUnknownEditVariable" })
local ret, err = solver:add_edit_vars({ v1, v2 }, kiwi.strength.STRONG)
assert.Nil(err)
local arg = { v1, v2, v3 }
ret, err = solver:remove_edit_vars(arg)
assert.equal(arg, ret)
assert.True(kiwi.is_error(err))
---@diagnostic disable: need-check-nil
assert.True(kiwi.is_solver(err.solver))
assert.equal(v3, err.item)
assert.equal("KiwiErrUnknownEditVariable", err.kind)
assert.equal("The edit variable has not been added to the solver.", err.message)
---@diagnostic enable: need-check-nil
end)
it("tolerates a nil self", function()
assert.has_error(function()
kiwi.Solver.remove_edit_vars(nil, { v1, v2 }) ---@diagnostic disable-line: param-type-mismatch
end)
end)
end)
end)
end)

186
spec/var_spec.lua Normal file
View File

@@ -0,0 +1,186 @@
expose("module", function()
require("kiwi")
end)
describe("Var", function()
local kiwi = require("kiwi")
it("construction", function()
assert.True(kiwi.is_var(kiwi.Var()))
assert.False(kiwi.is_var(kiwi.Constraint()))
assert.error(function()
kiwi.Var({})
end)
end)
describe("method", function()
local v
before_each(function()
v = kiwi.Var("goo")
end)
it("has settable name", function()
assert.equal("goo", v:name())
v:set_name("Δ")
assert.equal("Δ", v:name())
assert.error(function()
v:set_name({})
end)
end)
it("has a initial value of 0.0", function()
assert.equal(0.0, v:value())
end)
it("has a settable value", function()
v:set(47.0)
assert.equal(47.0, v:value())
end)
it("neg", function()
local neg = -v --[[@as kiwi.Term]]
assert.True(kiwi.is_term(neg))
assert.equal(v, neg.var)
assert.equal(-1.0, neg.coefficient)
end)
describe("bin op", function()
local v2
before_each(function()
v2 = kiwi.Var("foo")
end)
it("mul", function()
for _, prod in ipairs({ v * 2.0, 2 * v }) do
assert.True(kiwi.is_term(prod))
assert.equal(v, prod.var)
assert.equal(2.0, prod.coefficient)
end
assert.error(function()
local _ = v * v2
end)
end)
it("div", function()
local quot = v / 2.0
assert.True(kiwi.is_term(quot))
assert.equal(v, quot.var)
assert.equal(0.5, quot.coefficient)
assert.error(function()
local _ = v / v2
end)
end)
it("add", function()
for _, sum in ipairs({ v + 2.0, 2 + v }) do
assert.True(kiwi.is_expression(sum))
assert.equal(2.0, sum.constant)
local terms = sum:terms()
assert.equal(1, #terms)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v, terms[1].var)
end
local sum = v + v2
assert.True(kiwi.is_expression(sum))
assert.equal(0, sum.constant)
local terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
assert.error(function()
local _ = v + "foo"
end)
assert.error(function()
local _ = v + {}
end)
end)
it("sub", function()
local constants = { -2, 2 }
for i, diff in ipairs({ v - 2.0, 2 - v }) do
local constant = constants[i]
assert.True(kiwi.is_expression(diff))
assert.equal(constant, diff.constant)
local terms = diff:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(constant < 0 and 1 or -1, terms[1].coefficient)
end
local diff = v - v2
assert.True(kiwi.is_expression(diff))
assert.equal(0, diff.constant)
local terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.error(function()
local _ = v - "foo"
end)
assert.error(function()
local _ = v - {}
end)
end)
it("constraint var op expr", function()
local ops = { "LE", "EQ", "GE" }
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = v[meth](v, v2 + 1)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
-- order can be randomized due to use of map
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(1.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-1.0, t[2].coefficient)
assert.equal(-1, e.constant)
assert.equal(ops[i], c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end
end)
it("constraint var op var", function()
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = v[meth](v, v2)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local t = e:terms()
assert.equal(2, #t)
-- order can be randomized due to use of map
if t[1].var ~= v then
t[1], t[2] = t[2], t[1]
end
assert.equal(v, t[1].var)
assert.equal(1.0, t[1].coefficient)
assert.equal(v2, t[2].var)
assert.equal(-1.0, t[2].coefficient)
assert.equal(0, e.constant)
end
end)
end)
end)
end)

30
t.lua Normal file
View File

@@ -0,0 +1,30 @@
local kiwi = require("kiwi")
local c
--debug.getupvalue
do
local v1 = kiwi.Var("v1")
local v2 = kiwi.Var("v2")
local v3 = kiwi.Var("v3")
local v4 = kiwi.Var("v4")
local v5 = kiwi.Var("v5")
local c = (3 * v1 + 4 * v2 + 6 * v3):eq(0)
local e = c:expression()
print(e)
end
collectgarbage("collect")
collectgarbage("collect")
-- c = (3 * v1 + 4 * v2 + 6 * v3):eq(0)
-- local t = c:expression():terms()
-- print(t[2].var)
-- for k, v in ipairs(t) do
-- print(k, v.var, v.coefficient)
-- end
-- for k, v in pairs(kiwi.ErrKind) do
-- print(k, v)
-- end

204
tmp/enaml.lua Normal file
View File

@@ -0,0 +1,204 @@
local kiwi = require("kiwi")
local Var = kiwi.Var
---@param solver kiwi.Solver
local function build_solver(solver)
-- create custom strength
--
local width = Var("width")
local height = Var("height")
local mmedium = kiwi.strength.create(0.0, 1.0, 0.0, 1.25)
local smedium = kiwi.strength.create(0.0, 100, 0.0)
local left = Var("left")
local top = Var("top")
local contents_top = Var("contents_top")
local contents_bottom = Var("contents_bottom")
local contents_left = Var("contents_left")
local contents_right = Var("contents_right")
local midline = Var("midline")
local ctleft = Var("ctleft")
local ctheight = Var("ctheight")
local cttop = Var("cttop")
local ctwidth = Var("ctwidth")
local lb1left = Var("lb1left")
local lb1height = Var("lb1height")
local lb1top = Var("lb1top")
local lb1width = Var("lb1width")
local lb2left = Var("lb2left")
local lb2height = Var("lb2height")
local lb2top = Var("lb2top")
local lb2width = Var("lb2width")
local lb3left = Var("lb3left")
local lb3height = Var("lb3height")
local lb3top = Var("lb3top")
local lb3width = Var("lb3width")
local fl1left = Var("fl1left")
local fl1height = Var("fl1height")
local fl1top = Var("fl1top")
local fl1width = Var("fl1width")
local fl2left = Var("fl2left")
local fl2height = Var("fl2height")
local fl2top = Var("fl2top")
local fl2width = Var("fl2width")
local fl3left = Var("fl3left")
local fl3height = Var("fl3height")
local fl3top = Var("fl3top")
local fl3width = Var("fl3width")
solver:add_edit_var(width, kiwi.strength.STRONG)
solver:add_edit_var(height, kiwi.strength.STRONG)
local strong, medium, weak = kiwi.strength.STRONG, kiwi.strength.MEDIUM, kiwi.strength.WEAK
local constraints = {
(left + -0):ge(0),
(height + 0):eq(0, medium),
(top + -0):ge(0),
(width + -0):ge(0),
(height + -0):ge(0),
(-top + contents_top + -10):eq(0),
(lb3height + -16):eq(0, strong),
(lb3height + -16):ge(0, strong),
(ctleft + -0):ge(0),
(cttop + -0):ge(0),
(ctwidth + -0):ge(0),
(ctheight + -0):ge(0),
(fl3left + -0):ge(0),
(ctheight + -24):ge(0, smedium),
(ctwidth + -1.67772e+07):le(0, smedium),
(ctheight + -24):le(0, smedium),
(fl3top + -0):ge(0),
(fl3width + -0):ge(0),
(fl3height + -0):ge(0),
(lb1width + -67):eq(0, weak),
(lb2width + -0):ge(0),
(lb2height + -0):ge(0),
(fl2height + -0):ge(0),
(lb3left + -0):ge(0),
(fl2width + -125):ge(0, strong),
(fl2height + -21):eq(0, strong),
(fl2height + -21):ge(0, strong),
(lb3top + -0):ge(0),
(lb3width + -0):ge(0),
(fl1left + -0):ge(0),
(fl1width + -0):ge(0),
(lb1width + -67):ge(0, strong),
(fl2left + -0):ge(0),
(lb2width + -66):eq(0, weak),
(lb2width + -66):ge(0, strong),
(lb2height + -16):eq(0, strong),
(fl1height + -0):ge(0),
(fl1top + -0):ge(0),
(lb2top + -0):ge(0),
(-lb2top + lb3top + -lb2height + -10):eq(0, mmedium),
(-lb3top + -lb3height + fl3top + -10):ge(0),
(-lb3top + -lb3height + fl3top + -10):eq(0, mmedium),
(contents_bottom + -fl3height + -fl3top + -0):eq(0, mmedium),
(fl1top + -contents_top + 0):ge(0),
(fl1top + -contents_top + 0):eq(0, mmedium),
(contents_bottom + -fl3height + -fl3top + -0):ge(0),
(-left + -width + contents_right + 10):eq(0),
(-top + -height + contents_bottom + 10):eq(0),
(-left + contents_left + -10):eq(0),
(lb3left + -contents_left + 0):eq(0, mmedium),
(fl1left + -midline + 0):eq(0, strong),
(fl2left + -midline + 0):eq(0, strong),
(ctleft + -midline + 0):eq(0, strong),
(fl1top + 0.5 * fl1height + -lb1top + -0.5 * lb1height + 0):eq(0, strong),
(lb1left + -contents_left + 0):ge(0),
(lb1left + -contents_left + 0):eq(0, mmedium),
(-lb1left + fl1left + -lb1width + -10):ge(0),
(-lb1left + fl1left + -lb1width + -10):eq(0, mmedium),
(-fl1left + contents_right + -fl1width + -0):ge(0),
(width + 0):eq(0, medium),
(-fl1top + fl2top + -fl1height + -10):ge(0),
(-fl1top + fl2top + -fl1height + -10):eq(0, mmedium),
(cttop + -fl2top + -fl2height + -10):ge(0),
(-ctheight + -cttop + fl3top + -10):ge(0),
(contents_bottom + -fl3height + -fl3top + -0):ge(0),
(cttop + -fl2top + -fl2height + -10):eq(0, mmedium),
(-fl1left + contents_right + -fl1width + -0):eq(0, mmedium),
(-lb2top + -0.5 * lb2height + fl2top + 0.5 * fl2height + 0):eq(0, strong),
(-contents_left + lb2left + 0):ge(0),
(-contents_left + lb2left + 0):eq(0, mmedium),
(fl2left + -lb2width + -lb2left + -10):ge(0),
(-ctheight + -cttop + fl3top + -10):eq(0, mmedium),
(contents_bottom + -fl3height + -fl3top + -0):eq(0, mmedium),
(lb1top + -0):ge(0),
(lb1width + -0):ge(0),
(lb1height + -0):ge(0),
(fl2left + -lb2width + -lb2left + -10):eq(0, mmedium),
(-fl2left + -fl2width + contents_right + -0):eq(0, mmedium),
(-fl2left + -fl2width + contents_right + -0):ge(0),
(lb3left + -contents_left + 0):ge(0),
(lb1left + -0):ge(0),
(0.5 * ctheight + cttop + -lb3top + -0.5 * lb3height + 0):eq(0, strong),
(ctleft + -lb3left + -lb3width + -10):ge(0),
(-ctwidth + -ctleft + contents_right + -0):ge(0),
(ctleft + -lb3left + -lb3width + -10):eq(0, mmedium),
(fl3left + -contents_left + 0):ge(0),
(fl3left + -contents_left + 0):eq(0, mmedium),
(-ctwidth + -ctleft + contents_right + -0):eq(0, mmedium),
(-fl3left + contents_right + -fl3width + -0):eq(0, mmedium),
(-contents_top + lb1top + 0):ge(0),
(-contents_top + lb1top + 0):eq(0, mmedium),
(-fl3left + contents_right + -fl3width + -0):ge(0),
(lb2top + -lb1top + -lb1height + -10):ge(0),
(-lb2top + lb3top + -lb2height + -10):ge(0),
(lb2top + -lb1top + -lb1height + -10):eq(0, mmedium),
(fl1height + -21):eq(0, strong),
(fl1height + -21):ge(0, strong),
(lb2left + -0):ge(0),
(lb2height + -16):ge(0, strong),
(fl2top + -0):ge(0),
(fl2width + -0):ge(0),
(lb1height + -16):ge(0, strong),
(lb1height + -16):eq(0, strong),
(fl3width + -125):ge(0, strong),
(fl3height + -21):eq(0, strong),
(fl3height + -21):ge(0, strong),
(lb3height + -0):ge(0),
(ctwidth + -119):ge(0, smedium),
(lb3width + -24):eq(0, weak),
(lb3width + -24):ge(0, strong),
(fl1width + -125):ge(0, strong),
}
for _, c in ipairs(constraints) do
print(c)
solver:add_constraint(c)
end
return width, height
end
local function main()
local sizes = {
{ w = 400, h = 600 },
{ w = 600, h = 400 },
{ w = 800, h = 1200 },
{ w = 1200, h = 800 },
{ w = 400, h = 800 },
{ w = 800, h = 400 },
}
for i = 1, 1 do
local solver = kiwi.Solver()
local width, height = build_solver(solver)
for _, size in ipairs(sizes) do
solver:suggest_value(width, size.w)
solver:suggest_value(height, size.h)
solver:update_vars()
print(width:value(), height:value())
end
end
end
main()
collectgarbage("collect")
collectgarbage("collect")
collectgarbage("collect")
collectgarbage("collect")

90
tmp/ex.lua Normal file
View File

@@ -0,0 +1,90 @@
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
local constraints
for i = 1, 200 do
q = kiwi.Term(left_edge, 2.0)
end
for i = 1, 50, 1 do
-- stylua: ignore start
constraints = {
left_edge :eq(0.0),
-- two buttons are the same width
b1.width :eq(b2.width),
-- button1 starts 50 for the left margin
(1.00 * 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
--
k = kiwi.Strength.create(0.0, 1.0, 0.0, 1.25)
end
local solver = kiwi.Solver()
for _, c in ipairs(constraints) do
print(c)
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()
-- solver:dump()
-- print(b1) -- Button(50, 113)
-- print(b2) -- Button(337, 113)
-- print(right_edge:value()) -- 500
-- print(solver:dumps())
-- solver = kiwi.Solver()
-- local trailing = Var("trailing")
-- local leading = Var("leading")
-- solver:add_constraint(kiwi.new_single_constraint(trailing, 86))
-- solver:add_constraint(kiwi.new_pair_constraint(trailing, leading, 8.0))
-- print(solver:dumps())
-- solver:update_vars()
-- print(trailing:value()) -- 86
-- print(leading:value()) -- 76

28
tmp/ex2.lua Normal file
View File

@@ -0,0 +1,28 @@
local kiwi = require("kiwi")
local Var = kiwi.Var
local function doit(v, i)
v:set(i)
end
local v = Var("fuckit")
local s = kiwi.Solver()
local e
for i = 1, 1000 do
e = kiwi.Expression(5.0, kiwi.Term(v, 3.0), kiwi.Term(v, 2.0))
end
--local e = (v + 1)
s:add_constraint(e:eq(100))
print(v:value())
s:update_vars()
v:set_name("fuck")
print(v:value())
s:dump()

49
tmp/ex3.lua Normal file
View File

@@ -0,0 +1,49 @@
local kiwi = require("kiwi")
local ffi = require("ffi")
local t
local e
local strformat = string.format
local Strength = kiwi.Strength
--local bad_str = "-1 v8 + -1 v4 + -1 v2 + -4 v9 + -2 v3 + -1 v6 + -1 v5 + -1 v1 + -2 v7 + -5"
-- local b = ""
function s(self)
local ops = {
[0] = "<=",
">=",
"==",
}
local strengths = {
[Strength.REQUIRED] = "required",
[Strength.STRONG] = "strong",
[Strength.MEDIUM] = "medium",
[Strength.WEAK] = "weak",
}
local strength = self:strength()
--local e = self:expression()
local s = strformat("%s %s %s", "aaa", tostring(self:expression()), tonumber(self:op()))
return s
end
for i = 1, 2000 do
local v1 = kiwi.Var("v1")
local v2 = kiwi.Var("v2")
local v3 = kiwi.Var("v3")
local v4 = kiwi.Var("v4")
local v5 = kiwi.Var("v5")
local v6 = kiwi.Var("v6")
local v7 = kiwi.Var("v7")
local v8 = kiwi.Var("v8")
local v9 = kiwi.Var("v9")
local v10 = kiwi.Var("v9")
do
--
local c = ((-(v1 + v2 + 2 * v3 + v4 + 3)):eq(v5 + v6 + 2 * v7 + v8 + 4 * v9 + 3))
s(c)
end
end
print(t)

37
tmp/ex4.lua Normal file
View File

@@ -0,0 +1,37 @@
local kiwi = require("kiwi")
local ffi = require("ffi")
local Var = kiwi.Var
local v1 = Var("v1")
local v2 = Var("v2")
local v3 = Var("v3")
local v4 = Var("v4")
local v5 = Var("v5")
local v6 = Var("v6")
local f1 = kiwi.f1
local f2 = kiwi.f2
local single = kiwi.constraints.single
local function execute_times(f, times)
local begin = os.clock()
for _ = 1, times do
f()
end
local finish = os.clock()
return finish - begin
end
local t = execute_times(function()
return kiwi.constraints.pair_ratio(v1, 2.0, v2, 3.0)
end, 6000000)
print(t)
-- local t = execute_times(function()
-- return pair_ratio2(v1, 2.0, v2, 3.0)
-- end, 2000000)
-- print(t)

41
tmp/ex5.lua Normal file
View File

@@ -0,0 +1,41 @@
local kiwi = require("kiwi")
local Var = kiwi.Var
x1 = Var("x1")
x2 = Var("x2")
xm = Var("xm")
local constraints = { x1:ge(0), x2:le(100), x2:ge(x1 + 10), xm:eq((x1 + x2) / 2) }
local solver = kiwi.Solver()
for _, c in ipairs(constraints) do
solver:add_constraint(c)
end
local c = kiwi.constraints.single(x1, 40, "EQ", kiwi.Strength.WEAK)
solver:add_constraint(c)
constraints[#constraints + 1] = c
solver:add_edit_var(xm, kiwi.Strength.STRONG)
solver:suggest_value(xm, 60)
solver:update_vars()
print(xm:value(), x1:value(), x2:value())
for _, c in ipairs(constraints) do
print(c, c:violated())
end
solver:suggest_value(xm, 90)
solver:update_vars()
print(xm:value(), x1:value(), x2:value())
for _, c in ipairs(constraints) do
print(c, c:violated())
end

26
tmp/t.lua Normal file
View File

@@ -0,0 +1,26 @@
local k = require("kiwi")
s = k.Solver()
--s:set_error_mask({ "KiwiErrUnknownEditVariable" }, false)
local v1 = k.Var("v1")
local v2 = k.Var("v2")
local v3 = k.Var("v3")
local v4 = k.Var("v4")
print((2 * v1 - 5 * v2))
os.exit()
local c1 = k.constraints.pair_ratio(v1, -4, v2, 22, "GE", 1000)
local c2 = k.constraints.pair_ratio(v3, -6, v4, 29, "LE", 1000)
local vo = c1:expression():terms()[1].var
print(v1 == vo)
-- print(v1:name())
-- print(v1 * 5)
local e = k.Expression(6, k.Term(v1, 2), k.Term(v2, 3))
--print(e)
--print(k.strength.MEDIUM)

66
tmp/test.lua Normal file
View File

@@ -0,0 +1,66 @@
local kiwi = require("kiwi")
local Var = kiwi.Var
local Strength = kiwi.Strength
local Button_mt = {}
---@class Button
---@field left kiwi.Var
---@field width kiwi.Var
---@overload fun(identifier: string): Button
local Button = setmetatable({}, Button_mt)
function Button_mt.__call(_, identifier)
return setmetatable({
left = Var("left" .. identifier),
width = Var("width" .. identifier),
}, {
__index = Button,
__tostring = function(self)
return "Button(" .. self.left:value() .. ", " .. self.width:value() .. ")"
end,
})
end
local b1 = Button("b1")
local b2 = Button("b2")
local left_limit = kiwi.Var("left")
local right_limit = kiwi.Var("width")
-- stylua: ignore start
local constraints = {
left_limit :eq( 0 ),
b1.width :eq( b2.width ),
b1.left :eq( left_limit + 50),
right_limit:eq( b2.left + b2.width + 50),
b2.left :ge( b1.left + b1.width + 100),
b1.width :ge( 87 ),
b1.width :eq( 87, Strength.STRONG),
b2.width :ge( 113 ),
b2.width :eq( 113, Strength.STRONG),
}
-- stylua: ignore end
local s = kiwi.Solver()
for _, c in ipairs(constraints) do
s:add_constraint(c)
end
print(constraints[4]:expression())
print(constraints[5]:expression())
kiwi.Constraint(b1.left + 1, "EQ")
s:update_vars()
print(b1)
print(b2)
print(left_limit:value())
print(right_limit:value())
print(s:dumps())