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
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
View File

@@ -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
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)
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)
CCFLAGS := $(CFLAGS)
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)

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.
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.

View File

@@ -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"

View File

@@ -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);

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

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(-1.0, terms[2].coefficient)
-- TODO: terms and expressions
assert.error(function()
local _ = v - "foo"
end)