Compare commits
10 Commits
c35cea6213
...
08e9bf08e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 08e9bf08e7 | |||
| ef29b8abcb | |||
| 59cb4b3c4f | |||
| 55a3aa1e6f | |||
| dc36e719eb | |||
| d2e769ea30 | |||
| 3e56c503e4 | |||
| f68c24d9ea | |||
| 2b76ba96ac | |||
| 98a3fff28f |
68
.github/workflows/busted.yml
vendored
68
.github/workflows/busted.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Busted
|
||||
|
||||
on: [push, pull_request]
|
||||
@@ -35,19 +36,46 @@ jobs:
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
luarocks install busted
|
||||
luarocks install luacov-coveralls
|
||||
- name: Build C library
|
||||
luarocks install luacov-reporter-lcov
|
||||
- name: Build C++ library
|
||||
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
|
||||
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
|
||||
- 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
|
||||
busted -c -v
|
||||
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:
|
||||
if: always()
|
||||
@@ -58,3 +86,25 @@ jobs:
|
||||
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
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,6 +3,11 @@
|
||||
/lua_modules
|
||||
/.luarocks
|
||||
/config.mk
|
||||
*.gcda
|
||||
*.gcno
|
||||
*.gcov
|
||||
*.lcov
|
||||
*.out
|
||||
*.pch
|
||||
*.gch
|
||||
*.lib
|
||||
@@ -11,5 +16,6 @@
|
||||
*.obj
|
||||
*.exp
|
||||
*.dll
|
||||
*.rock
|
||||
.cache/
|
||||
compile_commands.json
|
||||
|
||||
5
.luacov
Normal file
5
.luacov
Normal file
@@ -0,0 +1,5 @@
|
||||
modules = {
|
||||
kiwi = "kiwi.lua",
|
||||
}
|
||||
reporter = "lcov"
|
||||
reportfile = "luacov.lcov"
|
||||
64
Makefile
64
Makefile
@@ -5,36 +5,57 @@ LIBFLAG := -shared
|
||||
LIB_EXT := $(if $(filter Windows_NT,$(OS)),dll,so)
|
||||
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)
|
||||
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
|
||||
is_clang = 1
|
||||
is_gcc =
|
||||
|
||||
else
|
||||
is_clang = $(filter %clang++,$(CXX))
|
||||
is_gcc = $(filter %g++,$(CXX))
|
||||
|
||||
SANITIZE_FLAGS += -fsanitize=bounds-strict
|
||||
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
|
||||
|
||||
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)),)
|
||||
@@ -52,10 +73,13 @@ endif
|
||||
|
||||
CCFLAGS += -Wall -fvisibility=hidden -Wformat=2 -Wconversion -Wimplicit-fallthrough
|
||||
|
||||
ifdef FCOV
|
||||
CCFLAGS += $(COVERAGE_FLAGS)
|
||||
endif
|
||||
ifdef FSANITIZE
|
||||
CCFLAGS += $(SANITIZE_FLAGS)
|
||||
endif
|
||||
ifndef FNOLTO
|
||||
ifdef FLTO
|
||||
CCFLAGS += $(LTO_FLAGS)
|
||||
endif
|
||||
|
||||
@@ -69,7 +93,7 @@ else
|
||||
endif
|
||||
|
||||
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)
|
||||
override CPPFLAGS += -DLUA_BUILD_AS_DLL
|
||||
@@ -80,11 +104,9 @@ ifdef LUA
|
||||
LUA_VERSION ?= $(lastword $(shell "$(LUA)" -e "print(_VERSION)"))
|
||||
endif
|
||||
|
||||
ifndef LUA_VERSION
|
||||
LJKIWI_CKIWI := 1
|
||||
else
|
||||
ifeq ($(LUA_VERSION),5.1)
|
||||
LJKIWI_CKIWI := 1
|
||||
ifdef LUA_VERSION
|
||||
ifneq ($(LUA_VERSION),5.1)
|
||||
LJKIWI_CFFI ?= 0
|
||||
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 \
|
||||
util.h variable.h version.h
|
||||
|
||||
objs := luakiwi.o
|
||||
ifdef LJKIWI_CKIWI
|
||||
ifneq ($(LJKIWI_LUA),0)
|
||||
objs += luakiwi.o
|
||||
endif
|
||||
ifneq ($(LJKIWI_CFFI),0)
|
||||
objs += ckiwi.o
|
||||
endif
|
||||
|
||||
@@ -107,7 +131,7 @@ install:
|
||||
$(CP) -f kiwi.lua $(INST_LUADIR)/kiwi.lua
|
||||
|
||||
mostlyclean:
|
||||
$(RM) -f ljkiwi.$(LIB_EXT) $(objs)
|
||||
$(RM) -f ljkiwi.$(LIB_EXT) $(objs) $(objs:.o=.gcda) $(objs:.o=.gcno)
|
||||
|
||||
clean: mostlyclean
|
||||
$(RM) -f $(PCH)
|
||||
|
||||
11
README.md
11
README.md
@@ -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.
|
||||
Cassowary constraint solving is a technique that is particularly well suited to user interface layout. It is the algorithm Apple uses for iOS and OS X Auto Layout.
|
||||
|
||||
There are a few Lua implementations or attempts. The SILE typesetting system has a pure Lua implementation of the original Cassowary code, which appears to be correct but is quite slow. There are two extant Lua ports of Kiwi, one that is based on a C rewrite of Kiwi. However testing of these was not encouraging with either segfaults or incorrect results.
|
||||
Since the C++ Kiwi library is well tested and widely used it was simpler to provide a LuaJIT FFI wrapper. There is also a Lua C API binding with support for 5.1 through 5.4.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <kiwi/kiwi.h>
|
||||
|
||||
#include <climits>
|
||||
#include <cstdarg>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#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) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -373,4 +374,31 @@ char* kiwi_solver_dumps(const KiwiSolver* s) {
|
||||
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"
|
||||
|
||||
@@ -61,7 +61,7 @@ typedef struct KiwiExpression {
|
||||
#if defined(LJKIWI_LUAJIT_DEF)
|
||||
KiwiTerm terms_[?];
|
||||
#elif defined(LJKIWI_USE_FAM_1)
|
||||
KiwiTerm terms_[1]; // LuaJIT: struct KiwiTerm terms_[?];
|
||||
KiwiTerm terms_[1];
|
||||
#else
|
||||
KiwiTerm terms_[];
|
||||
#endif
|
||||
@@ -74,8 +74,22 @@ typedef struct KiwiErr {
|
||||
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);
|
||||
|
||||
41
kiwi.lua
41
kiwi.lua
@@ -64,8 +64,16 @@ typedef struct KiwiErr {
|
||||
bool must_free;
|
||||
} KiwiErr;
|
||||
|
||||
typedef struct KiwiTuple {
|
||||
int count;
|
||||
const double* values[?];
|
||||
} KiwiTuple;
|
||||
|
||||
struct KiwiSolver;
|
||||
|
||||
void kiwi_tuple_init(KiwiTuple* tuple, int count, ...);
|
||||
void kiwi_tuple_destroy(KiwiTuple* tuple);
|
||||
|
||||
KiwiVar* kiwi_var_construct(const char* name);
|
||||
void kiwi_var_release(KiwiVar* var);
|
||||
void kiwi_var_retain(KiwiVar* var);
|
||||
@@ -175,6 +183,7 @@ function kiwi.is_var(o)
|
||||
end
|
||||
|
||||
local Term = ffi.typeof("struct KiwiTerm") --[[@as kiwi.Term]]
|
||||
local SIZEOF_TERM = assert(ffi.sizeof(Term))
|
||||
kiwi.Term = Term
|
||||
|
||||
function kiwi.is_term(o)
|
||||
@@ -195,18 +204,28 @@ function kiwi.is_constraint(o)
|
||||
return ffi_istype(Constraint, o)
|
||||
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 var kiwi.Var
|
||||
---@param coeff number?
|
||||
---@nodiscard
|
||||
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]]
|
||||
for i = 0, expr.term_count - 1 do
|
||||
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
|
||||
ffi_copy(ret.terms_, expr.terms_, SIZEOF_TERM * expr.term_count)
|
||||
local dt = ret.terms_[expr.term_count]
|
||||
dt.var = var
|
||||
dt.coefficient = coeff or 1.0
|
||||
@@ -282,8 +301,6 @@ local OP_NAMES = {
|
||||
EQ = "==",
|
||||
}
|
||||
|
||||
local SIZEOF_TERM = ffi.sizeof(Term) --[[@as integer]]
|
||||
|
||||
local tmpexpr = ffi_new(Expression, 2) --[[@as kiwi.Expression]]
|
||||
local tmpexpr_r = ffi_new(Expression, 1) --[[@as kiwi.Expression]]
|
||||
|
||||
@@ -622,13 +639,7 @@ do
|
||||
---@nodiscard
|
||||
local function new_expr_constant(expr, constant)
|
||||
local ret = ffi_gc(ffi_new(Expression, expr.term_count), ljkiwi.kiwi_expression_destroy) --[[@as kiwi.Expression]]
|
||||
|
||||
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
|
||||
ffi_copy(ret.terms_, expr.terms_, SIZEOF_TERM * expr.term_count)
|
||||
ret.constant = constant
|
||||
ret.term_count = expr.term_count
|
||||
ljkiwi.kiwi_expression_retain(ret)
|
||||
|
||||
@@ -33,7 +33,6 @@ public:
|
||||
double value() const { return m_value; }
|
||||
void setValue(double value) { m_value = value; }
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
double m_value;
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ KiwiTerm* term_new(lua_State* L) {
|
||||
|
||||
inline KiwiExpression* expr_new(lua_State* L, int nterms) {
|
||||
auto* expr = static_cast<KiwiExpression*>(lua_newuserdata(L, KiwiExpression::sz(nterms)));
|
||||
expr->term_count = 0;
|
||||
expr->owner = nullptr;
|
||||
push_type(L, EXPR);
|
||||
lua_setmetatable(L, -2);
|
||||
@@ -265,7 +266,8 @@ inline ConstraintData* constraint_new(
|
||||
push_type(L, CONSTRAINT);
|
||||
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_error(L);
|
||||
}
|
||||
@@ -929,9 +931,8 @@ int lkiwi_expr_new(lua_State* L) {
|
||||
|
||||
auto* expr = expr_new(L, nterms);
|
||||
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);
|
||||
expr->terms[i].var = retain_unmanaged(term->var);
|
||||
expr->terms[i].coefficient = term->coefficient;
|
||||
|
||||
40
rockspecs/kiwi-0.1.1-1.rockspec
Normal file
40
rockspecs/kiwi-0.1.1-1.rockspec
Normal 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
240
spec/expression_spec.lua
Normal 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
245
spec/term_spec.lua
Normal 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)
|
||||
@@ -127,6 +127,8 @@ describe("Var", function()
|
||||
assert.equal(v2, terms[2].var)
|
||||
assert.equal(-1.0, terms[2].coefficient)
|
||||
|
||||
-- TODO: terms and expressions
|
||||
|
||||
assert.error(function()
|
||||
local _ = v - "foo"
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user