diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..25f8fd9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,146 @@ +name: CI + +on: + pull_request: + push: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + YICES_REF: yices-2.7.0 + +jobs: + unix: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - uses: actions/checkout@v5 + + - name: Checkout Yices + uses: actions/checkout@v5 + with: + repository: SRI-CSL/yices2 + ref: ${{ env.YICES_REF }} + path: y2 + fetch-depth: 1 + sparse-checkout: | + Makefile + Makefile.build + configure + configure.ac + config.guess + config.sub + gmaketest + install-sh + make.include.in + autoconf + configs + scripts + src + utils + sparse-checkout-cone-mode: false + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y ant autoconf automake g++ gcc gperf libgmp-dev libtool m4 make pkg-config + + - name: Install macOS dependencies + if: runner.os == 'macOS' + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + run: | + brew install ant autoconf automake gmp gperf libtool make pkg-config + + - name: Build and test + env: + YICES_SRC: ${{ github.workspace }}/y2 + YICES_PREFIX: ${{ runner.temp }}/yices-install + run: bash scripts/ci-build-test-unix.sh + + windows: + name: windows-latest + runs-on: windows-latest + + steps: + - run: git config --system core.longpaths true + + - run: git config --global core.autocrlf false && git config --global core.eol lf + + - uses: actions/checkout@v5 + + - name: Checkout Yices + uses: actions/checkout@v5 + with: + repository: SRI-CSL/yices2 + ref: ${{ env.YICES_REF }} + path: y2 + fetch-depth: 1 + sparse-checkout: | + Makefile + Makefile.build + configure + configure.ac + config.guess + config.sub + gmaketest + install-sh + make.include.in + autoconf + configs + scripts + src + utils + sparse-checkout-cone-mode: false + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + + - name: Install Ant + shell: powershell + run: choco install ant -y + + - name: Install Cygwin dependencies + uses: cygwin/cygwin-install-action@v4 + with: + packages: | + autoconf, + automake, + cmake, + coreutils, + curl, + gperf, + libtool, + m4, + make, + mingw64-x86_64-gcc-core, + mingw64-x86_64-gcc-g++, + moreutils, + wget + + - name: Build and test + shell: bash + env: + CYGWIN: winsymlinks:native binmode + YICES_SRC: ${{ github.workspace }}/y2 + YICES_PREFIX: /tmp/yices-install + run: bash scripts/ci-build-test-windows.sh diff --git a/ant.sh b/ant.sh index 42b359c..5ee5d95 100755 --- a/ant.sh +++ b/ant.sh @@ -1,6 +1,8 @@ #!/bin/bash #uses the build.sh to mimic what 'ant install' does without 'ant' +set -e + REPO_ROOT=${PWD} YC=${REPO_ROOT}/build/classes @@ -8,6 +10,6 @@ YI=${REPO_ROOT}/dist/lib YICES_CLASSPATH=${YC} YICES_JNI=${YI} ./build.sh -jar -cvfm ${YI}/yices.jar ${REPO_ROOT}/MANIFEST.txt -C ${YC} ${YC}/com/sri/yices/*.class +jar -cvfm ${YI}/yices.jar ${REPO_ROOT}/MANIFEST.txt -C ${YC} . java -Djava.library.path=${REPO_ROOT}/dist/lib -jar ${REPO_ROOT}/dist/lib/yices.jar diff --git a/build.xml b/build.xml index 8dceaf6..b3696ce 100644 --- a/build.xml +++ b/build.xml @@ -223,7 +223,7 @@ Executing "make ${library}" in ${code} + failonerror="true"> @@ -233,7 +233,7 @@ + failonerror="true"> @@ -250,7 +250,7 @@ + failonerror="true"> @@ -286,6 +286,7 @@ + diff --git a/scripts/ci-build-test-unix.sh b/scripts/ci-build-test-unix.sh new file mode 100644 index 0000000..cb9aa91 --- /dev/null +++ b/scripts/ci-build-test-unix.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +YICES_SRC="${YICES_SRC:?set YICES_SRC}" +YICES_PREFIX="${YICES_PREFIX:?set YICES_PREFIX}" + +configure_brew_env() { + if [[ "$(uname)" != "Darwin" ]]; then + return + fi + + local brew_prefix + if [[ -d /opt/homebrew ]]; then + brew_prefix=/opt/homebrew + else + brew_prefix=/usr/local + fi + + export CPPFLAGS="-I${brew_prefix}/include ${CPPFLAGS:-}" + export LDFLAGS="-L${brew_prefix}/lib ${LDFLAGS:-}" + export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" + export DYLD_LIBRARY_PATH="${brew_prefix}/lib${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}" +} + +build_yices() { + rm -rf "${YICES_PREFIX}" + mkdir -p "${YICES_PREFIX}" + pushd "${YICES_SRC}" >/dev/null + autoconf + ./configure --prefix="${YICES_PREFIX}" --enable-thread-safety --disable-mcsat + make MODE=release + make MODE=release install + popd >/dev/null +} + +run_java_ci() { + export CPPFLAGS="-I${YICES_PREFIX}/include ${CPPFLAGS:-}" + export LDFLAGS="-L${YICES_PREFIX}/lib ${LDFLAGS:-}" + + if [[ "$(uname)" == "Darwin" ]]; then + export DYLD_LIBRARY_PATH="${YICES_PREFIX}/lib${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}" + else + export LD_LIBRARY_PATH="${YICES_PREFIX}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + fi + + export YICES_JNI="${REPO_ROOT}/dist/lib" + export YICES_CLASSPATH="${REPO_ROOT}/build/classes" + + pushd "${REPO_ROOT}" >/dev/null + ant clean test + popd >/dev/null +} + +configure_brew_env +build_yices +run_java_ci diff --git a/scripts/ci-build-test-windows.sh b/scripts/ci-build-test-windows.sh new file mode 100644 index 0000000..d590571 --- /dev/null +++ b/scripts/ci-build-test-windows.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +YICES_SRC="$(cygpath -u "${YICES_SRC:?set YICES_SRC}")" +YICES_PREFIX="${YICES_PREFIX:?set YICES_PREFIX}" +JAVA_HOME_UNIX="$(cygpath -u "${JAVA_HOME:?set JAVA_HOME}")" +JAVAC="${JAVA_HOME_UNIX}/bin/javac" +JAVA="${JAVA_HOME_UNIX}/bin/java" +JAR="${JAVA_HOME_UNIX}/bin/jar" + +win_path() { + cygpath -w "$1" +} + +win_path_list() { + cygpath -wp "$1" +} + +build_gmp() { + mkdir -p /tools/dynamic_gmp /tools/static_gmp + pushd /tools >/dev/null + + local archive=gmp-6.3.0.tar.xz + if [[ ! -f "${archive}" ]]; then + curl -fL --retry 5 --retry-delay 2 --connect-timeout 20 --max-time 600 \ + https://ftp.gnu.org/gnu/gmp/${archive} -o "${archive}" || \ + wget --tries=5 --timeout=20 -O "${archive}" https://ftp.gnu.org/gnu/gmp/${archive} || \ + curl -fL --retry 5 --retry-delay 2 --connect-timeout 20 --max-time 600 \ + https://mirrors.kernel.org/gnu/gmp/${archive} -o "${archive}" || \ + wget --tries=5 --timeout=20 -O "${archive}" https://mirrors.kernel.org/gnu/gmp/${archive} || \ + curl -fL --retry 5 --retry-delay 2 --connect-timeout 20 --max-time 600 \ + https://gmplib.org/download/gmp/${archive} -o "${archive}" || \ + wget --tries=5 --timeout=20 -O "${archive}" https://gmplib.org/download/gmp/${archive} + fi + + rm -rf gmp-6.3.0 + tar xf "${archive}" + cd gmp-6.3.0 + ./configure --host=x86_64-w64-mingw32 --build=i686-pc-cygwin --enable-cxx --enable-shared --disable-static --prefix=/tools/dynamic_gmp + make + make install + make clean + ./configure --host=x86_64-w64-mingw32 --build=i686-pc-cygwin --enable-cxx --enable-static --disable-shared --prefix=/tools/static_gmp + make + make install + + popd >/dev/null +} + +build_yices() { + pushd "${YICES_SRC}" >/dev/null + autoconf + ./configure \ + --host=x86_64-w64-mingw32 \ + --enable-thread-safety \ + CPPFLAGS="-I/tools/dynamic_gmp/include" \ + LDFLAGS="-L/tools/dynamic_gmp/lib" \ + --with-static-gmp=/tools/static_gmp/lib/libgmp.a \ + --with-static-gmp-include-dir=/tools/static_gmp/include + export LD_LIBRARY_PATH="/usr/local/lib/:${LD_LIBRARY_PATH:-}" + make OPTION=mingw64 MODE=release dist + + local dist_dir + dist_dir="$(find build -type d -path '*/release/dist' | head -n 1)" + if [[ -z "${dist_dir}" ]]; then + echo "failed to locate Yices dist directory" >&2 + exit 1 + fi + + rm -rf "${YICES_PREFIX}" + mkdir -p "${YICES_PREFIX}" + cp -R "${dist_dir}/." "${YICES_PREFIX}/" + popd >/dev/null +} + +build_java_bindings() { + mkdir -p "${REPO_ROOT}/build/classes" "${REPO_ROOT}/build/test_classes" "${REPO_ROOT}/dist/lib" + + pushd "${REPO_ROOT}" >/dev/null + find src/main/java/com/sri/yices -name '*.java' | sort > build/main-sources.txt + "${JAVAC}" \ + -d "$(win_path "${REPO_ROOT}/build/classes")" \ + -h "$(win_path "${REPO_ROOT}/src/main/java/com/sri/yices")" \ + @"build/main-sources.txt" + + x86_64-w64-mingw32-g++ \ + -I "${JAVA_HOME_UNIX}/include" \ + -I "${JAVA_HOME_UNIX}/include/win32" \ + -I "${YICES_PREFIX}/include" \ + -I /tools/dynamic_gmp/include \ + -g -Wall -fpermissive \ + -c src/main/java/com/sri/yices/yicesJNIforWindows.cpp \ + -o build/yicesJNIforWindows.o + + x86_64-w64-mingw32-g++ \ + -shared \ + -o dist/lib/yices2java.dll \ + build/yicesJNIforWindows.o \ + -L "${YICES_PREFIX}/lib" \ + -L /tools/dynamic_gmp/lib \ + -lyices -lgmp + + "${JAR}" -cvfm \ + "$(win_path "${REPO_ROOT}/dist/lib/yices.jar")" \ + "$(win_path "${REPO_ROOT}/MANIFEST.txt")" \ + -C "$(win_path "${REPO_ROOT}/build/classes")" . + popd >/dev/null +} + +stage_runtime_dlls() { + cp "${YICES_PREFIX}/bin/libyices.dll" "${REPO_ROOT}/dist/lib/" + cp /tools/dynamic_gmp/bin/libgmp-10.dll "${REPO_ROOT}/dist/lib/" + cp /usr/x86_64-w64-mingw32/sys-root/mingw/bin/libstdc++-6.dll "${REPO_ROOT}/dist/lib/" + cp /usr/x86_64-w64-mingw32/sys-root/mingw/bin/libgcc_s_seh-1.dll "${REPO_ROOT}/dist/lib/" + cp /usr/x86_64-w64-mingw32/sys-root/mingw/bin/libwinpthread-1.dll "${REPO_ROOT}/dist/lib/" +} + +run_tests() { + pushd "${REPO_ROOT}" >/dev/null + find src/test/java -name '*.java' | sort > build/test-sources.txt + + local compile_cp + compile_cp="$(win_path_list "${REPO_ROOT}/dist/lib/yices.jar:${REPO_ROOT}/lib/junit-4.12.jar:${REPO_ROOT}/lib/hamcrest-core-1.3.jar")" + "${JAVAC}" \ + -cp "${compile_cp}" \ + -d "$(win_path "${REPO_ROOT}/build/test_classes")" \ + @"build/test-sources.txt" + + local runtime_cp runtime_path + runtime_cp="$(win_path_list "${REPO_ROOT}/build/test_classes:${REPO_ROOT}/dist/lib/yices.jar:${REPO_ROOT}/lib/junit-4.12.jar:${REPO_ROOT}/lib/hamcrest-core-1.3.jar")" + runtime_path="$(win_path_list "${REPO_ROOT}/dist/lib:/tools/dynamic_gmp/bin:/usr/x86_64-w64-mingw32/sys-root/mingw/bin")" + + env PATH="${runtime_path}" \ + "${JAVA}" \ + "-Djava.library.path=$(win_path "${REPO_ROOT}/dist/lib")" \ + -cp "${runtime_cp}" \ + org.junit.runner.JUnitCore \ + com.sri.yices.TestBigRationals \ + com.sri.yices.TestConstructor \ + com.sri.yices.TestContext \ + com.sri.yices.TestStatus \ + com.sri.yices.TestTypes \ + com.sri.yices.TestYices \ + com.sri.yices.TestModels \ + com.sri.yices.TestTermComponents \ + com.sri.yices.TestThreads + popd >/dev/null +} + +build_gmp +build_yices +build_java_bindings +stage_runtime_dlls +run_tests diff --git a/src/main/java/com/sri/yices/Context.java b/src/main/java/com/sri/yices/Context.java index 53ac5b6..6803df5 100644 --- a/src/main/java/com/sri/yices/Context.java +++ b/src/main/java/com/sri/yices/Context.java @@ -331,7 +331,7 @@ public int getModelInterpolant() { // Since 2.6.4 public Status checkWithAssumptions(Parameters params, int[] assumptions) { - int code = Yices.checkContextWithAssumptions(ptr, params.getPtr(), assumptions); + int code = Yices.checkContextWithAssumptions(ptr, params == null ? 0 : params.getPtr(), assumptions); if (code < 0) { throw new YicesException(); } @@ -340,7 +340,7 @@ public Status checkWithAssumptions(Parameters params, int[] assumptions) { // Since 2.6.4 public Status checkWithModel(Parameters params, Model model, int[] assumptions) { - int code = Yices.checkContextWithModel(ptr, params.getPtr(), model.getPtr(), assumptions); + int code = Yices.checkContextWithModel(ptr, params == null ? 0 : params.getPtr(), model.getPtr(), assumptions); if (code < 0) { YicesException error = YicesException.checkVersion(2, 6, 4); if (error == null) { diff --git a/src/main/java/com/sri/yices/Makefile b/src/main/java/com/sri/yices/Makefile index 158571b..88eaff0 100644 --- a/src/main/java/com/sri/yices/Makefile +++ b/src/main/java/com/sri/yices/Makefile @@ -56,9 +56,15 @@ libyices2java_install_name := $(YICES_JNI)/libyices2java.dylib # we ignore versions and soname for now # default include directories for jni.h and jni_md.h -CPPFLAGS := -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/$(OS) -CXXFLAGS := -g -fPIC -LIBS := -lyices -lgmp +GMP_CPPFLAGS ?= $(shell pkg-config --cflags gmp 2>/dev/null) +GMP_LIBS ?= $(shell pkg-config --libs gmp 2>/dev/null) + +CPPFLAGS += -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/$(OS) $(GMP_CPPFLAGS) +CXXFLAGS += -g -fPIC +ifeq ($(strip $(GMP_LIBS)),) +GMP_LIBS := -lgmp +endif +LIBS += -lyices $(GMP_LIBS) CXX ?= g++ @@ -77,16 +83,16 @@ $(YICES_CLASSPATH)/com/sri/yices/%.class: %.java $(JAVAC) -d $(YICES_CLASSPATH) *.java com_sri_yices_Yices.h: $(YICES_CLASSPATH)/com/sri/yices/Yices.class - $(JAVAC) -h . *.java + $(JAVAC) -d $(YICES_CLASSPATH) -h . *.java yicesJNI.o: yicesJNI.cpp com_sri_yices_Yices.h $(CXX) $(CPPFLAGS) $(CXXFLAGS) -Wall -c yicesJNI.cpp libyices2java.dylib: yicesJNI.o - $(CXX) $(CPPFLAGS) $(CXXFLAGS) -dynamiclib -o $@ yicesJNI.o $(LIBS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) -dynamiclib -o $@ yicesJNI.o $(LIBS) libyices2java.so: yicesJNI.o - $(CXX) $(CFLAGS) $(LDFLAGS) -shared -o $@ yicesJNI.o $(LIBS) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) -shared -o $@ yicesJNI.o $(LIBS) LIBDIR := $(YICES_JNI) diff --git a/src/main/java/com/sri/yices/YValTag.java b/src/main/java/com/sri/yices/YValTag.java index eb43561..a960762 100644 --- a/src/main/java/com/sri/yices/YValTag.java +++ b/src/main/java/com/sri/yices/YValTag.java @@ -13,6 +13,7 @@ public enum YValTag { BOOL, RATIONAL, ALGEBRAIC, + FINITEFIELD, BV, SCALAR, TUPLE, diff --git a/src/main/java/com/sri/yices/yicesJNI.cpp b/src/main/java/com/sri/yices/yicesJNI.cpp index 27290e2..abb7c96 100644 --- a/src/main/java/com/sri/yices/yicesJNI.cpp +++ b/src/main/java/com/sri/yices/yicesJNI.cpp @@ -2674,7 +2674,7 @@ JNIEXPORT jbooleanArray JNICALL Java_com_sri_yices_Yices_bvConstValue(JNIEnv *en assert(code >= 0); result = convertToBoolArray(env, n, tmp); delete [] tmp; - } catch (std::bad_alloc) { + } catch (std::bad_alloc&) { out_of_mem_exception(env); } } @@ -3215,10 +3215,10 @@ JNIEXPORT jint JNICALL Java_com_sri_yices_Yices_checkContextWithInterpolation(JN ctx.model = NULL; try { result = yices_check_context_with_interpolation(&ctx, reinterpret_cast(params), build_model); - if (result == STATUS_UNSAT) { + if (result == YICES_STATUS_UNSAT) { // set the interpolant array env->SetIntArrayRegion(interpolant, 0, 1, &ctx.interpolant); - } else if(build_model && result == STATUS_SAT ) { + } else if(build_model && result == YICES_STATUS_SAT ) { model_t *model = ctx.model; jlong mdl = reinterpret_cast(model); // set the model array @@ -3817,7 +3817,7 @@ JNIEXPORT jint JNICALL Java_com_sri_yices_Yices_checkFormula(JNIEnv *env, jclass } if (wantModel) { code = yices_check_formula(formula, ls, &model, ds); - if (code == STATUS_SAT) { + if (code == YICES_STATUS_SAT) { mdl = reinterpret_cast(model); env->SetLongArrayRegion(marr, 0, 1, &mdl); } @@ -3873,7 +3873,7 @@ JNIEXPORT jint JNICALL Java_com_sri_yices_Yices_checkFormulas(JNIEnv *env, jclas } if (wantModel) { code = yices_check_formulas(tarr, n, ls, &model, ds); - if (code == STATUS_SAT) { + if (code == YICES_STATUS_SAT) { mdl = reinterpret_cast(model); env->SetLongArrayRegion(marr, 0, 1, &mdl); } diff --git a/src/test/java/com/sri/yices/TestTermComponents.java b/src/test/java/com/sri/yices/TestTermComponents.java new file mode 100644 index 0000000..7329fba --- /dev/null +++ b/src/test/java/com/sri/yices/TestTermComponents.java @@ -0,0 +1,132 @@ +package com.sri.yices; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assume.assumeTrue; + +public class TestTermComponents { + @Test + public void testProjectedComponentsWhenAvailable() throws Exception { + assumeTrue(TestAssumptions.IS_YICES_INSTALLED); + + Method sumComponent = optionalMethod("sumComponent"); + Method productComponent = optionalMethod("productComponent"); + if (sumComponent == null || productComponent == null) { + return; + } + + int x = Terms.newUninterpretedTerm("term_components_x", Types.INT); + int y = Terms.newUninterpretedTerm("term_components_y", Types.INT); + int bvType = Types.bvType(8); + int bx = Terms.newUninterpretedTerm("term_components_bx", bvType); + int by = Terms.newUninterpretedTerm("term_components_by", bvType); + + try { + int arith = Terms.rationalPoly(new long[] { 3, -5 }, new long[] { 2, 1 }, new int[] { x, y }); + Assert.assertTrue(Terms.isSum(arith)); + + Map arithComponents = collectSumComponents(sumComponent, arith); + Assert.assertEquals(2, arithComponents.size()); + Assert.assertEquals(new BigRational(BigInteger.valueOf(3), BigInteger.valueOf(2)), arithComponents.get(x)); + Assert.assertEquals(new BigRational(BigInteger.valueOf(-5), BigInteger.ONE), arithComponents.get(y)); + + int bsum = Terms.bvAdd( + Terms.bvMul(Terms.bvConst(8, 3), bx), + Terms.bvMul(Terms.bvConst(8, 5), by)); + Assert.assertTrue(Terms.isBvSum(bsum)); + + Map bvComponents = collectBvSumComponents(sumComponent, bsum); + Assert.assertEquals(2, bvComponents.size()); + Assert.assertArrayEquals(Terms.bvConstValue(Terms.bvConst(8, 3)), bvComponents.get(bx)); + Assert.assertArrayEquals(Terms.bvConstValue(Terms.bvConst(8, 5)), bvComponents.get(by)); + + int product = Terms.mul(Terms.power(x, 3), Terms.power(y, 2)); + Assert.assertTrue(Terms.isProduct(product)); + + Map productComponents = collectProductComponents(productComponent, product); + Assert.assertEquals(2, productComponents.size()); + Assert.assertEquals(Integer.valueOf(3), productComponents.get(x)); + Assert.assertEquals(Integer.valueOf(2), productComponents.get(y)); + } finally { + Terms.removeName("term_components_x"); + Terms.removeName("term_components_y"); + Terms.removeName("term_components_bx"); + Terms.removeName("term_components_by"); + } + } + + private static Method optionalMethod(String name) { + try { + return Yices.class.getMethod(name, int.class, int.class); + } catch (NoSuchMethodException e) { + return null; + } + } + + private static Map collectSumComponents(Method method, int term) throws Exception { + Map result = new HashMap<>(); + for (int idx = 0; ; idx++) { + Object component = invoke(method, term, idx); + if (component == null) { + break; + } + int child = (Integer) component.getClass().getMethod("getTerm").invoke(component); + Object factor = component.getClass().getMethod("getFactor").invoke(component); + Assert.assertTrue(factor instanceof BigRational); + result.put(child, (BigRational) factor); + } + return result; + } + + private static Map collectBvSumComponents(Method method, int term) throws Exception { + Map result = new HashMap<>(); + for (int idx = 0; ; idx++) { + Object component = invoke(method, term, idx); + if (component == null) { + break; + } + int child = (Integer) component.getClass().getMethod("getTerm").invoke(component); + Object factor = component.getClass().getMethod("getFactor").invoke(component); + if (factor instanceof boolean[]) { + result.put(child, (boolean[]) factor); + } + } + return result; + } + + private static Map collectProductComponents(Method method, int term) throws Exception { + Map result = new HashMap<>(); + for (int idx = 0; ; idx++) { + Object component = invoke(method, term, idx); + if (component == null) { + break; + } + int child = (Integer) component.getClass().getMethod("getTerm").invoke(component); + int power = (Integer) component.getClass().getMethod("getPower").invoke(component); + result.put(child, power); + } + return result; + } + + private static Object invoke(Method method, int term, int idx) throws Exception { + try { + return method.invoke(null, term, idx); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw e; + } + } +}