18 Commits

Author SHA1 Message Date
08e9bf08e7 Initial implementation of tuple demonstration
This allows allocating a buffer of double pointers pointing to
the memory location in a bunch of variables. It is not particularly
ergonomic as is, and it seems unlikely the real world performance
benefit will exist.
2024-02-27 21:59:10 -06:00
ef29b8abcb unprivate kiwi::VariableData 2024-02-27 21:53:16 -06:00
59cb4b3c4f Update README 2024-02-27 13:24:24 -06:00
55a3aa1e6f allow debugging from luarocks build 2024-02-27 12:59:58 -06:00
dc36e719eb squelch silly MSVC warning 2024-02-26 23:16:11 -06:00
d2e769ea30 Add release to workflow 2024-02-26 22:46:21 -06:00
3e56c503e4 fix code coverage 2024-02-26 22:24:48 -06:00
f68c24d9ea Add some more unit tests 2024-02-26 17:28:23 -06:00
2b76ba96ac Replace a few loops with ffi.copy where possible 2024-02-26 16:59:01 -06:00
98a3fff28f Add gcov 2024-02-26 14:53:49 -06:00
c35cea6213 Slightly more robust resource management 2024-02-26 12:47:46 -06:00
94a8bdca79 Better exception safety for Lua binding 2024-02-26 12:15:33 -06:00
ae5e4b3419 Makefile tweaks 2024-02-26 11:05:32 -06:00
3ffd84e348 release 0.1.0 2024-02-25 20:48:26 -06:00
6d7dbbfe74 CI attempt 1 2024-02-25 20:28:42 -06:00
f18610d526 add windows (MSVC) configuration
Update makefiles
2024-02-25 17:17:46 -06:00
9b245b10e3 Initial version, pending windows 2024-02-25 05:38:29 -06:00
359c31a0af Replace kiwi Constraint and Variable types
Going to replace these types since they are so stupid simple and
the originals are not conducive to integrating efficienctly outside
C++. We need a well defined way to get a pointer/reference to the
shared data. The proxy objects frustrate that, but they are
what is baked into the library. The public interface is not altered
except for the ability to access and construct from pointers.
2024-02-25 05:13:08 -06:00
28 changed files with 5680 additions and 146 deletions

View File

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

View File

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

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

@@ -0,0 +1,110 @@
---
name: Busted
on: [push, pull_request]
jobs:
busted:
strategy:
fail-fast: false
matrix:
lua_version:
[
"luajit-openresty",
"luajit-2.1.0-beta3",
"luajit-git",
"5.4.6",
"5.1.5",
"5.3.6",
]
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
if: ${{ !startsWith(matrix.lua_version, 'luajit-') }}
- 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-reporter-lcov
- name: Build C++ library
run: |
luarocks make --no-install
env:
LJKIWI_LUA: ${{ startsWith(matrix.lua_version, 'luajit-') && '0' || '1' }}
LJKIWI_CFFI: ${{ startsWith(matrix.lua_version, 'luajit-') && '1' || '0' }}
FCOV: ${{ startsWith(matrix.os, 'ubuntu-') && '1' || '' }}
# Can't assume so versions, have to update this manually below
FSANITIZE: ${{ matrix.os == 'ubuntu-latest' && '1' || '' }}
- name: Run busted tests
run: |
busted -c -v
env:
LD_PRELOAD: |-
${{ matrix.os == 'ubuntu-latest' &&
'/usr/lib/x86_64-linux-gnu/libasan.so.6:/usr/lib/x86_64-linux-gnu/libstdc++.so.6:/usr/lib/x86_64-linux-gnu/libubsan.so.1'
|| '' }}
- name: Run gcov
if: success() && startsWith(matrix.os, 'ubuntu-')
run: |
gcov -p -b -s"$(pwd)" -r *.gcda
rm -f 'kiwi#'*.gcov
- name: generate Lua lcov test reports
if: |-
success() && !startsWith(matrix.os, 'windows-')
&& startsWith(matrix.lua_version, 'luajit-')
run: luacov
- name: Report test coverage
if: |-
success() && !startsWith(matrix.os, 'windows-')
&& (startsWith(matrix.lua_version, 'luajit-') || startsWith(matrix.os, 'ubuntu-'))
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
flag-name: run ${{ join(matrix.*, ' - ') }}
finish:
if: always()
needs: busted
runs-on: ubuntu-latest
steps:
- name: Close coveralls build
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
publish:
if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
needs: busted
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup lua
uses: jkl1337/gh-actions-lua@master
with:
luaVersion: "5.4.6"
- name: Setup luarocks
uses: jkl1337/gh-actions-luarocks@master
- name: Build C++ library
run: |
luarocks make --no-install
- name: Build rock
run: |
luarocks install dkjson
luarocks upload --api-key ${{ secrets.LUAROCKS_API_KEY }} \
rockspecs/kiwi-${GITHUB_REF_NAME#v}-1.rockspec

14
.gitignore vendored
View File

@@ -2,6 +2,20 @@
/lua
/lua_modules
/.luarocks
/config.mk
*.gcda
*.gcno
*.gcov
*.lcov
*.out
*.pch
*.gch
*.lib
*.so
*.o
*.obj
*.exp
*.dll
*.rock
.cache/
compile_commands.json

5
.luacov Normal file
View File

@@ -0,0 +1,5 @@
modules = {
kiwi = "kiwi.lua",
}
reporter = "lcov"
reportfile = "luacov.lcov"

View File

@@ -8,6 +8,6 @@
"lua_modules/share/lua/5.1/?.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
}

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.

155
Makefile Normal file
View File

@@ -0,0 +1,155 @@
CXX := $(CROSS)g++
CP := cp
RM := rm
LIBFLAG := -shared
LIB_EXT := $(if $(filter Windows_NT,$(OS)),dll,so)
LUA_INCDIR := /usr/include
SRCDIR := .
SANITIZE_FLAGS := -fsanitize=undefined -fsanitize=address -fsanitize=alignment \
-fsanitize=shift -fsanitize=unreachable -fsanitize=bool -fsanitize=enum
ifdef FDEBUG
OPTFLAG := -O2
else
OPTFLAG := -Og -g
endif
COVERAGE_FLAGS := --coverage
LTO_FLAGS := -flto=auto
ifeq ($(OS),Windows_NT)
is_clang = $(filter %clang++,$(CXX))
is_gcc = $(filter %g++,$(CXX))
ifdef FSANITIZE
$(error "FSANITIZE is not supported on Windows")
endif
else
uname_s := $(shell uname -s)
ifeq ($(uname_s),Darwin)
is_clang = 1
is_gcc =
CC := env MACOSX_DEPLOYMENT_TARGET=11.0 gcc
CXX := env MACOSX_DEPLOYMENT_TARGET=11.0 g++
LIBFLAG := -bundle -undefined dynamic_lookup
else
is_clang = $(filter %clang++,$(CXX))
is_gcc = $(filter %g++,$(CXX))
SANITIZE_FLAGS += -fsanitize=bounds-strict
endif
endif
-include config.mk
ifeq ($(origin LUAROCKS), command line)
ifdef FCOV
CCFLAGS := $(patsubst -O%,,$(CFLAGS))
else
ifdef FDEBUG
CCFLAGS := $(patsubst -O%,,$(CFLAGS)) -Og -g
else
CCFLAGS := $(CFLAGS)
endif
endif
override CFLAGS := -std=c99 $(CCFLAGS)
ifneq ($(filter %gcc,$(CC)),)
CXX := $(patsubst %gcc,%g++,$(CC))
else
ifneq ($(filter %clang,$(CC)),)
CXX := $(patsubst %clang,%clang++,$(CC))
endif
endif
else
CCFLAGS += -fPIC $(OPTFLAG)
override CFLAGS += -std=c99 $(CCFLAGS)
endif
CCFLAGS += -Wall -fvisibility=hidden -Wformat=2 -Wconversion -Wimplicit-fallthrough
ifdef FCOV
CCFLAGS += $(COVERAGE_FLAGS)
endif
ifdef FSANITIZE
CCFLAGS += $(SANITIZE_FLAGS)
endif
ifdef FLTO
CCFLAGS += $(LTO_FLAGS)
endif
ifneq ($(is_gcc),)
#PCH := ljkiwi.hpp.gch
else
ifneq ($(is_clang),)
override CXXFLAGS += -pedantic -Wno-c99-extensions
#PCH := ljkiwi.hpp.pch
endif
endif
override CPPFLAGS += -I$(SRCDIR) -I$(SRCDIR)/kiwi -I"$(LUA_INCDIR)"
override CXXFLAGS += -std=c++17 -fno-rtti $(CCFLAGS)
ifeq ($(OS),Windows_NT)
override CPPFLAGS += -DLUA_BUILD_AS_DLL
override LIBFLAG += "$(LUA_LIBDIR)/$(LUALIB)"
endif
ifdef LUA
LUA_VERSION ?= $(lastword $(shell "$(LUA)" -e "print(_VERSION)"))
endif
ifdef LUA_VERSION
ifneq ($(LUA_VERSION),5.1)
LJKIWI_CFFI ?= 0
endif
endif
kiwi_lib_srcs := AssocVector.h constraint.h debug.h errors.h expression.h kiwi.h maptype.h \
row.h shareddata.h solver.h solverimpl.h strength.h symbol.h symbolics.h term.h \
util.h variable.h version.h
ifneq ($(LJKIWI_LUA),0)
objs += luakiwi.o
endif
ifneq ($(LJKIWI_CFFI),0)
objs += ckiwi.o
endif
vpath %.cpp $(SRCDIR)/ckiwi $(SRCDIR)/luakiwi
vpath %.h $(SRCDIR)/ckiwi $(SRCDIR)/luakiwi $(SRCDIR)/kiwi/kiwi
all: ljkiwi.$(LIB_EXT)
install:
$(CP) -f ljkiwi.$(LIB_EXT) $(INST_LIBDIR)/ljkiwi.$(LIB_EXT)
$(CP) -f kiwi.lua $(INST_LUADIR)/kiwi.lua
mostlyclean:
$(RM) -f ljkiwi.$(LIB_EXT) $(objs) $(objs:.o=.gcda) $(objs:.o=.gcno)
clean: mostlyclean
$(RM) -f $(PCH)
ckiwi.o: $(PCH) ckiwi.cpp ckiwi.h $(kiwi_lib_srcs)
luakiwi.o: $(PCH) luakiwi-int.h luacompat.h $(kiwi_lib_srcs)
$(PCH): $(kiwi_lib_srcs)
ljkiwi.$(LIB_EXT): $(objs)
$(CXX) $(CCFLAGS) $(LIBFLAG) -o $@ $(objs)
%.hpp.gch: %.hpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -x c++-header -o $@ $<
%.hpp.pch: %.hpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -o $@ -x c++-header $<
%.o: %.cpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $<
.PHONY: all install clean mostlyclean

21
Makefile.win Normal file
View File

@@ -0,0 +1,21 @@
T= ljkiwi
OBJS= luakiwi.obj
lib: $T.dll
{luakiwi\}.cpp.obj:
$(CC) $(CFLAGS) /W4 /wd4200 /c /D_CRT_SECURE_NO_DEPRECATE /I. /I kiwi /I"$(LUA_INCDIR)" /EHs /Fo$@ $(CFLAGS) $<
$T.dll: $(OBJS)
link $(LIBFLAG) /out:$T.dll "$(LUA_LIBDIR)\$(LUALIB)" $(OBJS)
IF EXIST $T.dll.manifest mt -manifest $T.dll.manifest -outputresource:$T.dll;2
install: $T.dll
IF NOT EXIST "$(INST_LIBDIR)" mkdir "$(INST_LIBDIR)"
copy $T.dll "$(INST_LIBDIR)"
copy kiwi.lua "$(INST_LUADIR)"
clean:
del $T.dll $(OBJS) $T.lib $T.exp
IF EXIST $T.dll.manifest del $T.dll.manifest

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
ljkiwi - Free LuaJIT FFI and Lua C API 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/kiwi)](https://luarocks.org/modules/jkl/kiwi)
# 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 and they appear mostly unmaintained.
Since the C++ Kiwi library is well tested, it was simpler to provide a LuaJIT FFI wrapper. Now, there is also a Lua C API binding with support for 5.1 through 5.4, and since
in most common use cases all the heavy lifting is done in the library, there is usually
no practical perfomance difference between the two.
This package has no dependencies other than a supported C++14 compiler to compile the included Kiwi library and a small C wrapper.
## 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
The API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server.

404
ckiwi/ckiwi.cpp Normal file
View File

@@ -0,0 +1,404 @@
#include "ckiwi.h"
#include <kiwi/kiwi.h>
#include <climits>
#include <cstdarg>
#include <cstdlib>
#include <cstring>
#include <string>
#if defined(__GNUC__) && !defined(LJKIWI_NO_BUILTIN)
#define lk_likely(x) (__builtin_expect(((x) != 0), 1))
#define lk_unlikely(x) (__builtin_expect(((x) != 0), 0))
#else
#define lk_likely(x) (x)
#define lk_unlikely(x) (x)
#endif
namespace {
using namespace kiwi;
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>
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>
const KiwiErr* wrap_err(P self, F&& f) {
if (lk_unlikely(!self)) {
return &kKiwiErrNullObjectArg0;
}
return wrap_err([&]() { f(self->solver); });
}
template<typename P, typename R, typename F>
const KiwiErr* wrap_err(P* self, R* item, F&& f) {
if (lk_unlikely(!self)) {
return &kKiwiErrNullObjectArg0;
} else if (lk_unlikely(!item)) {
return &kKiwiErrNullObjectArg1;
}
return wrap_err([&]() { f(self->solver, item); });
}
template<typename T, typename... Args>
T* make_unmanaged(Args... args) {
auto* o = new T(std::forward<Args>(args)...);
o->m_refcount = 1;
return o;
}
template<typename T>
void release_unmanaged(T* p) {
if (lk_likely(p)) {
if (--p->m_refcount == 0)
delete p;
}
}
template<typename T>
T* retain_unmanaged(T* p) {
if (lk_likely(p))
p->m_refcount++;
return p;
}
} // namespace
extern "C" {
KiwiVar* kiwi_var_construct(const char* name) {
return make_unmanaged<VariableData>(lk_likely(name) ? name : "");
}
void kiwi_var_release(KiwiVar* var) {
release_unmanaged(var);
}
void kiwi_var_retain(KiwiVar* var) {
retain_unmanaged(var);
}
const char* kiwi_var_name(const KiwiVar* var) {
return lk_likely(var) ? var->name().c_str() : "(<null>)";
}
void kiwi_var_set_name(KiwiVar* var, const char* name) {
if (lk_likely(var && name))
var->setName(name);
}
double kiwi_var_value(const KiwiVar* var) {
return lk_likely(var) ? var->value() : std::numeric_limits<double>::quiet_NaN();
}
void kiwi_var_set_value(KiwiVar* var, double value) {
if (lk_likely(var))
var->setValue(value);
}
void kiwi_expression_retain(KiwiExpression* expr) {
if (lk_unlikely(!expr))
return;
for (auto* t = expr->terms_; t != expr->terms_ + expr->term_count; ++t) {
retain_unmanaged(t->var);
}
expr->owner = expr;
}
void kiwi_expression_destroy(KiwiExpression* expr) {
if (lk_unlikely(!expr || !expr->owner))
return;
if (expr->owner == expr) {
for (auto* t = expr->terms_; t != expr->terms_ + expr->term_count; ++t) {
release_unmanaged(t->var);
}
} else {
release_unmanaged(static_cast<ConstraintData*>(expr->owner));
}
}
KiwiConstraint* kiwi_constraint_construct(
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(static_cast<decltype(terms)::size_type>(
(lhs && lhs->term_count > 0 ? lhs->term_count : 0)
+ (rhs && rhs->term_count > 0 ? rhs->term_count : 0)
));
if (lhs) {
for (int i = 0; i < lhs->term_count; ++i) {
const auto& t = lhs->terms_[i];
if (t.var)
terms.emplace_back(Variable(t.var), t.coefficient);
}
}
if (rhs) {
for (int i = 0; i < rhs->term_count; ++i) {
const auto& t = rhs->terms_[i];
if (t.var)
terms.emplace_back(Variable(t.var), -t.coefficient);
}
}
return make_unmanaged<ConstraintData>(
Expression(std::move(terms), (lhs ? lhs->constant : 0.0) - (rhs ? rhs->constant : 0.0)),
static_cast<RelationalOperator>(op),
strength
);
}
void kiwi_constraint_release(KiwiConstraint* c) {
release_unmanaged(c);
}
void kiwi_constraint_retain(KiwiConstraint* c) {
retain_unmanaged(c);
}
double kiwi_constraint_strength(const KiwiConstraint* c) {
return lk_likely(c) ? c->strength() : std::numeric_limits<double>::quiet_NaN();
}
enum KiwiRelOp kiwi_constraint_op(const KiwiConstraint* c) {
return lk_likely(c) ? static_cast<KiwiRelOp>(c->op()) : KiwiRelOp::KIWI_OP_EQ;
}
bool kiwi_constraint_violated(const KiwiConstraint* c) {
return lk_likely(c) ? c->violated() : false;
}
int kiwi_constraint_expression(KiwiConstraint* c, KiwiExpression* out, int out_size) {
if (lk_unlikely(!c))
return 0;
const auto& expr = c->expression();
const auto& terms = expr.terms();
int n = terms.size() < INT_MAX ? static_cast<int>(terms.size()) : INT_MAX;
if (!out || out_size < n)
return n;
for (int i = 0; i < n; ++i) {
const auto& t = terms[static_cast<std::size_t>(i)];
out->terms_[i].var = const_cast<Variable&>(t.variable()).ptr();
out->terms_[i].coefficient = t.coefficient();
}
out->constant = expr.constant();
out->term_count = n;
out->owner = retain_unmanaged(c);
return n;
}
struct KiwiSolver {
unsigned error_mask;
Solver solver;
};
KiwiSolver* kiwi_solver_construct(unsigned error_mask) {
return new KiwiSolver {error_mask};
}
void kiwi_solver_destroy(KiwiSolver* s) {
if (lk_likely(s))
delete s;
}
unsigned kiwi_solver_get_error_mask(const KiwiSolver* s) {
return lk_likely(s) ? s->error_mask : 0;
}
void kiwi_solver_set_error_mask(KiwiSolver* s, unsigned mask) {
if (lk_likely(s))
s->error_mask = mask;
}
const KiwiErr* kiwi_solver_add_constraint(KiwiSolver* s, KiwiConstraint* constraint) {
return wrap_err(s, constraint, [](auto&& s, auto&& c) { s.addConstraint(Constraint(c)); });
}
const KiwiErr* kiwi_solver_remove_constraint(KiwiSolver* s, KiwiConstraint* constraint) {
return wrap_err(s, constraint, [](auto&& s, auto&& c) { s.removeConstraint(Constraint(c)); });
}
bool kiwi_solver_has_constraint(const KiwiSolver* s, KiwiConstraint* constraint) {
if (lk_unlikely(!s || !constraint))
return 0;
return s->solver.hasConstraint(Constraint(constraint));
}
const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* s, KiwiVar* var, double strength) {
return wrap_err(s, var, [strength](auto&& s, auto&& v) {
s.addEditVariable(Variable(v), strength);
});
}
const KiwiErr* kiwi_solver_remove_edit_var(KiwiSolver* s, KiwiVar* var) {
return wrap_err(s, var, [](auto&& s, auto&& v) { s.removeEditVariable(Variable(v)); });
}
bool kiwi_solver_has_edit_var(const KiwiSolver* s, KiwiVar* var) {
if (lk_unlikely(!s || !var))
return 0;
return s->solver.hasEditVariable(Variable(var));
}
const KiwiErr* kiwi_solver_suggest_value(KiwiSolver* s, KiwiVar* var, double value) {
return wrap_err(s, var, [value](auto&& s, auto&& v) { s.suggestValue(Variable(v), value); });
}
void kiwi_solver_update_vars(KiwiSolver* s) {
if (lk_likely(s))
s->solver.updateVariables();
}
void kiwi_solver_reset(KiwiSolver* s) {
if (lk_likely(s))
s->solver.reset();
}
void kiwi_solver_dump(const KiwiSolver* s) {
if (lk_likely(s))
s->solver.dump();
}
char* kiwi_solver_dumps(const KiwiSolver* s) {
if (lk_unlikely(!s))
return nullptr;
const auto& str = s->solver.dumps();
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;
}
void kiwi_tuple_init(KiwiTuple* tuple, int count, ...) {
if (lk_unlikely(!tuple))
return;
va_list args;
va_start(args, count);
for (int i = 0; i < count; ++i) {
auto* var = va_arg(args, KiwiVar*);
retain_unmanaged(var);
tuple->values[i] = &var->m_value;
}
tuple->count = count;
va_end(args);
}
void kiwi_tuple_destroy(KiwiTuple* tuple) {
if (lk_unlikely(!tuple))
return;
for (int i = 0; i < tuple->count; ++i) {
auto* value = const_cast<double*>(tuple->values[i]);
release_unmanaged(
reinterpret_cast<KiwiVar*>(reinterpret_cast<char*>(value) - offsetof(KiwiVar, m_value))
);
}
}
} // extern "C"

145
ckiwi/ckiwi.h Normal file
View File

@@ -0,0 +1,145 @@
#ifndef LJKIWI_CKIWI_H_
#define LJKIWI_CKIWI_H_
#if !defined(_MSC_VER) || _MSC_VER >= 1900
#undef LJKIWI_USE_FAM_1
#else
#define LJKIWI_USE_FAM_1
#endif
#ifdef __cplusplus
namespace kiwi {
class VariableData;
class ConstraintData;
} // namespace kiwi
typedef kiwi::VariableData KiwiVar;
typedef kiwi::ConstraintData KiwiConstraint;
extern "C" {
#else
typedef struct KiwiVar KiwiVar;
typedef struct KiwiConstraint KiwiConstraint;
#endif
#if defined __GNUC__ && (!defined _WIN32 || defined __CYGWIN__)
#define LJKIWI_EXP __attribute__((visibility("default")))
#elif defined _WIN32
#define LJKIWI_EXP __declspec(dllexport)
#endif
// 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 KiwiTerm {
KiwiVar* var;
double coefficient;
} KiwiTerm;
typedef struct KiwiExpression {
double constant;
int term_count;
void* owner;
#if defined(LJKIWI_LUAJIT_DEF)
KiwiTerm terms_[?];
#elif defined(LJKIWI_USE_FAM_1)
KiwiTerm terms_[1];
#else
KiwiTerm terms_[];
#endif
} KiwiExpression;
typedef struct KiwiErr {
enum KiwiErrKind kind;
const char* message;
bool must_free;
} KiwiErr;
typedef struct KiwiTuple {
int count;
#if defined(LJKIWI_LUAJIT_DEF)
const double* values[?];
#elif defined(LJKIWI_USE_FAM_1)
const double* values[1];
#else
const double* values[];
#endif
} KiwiTuple;
struct KiwiSolver;
LJKIWI_EXP void kiwi_tuple_init(KiwiTuple* tuple, int count, ...);
LJKIWI_EXP void kiwi_tuple_destroy(KiwiTuple* tuple);
LJKIWI_EXP KiwiVar* kiwi_var_construct(const char* name);
LJKIWI_EXP void kiwi_var_release(KiwiVar* var);
LJKIWI_EXP void kiwi_var_retain(KiwiVar* var);
LJKIWI_EXP const char* kiwi_var_name(const KiwiVar* var);
LJKIWI_EXP void kiwi_var_set_name(KiwiVar* var, const char* name);
LJKIWI_EXP double kiwi_var_value(const KiwiVar* var);
LJKIWI_EXP void kiwi_var_set_value(KiwiVar* var, double value);
LJKIWI_EXP void kiwi_expression_retain(KiwiExpression* expr);
LJKIWI_EXP void kiwi_expression_destroy(KiwiExpression* expr);
LJKIWI_EXP KiwiConstraint* kiwi_constraint_construct(
const KiwiExpression* lhs,
const KiwiExpression* rhs,
enum KiwiRelOp op,
double strength
);
LJKIWI_EXP void kiwi_constraint_release(KiwiConstraint* c);
LJKIWI_EXP void kiwi_constraint_retain(KiwiConstraint* c);
LJKIWI_EXP double kiwi_constraint_strength(const KiwiConstraint* c);
LJKIWI_EXP enum KiwiRelOp kiwi_constraint_op(const KiwiConstraint* c);
LJKIWI_EXP bool kiwi_constraint_violated(const KiwiConstraint* c);
LJKIWI_EXP int kiwi_constraint_expression(KiwiConstraint* c, KiwiExpression* out, int out_size);
LJKIWI_EXP KiwiSolver* kiwi_solver_construct(unsigned error_mask);
LJKIWI_EXP void kiwi_solver_destroy(KiwiSolver* s);
LJKIWI_EXP unsigned kiwi_solver_get_error_mask(const KiwiSolver* s);
LJKIWI_EXP void kiwi_solver_set_error_mask(KiwiSolver* s, unsigned mask);
LJKIWI_EXP const KiwiErr* kiwi_solver_add_constraint(KiwiSolver* s, KiwiConstraint* constraint);
LJKIWI_EXP const KiwiErr*
kiwi_solver_remove_constraint(KiwiSolver* s, KiwiConstraint* constraint);
LJKIWI_EXP bool kiwi_solver_has_constraint(const KiwiSolver* s, KiwiConstraint* constraint);
LJKIWI_EXP const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* s, KiwiVar* var, double strength);
LJKIWI_EXP const KiwiErr* kiwi_solver_remove_edit_var(KiwiSolver* s, KiwiVar* var);
LJKIWI_EXP bool kiwi_solver_has_edit_var(const KiwiSolver* s, KiwiVar* var);
LJKIWI_EXP const KiwiErr* kiwi_solver_suggest_value(KiwiSolver* s, KiwiVar* var, double value);
LJKIWI_EXP void kiwi_solver_update_vars(KiwiSolver* sp);
LJKIWI_EXP void kiwi_solver_reset(KiwiSolver* sp);
LJKIWI_EXP void kiwi_solver_dump(const KiwiSolver* sp);
LJKIWI_EXP char* kiwi_solver_dumps(const KiwiSolver* sp);
// LuaJIT end
#ifdef __cplusplus
} // extern "C"
#endif
// Local Variables:
// mode: c++
// End:
#endif // LJKIWI_CKIWI_H_

1191
kiwi.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -26,17 +26,70 @@ enum RelationalOperator
OP_EQ
};
class Constraint
class Constraint;
class ConstraintData : public SharedData
{
static Expression reduce(const Expression &expr)
{
std::map<Variable, double> vars;
for (const auto & term : expr.terms())
vars[term.variable()] += term.coefficient();
std::vector<Term> terms(vars.begin(), vars.end());
return Expression(std::move(terms), expr.constant());
}
public:
ConstraintData(const Expression &expr,
RelationalOperator op,
double strength) : SharedData(),
m_expression(reduce(expr)),
m_strength(strength::clip(strength)),
m_op(op) {}
ConstraintData(const ConstraintData &other, double strength) : SharedData(),
m_expression(other.m_expression),
m_strength(strength::clip(strength)),
m_op(other.m_op) {}
~ConstraintData() = default;
const Expression &expression() const { return m_expression; }
RelationalOperator op() const { return m_op; }
double strength() const { return m_strength; }
bool violated() const
{
switch (m_op)
{
case OP_EQ: return !impl::nearZero(m_expression.value());
case OP_GE: return m_expression.value() < 0.0;
case OP_LE: return m_expression.value() > 0.0;
}
std::abort();
}
private:
Expression m_expression;
double m_strength;
RelationalOperator m_op;
ConstraintData(const ConstraintData &other) = delete;
ConstraintData &operator=(const ConstraintData &other) = delete;
};
class Constraint
{
public:
explicit Constraint(ConstraintData *p) : m_data(p) {}
Constraint() = default;
Constraint(const Expression &expr,
RelationalOperator op,
double strength = strength::required) : m_data(new ConstraintData(expr, op, strength)) {}
Constraint(const Constraint &other, double strength) : m_data(new ConstraintData(other, strength)) {}
Constraint(const Constraint &other, double strength) : m_data(new ConstraintData(*other.m_data, strength)) {}
Constraint(const Constraint &) = default;
@@ -44,32 +97,10 @@ public:
~Constraint() = default;
const Expression &expression() const
{
return m_data->m_expression;
}
RelationalOperator op() const
{
return m_data->m_op;
}
double strength() const
{
return m_data->m_strength;
}
bool violated() const
{
switch (m_data->m_op)
{
case OP_EQ: return !impl::nearZero(m_data->m_expression.value());
case OP_GE: return m_data->m_expression.value() < 0.0;
case OP_LE: return m_data->m_expression.value() > 0.0;
}
std::abort();
}
const Expression &expression() const { return m_data->expression(); }
RelationalOperator op() const { return m_data->op(); }
double strength() const { return m_data->strength(); }
bool violated() const { return m_data->violated(); }
bool operator!() const
{
@@ -81,45 +112,9 @@ public:
Constraint& operator=(Constraint &&) noexcept = default;
private:
static Expression reduce(const Expression &expr)
{
std::map<Variable, double> vars;
for (const auto & term : expr.terms())
vars[term.variable()] += term.coefficient();
std::vector<Term> terms(vars.begin(), vars.end());
return Expression(std::move(terms), expr.constant());
}
class ConstraintData : public SharedData
{
SharedDataPtr<ConstraintData> m_data;
public:
ConstraintData(const Expression &expr,
RelationalOperator op,
double strength) : SharedData(),
m_expression(reduce(expr)),
m_strength(strength::clip(strength)),
m_op(op) {}
ConstraintData(const Constraint &other, double strength) : SharedData(),
m_expression(other.expression()),
m_strength(strength::clip(strength)),
m_op(other.op()) {}
~ConstraintData() = default;
Expression m_expression;
double m_strength;
RelationalOperator m_op;
private:
ConstraintData(const ConstraintData &other);
ConstraintData &operator=(const ConstraintData &other);
};
SharedDataPtr<ConstraintData> m_data;
friend bool operator<(const Constraint &lhs, const Constraint &rhs)
{

View File

@@ -145,7 +145,7 @@ public:
/* Dump a representation of the solver internals to stdout.
*/
void dump()
void dump() const
{
debug::dump( m_impl );
}
@@ -153,7 +153,7 @@ public:
/* 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 );
}
@@ -161,7 +161,7 @@ public:
/* Dump a representation of the solver internals to a string.
*/
std::string dumps()
std::string dumps() const
{
return debug::dumps( m_impl );
}

View File

@@ -13,66 +13,57 @@
namespace kiwi
{
class Variable
class VariableData : public SharedData
{
public:
VariableData(std::string name) : SharedData(),
m_name(std::move(name)),
m_value(0.0) {}
public:
class Context
{
public:
Context() = default;
virtual ~Context() {} // LCOV_EXCL_LINE
VariableData(const char *name) : SharedData(),
m_name(name),
m_value(0.0) {}
~VariableData() = default;
const std::string &name() const { return m_name; }
void setName(const char *name) { m_name = name; }
void setName(const std::string &name) { m_name = name; }
double value() const { return m_value; }
void setValue(double value) { m_value = value; }
std::string m_name;
double m_value;
VariableData(const VariableData &other) = delete;
VariableData &operator=(const VariableData &other) = delete;
};
Variable(Context *context = 0) : m_data(new VariableData("", context)) {}
class Variable
{
public:
explicit Variable(VariableData *p) : m_data(p) {}
VariableData *ptr() { return m_data; }
Variable(std::string name, Context *context = 0) : m_data(new VariableData(std::move(name), context)) {}
Variable(const char *name, Context *context = 0) : m_data(new VariableData(name, context)) {}
Variable() : m_data(new VariableData("")) {}
Variable(std::string name) : m_data(new VariableData(std::move(name))) {}
Variable(const char *name) : m_data(new VariableData(name)) {}
Variable(const Variable&) = default;
Variable(Variable&&) noexcept = default;
~Variable() = default;
const std::string &name() const
{
return m_data->m_name;
}
const std::string &name() const { return m_data->name(); }
void setName(const char *name) { m_data->setName(name); }
void setName(const std::string &name) { m_data->setName(name); }
void setName(const char *name)
{
m_data->m_name = name;
}
void setName(const std::string &name)
{
m_data->m_name = name;
}
Context *context() const
{
return m_data->m_context.get();
}
void setContext(Context *context)
{
m_data->m_context.reset(context);
}
double value() const
{
return m_data->m_value;
}
void setValue(double value)
{
m_data->m_value = value;
}
double value() const { return m_data->value(); }
void setValue(double value) { m_data->setValue(value); }
// operator== is used for symbolics
bool equals(const Variable &other)
bool equals(const Variable &other) const
{
return m_data == other.m_data;
}
@@ -82,32 +73,6 @@ public:
Variable& operator=(Variable&&) noexcept = default;
private:
class VariableData : public SharedData
{
public:
VariableData(std::string name, Context *context) : SharedData(),
m_name(std::move(name)),
m_context(context),
m_value(0.0) {}
VariableData(const char *name, Context *context) : SharedData(),
m_name(name),
m_context(context),
m_value(0.0) {}
~VariableData() = default;
std::string m_name;
std::unique_ptr<Context> m_context;
double m_value;
private:
VariableData(const VariableData &other);
VariableData &operator=(const VariableData &other);
};
SharedDataPtr<VariableData> m_data;
friend bool operator<(const Variable &lhs, const Variable &rhs)

1
ljkiwi.hpp Normal file
View File

@@ -0,0 +1 @@
#include <kiwi/kiwi.h>

147
luakiwi/luacompat.h Normal file
View File

@@ -0,0 +1,147 @@
#ifndef LJKIWI_LUACOMPAT_H_
#define LJKIWI_LUACOMPAT_H_
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501 && defined(__GNUC__)
#define LJKIWI_LJ_COMPAT_ATTR __attribute__((weak, visibility("default")))
#else
#define LJKIWI_LJ_COMPAT_ATTR static
#endif
#if !defined(LUA_VERSION_NUM) || LUA_VERSION_NUM == 501
static int lua_absindex(lua_State* L, int i) {
if (i < 0 && i > LUA_REGISTRYINDEX)
i += lua_gettop(L) + 1;
return i;
}
LJKIWI_LJ_COMPAT_ATTR 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;
}
LJKIWI_LJ_COMPAT_ATTR 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
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // LJKIWI_LUACOMPAT_H_

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

@@ -0,0 +1,319 @@
#ifndef LUAKIWI_INT_H_
#define LUAKIWI_INT_H_
#include <kiwi/kiwi.h>
#include <cstring>
#include <new>
#include "luacompat.h"
#if defined(__GNUC__) && !defined(LJKIWI_NO_BUILTIN)
#define lk_likely(x) (__builtin_expect(((x) != 0), 1))
#define lk_unlikely(x) (__builtin_expect(((x) != 0), 0))
#else
#define lk_likely(x) (x)
#define lk_unlikely(x) (x)
#endif
namespace {
using namespace kiwi;
// Lua 5.1 compatibility for missing lua_arith.
inline void compat_arith_unm(lua_State* L) {
#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM == 501
int isnum;
lua_Number n = lua_tonumberx(L, -1, &isnum);
if (isnum) {
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.
inline 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 {
for (int 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 */
}
template<typename T, std::size_t N>
constexpr int array_count(T (&)[N]) {
return static_cast<int>(N);
}
inline void newlib(lua_State* L, const luaL_Reg* l) {
lua_newtable(L);
setfuncs(L, l, 0);
}
enum KiwiErrKind {
KiwiErrNone,
KiwiErrUnsatisfiableConstraint = 1,
KiwiErrUnknownConstraint,
KiwiErrDuplicateConstraint,
KiwiErrUnknownEditVariable,
KiwiErrDuplicateEditVariable,
KiwiErrBadRequiredStrength,
KiwiErrInternalSolverError,
KiwiErrAlloc,
KiwiErrNullObject,
KiwiErrUnknown,
};
struct KiwiTerm {
VariableData* var;
double coefficient;
};
struct KiwiExpression {
double constant;
int term_count;
ConstraintData* owner;
#if !defined(_MSC_VER) || _MSC_VER >= 1900
KiwiTerm terms[];
static constexpr std::size_t sz(int count) {
return sizeof(KiwiExpression)
+ sizeof(KiwiTerm) * static_cast<std::size_t>(count > 0 ? count : 0);
}
#else
KiwiTerm terms[1];
static constexpr std::size_t sz(int count) {
return sizeof(KiwiExpression)
+ sizeof(KiwiTerm) * static_cast<std::size_t>(count > 0 ? count : 0);
}
#endif
KiwiExpression() = delete;
KiwiExpression(const KiwiExpression&) = delete;
KiwiExpression& operator=(const KiwiExpression&) = delete;
~KiwiExpression() = delete;
};
// This mechanism was initially designed for LuaJIT FFI.
struct KiwiErr {
enum KiwiErrKind kind;
const char* message;
bool must_delete;
};
struct KiwiSolver {
unsigned error_mask;
Solver solver;
};
inline 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*>(::operator new(sizeof(KiwiErr) + msg_n, std::nothrow));
if (!mem) {
return base;
}
auto* msg = mem + sizeof(KiwiErr);
std::memcpy(msg, ex.what(), msg_n);
return new (mem) KiwiErr {base->kind, msg, true};
}
template<typename F>
inline const KiwiErr* wrap_err(F&& f) {
static const constexpr KiwiErr kKiwiErrUnhandledCxxException {
KiwiErrUnknown,
"An unhandled C++ exception occurred."
};
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); });
}
template<typename T, typename... Args>
inline T* make_unmanaged(Args... args) {
auto* p = new (std::nothrow) T(std::forward<Args>(args)...);
if (lk_unlikely(!p))
return nullptr;
p->m_refcount = 1;
return p;
}
template<typename T>
inline void release_unmanaged(T* p) {
if (lk_likely(p)) {
if (--p->m_refcount == 0)
delete p;
}
}
template<typename T>
inline T* retain_unmanaged(T* p) {
if (lk_likely(p))
p->m_refcount++;
return p;
}
inline ConstraintData* kiwi_constraint_new(
const KiwiExpression* lhs,
const KiwiExpression* rhs,
RelationalOperator op,
double strength
) {
if (strength < 0.0) {
strength = kiwi::strength::required;
}
try {
std::vector<Term> terms;
terms.reserve(static_cast<decltype(terms)::size_type>(
(lhs && lhs->term_count > 0 ? lhs->term_count : 0)
+ (rhs && rhs->term_count > 0 ? rhs->term_count : 0)
));
if (lhs) {
for (int i = 0; i < lhs->term_count; ++i) {
const auto& t = lhs->terms[i];
terms.emplace_back(Variable(t.var), t.coefficient);
}
}
if (rhs) {
for (int i = 0; i < rhs->term_count; ++i) {
const auto& t = rhs->terms[i];
terms.emplace_back(Variable(t.var), -t.coefficient);
}
}
return make_unmanaged<ConstraintData>(
Expression(std::move(terms), (lhs ? lhs->constant : 0.0) - (rhs ? rhs->constant : 0.0)),
static_cast<RelationalOperator>(op),
strength
);
} catch (...) {
return nullptr;
}
}
inline const KiwiErr* kiwi_solver_add_constraint(Solver& s, ConstraintData* constraint) {
return wrap_err(s, constraint, [](auto&& solver, auto&& c) {
solver.addConstraint(Constraint(c));
});
}
inline const KiwiErr* kiwi_solver_remove_constraint(Solver& s, ConstraintData* constraint) {
return wrap_err(s, constraint, [](auto&& solver, auto&& c) {
solver.removeConstraint(Constraint(c));
});
}
inline const KiwiErr* kiwi_solver_add_edit_var(Solver& s, VariableData* var, double strength) {
return wrap_err(s, var, [strength](auto&& solver, auto&& v) {
solver.addEditVariable(Variable(v), strength);
});
}
inline const KiwiErr* kiwi_solver_remove_edit_var(Solver& s, VariableData* var) {
return wrap_err(s, var, [](auto&& solver, auto&& v) {
solver.removeEditVariable(Variable(v));
});
}
inline const KiwiErr* kiwi_solver_suggest_value(Solver& s, VariableData* var, double value) {
return wrap_err(s, var, [value](auto&& solver, auto&& v) {
solver.suggestValue(Variable(v), value);
});
}
} // namespace
// Local Variables:
// mode: c++
// End:
#endif // LUAKIWI_INT_H_

1711
luakiwi/luakiwi.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
rockspec_format = "3.0"
package = "kiwi"
version = "0.1.0-1"
source = {
url = "git+https://github.com/jkl1337/ljkiwi",
tag = "v0.1.0",
}
description = {
summary = "LuaJIT FFI and Lua binding for the Kiwi constraint solver.",
detailed = [[
kiwi is a LuaJIT FFI and Lua binding for the Kiwi constraint solver. Kiwi is a fast
implementation of the Cassowary constraint solving algorithm. kiwi provides
reasonably efficient bindings using the LuaJIT FFI and convential Lua C bindings.]],
license = "MIT",
issues_url = "https://github.com/jkl1337/ljkiwi/issues",
maintainer = "John Luebs",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "make",
build_variables = {
LUAROCKS = "1",
LUA = "$(LUA)",
CFLAGS = "$(CFLAGS)",
LUA_INCDIR = "$(LUA_INCDIR)",
LUA_LIBDIR = "$(LUA_LIBDIR)",
LUALIB = "$(LUALIB)",
LIBFLAG = "$(LIBFLAG)",
LIB_EXT = "$(LIB_EXTENSION)",
OBJ_EXT = "$(OBJ_EXTENSION)",
},
install_variables = {
INST_LIBDIR = "$(LIBDIR)",
INST_LUADIR = "$(LUADIR)",
LIB_EXT = "$(LIB_EXTENSION)",
},
}

View File

@@ -0,0 +1,40 @@
rockspec_format = "3.0"
package = "kiwi"
version = "0.1.1-1"
source = {
url = "git+https://github.com/jkl1337/ljkiwi",
tag = "v0.1.1",
}
description = {
summary = "LuaJIT FFI and Lua binding for the Kiwi constraint solver.",
detailed = [[
kiwi is a LuaJIT FFI and Lua binding for the Kiwi constraint solver. Kiwi is a fast
implementation of the Cassowary constraint solving algorithm. kiwi provides
reasonably efficient bindings using the LuaJIT FFI and convential Lua C bindings.]],
license = "MIT",
issues_url = "https://github.com/jkl1337/ljkiwi/issues",
maintainer = "John Luebs",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "make",
build_variables = {
LUAROCKS = "1",
LUA = "$(LUA)",
CFLAGS = "$(CFLAGS)",
LUA_INCDIR = "$(LUA_INCDIR)",
LUA_LIBDIR = "$(LUA_LIBDIR)",
LUALIB = "$(LUALIB)",
LIBFLAG = "$(LIBFLAG)",
LIB_EXT = "$(LIB_EXTENSION)",
OBJ_EXT = "$(OBJ_EXTENSION)",
},
install_variables = {
INST_LIBDIR = "$(LIBDIR)",
INST_LUADIR = "$(LUADIR)",
LIB_EXT = "$(LIB_EXTENSION)",
},
}

View File

@@ -0,0 +1,39 @@
rockspec_format = "3.0"
package = "kiwi"
version = "scm-1"
source = {
url = "git+https://github.com/jkl1337/ljkiwi",
}
description = {
summary = "LuaJIT FFI and Lua binding for the Kiwi constraint solver.",
detailed = [[
kiwi is a LuaJIT FFI and Lua binding for the Kiwi constraint solver. Kiwi is a fast
implementation of the Cassowary constraint solving algorithm. kiwi provides
reasonably efficient bindings using the LuaJIT FFI and convential Lua C bindings.]],
license = "MIT",
issues_url = "https://github.com/jkl1337/ljkiwi/issues",
maintainer = "John Luebs",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "make",
build_variables = {
LUAROCKS = "1",
LUA = "$(LUA)",
CFLAGS = "$(CFLAGS)",
LUA_INCDIR = "$(LUA_INCDIR)",
LUA_LIBDIR = "$(LUA_LIBDIR)",
LUALIB = "$(LUALIB)",
LIBFLAG = "$(LIBFLAG)",
LIB_EXT = "$(LIB_EXTENSION)",
OBJ_EXT = "$(OBJ_EXTENSION)",
},
install_variables = {
INST_LIBDIR = "$(LIBDIR)",
INST_LUADIR = "$(LUADIR)",
LIB_EXT = "$(LIB_EXTENSION)",
},
}

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)

240
spec/expression_spec.lua Normal file
View File

@@ -0,0 +1,240 @@
expose("module", function()
require("kiwi")
end)
describe("Expression", function()
local kiwi = require("kiwi")
local LUA_VERSION = tonumber(_VERSION:match("%d+%.%d+"))
it("construction", function()
local v = kiwi.Var("foo")
local v2 = kiwi.Var("bar")
local v3 = kiwi.Var("aux")
local e1 = kiwi.Expression(0, v * 1, v2 * 2, v3 * 3)
local e2 = kiwi.Expression(10, v * 1, v2 * 2, v3 * 3)
local constants = { 0, 10 }
for i, e in ipairs({ e1, e2 }) do
assert.equal(constants[i], e.constant)
local terms = e:terms()
assert.equal(3, #terms)
assert.equal(v, terms[1].var)
assert.equal(1.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(2.0, terms[2].coefficient)
assert.equal(v3, terms[3].var)
assert.equal(3.0, terms[3].coefficient)
end
if LUA_VERSION <= 5.2 then
assert.equal("1 foo + 2 bar + 3 aux + 10", tostring(e2))
else
assert.equal("1.0 foo + 2.0 bar + 3.0 aux + 10.0", tostring(e2))
end
assert.error(function()
kiwi.Expression(0, 0, v2 * 2, v3 * 3)
end)
end)
describe("method", function()
local v, t, e
before_each(function()
v = kiwi.Var("foo")
v:set(42)
t = kiwi.Term(v, 10)
e = t + 5
end)
it("has value", function()
v:set(42)
assert.equal(425, e:value())
v:set(87)
assert.equal(875, e:value())
end)
it("can be copied", function()
local e2 = e:copy()
assert.equal(e.constant, e2.constant)
local t1, t2 = e:terms(), e2:terms()
assert.equal(#t1, #t2)
for i = 1, #t1 do
assert.equal(t1[i].var, t2[i].var)
assert.equal(t1[i].coefficient, t2[i].coefficient)
end
end)
it("neg", function()
local neg = -e --[[@as kiwi.Expression]]
assert.True(kiwi.is_expression(neg))
local terms = neg:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(-10.0, terms[1].coefficient)
assert.equal(-5, neg.constant)
end)
describe("bin op", function()
local v2, t2, e2
before_each(function()
v2 = kiwi.Var("bar")
t2 = kiwi.Term(v2)
e2 = v2 - 10
end)
it("mul", function()
for _, prod in ipairs({ e * 2.0, 2 * e }) do
assert.True(kiwi.is_expression(prod))
local terms = prod:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(20.0, terms[1].coefficient)
assert.equal(10, prod.constant)
end
assert.error(function()
local _ = e * v
end)
end)
it("div", function()
local quot = e / 2.0
assert.True(kiwi.is_expression(quot))
local terms = quot:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(5.0, terms[1].coefficient)
assert.equal(2.5, quot.constant)
assert.error(function()
local _ = e / v2
end)
end)
it("add", function()
for _, sum in ipairs({ e + 2.0, 2 + e }) do
assert.True(kiwi.is_expression(sum))
assert.equal(7.0, sum.constant)
local terms = sum:terms()
assert.equal(1, #terms)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v, terms[1].var)
end
local sum = e + v2
assert.True(kiwi.is_expression(sum))
assert.equal(5, sum.constant)
local terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
sum = e + t2
assert.True(kiwi.is_expression(sum))
assert.equal(5, sum.constant)
terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
sum = e + e2
assert.True(kiwi.is_expression(sum))
assert.equal(-5, sum.constant)
terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
assert.error(function()
local _ = t + "foo"
end)
assert.error(function()
local _ = t + {}
end)
end)
it("sub", function()
local constants = { 3, -3 }
for i, diff in ipairs({ e - 2.0, 2 - e }) 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 -10.0 or 10.0, terms[1].coefficient)
end
local diff = e - v2
assert.True(kiwi.is_expression(diff))
assert.equal(5, diff.constant)
local terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
diff = e - t2
assert.True(kiwi.is_expression(diff))
assert.equal(5, diff.constant)
terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
diff = e - e2
assert.True(kiwi.is_expression(diff))
assert.equal(15, diff.constant)
terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.error(function()
local _ = e - "foo"
end)
assert.error(function()
local _ = e - {}
end)
end)
it("constraint expr op expr", function()
local ops = { "LE", "EQ", "GE" }
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = e[meth](e, e2)
assert.True(kiwi.is_constraint(c))
local expr = c:expression()
local terms = expr:terms()
assert.equal(2, #terms)
-- order can be randomized due to use of map
if terms[1].var ~= v then
terms[1], terms[2] = terms[2], terms[1]
end
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.equal(15, expr.constant)
assert.equal(ops[i], c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end
end)
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)

245
spec/term_spec.lua Normal file
View File

@@ -0,0 +1,245 @@
expose("module", function()
require("kiwi")
end)
describe("Term", function()
local kiwi = require("kiwi")
local LUA_VERSION = tonumber(_VERSION:match("%d+%.%d+"))
it("construction", function()
local v = kiwi.Var("foo")
local t = kiwi.Term(v)
assert.equal(v, t.var)
assert.equal(1.0, t.coefficient)
t = kiwi.Term(v, 100)
assert.equal(v, t.var)
assert.equal(100, t.coefficient)
if LUA_VERSION <= 5.2 then
assert.equal("100 foo", tostring(t))
else
assert.equal("100.0 foo", tostring(t))
end
assert.error(function()
kiwi.Term("")
end)
end)
describe("method", function()
local v, v2, t, t2
before_each(function()
v = kiwi.Var("foo")
t = kiwi.Term(v, 10)
end)
it("has value", function()
v:set(42)
assert.equal(420, t:value())
v:set(87)
assert.equal(870, t:value())
end)
it("has toexpr", function()
local e = t:toexpr()
assert.True(kiwi.is_expression(e))
assert.equal(0, e.constant)
local terms = e:terms()
assert.equal(1, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
end)
it("neg", function()
local neg = -t --[[@as kiwi.Term]]
assert.True(kiwi.is_term(neg))
assert.equal(v, neg.var)
assert.equal(-10, neg.coefficient)
end)
describe("bin op", function()
before_each(function()
v2 = kiwi.Var("bar")
t2 = kiwi.Term(v2)
end)
it("mul", function()
for _, prod in ipairs({ t * 2.0, 2 * t }) do
assert.True(kiwi.is_term(prod))
assert.equal(v, prod.var)
assert.equal(20, prod.coefficient)
end
assert.error(function()
local _ = t * v
end)
end)
it("div", function()
local quot = t / 2.0
assert.True(kiwi.is_term(quot))
assert.equal(v, quot.var)
assert.equal(5.0, quot.coefficient)
assert.error(function()
local _ = v / v2
end)
end)
it("add", function()
for _, sum in ipairs({ t + 2.0, 2 + t }) do
assert.True(kiwi.is_expression(sum))
assert.equal(2.0, sum.constant)
local terms = sum:terms()
assert.equal(1, #terms)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v, terms[1].var)
end
local sum = t + 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(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
sum = t + t2
assert.True(kiwi.is_expression(sum))
assert.equal(0, sum.constant)
terms = sum:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
local t3 = kiwi.Term(v2, 20)
sum = t3 + sum
assert.True(kiwi.is_expression(sum))
assert.equal(0, sum.constant)
terms = sum:terms()
assert.equal(3, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
assert.equal(v2, terms[3].var)
assert.equal(20.0, terms[3].coefficient)
assert.error(function()
local _ = t + "foo"
end)
assert.error(function()
local _ = t + {}
end)
end)
it("sub", function()
local constants = { -2, 2 }
for i, diff in ipairs({ t - 2.0, 2 - t }) 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 10.0 or -10.0, terms[1].coefficient)
end
local diff = t - 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(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
diff = t - t2
assert.True(kiwi.is_expression(diff))
assert.equal(0, diff.constant)
terms = diff:terms()
assert.equal(2, #terms)
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
local t3 = kiwi.Term(v2, 20)
diff = t3 - diff
assert.True(kiwi.is_expression(diff))
assert.equal(0, diff.constant)
terms = diff:terms()
assert.equal(3, #terms)
assert.equal(v, terms[1].var)
assert.equal(-10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(1.0, terms[2].coefficient)
assert.equal(v2, terms[3].var)
assert.equal(20.0, terms[3].coefficient)
assert.error(function()
local _ = t - "foo"
end)
assert.error(function()
local _ = t - {}
end)
end)
it("constraint term op expr", function()
local ops = { "LE", "EQ", "GE" }
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = t[meth](t, v2 + 1)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local terms = e:terms()
assert.equal(2, #terms)
-- order can be randomized due to use of map
if terms[1].var ~= v then
terms[1], terms[2] = terms[2], terms[1]
end
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.equal(-1, e.constant)
assert.equal(ops[i], c:op())
assert.equal(kiwi.strength.REQUIRED, c:strength())
end
end)
it("constraint term op term", function()
for i, meth in ipairs({ "le", "eq", "ge" }) do
local c = t[meth](t, t2)
assert.True(kiwi.is_constraint(c))
local e = c:expression()
local terms = e:terms()
assert.equal(2, #terms)
-- order can be randomized due to use of map
if terms[1].var ~= v then
terms[1], terms[2] = terms[2], terms[1]
end
assert.equal(v, terms[1].var)
assert.equal(10.0, terms[1].coefficient)
assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient)
assert.equal(0, e.constant)
end
end)
end)
end)
end)

188
spec/var_spec.lua Normal file
View File

@@ -0,0 +1,188 @@
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)
-- TODO: terms and expressions
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)