Compare commits

10 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
14 changed files with 721 additions and 57 deletions

View File

@@ -1,3 +1,4 @@
---
name: Busted name: Busted
on: [push, pull_request] on: [push, pull_request]
@@ -35,19 +36,46 @@ jobs:
- name: Setup dependencies - name: Setup dependencies
run: | run: |
luarocks install busted luarocks install busted
luarocks install luacov-coveralls luarocks install luacov-reporter-lcov
- name: Build C library - name: Build C++ library
run: | run: |
${{ matrix.os == 'ubuntu-latest' && 'FSANITIZE=1' || '' }} luarocks make --no-install 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 - name: Run busted tests
run: | run: |
${{ matrix.os == 'ubuntu-latest' && 'LD_PRELOAD=/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' || '' }} busted -c -v busted -c -v
- name: Report test coverage
if: success() && !startsWith(matrix.os, 'windows-') && startsWith(matrix.lua_version, 'luajit-')
continue-on-error: true
run: luacov-coveralls -e .luarocks -e spec
env: env:
COVERALLS_REPO_TOKEN: ${{ github.token }} 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: finish:
if: always() if: always()
@@ -58,3 +86,25 @@ jobs:
uses: coverallsapp/github-action@v2 uses: coverallsapp/github-action@v2
with: with:
parallel-finished: true 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

6
.gitignore vendored
View File

@@ -3,6 +3,11 @@
/lua_modules /lua_modules
/.luarocks /.luarocks
/config.mk /config.mk
*.gcda
*.gcno
*.gcov
*.lcov
*.out
*.pch *.pch
*.gch *.gch
*.lib *.lib
@@ -11,5 +16,6 @@
*.obj *.obj
*.exp *.exp
*.dll *.dll
*.rock
.cache/ .cache/
compile_commands.json compile_commands.json

5
.luacov Normal file
View File

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

View File

@@ -5,36 +5,57 @@ LIBFLAG := -shared
LIB_EXT := $(if $(filter Windows_NT,$(OS)),dll,so) LIB_EXT := $(if $(filter Windows_NT,$(OS)),dll,so)
LUA_INCDIR := /usr/include LUA_INCDIR := /usr/include
SRCDIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) 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) ifeq ($(OS),Windows_NT)
is_clang = $(filter %clang++,$(CXX)) is_clang = $(filter %clang++,$(CXX))
is_gcc = $(filter %g++,$(CXX)) is_gcc = $(filter %g++,$(CXX))
ifdef FSANITIZE
$(error "FSANITIZE is not supported on Windows")
endif
else else
uname_s := $(shell uname -s) uname_s := $(shell uname -s)
ifeq ($(uname_s),Darwin) ifeq ($(uname_s),Darwin)
is_clang = 1
is_gcc =
CC := env MACOSX_DEPLOYMENT_TARGET=11.0 gcc CC := env MACOSX_DEPLOYMENT_TARGET=11.0 gcc
CXX := env MACOSX_DEPLOYMENT_TARGET=11.0 g++ CXX := env MACOSX_DEPLOYMENT_TARGET=11.0 g++
LIBFLAG := -bundle -undefined dynamic_lookup LIBFLAG := -bundle -undefined dynamic_lookup
is_clang = 1
is_gcc =
else else
is_clang = $(filter %clang++,$(CXX)) is_clang = $(filter %clang++,$(CXX))
is_gcc = $(filter %g++,$(CXX)) is_gcc = $(filter %g++,$(CXX))
SANITIZE_FLAGS += -fsanitize=bounds-strict
endif endif
endif endif
OPTFLAG := -O2
SANITIZE_FLAGS := -fsanitize=undefined -fsanitize=address -fsanitize=alignment -fsanitize=bounds-strict \
-fsanitize=shift -fsanitize=unreachable -fsanitize=bool \
-fsanitize=enum
LTO_FLAGS := -flto=auto
-include config.mk -include config.mk
ifeq ($(origin LUAROCKS), command line) ifeq ($(origin LUAROCKS), command line)
ifdef FCOV
CCFLAGS := $(patsubst -O%,,$(CFLAGS))
else
ifdef FDEBUG
CCFLAGS := $(patsubst -O%,,$(CFLAGS)) -Og -g
else
CCFLAGS := $(CFLAGS) CCFLAGS := $(CFLAGS)
endif
endif
override CFLAGS := -std=c99 $(CCFLAGS) override CFLAGS := -std=c99 $(CCFLAGS)
ifneq ($(filter %gcc,$(CC)),) ifneq ($(filter %gcc,$(CC)),)
@@ -52,10 +73,13 @@ endif
CCFLAGS += -Wall -fvisibility=hidden -Wformat=2 -Wconversion -Wimplicit-fallthrough CCFLAGS += -Wall -fvisibility=hidden -Wformat=2 -Wconversion -Wimplicit-fallthrough
ifdef FCOV
CCFLAGS += $(COVERAGE_FLAGS)
endif
ifdef FSANITIZE ifdef FSANITIZE
CCFLAGS += $(SANITIZE_FLAGS) CCFLAGS += $(SANITIZE_FLAGS)
endif endif
ifndef FNOLTO ifdef FLTO
CCFLAGS += $(LTO_FLAGS) CCFLAGS += $(LTO_FLAGS)
endif endif
@@ -69,7 +93,7 @@ else
endif endif
override CPPFLAGS += -I$(SRCDIR) -I$(SRCDIR)/kiwi -I"$(LUA_INCDIR)" override CPPFLAGS += -I$(SRCDIR) -I$(SRCDIR)/kiwi -I"$(LUA_INCDIR)"
override CXXFLAGS += -std=c++14 -fno-rtti $(CCFLAGS) override CXXFLAGS += -std=c++17 -fno-rtti $(CCFLAGS)
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
override CPPFLAGS += -DLUA_BUILD_AS_DLL override CPPFLAGS += -DLUA_BUILD_AS_DLL
@@ -80,11 +104,9 @@ ifdef LUA
LUA_VERSION ?= $(lastword $(shell "$(LUA)" -e "print(_VERSION)")) LUA_VERSION ?= $(lastword $(shell "$(LUA)" -e "print(_VERSION)"))
endif endif
ifndef LUA_VERSION ifdef LUA_VERSION
LJKIWI_CKIWI := 1 ifneq ($(LUA_VERSION),5.1)
else LJKIWI_CFFI ?= 0
ifeq ($(LUA_VERSION),5.1)
LJKIWI_CKIWI := 1
endif endif
endif endif
@@ -92,8 +114,10 @@ kiwi_lib_srcs := AssocVector.h constraint.h debug.h errors.h expression.h kiwi.h
row.h shareddata.h solver.h solverimpl.h strength.h symbol.h symbolics.h term.h \ row.h shareddata.h solver.h solverimpl.h strength.h symbol.h symbolics.h term.h \
util.h variable.h version.h util.h variable.h version.h
objs := luakiwi.o ifneq ($(LJKIWI_LUA),0)
ifdef LJKIWI_CKIWI objs += luakiwi.o
endif
ifneq ($(LJKIWI_CFFI),0)
objs += ckiwi.o objs += ckiwi.o
endif endif
@@ -107,7 +131,7 @@ install:
$(CP) -f kiwi.lua $(INST_LUADIR)/kiwi.lua $(CP) -f kiwi.lua $(INST_LUADIR)/kiwi.lua
mostlyclean: mostlyclean:
$(RM) -f ljkiwi.$(LIB_EXT) $(objs) $(RM) -f ljkiwi.$(LIB_EXT) $(objs) $(objs:.o=.gcda) $(objs:.o=.gcno)
clean: mostlyclean clean: mostlyclean
$(RM) -f $(PCH) $(RM) -f $(PCH)

View File

@@ -9,13 +9,12 @@ ljkiwi - Free LuaJIT FFI and Lua C API kiwi (Cassowary derived) constraint solve
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. 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. 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. 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 and widely used it was simpler to provide a LuaJIT FFI wrapper. There is also a Lua C API binding with support for 5.1 through 5.4. 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. This package has no dependencies other than a supported C++14 compiler 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 ## Example
@@ -91,4 +90,4 @@ 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. 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 ## Documentation
WIP - However the API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server. The API is fully annotated and will work with lua-language-server. Documentation can also be generated with lua-language-server.

View File

@@ -3,6 +3,7 @@
#include <kiwi/kiwi.h> #include <kiwi/kiwi.h>
#include <climits> #include <climits>
#include <cstdarg>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <string> #include <string>
@@ -326,7 +327,7 @@ bool kiwi_solver_has_constraint(const KiwiSolver* s, KiwiConstraint* constraint)
} }
const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* s, KiwiVar* var, double strength) { const KiwiErr* kiwi_solver_add_edit_var(KiwiSolver* s, KiwiVar* var, double strength) {
return wrap_err(s, var, [strength](auto& s, auto&& v) { return wrap_err(s, var, [strength](auto&& s, auto&& v) {
s.addEditVariable(Variable(v), strength); s.addEditVariable(Variable(v), strength);
}); });
} }
@@ -373,4 +374,31 @@ char* kiwi_solver_dumps(const KiwiSolver* s) {
return buf; 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" } // extern "C"

View File

@@ -61,7 +61,7 @@ typedef struct KiwiExpression {
#if defined(LJKIWI_LUAJIT_DEF) #if defined(LJKIWI_LUAJIT_DEF)
KiwiTerm terms_[?]; KiwiTerm terms_[?];
#elif defined(LJKIWI_USE_FAM_1) #elif defined(LJKIWI_USE_FAM_1)
KiwiTerm terms_[1]; // LuaJIT: struct KiwiTerm terms_[?]; KiwiTerm terms_[1];
#else #else
KiwiTerm terms_[]; KiwiTerm terms_[];
#endif #endif
@@ -74,8 +74,22 @@ typedef struct KiwiErr {
bool must_free; bool must_free;
} KiwiErr; } 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; 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 KiwiVar* kiwi_var_construct(const char* name);
LJKIWI_EXP void kiwi_var_release(KiwiVar* var); LJKIWI_EXP void kiwi_var_release(KiwiVar* var);
LJKIWI_EXP void kiwi_var_retain(KiwiVar* var); LJKIWI_EXP void kiwi_var_retain(KiwiVar* var);

View File

@@ -64,8 +64,16 @@ typedef struct KiwiErr {
bool must_free; bool must_free;
} KiwiErr; } KiwiErr;
typedef struct KiwiTuple {
int count;
const double* values[?];
} KiwiTuple;
struct KiwiSolver; struct KiwiSolver;
void kiwi_tuple_init(KiwiTuple* tuple, int count, ...);
void kiwi_tuple_destroy(KiwiTuple* tuple);
KiwiVar* kiwi_var_construct(const char* name); KiwiVar* kiwi_var_construct(const char* name);
void kiwi_var_release(KiwiVar* var); void kiwi_var_release(KiwiVar* var);
void kiwi_var_retain(KiwiVar* var); void kiwi_var_retain(KiwiVar* var);
@@ -175,6 +183,7 @@ function kiwi.is_var(o)
end end
local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]] local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]]
local SIZEOF_TERM = assert(ffi.sizeof(Term))
kiwi.Term = Term kiwi.Term = Term
function kiwi.is_term(o) function kiwi.is_term(o)
@@ -195,18 +204,28 @@ function kiwi.is_constraint(o)
return ffi_istype(Constraint, o) return ffi_istype(Constraint, o)
end end
local Tuple = ffi.typeof("struct KiwiTuple") --[[@as kiwi.Tuple]]
kiwi.Tuple = Tuple
---@class kiwi.Tuple: ffi.cdata*
---@field values ffi.cdata*
---@field count integer
---@overload fun(vars: kiwi.Var[]): kiwi.Tuple
ffi.metatype(Tuple, {
__new = function(self, vars)
local t = ffi_new(self, #vars)
ljkiwi.kiwi_tuple_init(t, #vars, unpack(vars))
return ffi_gc(t, ljkiwi.kiwi_tuple_destroy)
end,
})
---@param expr kiwi.Expression ---@param expr kiwi.Expression
---@param var kiwi.Var ---@param var kiwi.Var
---@param coeff number? ---@param coeff number?
---@nodiscard ---@nodiscard
local function add_expr_term(expr, var, coeff) local function add_expr_term(expr, var, coeff)
local ret = ffi_gc(ffi_new(Expression, expr.term_count + 1), ljkiwi.kiwi_expression_destroy) --[[@as kiwi.Expression]] local ret = ffi_gc(ffi_new(Expression, expr.term_count + 1), ljkiwi.kiwi_expression_destroy) --[[@as kiwi.Expression]]
for i = 0, expr.term_count - 1 do ffi_copy(ret.terms_, expr.terms_, SIZEOF_TERM * expr.term_count)
local st = expr.terms_[i] --[[@as kiwi.Term]]
local dt = ret.terms_[i] --[[@as kiwi.Term]]
dt.var = st.var
dt.coefficient = st.coefficient
end
local dt = ret.terms_[expr.term_count] local dt = ret.terms_[expr.term_count]
dt.var = var dt.var = var
dt.coefficient = coeff or 1.0 dt.coefficient = coeff or 1.0
@@ -282,8 +301,6 @@ local OP_NAMES = {
EQ = "==", EQ = "==",
} }
local SIZEOF_TERM = ffi.sizeof(Term) --[[@as integer]]
local tmpexpr = ffi_new(Expression, 2) --[[@as kiwi.Expression]] local tmpexpr = ffi_new(Expression, 2) --[[@as kiwi.Expression]]
local tmpexpr_r = ffi_new(Expression, 1) --[[@as kiwi.Expression]] local tmpexpr_r = ffi_new(Expression, 1) --[[@as kiwi.Expression]]
@@ -622,13 +639,7 @@ do
---@nodiscard ---@nodiscard
local function new_expr_constant(expr, constant) local function new_expr_constant(expr, constant)
local ret = ffi_gc(ffi_new(Expression, expr.term_count), ljkiwi.kiwi_expression_destroy) --[[@as kiwi.Expression]] local ret = ffi_gc(ffi_new(Expression, expr.term_count), ljkiwi.kiwi_expression_destroy) --[[@as kiwi.Expression]]
ffi_copy(ret.terms_, expr.terms_, SIZEOF_TERM * expr.term_count)
for i = 0, expr.term_count - 1 do
local dt = ret.terms_[i] --[[@as kiwi.Term]]
local st = expr.terms_[i] --[[@as kiwi.Term]]
dt.var = st.var
dt.coefficient = st.coefficient
end
ret.constant = constant ret.constant = constant
ret.term_count = expr.term_count ret.term_count = expr.term_count
ljkiwi.kiwi_expression_retain(ret) ljkiwi.kiwi_expression_retain(ret)

View File

@@ -33,7 +33,6 @@ public:
double value() const { return m_value; } double value() const { return m_value; }
void setValue(double value) { m_value = value; } void setValue(double value) { m_value = value; }
private:
std::string m_name; std::string m_name;
double m_value; double m_value;

View File

@@ -248,6 +248,7 @@ KiwiTerm* term_new(lua_State* L) {
inline KiwiExpression* expr_new(lua_State* L, int nterms) { inline KiwiExpression* expr_new(lua_State* L, int nterms) {
auto* expr = static_cast<KiwiExpression*>(lua_newuserdata(L, KiwiExpression::sz(nterms))); auto* expr = static_cast<KiwiExpression*>(lua_newuserdata(L, KiwiExpression::sz(nterms)));
expr->term_count = 0;
expr->owner = nullptr; expr->owner = nullptr;
push_type(L, EXPR); push_type(L, EXPR);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);
@@ -265,7 +266,8 @@ inline ConstraintData* constraint_new(
push_type(L, CONSTRAINT); push_type(L, CONSTRAINT);
lua_setmetatable(L, -2); lua_setmetatable(L, -2);
if (lk_unlikely(!(*c = kiwi_constraint_new(lhs, rhs, op, strength)))) { *c = kiwi_constraint_new(lhs, rhs, op, strength);
if (lk_unlikely(!*c)) {
lua_rawgeti(L, lua_upvalueindex(1), MEM_ERR_MSG); lua_rawgeti(L, lua_upvalueindex(1), MEM_ERR_MSG);
lua_error(L); lua_error(L);
} }
@@ -929,9 +931,8 @@ int lkiwi_expr_new(lua_State* L) {
auto* expr = expr_new(L, nterms); auto* expr = expr_new(L, nterms);
expr->constant = constant; expr->constant = constant;
expr->term_count = nterms;
for (int i = 0; i < nterms; i++) { for (int i = 0; i < nterms; ++i, ++expr->term_count) {
const auto* term = get_term(L, i + 2); const auto* term = get_term(L, i + 2);
expr->terms[i].var = retain_unmanaged(term->var); expr->terms[i].var = retain_unmanaged(term->var);
expr->terms[i].coefficient = term->coefficient; expr->terms[i].coefficient = term->coefficient;

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)",
},
}

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)

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)

View File

@@ -127,6 +127,8 @@ describe("Var", function()
assert.equal(v2, terms[2].var) assert.equal(v2, terms[2].var)
assert.equal(-1.0, terms[2].coefficient) assert.equal(-1.0, terms[2].coefficient)
-- TODO: terms and expressions
assert.error(function() assert.error(function()
local _ = v - "foo" local _ = v - "foo"
end) end)