From dfe299d3996e132e90da6935fd34961056305b94 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 29 Oct 2026 15:45:12 +0100 Subject: [PATCH 1/4] Add the instance module for reading benchmark instance files Add gecode/instance, a small library that reads textual benchmark instances with declarative reader specs supplied by the program. A spec is a short sequence of typed declarations and statements: values such as n: int, fixed-size and triangular arrays, lists that end at a sentinel or at the end of a line, line patterns with typed captures, keyed sections, repeated records, and a small if/let/check with integer arithmetic. The types are int, float, num, word, and rest, ranges such as 0.. $@ +endif +$(INSTANCEDLL) $(INSTANCELIB): $(INSTANCEOBJ) $(INSTANCERES) \ + $(SUPPORTDLL) $(KERNELDLL) $(INTDLL) + $(CXX) $(DLLFLAGS) $(INSTANCEOBJ) $(INSTANCERES) \ + @DLLPATH@ @LINKOUTPUT@$(INSTANCEDLL) $(GLDFLAGS) + $(FIXMANIFEST) $(INSTANCEDLL).manifest + $(MANIFEST) -manifest $(INSTANCEDLL).manifest \ + -outputresource:$(INSTANCEDLL)\;2 +ifeq "@enable_resource@" "yes" $(DRIVERRC): $(RCGEN) $(DRIVERDLL) $(DRIVERHDR) $(DRIVERSRC) > $@ endif $(DRIVERDLL) $(DRIVERLIB): $(DRIVEROBJ) $(DRIVERRES) \ $(SUPPORTDLL) $(KERNELDLL) \ - $(SEARCHDLL) $(INTDLL) $(MMDLL) $(GISTDLL) + $(SEARCHDLL) $(INTDLL) $(INSTANCEDLL) $(MMDLL) $(GISTDLL) $(CXX) $(DLLFLAGS) $(DRIVEROBJ) $(DRIVERRES) \ @DLLPATH@ @LINKOUTPUT@$(DRIVERDLL) $(GLDFLAGS) $(FIXMANIFEST) $(DRIVERDLL).manifest @@ -1968,7 +2053,7 @@ $(FLATZINCRC): endif $(FLATZINCDLL) $(FLATZINCLIB): $(FLATZINCOBJ) $(FLATZINCRES) \ $(SUPPORTDLL) $(KERNELDLL) $(SEARCHDLL) $(INTDLL) \ - $(SETDLL) $(FLOATDLL) $(GISTDLL) $(MMDLL) $(DRIVERDLL) + $(SETDLL) $(FLOATDLL) $(GISTDLL) $(MMDLL) $(INSTANCEDLL) $(DRIVERDLL) $(CXX) $(DLLFLAGS) $(FLATZINCOBJ) $(FLATZINCRES) \ @DLLPATH@ @LINKOUTPUT@$(FLATZINCDLL) $(GLDFLAGS) $(LINKQT) $(LINKDL) $(FIXMANIFEST) $(FLATZINCDLL).manifest @@ -2045,6 +2130,9 @@ $(SETSTATICLIB): $(SETOBJ) $(MMSTATICLIB): $(MMOBJ) $(AR) $(ARFLAGS) $@ $(MMOBJ) $(RANLIB) $@ +$(INSTANCESTATICLIB): $(INSTANCEOBJ) + $(AR) $(ARFLAGS) $@ $(INSTANCEOBJ) + $(RANLIB) $@ $(DRIVERSTATICLIB): $(DRIVEROBJ) $(AR) $(ARFLAGS) $@ $(DRIVEROBJ) $(RANLIB) $@ @@ -2236,6 +2324,27 @@ $(FLATZINCEXE): $(FLATZINCEXEOBJ) $(FLATZINCEXERES) $(ALLLIB) $(FIXMANIFEST) $@.manifest $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 +.PHONY: instancetool +ifeq "@enable_instance@" "yes" +instancetool: $(INSTANCEEXE) +else +instancetool: +endif + +ifeq "@enable_resource@" "yes" +INSTANCEEXERES = $(INSTANCEEXE).res +$(INSTANCEEXE).rc: + $(RCGEN) $(INSTANCEEXE) $(INSTANCEEXESRC) > $@ +else +INSTANCEEXERES = +endif +$(INSTANCEEXE): $(INSTANCEEXEOBJ) $(INSTANCEEXERES) $(ALLLIB) + $(CXX) @EXEOUTPUT@$@ $(INSTANCEEXEOBJ) $(INSTANCEEXERES) \ + $(DLLPATH) $(CXXFLAGS) \ + $(LINKINSTANCE) $(LINKSEARCH) $(LINKINT) $(LINKKERNEL) $(LINKSUPPORT) $(GLDFLAGS) + $(FIXMANIFEST) $@.manifest + $(MANIFEST) -manifest $@.manifest -outputresource:$@\;1 + # # Autoconf @@ -2508,6 +2617,8 @@ clean: $(BLACKBOXLOG).dll_model $(BLACKBOXLOG).dll_parallel $(RMF) $(FLATZINCEXE:%.exe=%.pdb) $(FLATZINCEXE:%=%.manifest) \ $(FLATZINCEXE:%=%.rc) $(FLATZINCEXE:%=%.res) + $(RMF) $(INSTANCEEXE:%.exe=%.pdb) $(INSTANCEEXE:%=%.manifest) \ + $(INSTANCEEXE:%=%.rc) $(INSTANCEEXE:%=%.res) veryclean: clean $(RMF) $(LIBTARGETS) \ @@ -2523,6 +2634,7 @@ veryclean: clean $(RMF) $(TESTPUBLICFLOATSMOKEEXE) $(RMF) $(BLACKBOXFIXTURES) $(RMF) $(FLATZINCEXE) + $(RMF) $(INSTANCEEXE) $(RMF) doc ChangeLog $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcno) $(TESTOBJ:%$(OBJSUFFIX)=%.gcno) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.gcno) $(RMF) $(ALLOBJ:%$(OBJSUFFIX)=%.gcda) $(TESTOBJ:%$(OBJSUFFIX)=%.gcda) $(TESTAUXOBJ:%$(OBJSUFFIX)=%.gcda) diff --git a/README.md b/README.md index 6bd1d280b4..b79d9de963 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ state. | `--enable-set-vars` | `GECODE_ENABLE_SET_VARS` | Supported directly | Default `ON` | | `--enable-float-vars` | `GECODE_ENABLE_FLOAT_VARS` | Supported directly | Default `ON` | | `--enable-minimodel` | `GECODE_ENABLE_MINIMODEL` | Supported directly | Default `ON` | +| `--enable-instance` | `GECODE_ENABLE_INSTANCE` | Supported directly | Default `ON`; enabled with the driver | | `--enable-driver` | `GECODE_ENABLE_DRIVER` | Supported directly | Default `ON` | | `--enable-flatzinc` | `GECODE_ENABLE_FLATZINC` | Supported directly | Default `ON` | | `--enable-mpfr` | `GECODE_ENABLE_MPFR` | Supported directly | Default `ON`; uses `find_package(MPFR)` | diff --git a/changelog.in b/changelog.in index 7ec371b72d..91e777614e 100755 --- a/changelog.in +++ b/changelog.in @@ -53,7 +53,7 @@ # [ENTRY] # Module: # kernel|search|int|set|float|cpltset|scheduling|graph|minimodel| -# driver|iter|support|example|test|gist|flatzinc|other +# driver|instance|iter|support|example|test|gist|flatzinc|other # What: bug|documentation|performance|new|removed|change # Rank: minor|major # Experimental: true|false @@ -75,6 +75,42 @@ Date: unreleased [DESCRIPTION] This is the development changelog for the next Gecode release. +[ENTRY] +Module: instance +What: new +Rank: major +[DESCRIPTION] +Add the instance module for reading benchmark instance files. A short +declarative spec describes the format of a file and checks its data. +[MORE] +A file with several instances is read by selecting one of them. Errors give +the file, line, and column. The new gecode-instance tool prints the data in a +file or converts it to MiniZinc data. + +[ENTRY] +Module: driver +What: new +Rank: minor +[DESCRIPTION] +Let InstanceOptions read instance files with a reader spec. The driver +library now requires the instance library. +[MORE] +The option -spec reads files with a spec from a file, and -param sets spec +parameters. + +[ENTRY] +Module: example +What: change +Rank: minor +[DESCRIPTION] +Read instance files in the SAT, TSP, TSPTW, job shop, sudoku, advanced +sudoku, and nonogram examples. SAT replaces its own parser, which assumed one +clause per line. +[MORE] +The examples read TSPLIB files, the López-Ibáñez and Blum TSPTW collection, +OR-Library and JSPLIB job shop files, the scaled sudoku corpus format, and +the .non files exported by webpbn.com. + [ENTRY] Module: minimodel What: new diff --git a/cmake/GecodeConfig.cmake.in b/cmake/GecodeConfig.cmake.in index e570a4c2cd..7079bd39c9 100644 --- a/cmake/GecodeConfig.cmake.in +++ b/cmake/GecodeConfig.cmake.in @@ -10,6 +10,7 @@ set(_gecode_supported_components set float minimodel + instance driver flatzinc gist @@ -118,8 +119,10 @@ while(_gecode_dependency_queue) set(_gecode_component_dependencies int set search float) elseif(_gecode_dependency_component STREQUAL gist) set(_gecode_component_dependencies search int set float) + elseif(_gecode_dependency_component STREQUAL instance) + set(_gecode_component_dependencies int) elseif(_gecode_dependency_component STREQUAL driver) - set(_gecode_component_dependencies int search minimodel gist) + set(_gecode_component_dependencies int search minimodel instance gist) elseif(_gecode_dependency_component STREQUAL flatzinc) set(_gecode_component_dependencies minimodel driver gist) elseif(_gecode_dependency_component STREQUAL test) diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index a74ecadedd..e05de0b6d4 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -221,6 +221,14 @@ set(GECODE_MINIMODEL_SOURCES gecode/minimodel/set-rel.cpp ) +set(GECODE_INSTANCE_SOURCES + gecode/instance/data.cpp + gecode/instance/eval.cpp + gecode/instance/reader.cpp + gecode/instance/resolve.cpp + gecode/instance/spec.cpp +) + set(GECODE_DRIVER_SOURCES gecode/driver/options.cpp gecode/driver/script.cpp @@ -445,6 +453,7 @@ set(GECODE_TEST_SOURCES test/int/sorted.cpp test/int/unary.cpp test/int/unshare.cpp + test/instance.cpp test/ldsb.cpp test/nogoods.cpp test/region.cpp @@ -470,3 +479,4 @@ set(GECODE_TEST_BLACKBOX_EXEC_SOURCE test/flatzinc/blackbox-exec.cpp) set(GECODE_TEST_BLACKBOX_DLL_SOURCE test/flatzinc/blackbox-dll.cpp) set(GECODE_FLATZINC_EXE_SOURCE tools/flatzinc/fzn-gecode.cpp) +set(GECODE_INSTANCE_EXE_SOURCE tools/instance/gecode-instance.cpp) diff --git a/configure b/configure index 29247a22a9..8cc08e784c 100755 --- a/configure +++ b/configure @@ -662,6 +662,7 @@ VERSION subdirs enable_search enable_minimodel +enable_instance enable_examples enable_flatzinc enable_driver @@ -707,6 +708,7 @@ GECODE_DOXYGEN_DOT DOT LINKATOMIC DRIVER +INSTANCE FLATZINC GIST MM @@ -735,6 +737,7 @@ LIBEXT SBJEXT MANIFEST GECODE_BUILD_DRIVER_FLAG +GECODE_BUILD_INSTANCE_FLAG GECODE_BUILD_FLATZINC_FLAG GECODE_BUILD_GIST_FLAG GECODE_BUILD_MINIMODEL_FLAG @@ -866,6 +869,7 @@ enable_cpprofiler enable_driver enable_flatzinc enable_examples +enable_instance enable_minimodel enable_search ' @@ -1542,6 +1546,8 @@ Optional Features: --enable-flatzinc build FlatZinc interpreter [default=yes] --enable-examples build examples for the enabled variable types [default=yes] + --enable-instance build instance reader library (needs + --enable-int-vars) [default=yes] --enable-minimodel build modeling support library for the enabled variable types [default=yes] --enable-search build search engines [default=yes] @@ -7545,6 +7551,8 @@ gnu) GECODE_BUILD_FLATZINC_FLAG="" + GECODE_BUILD_INSTANCE_FLAG="" + GECODE_BUILD_DRIVER_FLAG="" ;; @@ -7589,6 +7597,8 @@ gnu) GECODE_BUILD_FLATZINC_FLAG="-DGECODE_BUILD_FLATZINC" + GECODE_BUILD_INSTANCE_FLAG="-DGECODE_BUILD_INSTANCE" + GECODE_BUILD_DRIVER_FLAG="-DGECODE_BUILD_DRIVER" ;; @@ -7624,6 +7634,8 @@ gnu) GECODE_BUILD_FLATZINC_FLAG="" + GECODE_BUILD_INSTANCE_FLAG="" + GECODE_BUILD_DRIVER_FLAG="" ;; @@ -8721,6 +8733,8 @@ fi FLATZINC="flatzinc" + INSTANCE="instance" + DRIVER="driver" # Check whether --enable-gcc-visibility was given. @@ -10441,6 +10455,8 @@ fi GECODE_BUILD_FLATZINC_FLAG="-DGECODE_BUILD_FLATZINC" + GECODE_BUILD_INSTANCE_FLAG="-DGECODE_BUILD_INSTANCE" + GECODE_BUILD_DRIVER_FLAG="-DGECODE_BUILD_DRIVER" @@ -10479,6 +10495,8 @@ fi FLATZINC="FlatZinc" + INSTANCE="Instance" + DRIVER="Driver" ;; @@ -10608,6 +10626,8 @@ printf "%s\n" "no" >&6; } GECODE_BUILD_FLATZINC_FLAG="" + GECODE_BUILD_INSTANCE_FLAG="" + GECODE_BUILD_DRIVER_FLAG="" ;; @@ -10652,6 +10672,8 @@ printf "%s\n" "no" >&6; } GECODE_BUILD_FLATZINC_FLAG="-DGECODE_BUILD_FLATZINC" + GECODE_BUILD_INSTANCE_FLAG="-DGECODE_BUILD_INSTANCE" + GECODE_BUILD_DRIVER_FLAG="-DGECODE_BUILD_DRIVER" ;; @@ -10687,6 +10709,8 @@ printf "%s\n" "no" >&6; } GECODE_BUILD_FLATZINC_FLAG="" + GECODE_BUILD_INSTANCE_FLAG="" + GECODE_BUILD_DRIVER_FLAG="" ;; @@ -11784,6 +11808,8 @@ fi FLATZINC="flatzinc" + INSTANCE="instance" + DRIVER="driver" @@ -12586,6 +12612,8 @@ fi GECODE_BUILD_FLATZINC_FLAG="-DGECODE_BUILD_FLATZINC" + GECODE_BUILD_INSTANCE_FLAG="-DGECODE_BUILD_INSTANCE" + GECODE_BUILD_DRIVER_FLAG="-DGECODE_BUILD_DRIVER" @@ -12624,6 +12652,8 @@ fi FLATZINC="FlatZinc" + INSTANCE="Instance" + DRIVER="Driver" ;; @@ -14388,6 +14418,7 @@ printf %s "checking whether to build driver... " >&6; } printf "%s\n" "yes" >&6; } enable_search="yes"; enable_int="yes"; + enable_instance="yes"; else enable_driver="no"; @@ -14437,6 +14468,7 @@ printf %s "checking whether to build driver... " >&6; } printf "%s\n" "yes" >&6; } enable_search="yes"; enable_int="yes"; + enable_instance="yes"; else enable_driver="no"; @@ -14461,6 +14493,7 @@ printf %s "checking whether to build examples... " >&6; } printf "%s\n" "yes" >&6; } enable_search="yes"; enable_driver="yes"; + enable_instance="yes"; enable_minimodel="yes"; else @@ -14472,6 +14505,33 @@ printf "%s\n" "no" >&6; } + # Check whether --enable-instance was given. +if test ${enable_instance+y} +then : + enableval=$enable_instance; +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build instance" >&5 +printf %s "checking whether to build instance... " >&6; } + if test "${enable_instance:-yes}" = "yes"; then + enable_instance="yes"; + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + if test "${enable_int_vars}" != "yes"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: the instance reader needs int variables, not building it" >&5 +printf "%s\n" "$as_me: WARNING: the instance reader needs int variables, not building it" >&2;} + enable_instance="no"; + fi + + else + enable_instance="no"; + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + fi + enable_instance=${enable_instance} + + + # Check whether --enable-minimodel was given. if test ${enable_minimodel+y} then : diff --git a/configure.ac b/configure.ac index 5e0ea06b12..a678d58c26 100644 --- a/configure.ac +++ b/configure.ac @@ -331,6 +331,7 @@ AC_GECODE_ENABLE_MODULE(driver, yes, [build script commandline driver library], [enable_search="yes"; enable_int="yes"; + enable_instance="yes"; ]) AC_GECODE_ENABLE_MODULE(flatzinc, yes, @@ -344,15 +345,25 @@ AC_GECODE_ENABLE_MODULE(driver, yes, [build script commandline driver library], [enable_search="yes"; enable_int="yes"; + enable_instance="yes"; ]) AC_GECODE_ENABLE_MODULE(examples, yes, [build examples for the enabled variable types], [enable_search="yes"; enable_driver="yes"; + enable_instance="yes"; enable_minimodel="yes"; ]) +AC_GECODE_ENABLE_MODULE(instance, yes, + [build instance reader library (needs --enable-int-vars)], + [if test "${enable_int_vars}" != "yes"; then + AC_MSG_WARN([the instance reader needs int variables, not building it]) + enable_instance="no"; + fi + ]) + AC_GECODE_ENABLE_MODULE(minimodel, yes, [build modeling support library for the enabled variable types]) diff --git a/docs/cmake-build.md b/docs/cmake-build.md index 59ab1bcb37..16af422513 100644 --- a/docs/cmake-build.md +++ b/docs/cmake-build.md @@ -112,7 +112,7 @@ to inspect test assignments. - If set directly, these override `BUILD_SHARED_LIBS`-based defaults. - `GECODE_INSTALL`: enables install/export/package rules - Defaults to `ON` when Gecode is top-level; defaults to `OFF` as subproject. -- `GECODE_ENABLE_*`: module toggles (search/int/set/float/minimodel/driver/flatzinc/gist, etc.) +- `GECODE_ENABLE_*`: module toggles (search/int/set/float/minimodel/instance/driver/flatzinc/gist, etc.) - Higher-level modules force their required lower-level modules on. For example, examples and FlatZinc require the core modeling modules, and minimodel requires int, set, and search support. @@ -197,7 +197,7 @@ target_link_libraries(app PRIVATE Gecode::gecode) Canonical component names: -- `support`, `kernel`, `search`, `int`, `set`, `float`, `minimodel`, `driver`, +- `support`, `kernel`, `search`, `int`, `set`, `float`, `minimodel`, `instance`, `driver`, `flatzinc`, `gist`, `test` Example: @@ -254,7 +254,7 @@ also require their corresponding Gecode libraries. Legacy component spellings are also accepted in `COMPONENTS`: - `gecodesupport`, `gecodekernel`, `gecodesearch`, `gecodeint`, `gecodeset`, - `gecodefloat`, `gecodeminimodel`, `gecodedriver`, `gecodeflatzinc`, + `gecodefloat`, `gecodeminimodel`, `gecodeinstance`, `gecodedriver`, `gecodeflatzinc`, `gecodegist` ### Package location hints diff --git a/doxygen/doxygen.conf.in b/doxygen/doxygen.conf.in index 82de20ae79..95b7b14013 100755 --- a/doxygen/doxygen.conf.in +++ b/doxygen/doxygen.conf.in @@ -469,7 +469,8 @@ INPUT = doxygen.hh \ @top_srcdir@/gecode \ @top_srcdir@/examples \ @top_srcdir@/test \ - @top_srcdir@/tools/flatzinc + @top_srcdir@/tools/flatzinc \ + @top_srcdir@/tools/instance # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp diff --git a/gecode.m4 b/gecode.m4 index a78152fcc9..962f9c99ce 100755 --- a/gecode.m4 +++ b/gecode.m4 @@ -767,6 +767,7 @@ AC_DEFUN([AC_GECODE_GCC_GENERAL_SWITCHES], AC_SUBST(MM, "minimodel") AC_SUBST(GIST, "gist") AC_SUBST(FLATZINC, "flatzinc") + AC_SUBST(INSTANCE, "instance") AC_SUBST(DRIVER, "driver")]) @@ -826,6 +827,7 @@ AC_DEFUN([AC_GECODE_NO_BUILDFLAGS], AC_SUBST(GECODE_BUILD_MINIMODEL_FLAG, "") AC_SUBST(GECODE_BUILD_GIST_FLAG, "") AC_SUBST(GECODE_BUILD_FLATZINC_FLAG, "") + AC_SUBST(GECODE_BUILD_INSTANCE_FLAG, "") AC_SUBST(GECODE_BUILD_DRIVER_FLAG, "")]) AC_DEFUN([AC_GECODE_BUILDFLAGS], @@ -838,6 +840,7 @@ AC_DEFUN([AC_GECODE_BUILDFLAGS], AC_SUBST(GECODE_BUILD_MINIMODEL_FLAG, "-DGECODE_BUILD_MINIMODEL") AC_SUBST(GECODE_BUILD_GIST_FLAG, "-DGECODE_BUILD_GIST") AC_SUBST(GECODE_BUILD_FLATZINC_FLAG, "-DGECODE_BUILD_FLATZINC") + AC_SUBST(GECODE_BUILD_INSTANCE_FLAG, "-DGECODE_BUILD_INSTANCE") AC_SUBST(GECODE_BUILD_DRIVER_FLAG, "-DGECODE_BUILD_DRIVER")]) @@ -967,6 +970,7 @@ AC_DEFUN([AC_GECODE_MSVC_SWITCHES], AC_SUBST(MM, "Minimodel") AC_SUBST(GIST, "Gist") AC_SUBST(FLATZINC, "FlatZinc") + AC_SUBST(INSTANCE, "Instance") AC_SUBST(DRIVER, "Driver")]) dnl Macro: diff --git a/gecode/instance.hh b/gecode/instance.hh new file mode 100644 index 0000000000..52902bc3f3 --- /dev/null +++ b/gecode/instance.hh @@ -0,0 +1,523 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INSTANCE_HH +#define GECODE_INSTANCE_HH + +#include +#include + +#include +#include +#include + +/* + * Configure linking + * + */ +#if !defined(GECODE_STATIC_LIBS) && \ + (defined(__CYGWIN__) || defined(__MINGW32__) || defined(_MSC_VER)) + +#ifdef GECODE_BUILD_INSTANCE +#define GECODE_INSTANCE_EXPORT __declspec( dllexport ) +#else +#define GECODE_INSTANCE_EXPORT __declspec( dllimport ) +#endif + +#else + +#ifdef GECODE_GCC_HAS_CLASS_VISIBILITY +#define GECODE_INSTANCE_EXPORT __attribute__ ((visibility("default"))) +#else +#define GECODE_INSTANCE_EXPORT +#endif + +#endif + +// Configure auto-linking +#ifndef GECODE_BUILD_INSTANCE +#define GECODE_LIBRARY_NAME "Instance" +#include +#endif + +/** + * \namespace Gecode::Instance + * \brief Reading instance files described by reader specs + * + * A reader spec is a short text that describes how a textual instance + * format is laid out: header counts, `KEY : VALUE` lines, sections, + * matrices, lists ended by sentinels, records, and character grids. + * Reading a file with a spec yields named values (Data): scalars and + * arrays, where the fields of repeated records are stored column-wise. + * + * Programs supply the specs for the formats they read, typically as + * string literals. The spec language is described in + * \ref TaskInstanceSpec. + * + * \ingroup TaskInstance + */ + +namespace Gecode { namespace Instance { + + /** + * \defgroup TaskInstance Reading instance files + * + * Reading textual benchmark instances (DIMACS, TSPLIB, job shop, RCPSP, + * sudoku, ...) with declarative reader specs. Scripts read instance + * files through InstanceOptions; the SAT, TSP, TSPTW, job shop, sudoku, + * and nonogram examples contain specs for their formats. The tool + * \c gecode-instance reads a file with a spec and prints a summary of the + * data or the data as MiniZinc data, which helps when writing a spec. + * + * \ingroup TaskModel + */ + + /** + * \defgroup TaskInstanceSpec The reader spec language + * + * A spec is a sequence of statements executed from top to bottom + * against the input. The input is read as whitespace-separated tokens, + * so line breaks do not matter unless a statement works on lines. Names + * defined by earlier statements can be used in later expressions. A + * spec for DIMACS CNF is: + * \code + * format dimacs-cnf + * + * comment "c" + * stop "%" + * line "p cnf {vars:0..} {clauses:0..}" + * repeat clauses { + * list clause: -vars..vars until 0 + * } + * \endcode + * + * Statements that read values, where \c t is a type (see below): + * - x: t: read one value. The name \c _ discards it. + * - x[e1][e2]...: t: an array of fixed size, read row-major. + * - w[n][n]: t layout e: a symmetric matrix stored in the + * TSPLIB layout named by \c e (\c FULL_MATRIX, \c UPPER_ROW, + * \c LOWER_ROW, \c UPPER_DIAG_ROW, \c LOWER_DIAG_ROW, and the + * column-wise variants). + * - ... offset e: add \c e to every number read, for example + * to make 1-based identifiers 0-based. For a triangular layout, the + * cells that are not read stay 0. + * - list x: t until 0 or until eol: a list ended by + * a sentinel value (which is not stored) or by the end of the line. + * A line list starts on the next line when the current one is used + * up; as blank lines are skipped, an empty row needs a marker in the + * format (such as \c 0) rather than a blank line. + * + * The names of these statements can be keywords, as in line: + * int, because a name followed by a colon always declares a value. + * + * Types: + * - \c int, \c float, \c num, \c word, and \c rest read one token. + * A \c num is an integer if possible and a float otherwise; \c rest + * reads the rest of the line and cannot be an array. + * - lo..hi and lo.. are the integers from \c lo to + * \c hi, including \c hi or not. One bound can be left out, so + * 0.. are the integers that are not negative, but + * ..< needs its upper bound. The bounds are expressions, + * evaluated once before the statement reads, and a value outside the + * range is an error such as expected an int in 0..9, found + * '12'. The range applies to the numbers read, before an offset, + * and not to the sentinel of a list. A range without an upper bound + * ends before a statement, a declaration, or an option such as + * \c layout. + * - alphabet "123456789" blank "." strip "|-+" base 1 reads + * the characters of a two-dimensional array, such as + * cells[n][n]: alphabet "123456789" blank ".": symbol \c k + * of the alphabet has value \c k plus \c base (1 by default), blank + * symbols are 0, and stripped symbols and white space are skipped. + * - type time = 0..gecode.int_max / 2 names a type, which + * later statements in the same block, and the blocks inside it, can + * use as in ready: time. Its bounds are evaluated where it is + * defined, and errors name it, as in expected time (an int in + * 0..1073741823), found '-1'. A type name must differ from the + * names of values. + * + * Statements that structure the input: + * - line "pattern": the rest of the current line must match + * the pattern, or the next line if the current one is used up. + * - find "pattern": skip lines until one matches. + * - skip line, skip e lines, expect "p" "cnf". + * - repeat e { ... }: read a record \c e times. Every name + * defined in the body becomes an array with one entry per + * repetition, so records are stored column-wise. Rows may differ in + * length. A row that an iteration does not define is empty, which + * needs rows that are arrays. + * - repeat until "999" "EOF" store x { ... }, repeat until + * eof { ... }: repeat until one of the tokens comes next (it is + * consumed, and stored in \c x if given), or until the end of input. + * - repeat e as nodes { each node ... }: name the list of + * records and each record (see below); either is optional, and + * \c each comes first in the body. + * - index b: inside a repeat, read the row number counted from + * \c b and check it. + * - sections until "EOF" skip other { "pattern" { ... } ... }: + * lines in any order, each handled by the first arm whose pattern + * matches it, until one of the tokens or the end of the input (the + * terminating token is optional, as for TSPLIB's \c EOF, and + * store x keeps it as for repeat). Unknown lines + * are errors unless \c skip \c other is given. Each arm must match + * exactly one line, unless it is written "pattern" optional: + * then it matches at most one. An arm written + * "pattern" last { ... } ends the statement after it has run + * (an arm that is both is written optional last). + * - if e { ... } else { ... }, let x = e, + * check e "message" (see below for checks on records). + * - param x or param x = e: a value given by the + * caller (see Params). A given value has the type of the default; a + * parameter without a default is an integer if possible, otherwise a + * float or a string. + * - instances { ... }: a file with several instances, one per + * pass through the body. Only one instance is kept: the parameter + * \c instance selects it by number (from 1) if it is a number, and + * otherwise by the value of \c name, which the body may define. Without + * the parameter, the file must contain a single instance. Reading stops + * after the selected instance, so this must be the last statement of a + * spec. A failed check, or a missing or repeated sections line, in an + * instance before the selected one does not stop reading, so that an + * invalid instance does not hide later ones; other errors do, as the + * input cannot be read on. Spec::catalog lists the instances of a file + * and the first such failure of each. + * + * Settings: format name (the name of the format, see + * Spec::name), comment "c" ... (prefixes of comment lines), + * stop "%" (prefixes of lines that end the data; a prefix + * ending in a letter or digit must not be followed by one, so \c "c" + * matches c foo but not \c cfoo), tokens ";" + * (characters that form tokens by themselves), separators "," + * (characters that separate tokens like white space), and ignore + * trailing (allow input after the last statement). Settings must + * be at the top level of the spec, not inside a block. + * + * Strings in a spec are written in double quotes and end on the same + * line. The escapes \\n, \\t, \\\\, and + * \\\" stand for a newline, a tab, a backslash, and a double + * quote; other escapes are errors. + * + * A line pattern is literal text with captures {x:t} of a type + * that is not an alphabet, such as {x:int} or + * {x:1..n}, and {$x} (the current value of \c x). + * A capture without a type, {x}, is a word; one without a + * name, such as {:int} or {_}, checks the text but + * does not store it. The ranges of captures are checked after the line + * matched, so they can use the captures before them. At the end of a + * pattern, ... matches the rest of the line. + * White space in a pattern matches any amount of white space; at least + * one blank is needed where two words would otherwise run together. + * A pattern must match the rest of the line. + * + * Expressions have integer arithmetic (+ - * / % ^, where + * division truncates towards zero, \c ^ is the power, and overflow and + * negative exponents are errors), comparisons of numbers and strings (an + * integer compared with a float is converted to a float), + * && || !, string literals, and the functions \c count(a) and + * \c size(a) (the number of elements of \c a), \c min, \c max, and + * \c sum (over all integers in their arguments), \c distinct (whether + * these integers differ), and \c defined(x). The operators bind from + * loosest to tightest: ||, &&, == !=, + * < > <= >=, + -, * / %, the unary + * - !, and \c ^ (which groups to the right), so -2^2 + * is -4 and 2^-1 is 2^(-1). The + * constants \c gecode.int_max and \c gecode.int_min are the limits of + * Gecode's integer values (Int::Limits); a spec cannot define names that + * start with gecode.. + * + * a[i] is element \c i of array \c a, counted from 0 as in + * Data: a number for a one-dimensional array and a row otherwise, so + * a[i][j] works for arrays of rows. An index out of range is an + * error. The comprehensions all(x in a: e), + * any(x in a: e), count(x in a: e), and + * sum(x in a: e) evaluate \c e for each element \c x of \c a: + * whether \c e holds for all or some elements, for how many it holds, + * and the sum of its integer values (true, false, 0, and 0 for an empty + * array). The name \c x is defined only in \c e and must differ from the + * names of the spec and of enclosing comprehensions. + * + * In a check, a name defined inside a repeat that does not enclose the + * check is a record name: it stands for one record, and the check must + * hold for every record. Checks can thus follow the statements that + * read the data. For the rows of a nonogram, read by + * repeat height { list row: int until eol }, + * \code + * check sum(row) + count(row) - 1 <= width "the clues of a row do not fit" + * \endcode + * checks every row, and a failure names the record, as in check + * failed for record 3 of 'row', and points at its input. Record + * names of one repeat are taken from the same record. With names of + * nested repeats, the check holds for every inner record of every outer + * record (record 2 of record 3). A check cannot use the record + * names of two other repeats. A name directly after \c in and the + * argument of \c size and \c defined are whole arrays: + * size(row) == height counts the rows, and + * sum(r in row: sum(r)) adds all clues. Only checks range over + * records: in \c let, \c if, and counts a record name is the whole array, + * so let first = row[0] is the first row. A check cannot use a + * name defined both inside and outside a repeat, or in several repeats, + * unless all of them enclose the check. When an \c all makes a check + * fail, the message names the element that fails it, as in + * element 2 of 'a', or the record. + * + * A repeat can name the list of its records and each record: + * \code + * repeat n as nodes { + * each node + * ready: int + * due: int + * } + * check node.ready <= node.due "the time window of a node is invalid" + * check nodes[0].ready == 0 "the depot must open at time 0" + * \endcode + * The fields of a record are the names defined in the repeat, and the + * lists of nested repeats. The list \c nodes is the whole list: + * nodes[i] is record \c i, and size(nodes) and + * x in nodes work as for arrays; a field needs one record, so + * nodes.ready is an error. The name \c node is a record name, + * like \c ready, and inside the repeat it is the current record. With + * repeat n as jobs { each job repeat m as steps { each step + * machine: int } }, job.steps are the steps of a job and + * job.machine their machines, so check + * distinct(job.machine) checks each job and names it on failure + * (record 3 of 'jobs'), while step.machine ranges + * over all steps (record 2 of 'steps' in record 3 of 'jobs'). + * Records are not values, and their names are not in the data and must + * differ from all other names. + * + * The values that expressions visit, including the records of checks, + * are limited to a multiple of the size of the input, so that checks + * cannot take time far beyond reading. + * + * \ingroup TaskInstance + */ + + /** + * \brief %Exception for errors in specs and instance files + * + * The message gives the location (file, line, and column), the offending + * line, and the path through the spec at the point of the error. The + * instance module reports every error in a spec or an instance by + * throwing this exception and never writes diagnostics itself; programs + * that check the data they read can throw it as well. Running out of + * memory throws MemoryExhausted or \c std::bad_alloc. + * + * \ingroup TaskInstance + */ + class GECODE_INSTANCE_EXPORT Error : public Exception { + protected: + /// The full message + std::string msg; + public: + /// Initialize with message \a m + explicit Error(const std::string& m); + /// Return the full message + virtual const char* what(void) const noexcept override; + }; + + /// Type of the values in a data entry + enum class ValueType { + INT, ///< Integers + FLOAT, ///< Floating point numbers + STRING ///< Strings + }; + + class DataImpl; + class SpecImpl; + class ParamsImpl; + + /** + * \brief Values for the `param` statements of a spec + * + * \ingroup TaskInstance + */ + class GECODE_INSTANCE_EXPORT Params { + private: + /// The parameters (copied on write) + std::shared_ptr impl; + public: + /// No parameters + Params(void); + /// Set parameter \a name to \a value + void set(const std::string& name, const std::string& value); + /** + * \brief Parse and set parameters given as `name=value` + * + * Several parameters are separated by commas, as in `a=1,b=2`, so a + * value cannot contain a comma, and empty items are skipped. Throws Error if a parameter does not + * have the form `name=value`. + */ + void set(const std::string& assignments); + /// Return whether \a name is set, and if so store its value in \a value + bool get(const std::string& name, std::string& value) const; + /// Return number of parameters + int size(void) const; + /// Return the name of parameter \a i + std::string name(int i) const; + }; + + /** + * \brief Named values read from an instance + * + * Every entry has a name, a value type (see ValueType), and a depth: + * depth 0 is a scalar, depth 1 an array, depth 2 an array of arrays, + * and so on. Arrays may be ragged, that is, rows can have different + * lengths. + * + * The accessors take a path of indices (from 0) that selects a row or a + * single value, such as d.ints("clause", {3}) for row 3 of + * \c clause or d.text("s", {1, 0, 2}); without a path they + * refer to the whole entry. They throw Error if the name is unknown, if + * the value has the wrong type or depth, if an index is out of range, or + * if an integer does not fit into an \c int. + * + * \ingroup TaskInstance + */ + class GECODE_INSTANCE_EXPORT Data { + friend class SpecImpl; + private: + /// The values + std::shared_ptr impl; + public: + /// Empty data + Data(void); + + /// \name Entries + //@{ + /// Return number of entries + int entries(void) const; + /// Return name of entry \a i, in the order they were defined + std::string name(int i) const; + /// Test whether there is an entry \a n + bool has(const std::string& n) const; + /// Return the value type of entry \a n + ValueType type(const std::string& n) const; + /// Return the depth of entry \a n (0 for scalars) + int depth(const std::string& n) const; + //@} + + /// \name Values + //@{ + /// Return the length of the array or row \a n at \a path + int size(const std::string& n, const IntArgs& path = IntArgs()) const; + /// Return the integer \a n at \a path + long long int integer(const std::string& n, const IntArgs& path = IntArgs()) const; + /// Return the number \a n at \a path (integers are converted) + double number(const std::string& n, const IntArgs& path = IntArgs()) const; + /// Return the string \a n at \a path + std::string text(const std::string& n, const IntArgs& path = IntArgs()) const; + /// Return all integers in the array or row \a n at \a path + IntArgs ints(const std::string& n, const IntArgs& path = IntArgs()) const; + /// Return all numbers in the array or row \a n at \a path + ArgArray numbers(const std::string& n, const IntArgs& path = IntArgs()) const; + //@} + + /// \name Output + //@{ + /** + * \brief Print all entries as MiniZinc data + * + * Rectangular arrays of up to six dimensions are printed as MiniZinc + * arrays, so the form depends on the data: rows that happen to have + * equal lengths give a two-dimensional array. Other arrays are + * printed as arrays of 1-tuples of arrays (MiniZinc 2.7 or later), + * preceded by a comment with its type, for example + * \code + * % x: array[int] of tuple(array[int] of int) + * x = [([5],), ([6, 7],)]; + * \endcode + * where row \c i is \c x[i].1. + */ + void dzn(std::ostream& os) const; + //@} + }; + + /** + * \brief Reader spec for a textual instance format + * + * \ingroup TaskInstance + */ + class GECODE_INSTANCE_EXPORT Spec { + private: + /// The parsed spec + std::shared_ptr impl; + public: + /// The empty spec (reads only empty input) + Spec(void); + + /// \name Creating specs + //@{ + /// Parse spec \a text, using \a origin in error messages + static Spec parse(const std::string& text, const std::string& origin = ""); + /// Load the spec in file \a path + static Spec load(const std::string& path); + //@} + + /// \name Information + //@{ + /// Return the format name declared by the spec (may be empty) + std::string name(void) const; + //@} + + /// \name Reading + //@{ + /** + * \brief Read instance file \a path + * + * The parameters \a p give values for the spec's \c param + * statements. + */ + Data read(const std::string& path, const Params& p = Params()) const; + /// Read instance \a text, using \a origin in error messages + Data read_text(const std::string& text, const std::string& origin = "", const Params& p = Params()) const; + /** + * \brief List the instances in file \a path + * + * The result has the integer \c instances, the number of instances in + * the file (1 if the spec has no \c instances statement), the + * string array \c name when every instance defines a \c name, and + * the string array \c failed when an instance fails a check or + * lacks or repeats a sections line: the first such failure of each + * instance, or the empty string. + */ + Data catalog(const std::string& path, const Params& p = Params()) const; + /// List the instances in \a text, using \a origin in error messages + Data catalog_text(const std::string& text, const std::string& origin = "", const Params& p = Params()) const; + //@} + }; + +}} + +#endif + +// STATISTICS: instance-any diff --git a/gecode/instance/data.cpp b/gecode/instance/data.cpp new file mode 100644 index 0000000000..17427f59d5 --- /dev/null +++ b/gecode/instance/data.cpp @@ -0,0 +1,602 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gecode { namespace Instance { + + /* + * Errors and parameters + * + */ + + Error::Error(const std::string& m) + : Exception("Instance", m.c_str()), msg(m) {} + + const char* + Error::what(void) const noexcept { + return msg.c_str(); + } + + Params::Params(void) + : impl(std::make_shared()) {} + + void + Params::set(const std::string& name, const std::string& value) { + if (impl.use_count() > 1) + impl = std::make_shared(*impl); + for (int i=0; inames.size(); i++) + if (impl->names[i] == name) { + impl->values[i] = value; + return; + } + impl->names.push(name); + impl->values.push(value); + } + + void + Params::set(const std::string& assignments) { + size_t b = 0; + while (b <= assignments.size()) { + size_t e = assignments.find(',', b); + if (e == std::string::npos) + e = assignments.size(); + std::string a = assignments.substr(b, e-b); + b = e+1; + if (a.empty()) + continue; + size_t eq = a.find('='); + if ((eq == std::string::npos) || (eq == 0)) + throw Error("parameter '" + a + "' must have the form name=value"); + set(a.substr(0,eq), a.substr(eq+1)); + } + } + + bool + Params::get(const std::string& name, std::string& value) const { + for (int i=0; inames.size(); i++) + if (impl->names[i] == name) { + value = impl->values[i]; + return true; + } + return false; + } + + int + Params::size(void) const { + return impl->names.size(); + } + + std::string + Params::name(int i) const { + if ((i < 0) || (i >= impl->names.size())) + throw Error("parameter index out of range"); + return impl->names[i]; + } + + /* + * Values + * + */ + + Column::Column(ValueType t, int depth) + : rows(0), pending(0), started(false), missing(false), d(depth) { + c.type = t; + } + + void + Column::skip(void) { + rows++; + missing = true; + if (!started) + pending++; + else if (d == 0) + throw ShapeError("is defined in only some repetitions"); + else + c.lengths[1].push(0); + } + + void + Column::add(Value& v) { + rows++; + if (!started) { + if (missing && (v.depth == 0)) + throw ShapeError("is defined in only some repetitions"); + started = true; + d = v.depth; + c.type = v.type; + c.depth = d+1; + c.lengths.clear(); + for (int k=0; k<=d; k++) + c.lengths.push(Seq()); + for (int i=0; i 0) { + // The type follows the values; integers become floats if needed + if (c.leaves() == 0) + c.type = v.type; + else if ((c.type == ValueType::STRING) != (v.type == ValueType::STRING)) + throw ShapeError("mixes strings and numbers"); + else if ((c.type == ValueType::INT) && (v.type == ValueType::FLOAT)) + c.to_float(); + } + for (int k=0; k(x)); + for (double x : v.reals) c.reals.push(x); + break; + default: + for (std::string& x : v.strs) c.strs.push(std::move(x)); + break; + } + } + + Value + Column::finish(void) { + if (!started) { + // No rows added: every row is missing, so empty if it is a list + if ((d == 0) && (rows > 0)) + throw ShapeError("is defined in no repetition"); + c.depth = d+1; + c.lengths.clear(); + for (int k=0; k<=d; k++) + c.lengths.push(Seq()); + for (int i=0; (d > 0) && (i depth) + starts.pop(); + while (starts.size() < depth) + starts.push(Seq()); + for (int k=0; k& s = starts[k]; + s.clear(); + s.reserve(lengths[k].size()+1); + int sum = 0; + for (int l : lengths[k]) { + s.push(sum); + sum += l; + } + s.push(sum); + } + } + + void + Value::range(int level, int pos, int& from, int& to) const { + assert(starts.size() == depth); + from = pos; to = pos+1; + for (int k=level; k& impl, const std::string& n) { + const Value* v = impl->find(n); + if (v == nullptr) + throw Error("no value named '" + n + "'"); + return *v; + } + + /** + * \brief Locate the list or leaf at \a path + * + * If \a leaf is true, the path must lead to a leaf and its index is + * returned. Otherwise it must lead to a list, and the list's level and + * position are stored in \a level and \a pos. + */ + int + locate(const Value& v, const std::string& n, const IntArgs& path, bool leaf, int& level, int& pos) { + int np = path.size(); + if (leaf ? (np != v.depth) : (np >= v.depth)) + throw Error(where(n,path,np) + (leaf ? " is not a single value" : " is not an array")); + level = 0; pos = 0; + for (int i=0; i= len)) + throw Error("index out of range in " + where(n,path,i+1) + " (size " + std::to_string(len) + ")"); + pos = v.starts[level][pos] + path[i]; + level++; + } + return pos; + } + + int + narrow(long long int x, const std::string& n) { + if ((x < INT_MIN) || (x > INT_MAX)) + throw Error("value " + std::to_string(x) + " in '" + n + "' does not fit into an int"); + return static_cast(x); + } + + long long int + integer_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + int i = locate(v,n,path,true,level,pos); + if (v.type != ValueType::INT) + throw Error(where(n,path,path.size()) + " is not an integer"); + return v.ints[i]; + } + + double + number_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + int i = locate(v,n,path,true,level,pos); + if (v.type == ValueType::STRING) + throw Error(where(n,path,path.size()) + " is not a number"); + return v.as_double(i); + } + + std::string + text_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + int i = locate(v,n,path,true,level,pos); + if (v.type != ValueType::STRING) + throw Error(where(n,path,path.size()) + " is not a string"); + return v.strs[i]; + } + + int + size_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + locate(v,n,path,false,level,pos); + return v.lengths[level][pos]; + } + + IntArgs + ints_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + locate(v,n,path,false,level,pos); + if ((v.type != ValueType::INT) && (v.leaves() > 0)) + throw Error(where(n,path,path.size()) + " does not contain integers"); + int from, to; + v.range(level,pos,from,to); + IntArgs a(to-from); + for (int i=from; i + numbers_at(const std::shared_ptr& impl, const std::string& n, const IntArgs& path) { + const Value& v = lookup(impl,n); + int level, pos; + locate(v,n,path,false,level,pos); + if ((v.type == ValueType::STRING) && (v.leaves() > 0)) + throw Error(where(n,path,path.size()) + " does not contain numbers"); + int from, to; + v.range(level,pos,from,to); + ArgArray a(to-from); + for (int i=from; i()) {} + + int + Data::entries(void) const { + return impl->entries.size(); + } + + std::string + Data::name(int i) const { + if ((i < 0) || (i >= impl->entries.size())) + throw Error("entry index out of range"); + return impl->entries[i].name; + } + + bool + Data::has(const std::string& n) const { + return impl->find(n) != nullptr; + } + + ValueType + Data::type(const std::string& n) const { + return lookup(impl,n).type; + } + + int + Data::depth(const std::string& n) const { + return lookup(impl,n).depth; + } + + int + Data::size(const std::string& n, const IntArgs& path) const { + return size_at(impl,n,path); + } + + long long int + Data::integer(const std::string& n, const IntArgs& path) const { + return integer_at(impl,n,path); + } + + double + Data::number(const std::string& n, const IntArgs& path) const { + return number_at(impl,n,path); + } + + std::string + Data::text(const std::string& n, const IntArgs& path) const { + return text_at(impl,n,path); + } + + IntArgs + Data::ints(const std::string& n, const IntArgs& path) const { + return ints_at(impl,n,path); + } + + ArgArray + Data::numbers(const std::string& n, const IntArgs& path) const { + return numbers_at(impl,n,path); + } + + /* + * Output + * + */ + + namespace { + + /** + * \brief Print \a f in the shortest form that reads back as the same double + * + * Values are always finite: input and arithmetic reject infinities. + */ + void + print_real(std::ostream& os, double f) { + // Start with all integer digits to avoid "2e+01" for 20 + int digits = (std::fabs(f) >= 1.0) && (std::fabs(f) < 1e17) + ? static_cast(std::floor(std::log10(std::fabs(f)))) + 1 : 1; + for (int prec=std::max(digits,1); prec<=17; prec++) { + // Independent of the global and the stream's locale + std::ostringstream ss; + ss.imbue(std::locale::classic()); + ss << std::setprecision(prec) << f; + std::istringstream back(ss.str()); + back.imbue(std::locale::classic()); + double g = 0.0; + back >> g; + if ((g == f) || (prec == 17)) { + std::string t = ss.str(); + if (t.find_first_of(".e") == std::string::npos) + t += ".0"; + os << t; + return; + } + } + } + + /// Print \a s as a MiniZinc string + void + print_quoted(std::ostream& os, const std::string& s) { + os << '"'; + for (char c : s) { + if ((c == '"') || (c == '\\')) { + os << '\\' << c; + } else if (c == '\n') { + os << "\\n"; + } else if (c == '\t') { + os << "\\t"; + } else if (static_cast(c) < 0x20) { + // Other control characters are written as \xXX + static const char hex[] = "0123456789abcdef"; + os << "\\x" << hex[(c >> 4) & 0xf] << hex[c & 0xf]; + } else { + os << c; + } + } + os << '"'; + } + + /// Print leaf \a i of \a v as MiniZinc + void + print_leaf(std::ostream& os, const Value& v, int i) { + switch (v.type) { + case ValueType::INT: os << std::to_string(v.ints[i]); break; + case ValueType::FLOAT: print_real(os, v.reals[i]); break; + default: print_quoted(os, v.strs[i]); break; + } + } + + /// Print name \a n as a MiniZinc identifier, quoted if needed + void + print_name(std::ostream& os, const std::string& n) { + static const std::unordered_set keywords = { + "ann", "annotation", "any", "array", "bool", "case", "constraint", "default", "diff", "div", "else", "elseif", + "endif", "enum", "false", "float", "function", "if", "in", "include", "int", "intersect", "let", "list", + "maximize", "minimize", "mod", "not", "of", "op", "opt", "output", "par", "predicate", "record", "satisfy", + "set", "solve", "string", "subset", "superset", "symdiff", "test", "then", "true", "tuple", "type", "union", + "var", "where", "xor" + }; + // MiniZinc identifiers start with a letter + bool plain = !n.empty() && is_letter(n[0]) && (keywords.count(n) == 0); + if (plain) + os << n; + else + os << '\'' << n << '\''; + } + + /** + * \brief Print the next list at \a level of \a v as a MiniZinc array of 1-tuples + * + * Entry \a k of \a next is the position of the next list at level \a k, + * and the entry for the depth of \a v is the next leaf. + */ + void + print_list(std::ostream& os, const Value& v, Seq& next, int level) { + int pos = next[level]++; + int len = v.lengths[level][pos]; + os << '['; + for (int i=0; i 0) os << ", "; + if (level+1 == v.depth) { + print_leaf(os, v, next[v.depth]++); + } else { + os << '('; + print_list(os, v, next, level+1); + os << ",)"; + } + } + os << ']'; + } + + /// Test whether all lists of each level have the same length + bool + rectangular(const Value& v) { + for (int k=1; kentries) { + const Value& v = en.value; + const std::string& n = en.name; + if (v.depth == 0) { + print_name(os, n); + os << " = "; + print_leaf(os, v, 0); + os << ";\n"; + continue; + } + int nl = v.leaves(); + // MiniZinc has array constructors up to array6d + if (rectangular(v) && (v.depth <= 6)) { + print_name(os, n); + os << " = "; + if ((v.depth == 2) && (v.size() > 0) && (v.lengths[1][0] > 0)) { + int cols = v.lengths[1][0]; + os << "[|"; + for (int i=0; i 0 ? "\n | " : " "); + else + os << ", "; + print_leaf(os, v, i); + } + os << " |];\n"; + continue; + } + // Arrays with an empty dimension need explicit index sets + bool empty = false; + for (int k=0; k 2) || ((v.depth == 2) && empty); + if (nd) { + os << "array" << v.depth << "d("; + for (int k=0; k 0 ? v.lengths[k][0] : 0) << ", "; + } + os << "["; + for (int i=0; i 0) os << ", "; + print_leaf(os, v, i); + } + os << "]" << (nd ? ")" : "") << ";\n"; + continue; + } + // Ragged or deeper: arrays of 1-tuples of arrays (MiniZinc 2.7 and later) + std::string type = std::string("array[int] of ") + + (v.type == ValueType::INT ? "int" : (v.type == ValueType::FLOAT ? "float" : "string")); + for (int k=1; k + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include + +#include +#include +#include + +namespace Gecode { namespace Instance { + + std::string + quote(const std::string& s) { + std::string r = "'"; + for (char c : s) { + if (c == '\n') r += "\\n"; + else if (c == '\t') r += "\\t"; + else r += c; + } + return r + "'"; + } + + bool + add(long long int x, long long int y, long long int& r) { + if (((y > 0) && (x > LLONG_MAX - y)) || ((y < 0) && (x < LLONG_MIN - y))) + return false; + r = x + y; + return true; + } + + bool + sub(long long int x, long long int y, long long int& r) { + if (((y < 0) && (x > LLONG_MAX + y)) || ((y > 0) && (x < LLONG_MIN + y))) + return false; + r = x - y; + return true; + } + + namespace { + + /// Store \a x * \a y in \a r unless it overflows + bool mul(long long int x, long long int y, long long int& r) { + // Sign-aware bounds that never negate LLONG_MIN + bool ok; + if (x > 0) + ok = (y > 0) ? (x <= LLONG_MAX / y) : (y >= LLONG_MIN / x); + else if (x < 0) + ok = (y > 0) ? (x >= LLONG_MIN / y) : ((y == 0) || (y >= LLONG_MAX / x)); + else + ok = true; + if (ok) + r = x * y; + return ok; + } + + /// Store \a x to the power of \a y >= 0 in \a r unless it overflows + bool pow(long long int x, long long int y, long long int& r) { + // Squaring the base only overflows if the result does, as the base + // is squared only while bits of the exponent remain + r = 1; + while (y > 0) { + if (((y & 1) != 0) && !mul(r, x, r)) + return false; + y >>= 1; + if ((y > 0) && !mul(x, x, x)) + return false; + } + return true; + } + + /** + * \brief Find the part of array \a v that the indices \a path select + * + * Return whether the indices are in range, and then the level of the + * part (the number of indices) and its position among the lists or + * leaves of that level. Needs the starts of \a v (see Value::index). + */ + bool locate(const Value& v, const Seq& path, int& level, int& pos) { + assert(v.starts.size() == v.depth); + level = 0; + pos = 0; + for (int i : path) { + if ((level >= v.depth) || (i < 0) || (i >= v.lengths[level][pos])) + return false; + pos = v.starts[level][pos] + i; + level++; + } + return true; + } + + /// Return the part of array \a v at position \a pos of level \a level + /// (see locate) + Value part(const Value& v, int level, int pos) { + Value r(v.type); + r.depth = v.depth-level; + // The lists of the part at each level, then its leaves + int from = pos, to = pos+1; + for (int k=level; k()); + for (int j=from; j 0) && (v.starts.size() == v.depth) && (i >= 0) && (i < v.size())); + return part(v, 1, i); + } + + } + + Evaluator::Evaluator(const SpecImpl& s, const std::string& text, const std::string& origin) + : spec(s), in(text, origin), spec_line(0), bindings(nullptr), checked(nullptr), record_at(nullptr), effort(0), + max_effort(base_effort + 16 * static_cast(text.size())) { + frames.push(Seq()); + } + + std::string + Evaluator::located(const std::string& msg) { + return located(msg, in.here()); + } + + std::string + Evaluator::located(const std::string& msg, const Place& at) { + std::ostringstream os; + size_t col = at.pos - at.start + 1; + os << in.origin << ":" << at.line << ":" << col << ": " << msg << "\n"; + std::string l = in.line_at(at.start); + if (l.size() > 100) l = l.substr(0, 100) + "..."; + os << std::setw(6) << at.line << " | " << l << "\n" + << " | " << std::string(col > 1 ? col-1 : 0, ' ') << "^"; + if (!context.empty()) { + os << "\n while reading: "; + for (int k=0; k 0 ? " > " : "") << x.text; + if (x.iteration > 0) { + os << " [" << x.iteration; + if (x.iterations >= 0) + os << " of " << x.iterations; + os << "]"; + } + } + } + if (spec_line > 0) + os << "\n spec: " << spec.origin << ":" << spec_line; + return os.str(); + } + + void + Evaluator::fail(const std::string& msg) { + throw Error(located(msg)); + } + + void + Evaluator::fail(const ShapeError& e, const std::string& n) { + fail("'" + n + "': " + e.msg); + } + + void + Evaluator::visit(long long int k) { + effort += k; + if (effort > max_effort) + fail("the expressions do too much work for the size of the input"); + } + + void + Evaluator::set(const std::string& n, Value&& v) { + // Expressions access the rows of arrays through the starts + v.index(); + Seq& f = frames.back(); + for (Entry& e : f) + if (e.name == n) { + e.value = std::move(v); + return; + } + f.push(Entry(n, std::move(v))); + } + + const Value* + Evaluator::lookup(const std::string& n) const { + for (const Binding* b = bindings; b != nullptr; b = b->outer) + if (b->name == n) + return b->value; + for (int k=frames.size(); k--; ) + for (const Entry& e : frames[k]) + if (e.name == n) + return &e.value; + return nullptr; + } + + bool + Evaluator::truth(const Value& v) { + if (v.depth > 0) return v.size() > 0; + switch (v.type) { + case ValueType::INT: return v.ints[0] != 0; + case ValueType::FLOAT: return v.reals[0] != 0.0; + default: return !v.strs[0].empty(); + } + } + + Value + Evaluator::boolean(bool b) { + return Value::integer(b ? 1 : 0); + } + + const Value& + Evaluator::ref(const Expr& e, Value& tmp) { + assert(e.records == nullptr); + if (e.kind == Expr::Kind::NAME) { + const Value* v = (e.record >= 0) ? records[e.record] : lookup(e.s); + if (v == nullptr) + fail("'" + e.s + "' is not defined yet"); + return *v; + } + if (e.kind == Expr::Kind::FIELD) + return field(e, tmp); + tmp = eval(e); + return tmp; + } + + Seq + Evaluator::record_path(int i) const { + Seq p; + for (int k=0; krecords[i].repeats.size(); k++) + p.push((*record_at)[k]); + return p; + } + + const Value& + Evaluator::starts(const Stmt* r, const std::string& n) { + const Value* v = lookup(record_starts(r->id)); + if (v == nullptr) + fail("'" + n + "' is not defined yet"); + return *v; + } + + int + Evaluator::count_records(const RecordRef& l) { + int level, pos; + const Value& v = starts(l.repeat, l.repeat->list); + if (!locate(v, l.path, level, pos) || (level >= v.depth)) + return 0; + return v.lengths[level][pos]; + } + + size_t + Evaluator::record_start(const RecordRef& l, int i) { + const Value* v = lookup(record_starts(l.repeat->id)); + Seq p(l.path); + p.push(i); + int level, pos; + if ((v == nullptr) || !locate(*v, p, level, pos) || (level != v->depth)) + return std::string::npos; + return static_cast(v->ints[pos]); + } + + RecordRef + Evaluator::records_of(const Expr& e) { + assert(e.records != nullptr); + RecordRef r; + r.repeat = e.records; + if (e.record >= 0) { + // A list of records that a check ranges over + if (records[e.record] == nullptr) + fail("'" + checked->records[e.record].label + "' is not defined yet"); + r.path = record_path(e.record); + return r; + } + switch (e.kind) { + case Expr::Kind::NAME: + for (const Binding* b = bindings; b != nullptr; b = b->outer) + if (b->name == e.s) + return *b->record; + // A record name outside a check is the current record + if (e.open > 0) + (void) starts(e.records, e.s); + return r; + case Expr::Kind::FIELD: + // A list of records in a record + r.path = records_of(*e.args[0]).path; + return r; + default: { + r = records_of(*e.args[0]); + long long int k = index(*e.args[1]); + int n = count_records(r); + if ((k < 0) || (k >= n)) + fail("index " + std::to_string(k) + " is out of range for '" + r.repeat->list + "' (size " + + std::to_string(n) + ", indices start at 0)"); + r.path.push(static_cast(k)); + visit(1); + return r; + } + } + } + + const Value& + Evaluator::field(const Expr& e, Value& tmp) { + if (e.record >= 0) { + if (records[e.record] == nullptr) + fail("'" + e.s + "' is not defined yet"); + return *records[e.record]; + } + RecordRef r = records_of(*e.args[0]); + const Value* v = lookup(e.s); + if (v == nullptr) + fail("'" + e.s + "' is not defined yet"); + if (r.path.empty()) + return *v; + int level, pos; + if (!locate(*v, r.path, level, pos)) + fail("'" + e.s + "' is not defined in this record of '" + r.repeat->list + "'"); + tmp = part(*v, level, pos); + visit(1 + tmp.leaves()); + return tmp; + } + + long long int + Evaluator::integer(const Value& v, const std::string& op) { + if ((v.depth > 0) || (v.type != ValueType::INT)) + fail("'" + op + "' needs integers"); + return v.ints[0]; + } + + bool + Evaluator::explains(const Expr& e) { + if (e.kind == Expr::Kind::BINARY) + return (e.op == Expr::Op::OR) || (e.op == Expr::Op::AND); + return (e.kind == Expr::Kind::COMPREHENSION) && (e.s == "all"); + } + + Value + Evaluator::eval(const Expr& e) { + Value r = evaluate(e); + if (!explains(e)) + why.clear(); + return r; + } + + Value + Evaluator::evaluate(const Expr& e) { + switch (e.kind) { + case Expr::Kind::INT: return Value::integer(e.i); + case Expr::Kind::FLOAT: return Value::real(e.f); + case Expr::Kind::STR: return Value::string(e.s); + case Expr::Kind::NAME: case Expr::Kind::FIELD: { + Value tmp(ValueType::INT); + return ref(e, tmp); + } + case Expr::Kind::UNARY: return unary(e); + case Expr::Kind::BINARY: return binary(e); + case Expr::Kind::INDEX: return subscript(e); + case Expr::Kind::COMPREHENSION: return comprehension(e); + default: return call(e); + } + } + + bool + Evaluator::test(const Expr& e) { + Value tmp(ValueType::INT); + return truth(ref(e, tmp)); + } + + bool + Evaluator::explained(const Expr& e) { + why.clear(); + return test(e); + } + + Value + Evaluator::unary(const Expr& e) { + if (e.op == Expr::Op::NOT) + return boolean(!test(*e.args[0])); + Value tmp(ValueType::INT); + const Value& a = ref(*e.args[0], tmp); + if (a.is_number() && (a.type == ValueType::FLOAT)) + return Value::real(-a.reals[0]); + long long int x = integer(a, e.s); + if (x == LLONG_MIN) + fail("integer overflow in '-'"); + return Value::integer(-x); + } + + Value + Evaluator::binary(const Expr& e) { + // Only evaluate the second operand if needed + if (e.op == Expr::Op::OR) + return boolean(explained(*e.args[0]) || explained(*e.args[1])); + if (e.op == Expr::Op::AND) + return boolean(explained(*e.args[0]) && explained(*e.args[1])); + Value ta(ValueType::INT), tb(ValueType::INT); + const Value& a = ref(*e.args[0], ta); + const Value& b = ref(*e.args[1], tb); + switch (e.op) { + case Expr::Op::EQ: case Expr::Op::NE: case Expr::Op::LT: case Expr::Op::GT: + case Expr::Op::LE: case Expr::Op::GE: + return boolean(compare(a, b, e)); + default: + break; + } + // Arithmetic is on integers: specs compute sizes and counts + long long int x = integer(a, e.s), y = integer(b, e.s), r = 0; + bool ok; + switch (e.op) { + case Expr::Op::ADD: ok = add(x, y, r); break; + case Expr::Op::SUB: ok = sub(x, y, r); break; + case Expr::Op::MUL: ok = mul(x, y, r); break; + case Expr::Op::POW: + if (y < 0) + fail("negative exponent in '^'"); + ok = pow(x, y, r); + break; + default: + // Division and remainder truncate towards zero + if (y == 0) + fail("division by zero"); + ok = (x != LLONG_MIN) || (y != -1); + if (ok) + r = (e.op == Expr::Op::DIV) ? (x / y) : (x % y); + break; + } + if (!ok) + fail("integer overflow in '" + e.s + "'"); + return Value::integer(r); + } + + bool + Evaluator::compare(const Value& a, const Value& b, const Expr& e) { + if ((a.depth > 0) || (b.depth > 0)) + fail("'" + e.s + "' compares single values, not arrays"); + int c; + if ((a.type == ValueType::STRING) || (b.type == ValueType::STRING)) { + if ((e.op != Expr::Op::EQ) && (e.op != Expr::Op::NE)) + fail("'" + e.s + "' compares numbers"); + c = ((a.type == b.type) && (a.strs[0] == b.strs[0])) ? 0 : 1; + } else if ((a.type == ValueType::INT) && (b.type == ValueType::INT)) { + c = (a.ints[0] < b.ints[0]) ? -1 : (a.ints[0] > b.ints[0] ? 1 : 0); + } else { + // An integer compared with a float is converted to a float + c = (a.as_double() < b.as_double()) ? -1 : (a.as_double() > b.as_double() ? 1 : 0); + } + switch (e.op) { + case Expr::Op::EQ: return c == 0; + case Expr::Op::NE: return c != 0; + case Expr::Op::LT: return c < 0; + case Expr::Op::GT: return c > 0; + case Expr::Op::LE: return c <= 0; + default: return c >= 0; + } + } + + Value + Evaluator::call(const Expr& e) { + const std::string& f = e.s; + if (f == "defined") + return boolean(defined(*e.args[0])); + if (((f == "count") || (f == "size")) && (e.args[0]->records != nullptr)) + return Value::integer(count_records(records_of(*e.args[0]))); + if ((f == "count") || (f == "size")) { + Value tmp(ValueType::INT); + const Value& v = ref(*e.args[0], tmp); + if (v.depth == 0) + fail("'" + f + "' takes an array"); + return Value::integer(v.size()); + } + if (f == "distinct") + return boolean(distinct(e)); + // min, max, and sum over all integers in all arguments + bool any = false; + long long int r = 0; + for (const std::unique_ptr& a : e.args) { + Value tmp(ValueType::INT); + const Value& v = ref(*a, tmp); + if ((v.type != ValueType::INT) && (v.leaves() > 0)) + fail("'" + f + "' takes integers"); + visit(v.leaves()); + for (long long int x : v.ints) { + if (f == "sum") { + if (!add(r, x, r)) + fail("integer overflow in 'sum'"); + } else if (!any || ((f == "min") ? (x < r) : (x > r))) { + r = x; + } + any = true; + } + } + if (!any && (f != "sum")) + fail("'" + f + "' of no integers"); + return Value::integer(r); + } + + bool + Evaluator::defined(const Expr& e) const { + for (const Binding* b = bindings; b != nullptr; b = b->outer) + if (b->name == e.s) + return true; + if (e.records != nullptr) + return lookup(record_starts(e.records->id)) != nullptr; + return lookup(e.s) != nullptr; + } + + bool + Evaluator::distinct(const Expr& e) { + Seq x; + for (const std::unique_ptr& a : e.args) { + Value tmp(ValueType::INT); + const Value& v = ref(*a, tmp); + if ((v.type != ValueType::INT) && (v.leaves() > 0)) + fail("'distinct' takes integers"); + visit(v.leaves()); + for (long long int i : v.ints) + x.push(i); + } + Support::quicksort(x.begin(), x.size()); + for (int k=1; kkind == Expr::Kind::INDEX) { + a = a->args[0].get(); + nested = true; + } + if ((a->kind != Expr::Kind::NAME) && (a->kind != Expr::Kind::FIELD)) + return "the array"; + return (nested ? "an element of '" : "'") + a->s + "'"; + } + + long long int + Evaluator::index(const Expr& e) { + Value tmp(ValueType::INT); + const Value& i = ref(e, tmp); + if ((i.depth > 0) || (i.type != ValueType::INT)) + fail("an index must be an integer"); + return i.ints[0]; + } + + std::string + Evaluator::scalar_hint(const Expr& e) const { + if (e.record < 0) + return ""; + const Record& r = checked->records[e.record]; + const std::string& l = r.repeats.back()->list; + std::string m = ", which is one value of each record here; to index the records, "; + if (l.empty()) + return m + "name them, as in 'repeat n as nodes { each node ... }', and use 'nodes[0]." + e.s + "'"; + return m + "use '" + l + "[0]." + e.s + "'"; + } + + Value + Evaluator::subscript(const Expr& e) { + assert(e.records == nullptr); + Value ta(ValueType::INT); + const Value& a = ref(*e.args[0], ta); + if (a.depth == 0) + fail("only arrays can be indexed, not " + indexed(e) + scalar_hint(*e.args[0])); + long long int k = index(*e.args[1]); + if ((k < 0) || (k >= a.size())) + fail("index " + std::to_string(k) + " is out of range for " + indexed(e) + " (size " + + std::to_string(a.size()) + ", indices start at 0)"); + Value r = element(a, static_cast(k)); + visit(1 + r.leaves()); + return r; + } + + std::string + Evaluator::array_name(const Expr& a) { + if (a.kind == Expr::Kind::NAME) + return "'" + a.s + "'"; + if ((a.kind == Expr::Kind::FIELD) && (a.args[0]->kind == Expr::Kind::NAME)) + return "'" + a.args[0]->s + "." + a.s + "'"; + return "the array"; + } + + RecordRef + Evaluator::element_records(const Expr& e, const Expr& a) { + RecordRef r; + r.repeat = e.elements; + if (a.kind == Expr::Kind::FIELD) + r.path = records_of(*a.args[0]).path; + return r; + } + + void + Evaluator::explain(const Expr& e, const RecordRef& r, int i) { + Witness w; + w.pos = std::string::npos; + std::string n = std::to_string(i+1); + if (e.elements == nullptr) { + w.text = "element " + n + " of " + array_name(*e.args[0]); + } else { + const std::string& l = e.elements->list; + w.text = "record " + n + " of " + (l.empty() ? array_name(*e.args[0]) : "'" + l + "'"); + w.pos = record_start(r, i); + } + why.push(std::move(w)); + } + + Value + Evaluator::comprehension(const Expr& e) { + const std::string& f = e.s; + const Expr& source = *e.args[0]; + Value ta(ValueType::INT); + const Value* a = nullptr; + RecordRef list, x; + int n; + if (source.records != nullptr) { + list = records_of(source); + n = count_records(list); + } else { + a = &ref(source, ta); + if (a->depth == 0) + fail("'" + f + "(" + e.var + " in ...)' needs an array"); + n = a->size(); + } + RecordRef elements; + if ((f == "all") && (e.elements != nullptr)) + elements = (a == nullptr) ? list : element_records(e, source); + Binding b(bindings, e.var); + long long int r = 0; + for (int i=0; i 0) || (v.type != ValueType::INT)) + fail(std::string(what) + " must be an integer"); + if (v.ints[0] < 0) + fail(std::string(what) + " is negative (" + std::to_string(v.ints[0]) + ")"); + if (v.ints[0] > INT_MAX) + fail(std::string(what) + " is too large (" + std::to_string(v.ints[0]) + ")"); + return v.ints[0]; + } + + bool + Evaluator::holds(const Stmt& s, std::string& error) { + records.clear(); + int deepest = -1; + for (int i=0; i at; + checked = &s; + record_at = &at; + bool ok = check_records(s, deepest, at, error); + checked = nullptr; + record_at = nullptr; + return ok; + } + + bool + Evaluator::ranges(const Stmt& s, int i, int level) const { + return (records[i] != nullptr) && (s.records[i].repeats.size() > level); + } + + bool + Evaluator::check_records(const Stmt& s, int deepest, Seq& at, std::string& error) { + int level = at.size(); + if ((deepest < 0) || (level == s.records[deepest].repeats.size())) { + if (explained(*s.expr)) + return true; + error = failure(s, deepest, at); + return false; + } + int n = -1, first = -1; + for (int i=0; idepth == 0) + fail("'" + s.records[i].label + "' has no records"); + if (first < 0) { + first = i; + n = records[i]->size(); + } else if (records[i]->size() != n) { + fail("'" + s.records[first].label + "' and '" + s.records[i].label + + "' have different numbers of records"); + } + } + Seq outer(records); + Seq rows; + // No reallocation, so the record names can point at the rows + rows.reserve(records.size()); + for (int r=0; r& at) { + const Value* p = lookup(r.starts); + int level, pos; + if ((p == nullptr) || !locate(*p, at, level, pos) || (level != p->depth)) + return std::string::npos; + return static_cast(p->ints[pos]); + } + + Place + Evaluator::place(size_t q) const { + return (q <= in.pos) ? in.place(q) : in.here(); + } + + std::string + Evaluator::record_text(const Record& r, const Seq& at) { + bool named = true; + for (const Stmt* x : r.repeats) + named = named && !x->list.empty(); + std::string m; + for (int k=at.size(); k--; ) { + m += "record " + std::to_string(at[k]+1) + " of "; + if (named) + m += "'" + r.repeats[k]->list + "'" + (k > 0 ? " in " : ""); + } + return named ? m : m + "'" + r.label + "'"; + } + + std::string + Evaluator::failure(const Stmt& s, int deepest, const Seq& at) { + // The innermost record found is where the check failed + std::string m; + size_t q = std::string::npos; + for (const Witness& w : why) { + m += (m.empty() ? "" : " in ") + w.text; + if (q == std::string::npos) + q = w.pos; + } + if (!at.empty()) { + m += (m.empty() ? "" : " in ") + record_text(s.records[deepest], at); + if (q == std::string::npos) + q = record_place(s.records[deepest], at); + } + m = "check failed" + (m.empty() ? "" : " for " + m); + if (!s.message.empty()) + m += ": " + s.message; + return located(m, place(q)); + } + +}} + +// STATISTICS: instance-any diff --git a/gecode/instance/eval.hpp b/gecode/instance/eval.hpp new file mode 100644 index 0000000000..b323e62b74 --- /dev/null +++ b/gecode/instance/eval.hpp @@ -0,0 +1,543 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INSTANCE_EVAL_HPP +#define GECODE_INSTANCE_EVAL_HPP + +#include + +namespace Gecode { namespace Instance { + + /// Return \a s in single quotes, with newlines and tabs escaped + std::string quote(const std::string& s); + /// Store \a x + \a y in \a r unless it overflows + bool add(long long int x, long long int y, long long int& r); + /// Store \a x - \a y in \a r unless it overflows + bool sub(long long int x, long long int y, long long int& r); + + /** + * \brief Records or a list of records of a named repeat + * + * The indices \a path select them in the record starts of the repeat + * (see record_starts), and in each name defined in the repeat. + */ + class RecordRef { + public: + /// The repeat + const Stmt* repeat; + /// The indices + Seq path; + /// Initialize without a repeat + RecordRef(void) + : repeat(nullptr) {} + }; + + /// A part of the explanation of a failed check, such as "record 3 of + /// 'jobs'", with the position of the record in the input if known + class Witness { + public: + /// The text + std::string text; + /// The position, or npos + size_t pos; + }; + + /// A name bound by a comprehension, visible while the binding exists + class Binding { + private: + /// The innermost binding, which this binding is while it exists + const Binding*& innermost; + public: + /// The name + const std::string& name; + /// The value, unless a record + const Value* value; + /// The record, if a record of a named repeat + const RecordRef* record; + /// The binding of the enclosing comprehension, or null + const Binding* outer; + /// Bind \a n inside the bindings starting at \a b + Binding(const Binding*& b, const std::string& n) + : innermost(b), name(n), value(nullptr), record(nullptr), outer(b) { + innermost = this; + } + /// Remove the binding + ~Binding(void) { + innermost = outer; + } + }; + + /// A position in the input, with its line + class Place { + public: + /// Position + size_t pos; + /// Line (1-based) + int line; + /// Start of the line + size_t start; + }; + + /// A statement being executed, for errors + class Context { + public: + /// What is executed, such as a name + std::string text; + /// The iteration of a repeat or instances statement, from 1, or 0 + long long int iteration; + /// The number of iterations, or -1 if unknown + long long int iterations; + /// Initialize for \a t in iteration \a i of \a n + explicit Context(const std::string& t, long long int i=0, long long int n=-1) + : text(t), iteration(i), iterations(n) {} + }; + + /// Instance text with a position and line tracking + class Input { + private: + /// What a character is in tokens + enum class CharKind : unsigned char { + PLAIN, ///< Part of a token + SEPARATOR, ///< Separates tokens, like white space + SPLIT ///< A token by itself + }; + /// The kind of each character + CharKind kind[256]; + /// Return the kind of \a c + CharKind kind_of(char c) const { + return kind[static_cast(c)]; + } + public: + /// The text + const std::string& text; + /// Where the text comes from + std::string origin; + /// Comment line prefixes + Seq comments; + /// Prefixes of lines that end the data + Seq stops; + /// Current position + size_t pos; + /// Current line (1-based) + int line; + /// Start of current line + size_t start; + + /// Initialize for \a t from \a o + Input(const std::string& t, const std::string& o) + : text(t), origin(o), pos(0), line(1), start(0) { + for (int c=0; c<256; c++) + kind[c] = is_space(static_cast(c)) ? CharKind::SEPARATOR : CharKind::PLAIN; + } + + /// Make the characters \a cs tokens by themselves, unless they separate tokens + void add_split(const std::string& cs) { + for (char c : cs) + if (kind_of(c) == CharKind::PLAIN) + kind[static_cast(c)] = CharKind::SPLIT; + } + /// Make the characters \a cs separate tokens like white space + void add_separators(const std::string& cs) { + for (char c : cs) + kind[static_cast(c)] = CharKind::SEPARATOR; + } + + /// Move forward to \a p + void advance(size_t p) { + while (pos < p) { + if (text[pos] == '\n') { + line++; + start = pos+1; + } + pos++; + } + } + /// Test whether only blanks precede the position on its line + bool line_start(void) const { + for (size_t k=start; k= text.size()) || !is_alnum(text[after]); + } + /// Move to the end of the current line + void to_eol(void) { + size_t p = text.find('\n', pos); + advance(p == std::string::npos ? text.size() : p); + } + /// Test whether \a c separates tokens + bool is_separator(char c) const { + return kind_of(c) == CharKind::SEPARATOR; + } + /// Test whether \a c forms a token by itself + bool is_split(char c) const { + return kind_of(c) == CharKind::SPLIT; + } + /// Test whether position \a q is within the current line + bool in_line(size_t q) const { + return (q < text.size()) && (text[q] != '\n'); + } + /// Skip white space, comment lines, and everything after a stop line + void skip(void) { + for (;;) { + const char* s = text.data(); + size_t n = text.size(); + while ((pos < n) && is_separator(s[pos])) { + if (s[pos] == '\n') { + line++; + start = pos+1; + } + pos++; + } + if ((pos >= text.size()) || !line_start()) + return; + bool comment = false, stop = false; + for (const std::string& c : comments) + comment = comment || starts_with(c); + for (const std::string& s : stops) + stop = stop || starts_with(s); + if (comment) { + to_eol(); + } else if (stop) { + advance(text.size()); + return; + } else { + return; + } + } + } + /// Skip to the next data (see skip), and return whether there is any + bool skip_to_data(void) { + skip(); + return pos < text.size(); + } + /// Skip blanks and separators, and return whether data follows on the line + bool skip_to_line_data(void) { + while (in_line(pos) && is_separator(text[pos])) + pos++; + return in_line(pos); + } + /// Return where the token at the position ends + size_t token_end(void) const { + size_t q = pos; + if ((q < text.size()) && is_split(text[q])) + return q+1; + while ((q < text.size()) && (kind_of(text[q]) == CharKind::PLAIN)) + q++; + return q; + } + /// Read the token at the position + std::string token(void) { + size_t q = token_end(); + std::string t = text.substr(pos, q-pos); + advance(q); + return t; + } + /** + * \brief Read the token at the position as an integer \a v, and + * return whether it is one + * + * Accepts what parse_int accepts, without making a string. + */ + bool int_token(long long int& v) { + const char* s = text.data(); + size_t n = text.size(), p = pos; + if ((p < n) && is_split(s[p])) { + if (!is_digit(s[p])) + return false; + v = s[p] - '0'; + pos = p+1; + return true; + } + bool negative = (p < n) && (s[p] == '-'); + if ((p < n) && is_sign(s[p])) + p++; + size_t digits = p; + // The magnitude, which may be one more than LLONG_MAX if negative + unsigned long long int limit = static_cast(LLONG_MAX) + (negative ? 1 : 0); + unsigned long long int m = 0; + for (; (p < n) && is_digit(s[p]) && (kind_of(s[p]) == CharKind::PLAIN); p++) { + unsigned int d = static_cast(s[p] - '0'); + if (m > (limit - d) / 10) + return false; + m = 10*m + d; + } + // The token must end after the digits + if ((p == digits) || ((p < n) && (kind_of(s[p]) == CharKind::PLAIN))) + return false; + v = negative ? static_cast(0ULL - m) : static_cast(m); + // A token holds no newline + pos = p; + return true; + } + /// Return the next token without consuming it + std::string peek(void) { + size_t p0 = pos, s0 = start; + int l0 = line; + std::string t = skip_to_data() ? token() : std::string(); + pos = p0; start = s0; line = l0; + return t; + } + /// Return the rest of the current line without trailing blanks + std::string rest(void) const { + size_t q = text.find('\n', pos); + if (q == std::string::npos) q = text.size(); + while ((q > pos) && is_blank(text[q-1])) q--; + return text.substr(pos, q-pos); + } + /// Return the line starting at \a s without trailing blanks + std::string line_at(size_t s) const { + size_t q = text.find('\n', s); + if (q == std::string::npos) q = text.size(); + while ((q > s) && is_blank(text[q-1])) q--; + return text.substr(s, q-s); + } + /// Return the current place + Place here(void) const { + Place p; + p.pos = pos; p.line = line; p.start = start; + return p; + } + /// Return the place of position \a q, which is not after the current one + Place place(size_t q) const { + assert(q <= pos); + // Counted back from the current place, as q is usually close to it + Place p; + p.pos = q; p.line = line; p.start = q; + for (size_t k=q; k 0) && (text[p.start-1] != '\n')) + p.start--; + return p; + } + /// Return where the next data starts (see skip), without moving there + size_t next_data(void) { + size_t p0 = pos, s0 = start; + int l0 = line; + skip(); + size_t r = pos; + pos = p0; start = s0; line = l0; + return r; + } + }; + + /** + * \brief Evaluator of the expressions of a spec on an input + * + * Holds the values defined so far and evaluates expressions and checks + * with them. Errors point at the current position in the input. + */ + class Evaluator { + public: + /// Initialize for spec \a s and input \a text from \a origin + Evaluator(const SpecImpl& s, const std::string& text, const std::string& origin); + protected: + /// The spec + const SpecImpl& spec; + /// The input + Input in; + /// Values: the outermost frame and one per active repeat iteration + Seq> frames; + /// Description of the statements being executed, for errors + Seq context; + /// Spec line of the current statement + int spec_line; + /// Names bound by the comprehensions being evaluated + const Binding* bindings; + /// The check being evaluated, or null + const Stmt* checked; + /// Values of its record names (see Record) + Seq records; + /// The records of the deepest of them, one index per repeat level + const Seq* record_at; + /// Why the last condition evaluated is false, for messages: the + /// elements of the all-comprehensions that failed, innermost first + Seq why; + /// Values visited by expressions + long long int effort; + /// Limit for effort, which grows with the input, so that repeated or + /// nested expressions cannot take time far beyond the input's size + const long long int max_effort; + /// Base of the limit for effort + static const long long int base_effort = 1LL << 20; + + /// Return the error message \a msg with the location and context + std::string located(const std::string& msg); + /// Return the error message \a msg at \a at with the context + std::string located(const std::string& msg, const Place& at); + /// Throw an error with message \a msg at the current position + [[noreturn]] void fail(const std::string& msg); + /// Throw an error for the shape error \a e of the value of \a n + [[noreturn]] void fail(const ShapeError& e, const std::string& n); + /// Account for visiting \a k values in expressions + void visit(long long int k); + /// Define \a n as \a v in the innermost frame + void set(const std::string& n, Value&& v); + /// Return the value of \a n, or null if it is not defined + const Value* lookup(const std::string& n) const; + + /* + * Expressions + */ + /// Test whether \a v is true + static bool truth(const Value& v); + /// Return \a b as an integer + static Value boolean(bool b); + /** + * \brief Evaluate \a e, avoiding a copy for names + * + * The value is stored in \a tmp unless \a e is a name. Callers pass + * an integer scalar without a leaf as \a tmp, which needs no memory. + */ + const Value& ref(const Expr& e, Value& tmp); + /// Return the indices of the record \a at of the check that select + /// the records of its record name \a i + Seq record_path(int i) const; + /// Return the record starts of repeat \a r, failing for \a n if the + /// repeat has not run yet + const Value& starts(const Stmt* r, const std::string& n); + /// Return the number of records in list \a l + int count_records(const RecordRef& l); + /// Return where record \a i of list \a l starts in the input, or npos + size_t record_start(const RecordRef& l, int i); + /// Evaluate \a e, a record or a list of records + RecordRef records_of(const Expr& e); + /** + * \brief Evaluate field \a e of a record or a list of records + * + * The field of a list is the array of the field of each record. The + * value is stored in \a tmp unless it needs no copy. + */ + const Value& field(const Expr& e, Value& tmp); + /// Return the integer of single value \a v, or fail for \a op + long long int integer(const Value& v, const std::string& op); + /// Test whether \a e passes on why it is false (see why) + static bool explains(const Expr& e); + /// Evaluate \a e, keeping why only if \a e explains it + Value eval(const Expr& e); + /// Evaluate \a e + Value evaluate(const Expr& e); + /// Return whether \a e is true, avoiding a copy for names + bool test(const Expr& e); + /// Return whether \a e is true, with only its own explanation in why + bool explained(const Expr& e); + /// Evaluate unary expression \a e + Value unary(const Expr& e); + /// Evaluate binary expression \a e + Value binary(const Expr& e); + /// Compare single values \a a and \a b with the operator of \a e + bool compare(const Value& a, const Value& b, const Expr& e); + /// Call a function (names and arguments are checked by the parser) + Value call(const Expr& e); + /// Test whether name \a e is defined + bool defined(const Expr& e) const; + /// Test whether all integers in the arguments of call \a e differ + bool distinct(const Expr& e); + /// Describe the array of index expression \a e, for messages + static std::string indexed(const Expr& e); + /// Return the value of index \a e + long long int index(const Expr& e); + /** + * \brief Return a hint for indexing \a e, a single value + * + * A record name in a check is one record, so that indexing the + * records needs their name. + */ + std::string scalar_hint(const Expr& e) const; + /// a[i]: element i, counted from 0, of the outermost list of a + Value subscript(const Expr& e); + /// Describe array \a a of a comprehension, for messages + static std::string array_name(const Expr& a); + /// Return the records that are the elements of array \a a of + /// comprehension \a e, an array of values (see Expr::elements) + RecordRef element_records(const Expr& e, const Expr& a); + /// Add why element \a i of the array of all-comprehension \a e, + /// with records \a r, is false + void explain(const Expr& e, const RecordRef& r, int i); + /** + * \brief all, any, count, and sum over the elements of an array + * + * The name of the comprehension \a e is bound to each element of + * the outermost list of the array in turn, or to each record of a + * list of records. \c all and \c any stop at the first element that + * decides the result. + */ + Value comprehension(const Expr& e); + /// Return the value of \a e, a count described by \a what + long long int count(const Expr& e, const char* what); + /** + * \brief Test whether check \a s holds, and otherwise store why it + * fails in \a error + * + * The check holds for every record of its record names (see Record) + * that are defined. The others are not defined when its expression + * uses them. + */ + bool holds(const Stmt& s, std::string& error); + /// Test whether record name \a i of check \a s is defined and ranges + /// over the repeat at \a level + bool ranges(const Stmt& s, int i, int level) const; + /** + * \brief Evaluate check \a s for the records inside the records \a at + * of its record name \a deepest, and return whether it holds + * + * \a at has a record number per repeat level, from the outermost. + * Each record name that ranges over a level is set to its record. + * If the check fails, \a error is why. + */ + bool check_records(const Stmt& s, int deepest, Seq& at, std::string& error); + /// Return where the record \a at of record name \a r starts in the + /// input, or npos if that is unknown + size_t record_place(const Record& r, const Seq& at); + /// Return the place of position \a q in the input, or the current + /// place if \a q is npos + Place place(size_t q) const; + /// Describe the records \a at of record name \a r, innermost first + static std::string record_text(const Record& r, const Seq& at); + /// Return why check \a s fails for the records \a at of its record + /// name \a deepest + std::string failure(const Stmt& s, int deepest, const Seq& at); + }; + +}} + +#endif + +// STATISTICS: instance-any diff --git a/gecode/instance/impl.hpp b/gecode/instance/impl.hpp new file mode 100644 index 0000000000..553544b236 --- /dev/null +++ b/gecode/instance/impl.hpp @@ -0,0 +1,735 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +/* + * Internal declarations shared by the implementation files of the + * instance module. Not part of the public interface. + */ + +#ifndef GECODE_INSTANCE_IMPL_HPP +#define GECODE_INSTANCE_IMPL_HPP + +#include + +#include +#include +#include + +namespace Gecode { namespace Instance { + + /* + * Character classes and number syntax, ASCII only and independent of the + * global locale + */ + + /// Test whether \a c is a digit + inline bool is_digit(char c) { + return (c >= '0') && (c <= '9'); + } + /// Test whether \a c is a letter + inline bool is_letter(char c) { + return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')); + } + /// Test whether \a c can occur in a name + inline bool is_alnum(char c) { + return is_digit(c) || is_letter(c) || (c == '_'); + } + /// Test whether \a c is white space + inline bool is_space(char c) { + return (c == ' ') || (c == '\t') || (c == '\n') || (c == '\r') || (c == '\v') || (c == '\f'); + } + /// Test whether \a c is white space within a line + inline bool is_blank(char c) { + return (c == ' ') || (c == '\t') || (c == '\r'); + } + /// Test whether \a c is a sign + inline bool is_sign(char c) { + return (c == '+') || (c == '-'); + } + /// Test whether \a c starts the exponent of a float + inline bool is_exponent(char c) { + return (c == 'e') || (c == 'E'); + } + /** + * \brief Return where the number starting at position \a p of \a s ends + * + * A number has digits, a sign first if \a sign, and, if \a real, also + * '.', 'e', 'E', and a sign directly after 'e' or 'E'. Only the + * characters are scanned: parse_int and parse_float check the syntax. + */ + inline size_t number_end(const std::string& s, size_t p, bool sign, bool real) { + size_t q = p; + for (; q < s.size(); q++) { + char c = s[q]; + if (is_digit(c)) + continue; + if (sign && (q == p) && is_sign(c)) + continue; + if (real && ((c == '.') || is_exponent(c))) + continue; + if (real && (q > p) && is_sign(c) && is_exponent(s[q-1])) + continue; + break; + } + return q; + } + /// Parse decimal integer \a t into \a v, and return whether it is one + bool parse_int(const std::string& t, long long int& v); + /// Parse decimal float \a t into \a v, and return whether it is one + bool parse_float(const std::string& t, double& v); + + /** + * \brief Growable array on the Gecode heap + * + * Supports move-only element types. Elements are moved when the array + * grows. + */ + template + class Seq { + private: + /// Elements + T* x; + /// Number of elements + int n; + /// Capacity + int cap; + /// Grow to capacity at least \a m + void grow(int m) { + // Doubling, computed without overflow + long long int d = std::min(2LL*cap, static_cast(INT_MAX)); + int c = std::max(std::max(m, static_cast(d)), 2); + T* y = static_cast(heap.ralloc(sizeof(T)*static_cast(c))); + for (int i=0; i cap) grow(m); + } + /// Append a copy of \a v + void push(const T& v) { + if (n == cap) { + T t(v); + grow(n+1); + new (&x[n]) T(std::move(t)); + } else { + new (&x[n]) T(v); + } + n++; + } + /// Append \a v + void push(T&& v) { + if (n == cap) { + T t(std::move(v)); + grow(n+1); + new (&x[n]) T(std::move(t)); + } else { + new (&x[n]) T(std::move(v)); + } + n++; + } + /// Remove last element + void pop(void) { + assert(n > 0); + x[--n].~T(); + } + /// Remove all elements + void clear(void) { + while (n > 0) + x[--n].~T(); + } + /// Return element \a i + T& operator [](int i) { + assert((i >= 0) && (i < n)); + return x[i]; + } + /// Return element \a i + const T& operator [](int i) const { + assert((i >= 0) && (i < n)); + return x[i]; + } + /// Return last element + T& back(void) { + assert(n > 0); + return x[n-1]; + } + /// Return last element + const T& back(void) const { + assert(n > 0); + return x[n-1]; + } + /// Iteration support + T* begin(void) { return x; } + /// Iteration support + T* end(void) { return x+n; } + /// Iteration support + const T* begin(void) const { return x; } + /// Iteration support + const T* end(void) const { return x+n; } + }; + + /// Error about the shape or type of a value, located by the caller + class ShapeError { + public: + /// Message + std::string msg; + /// Initialize with message \a m + explicit ShapeError(const std::string& m) : msg(m) {} + }; + + /** + * \brief A value: scalar or nested (possibly ragged) array + * + * The leaves are stored in a flat array of the value's type. The + * structure of a value of depth \a d is given by \a d levels of list + * lengths: level 0 has a single entry (the length of the outermost + * list), level \a k has one entry per list at depth \a k. The lengths at + * the last level sum to the number of leaves. + */ + class Value { + public: + /// Type of the leaves + ValueType type; + /// Nesting depth (0 for scalars) + int depth; + /// List lengths per level + Seq> lengths; + /// Start of each list among the lists or leaves one level down (see index) + Seq> starts; + /// Integer leaves + Seq ints; + /// Float leaves + Seq reals; + /// String leaves + Seq strs; + + /// The empty integer list + Value(void) + : type(ValueType::INT), depth(1) { + lengths.push(Seq()); + lengths[0].push(0); + } + /// Scalar of type \a t whose leaf is still to be added + explicit Value(ValueType t) + : type(t), depth(0) {} + /// Integer scalar + static Value integer(long long int v) { + Value x(ValueType::INT); x.ints.push(v); + return x; + } + /// Float scalar + static Value real(double v) { + Value x(ValueType::FLOAT); x.reals.push(v); + return x; + } + /// String scalar + static Value string(const std::string& v) { + Value x(ValueType::STRING); x.strs.push(v); + return x; + } + + /// Test whether scalar + bool is_scalar(void) const { return depth == 0; } + /// Test whether numeric scalar + bool is_number(void) const { return (depth == 0) && (type != ValueType::STRING); } + /// Return number of leaves + int leaves(void) const { + switch (type) { + case ValueType::INT: return ints.size(); + case ValueType::FLOAT: return reals.size(); + default: return strs.size(); + } + } + /// Return length of outermost list + int size(void) const { return depth == 0 ? 0 : lengths[0][0]; } + /// Return value of numeric scalar or leaf \a i as a double + double as_double(int i=0) const { + return type == ValueType::INT ? static_cast(ints[i]) : reals[i]; + } + /// Convert integer leaves to floats + void to_float(void) { + if (type != ValueType::INT) return; + for (long long int v : ints) + reals.push(static_cast(v)); + ints.clear(); + type = ValueType::FLOAT; + } + /// Append the value of scalar \a s, adapting the type + void push(const Value& s) { + if (leaves() == 0) { + type = s.type; + } else if ((type == ValueType::STRING) != (s.type == ValueType::STRING)) { + throw ShapeError("strings and numbers are mixed"); + } else if ((type == ValueType::INT) && (s.type == ValueType::FLOAT)) { + to_float(); + } + switch (s.type) { + case ValueType::INT: + if (type == ValueType::INT) ints.push(s.ints[0]); + else reals.push(static_cast(s.ints[0])); + break; + case ValueType::FLOAT: reals.push(s.reals[0]); break; + default: strs.push(s.strs[0]); break; + } + } + /// Test whether this is an empty list + bool is_empty_list(void) const { return (depth > 0) && (lengths[0][0] == 0); } + /// Make this the empty list of type \a t, keeping the memory for reuse + void clear_list(ValueType t) { + type = t; + depth = 1; + ints.clear(); reals.clear(); strs.clear(); + while (lengths.size() > 1) + lengths.pop(); + if (lengths.empty()) + lengths.push(Seq()); + lengths[0].clear(); + lengths[0].push(0); + } + + /// Compute \a starts, needed by range + void index(void); + /// Find leaf range of list \a pos at level \a level + void range(int level, int pos, int& from, int& to) const; + }; + + /** + * \brief Array built row by row from the values of a repeated statement + * + * Row \a r is the value the name had in iteration \a r. For example, + * the rows [1,2], [], and [3] give a value of depth 2 with lengths + * {3} and {2,0,1} and leaves 1,2,3. A missing row (from an iteration + * that did not define the name) becomes an empty list, which needs + * rows that are lists. + * + * Depth and type come from the rows added. A column without rows takes + * them from the statement defining the name. + */ + class Column { + private: + /// The array + Value c; + /// Number of rows + int rows; + /// Missing rows before the first row added + int pending; + /// Whether a row has been added + bool started; + /// Whether a row has been missing + bool missing; + /// Depth of the rows + int d; + public: + /// Empty column for rows of type \a t and depth \a depth + Column(ValueType t, int depth); + /// Add a row \a v; throws ShapeError with a phrase completing the name + void add(Value& v); + /// Add a missing row + void skip(void); + /// Return the array + Value finish(void); + }; + + /// A named value + class Entry { + public: + /// Name + std::string name; + /// Value + Value value; + /// Initialize as the empty integer list without a name + Entry(void) {} + /// Initialize as \a v named \a n + Entry(const std::string& n, Value&& v) + : name(n), value(std::move(v)) {} + }; + + /// Implementation of Data + class DataImpl { + public: + /// The entries in definition order + Seq entries; + /// Return entry \a n or null + const Value* find(const std::string& n) const { + for (const Entry& e : entries) + if (e.name == n) + return &e.value; + return nullptr; + } + }; + + /// Implementation of Params + class ParamsImpl { + public: + /// Names + Seq names; + /// Values + Seq values; + }; + + /* + * Spec syntax tree + */ + + class Stmt; + + /** + * \brief Expression + * + * An index expression a[i] has the operands \a a and \a i, a + * field r.f the operand \a r and the name \a f in \a s. A + * comprehension such as all(x in a: e) has the function in + * \a s, the bound name in \a var, and the operands \a a and \a e. + * + * The parser finds the expressions that are records or lists of records + * of a repeat (see Stmt::list and Stmt::each): \a records is the repeat. + */ + class Expr { + public: + /// Kind of expression + enum class Kind { INT, FLOAT, STR, NAME, BINARY, UNARY, CALL, INDEX, FIELD, COMPREHENSION } kind; + /// Operator of unary and binary expressions + enum class Op { + OR, AND, EQ, NE, LT, GT, LE, GE, ADD, SUB, MUL, DIV, MOD, POW, NEG, NOT + } op; + /// Integer literal + long long int i; + /// Float literal + double f; + /// String literal, name, operator, or function + std::string s; + /// Name bound by a comprehension + std::string var; + /// Index of a record name among the records of its check (-1 if none) + int record; + /// The repeat whose records or lists of records this is, or null + const Stmt* records; + /// 1 for a list of records of \a records, 0 for a record + int open; + /// For a comprehension, the repeat whose records are the elements of + /// its array, or null + const Stmt* elements; + /// Operands or arguments + Seq> args; + /// Initialize with kind \a k + explicit Expr(Kind k) + : kind(k), op(Op::OR), i(0), f(0.0), record(-1), records(nullptr), open(0), elements(nullptr) {} + }; + + /** + * \brief A record name of a check + * + * A name defined inside repeats that do not enclose the check, or the + * name of each record of such a repeat (see Stmt::each), so that the + * check holds for each of its records. + */ + class Record { + public: + /// The name of the values + std::string name; + /// The name in messages: the name, or the list of records + std::string label; + /// The repeats between the check and the definition, outermost first + Seq repeats; + /// Name of the record starts of the innermost of these repeats + std::string starts; + }; + + /** + * \brief Return the name of the record starts of the repeat numbered \a id + * + * A repeat stores where each of its records starts in the input under + * this name, which is not a spec name, so that a failed check can point + * at a record. Such names are not part of the data read. + */ + inline std::string record_starts(int id) { + return "#" + std::to_string(id); + } + /// Test whether \a n is a name used internally (see record_starts) + inline bool is_internal(const std::string& n) { + return !n.empty() && (n[0] == '#'); + } + + /// What a declaration or a capture reads + enum class ReadType { + INT, ///< An integer + FLOAT, ///< A float + NUM, ///< An integer if possible, otherwise a float + WORD, ///< A token as a string + REST, ///< The rest of the line as a string + SYMBOL ///< A character of an alphabet, as an integer + }; + + /** + * \brief Type of the values that a declaration or a capture reads + * + * An integer type can be a range \c lo..hi, or \c lo.. lo; + /// Upper bound of a range, or null + std::unique_ptr hi; + /// Whether the upper bound is excluded + bool exclusive; + /// The type statement defining a named type, or null + const Stmt* named; + /// Name of a named type + std::string name; + /// Symbols of an alphabet type + std::string alphabet; + /// Symbols that stand for 0 + std::string blank; + /// Symbols to skip + std::string strip; + /// Value of the first symbol + long long int base; + /// Initialize as \a r + explicit Type(ReadType r = ReadType::INT) + : read(r), exclusive(false), named(nullptr), base(1) {} + /// Test whether the type has a range, possibly through its name + bool ranged(void) const { + return (lo != nullptr) || (hi != nullptr) || (named != nullptr); + } + /// Return a use of the type defined by type statement \a d + Type use(const Stmt* d) const { + Type t(read); + t.named = d; + t.alphabet = alphabet; + t.blank = blank; + t.strip = strip; + t.base = base; + return t; + } + }; + + /// Line pattern such as "DIMENSION : {n:int}" + class Pattern { + public: + /// Pattern item + class Item { + public: + /// Kind of item + enum class Kind { LIT, SPACE, CAP, ANY, VAR } kind; + /// Literal text, capture name, or variable name + std::string text; + /// What a capture reads + Type type; + /// Whether a space item needs at least one blank + bool required; + /// Initialize with kind \a k + explicit Item(Kind k) : kind(k), required(false) {} + }; + /// Source text + std::string source; + /// Items + Seq items; + }; + + /// Sequence of statements + typedef Seq> Block; + + /// Arm of a sections statement + class Arm { + public: + /// Line pattern + Pattern pattern; + /// Statements run after the line matched + Block body; + /// Whether the input may lack a line matching the arm + bool optional; + /// Whether the sections statement ends after this arm + bool last; + /// Initialize + Arm(void) + : optional(false), last(false) {} + }; + + /// Statement + class Stmt { + public: + /// Kind of statement + enum class Kind { + COMMENT, STOP, TOKENS, SEPARATORS, TRAILING, PARAM, READ, LIST, + LINE, FIND, SKIP, EXPECT, INDEX, REPEAT, SECTIONS, IF, LET, CHECK, + TYPE, INSTANCES, EACH + } kind; + /// Line in the spec + int line; + /// What read and list statements read, or the type a type statement + /// defines + Type type; + /// Name defined + std::string name; + /// Characters for tokens and separators statements + std::string chars; + /// Message of a check statement + std::string message; + /// Name for the terminating token found + std::string store; + /// Name of the list of records of a repeat + std::string list; + /// Name of each record of a repeat, inside and after it + std::string each; + /// Array dimensions + Seq> dims; + /// Count, condition, layout, value, or default + std::unique_ptr expr; + /// List terminator + std::unique_ptr sentinel; + /// Added to every number read + std::unique_ptr offset; + /// Prefixes, literal tokens, or terminating tokens + Seq words; + /// List ends at end of line + bool until_eol; + /// Repeat until end of input + bool until_eof; + /// Repeat or sections end at a token from \a words + bool has_until; + /// Sections skip unknown lines + bool skip_other; + /// Pattern for line and find + Pattern pattern; + /// Body, then-branch + Block body; + /// Else-branch + Block orelse; + /// Arms of sections + Seq arms; + /// Number of a repeat, in the order of the spec + int id; + /// Record names of a check + Seq records; + /// Initialize with kind \a k at spec line \a l + Stmt(Kind k, int l) + : kind(k), line(l), until_eol(false), until_eof(false), has_until(false), skip_other(false), id(0) {} + }; + + /// A name that a block may define, with the type and depth of its values + class Decl { + public: + /// Name + std::string name; + /// Type of the leaves + ValueType type; + /// Depth + int depth; + }; + /// Add the names that block \a b may define to \a out + void declared(const Block& b, Seq& out); + + /// Throw an error with message \a m for line \a line of the spec from \a origin + [[noreturn]] void fail_at(const std::string& origin, int line, const std::string& m); + + /** + * \brief Resolve the names in the expressions of \a b, the body of a + * spec from \a origin + * + * Finds the expressions that are records or lists of records of a named + * repeat (see Expr) and the record names of the checks (see Record), and + * reports their misuse. + */ + void resolve(Block& b, const std::string& origin); + + /// Implementation of Spec + class SpecImpl { + public: + /// Where the spec comes from + std::string origin; + /// Format name + std::string name; + /// Statements + Block body; + /// Parse \a text from \a origin + SpecImpl(const std::string& text, const std::string& origin); + /** + * \brief Read \a input from \a origin + * + * With \a catalog, return the number of instances and their names + * instead of the data of one instance. + */ + Data read(const std::string& input, const std::string& origin, const Params& p, bool catalog) const; + }; + + /// Read file \a path into a string + std::string read_file(const std::string& path); + +}} + +#endif + +// STATISTICS: instance-any diff --git a/gecode/instance/reader.cpp b/gecode/instance/reader.cpp new file mode 100644 index 0000000000..4b619af641 --- /dev/null +++ b/gecode/instance/reader.cpp @@ -0,0 +1,1261 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include + +#include +#include + +namespace Gecode { namespace Instance { + + namespace { + + /** + * \brief Return the first column of row \a i of a stored triangle + * + * The triangle is upper if \a upper and includes the diagonal if + * \a diag. + */ + int triangle_from(bool upper, bool diag, int i) { + return upper ? (diag ? i : i+1) : 0; + } + /// Return the column after the last of row \a i of a stored triangle of an + /// \a n by \a n matrix (see triangle_from) + int triangle_to(bool upper, bool diag, int n, int i) { + return upper ? n : (diag ? i+1 : i); + } + + /// Return a description of what read type \a t reads, for messages + const char* describe(ReadType t) { + switch (t) { + case ReadType::INT: return "int"; + case ReadType::FLOAT: return "float"; + case ReadType::NUM: return "a number"; + case ReadType::WORD: return "a word"; + case ReadType::REST: return "the rest of the line"; + default: return "a symbol"; + } + } + + /// The bounds of a range type, evaluated + class Range { + public: + /// Whether there is a lower bound + bool low; + /// Whether there is an upper bound + bool high; + /// Lower bound + long long int lo; + /// Upper bound, included + long long int hi; + /// Initialize without bounds + Range(void) + : low(false), high(false), lo(0), hi(0) {} + /// Test whether \a v is in the range + bool contains(long long int v) const { + return (!low || (v >= lo)) && (!high || (v <= hi)); + } + /// Test whether there are bounds + bool bounded(void) const { + return low || high; + } + }; + + /// A type with its range evaluated + class Domain { + public: + /// The type + const Type& type; + /// The range + Range range; + /// Initialize for \a t with range \a r + Domain(const Type& t, const Range& r) + : type(t), range(r) {} + /// Return a description of the values, for messages + std::string describe(void) const { + std::string d = Instance::describe(type.read); + if (range.bounded()) + d = "an int in " + (range.low ? std::to_string(range.lo) : "") + ".." + + (range.high ? std::to_string(range.hi) : ""); + return type.name.empty() ? d : type.name + " (" + d + ")"; + } + }; + + /// Interpreter running a spec on an input + class Interpreter : public Evaluator { + public: + /// Initialize + Interpreter(const SpecImpl& s, const std::string& text, const std::string& origin, const Params& p, bool c) + : Evaluator(s, text, origin), params(p), trailing(false), catalog(c), instance_count(-1), select(false), + number(-1), current(0), instance_frame(0), selected(false), idle(0), token_start(0) {} + + /// Add the names of the param statements in \a b to \a out + static void declared_params(const Block& b, Seq& out) { + for (const std::unique_ptr& s : b) { + if (s->kind == Stmt::Kind::PARAM) + out.push(s->name); + if (s->kind == Stmt::Kind::INSTANCES) + out.push("instance"); + declared_params(s->body, out); + declared_params(s->orelse, out); + for (const Arm& a : s->arms) + declared_params(a.body, out); + } + } + + /// Run the spec and return the data + void run(DataImpl& d) { + // A misspelt parameter would otherwise be ignored silently + Seq known; + declared_params(spec.body, known); + for (int i=0; i c; + Entry n; + n.name = "instances"; + n.value = Value::integer(instance_count < 0 ? 1 : instance_count); + c.push(std::move(n)); + if (names.leaves() > 0) { + Entry e; + e.name = "name"; + e.value = names; + c.push(std::move(e)); + } + if (failures.leaves() > 0) { + Entry e; + e.name = "failed"; + e.value = failures; + c.push(std::move(e)); + } + frames[0] = std::move(c); + } + for (Entry& e : frames[0]) + if (!is_internal(e.name)) { + e.value.index(); + d.entries.push(std::move(e)); + } + } + + private: + /// The parameters + const Params& params; + /// Row numbers of the active repeats + Seq rows; + /// Whether input after the spec is allowed + bool trailing; + /// Whether only the instances are listed + bool catalog; + /// Number of instances in an instances statement (-1 if none) + int instance_count; + /// Names of the instances, for a catalog + Value names; + /// The first rejection of each instance, for a catalog + Value failures; + /// Whether the parameter instance selects an instance + bool select; + /// The value of the parameter instance + std::string want; + /// The number of the instance selected (-1 if selected by name) + long long int number; + /// Number of the instance being read (0 if none) + int current; + /// Frame of the instance being read + int instance_frame; + /// Error of the first rejection of the instance being read + std::string deferred_error; + /// Summary of that error + std::string deferred_message; + /// Whether the selected instance has been read + bool selected; + /// Repeat iterations and empty rows created without reading input + long long int idle; + /// Limit for idle: such work could otherwise be unbounded or take + /// time and memory far beyond the size of the input + static const long long int max_idle = 1LL << 20; + /// The ranges of the named types, by type statement + std::unordered_map type_ranges; + /// Where the last token read as a value starts + size_t token_start; + /// Lists of finished repeat iterations, kept for their memory + Seq spare; + /// Most lists kept in spare + static const int max_spare = 8; + + /// Account for \a k units of work that read no input + void work(long long int k) { + idle += k; + if (idle > max_idle) + fail("too many repetitions or empty rows without reading input"); + } + + + /* + * Reading + */ + /// Store \a t read as \a type in \a v, and return whether it fits + static bool convert(ReadType type, const std::string& t, Value& v) { + long long int iv; + double fv; + if ((type == ReadType::WORD) || (type == ReadType::REST)) { + v = Value::string(t); + return !t.empty(); + } + if ((type != ReadType::FLOAT) && parse_int(t, iv)) { + v = Value::integer(iv); + return true; + } + if ((type != ReadType::INT) && parse_float(t, fv)) { + v = Value::real(fv); + return true; + } + return false; + } + + /// Fail for want of \a what at the end of the input or the token there + [[noreturn]] void expected(const std::string& what) { + if (!in.skip_to_data()) + fail("expected " + what + ", found end of input"); + fail("expected " + what + ", found " + quote(in.peek())); + } + /** + * \brief Read one value of \a type into \a v, and return whether + * there is one + * + * At the end of the input or a token that is not of the type, the + * position stays before it. + */ + bool scan(ReadType type, Value& v) { + if (!in.skip_to_data()) + return false; + token_start = in.pos; + if (type == ReadType::REST) { + v = Value::string(in.rest()); + in.to_eol(); + return true; + } + std::string t = in.token(); + if (convert(type, t, v)) + return true; + in.pos = token_start; + return false; + } + /// Read one value of \a type, described by \a what + Value scalar(ReadType type, const std::string& what) { + Value v(ValueType::INT); + if (!scan(type, v)) + expected(what); + return v; + } + /// Read one value of domain \a d, without checking its range + Value scalar(const Domain& d) { + Value v(ValueType::INT); + if (!scan(d.type.read, v)) + expected(d.describe()); + return v; + } + /// Read an integer of domain \a d, which reads integers, without + /// checking its range + long long int int_scalar(const Domain& d) { + long long int x; + if (!in.skip_to_data()) + expected(d.describe()); + token_start = in.pos; + if (!in.int_token(x)) + expected(d.describe()); + return x; + } + /// Test whether value \a v is in the range of domain \a d + static bool fits(const Domain& d, const Value& v) { + return (v.type != ValueType::INT) || d.range.contains(v.ints[0]); + } + /// Fail for the value from position \a from to \a to on the current + /// line, which is not in domain \a d + [[noreturn]] void outside(const Domain& d, size_t from, size_t to) { + std::string t = in.text.substr(from, to-from); + in.pos = from; + fail("expected " + d.describe() + ", found " + quote(t)); + } + /// Read one value of domain \a d + Value value(const Domain& d) { + Value v = scalar(d); + if (!fits(d, v)) + outside(d, token_start, in.pos); + return v; + } + /// Read one value of domain \a d and append it to the list \a v + void push(const Domain& d, Value& v) { + if ((d.type.read == ReadType::INT) && (v.type == ValueType::INT)) { + long long int x = int_scalar(d); + if (!d.range.contains(x)) + outside(d, token_start, in.pos); + v.ints.push(x); + } else { + v.push(value(d)); + } + } + + /// Return the range of type \a t + Range range(const Type& t) { + if (t.named != nullptr) { + auto r = type_ranges.find(t.named); + if (r == type_ranges.end()) + fail("the type '" + t.name + "' is not defined yet"); + return r->second; + } + Range r; + if (t.lo) { + r.low = true; + r.lo = bound(*t.lo); + } + if (t.hi) { + r.high = true; + r.hi = bound(*t.hi); + if (t.exclusive && !sub(r.hi, 1, r.hi)) + fail("integer overflow in the upper bound of a range"); + } + return r; + } + /// Return the value of the bound \a e of a range + long long int bound(const Expr& e) { + // Ranges are evaluated for every value read, so common bounds + // such as 0 and -n avoid making a value + if (e.kind == Expr::Kind::INT) + return e.i; + if ((e.kind == Expr::Kind::UNARY) && (e.op == Expr::Op::NEG)) { + long long int x = bound(*e.args[0]); + if (x == LLONG_MIN) + fail("integer overflow in '-'"); + return -x; + } + Value tmp(ValueType::INT); + const Value& v = ref(e, tmp); + if ((v.depth > 0) || (v.type != ValueType::INT)) + fail("the bounds of a range must be integers"); + return v.ints[0]; + } + + /// Read an array of domain \a d with dimensions \a dims + Value array(const Domain& d, const Seq& dims) { + // Each factor is at most INT_MAX, so the product cannot overflow + long long int size = 1; + for (long long int n : dims) { + size *= n; + if (size > INT_MAX) + fail("array is too large"); + } + // Rows before an empty dimension cost memory without reading any input + if (size == 0) { + long long int rows = 1; + for (int k=0; (k < dims.size()) && (dims[k] > 0); k++) { + rows *= dims[k]; + work(rows); + } + } + Value v; + if (d.type.read == ReadType::FLOAT) + v.type = ValueType::FLOAT; + else if (d.type.read == ReadType::WORD) + v.type = ValueType::STRING; + // Read the values before allocating the structure, so that a wrong + // count fails at the end of the input rather than on allocation + for (long long int i=0; i()); + v.lengths.back().reserve(static_cast(lists)); + for (long long int i=0; i(n)); + lists *= n; + } + return v; + } + + /** + * \brief Read a symmetric \a n by \a n matrix of domain \a d stored + * in TSPLIB layout \a f with offset \a o + * + * The offset applies to the numbers read, not to the cells that only + * exist after mirroring. + */ + Value layout(const Domain& d, long long int n, long long int m, const std::string& f, long long int o) { + if (f == "FULL_MATRIX") { + Seq dims; + dims.push(n); dims.push(m); + Value v = array(d, dims); + offset(v, o); + return v; + } + if (n != m) + fail("layout " + f + " needs a square matrix"); + // Column-wise upper equals row-wise lower for symmetric matrices + bool upper, diag; + if ((f == "UPPER_ROW") || (f == "LOWER_COL")) { + upper = true; diag = false; + } else if ((f == "LOWER_ROW") || (f == "UPPER_COL")) { + upper = false; diag = false; + } else if ((f == "UPPER_DIAG_ROW") || (f == "LOWER_DIAG_COL")) { + upper = true; diag = true; + } else if ((f == "LOWER_DIAG_ROW") || (f == "UPPER_DIAG_COL")) { + upper = false; diag = true; + } else { + fail("unknown matrix layout " + quote(f)); + } + if (n*n > INT_MAX) + fail("matrix is too large"); + int nn = static_cast(n); + // Read the stored triangle, then mirror it into a full matrix + Value t; + if (d.type.read == ReadType::FLOAT) + t.type = ValueType::FLOAT; + for (int i=0; i()); + v.lengths[0].push(nn); + v.lengths.push(Seq()); + for (int i=0; i(o); + } + + /// Return the offset of statement \a s (0 if none) + long long int offset(const Stmt& s) { + if (!s.offset) + return 0; + Value o = eval(*s.offset); + if ((o.depth > 0) || (o.type != ValueType::INT)) + fail("offset must be an integer"); + return o.ints[0]; + } + + /// Store value \a v read by statement \a s, unless its name is _ + void store(const Stmt& s, Value&& v) { + if (s.name != "_") + set(s.name, std::move(v)); + } + + /// A value captured from a line + class Capture { + public: + /// The capture + const Pattern::Item* item; + /// The value + Value value; + /// Where its text starts in the line + size_t from; + /// Where its text ends in the line + size_t to; + }; + + /// Match pattern \a p against the rest of the current line, consuming it + bool match(const Pattern& p) { + Seq caps; + if (!matches(p, caps)) + return false; + take(caps); + return true; + } + + /// Test whether \a p matches the rest of the current line, giving the captures \a caps + bool matches(const Pattern& p, Seq& caps) { + in.skip(); + if (in.pos >= in.text.size()) + return false; + std::string l = in.rest(); + size_t c = 0; + for (int k=0; k& caps) { + // The captures can use each other in their ranges + for (const Capture& x : caps) + if (!x.item->text.empty() && (x.item->text != "_")) + set(x.item->text, Value(x.value)); + for (const Capture& x : caps) + if (x.item->type.ranged()) { + Domain d(x.item->type, range(x.item->type)); + if (!fits(d, x.value)) + outside(d, in.pos + x.from, in.pos + x.to); + } + in.to_eol(); + } + + /// Match item \a k of \a p at position \a c of line \a l + bool match_item(const Pattern& p, int k, const std::string& l, size_t& c, Seq& caps) { + typedef Pattern::Item Item; + const Item& it = p.items[k]; + switch (it.kind) { + case Item::Kind::SPACE: { + size_t c0 = c; + while ((c < l.size()) && is_blank(l[c])) + c++; + return !it.required || (c > c0) || (c == l.size()); + } + case Item::Kind::LIT: + if (l.compare(c, it.text.size(), it.text) != 0) + return false; + c += it.text.size(); + return true; + case Item::Kind::ANY: + c = l.size(); + return true; + case Item::Kind::VAR: { + std::string want = variable(it.text); + if (want.empty() || (l.compare(c, want.size(), want) != 0)) + return false; + c += want.size(); + return true; + } + case Item::Kind::CAP: { + // A word ends before a literal that follows it directly + char stop = 0; + if ((k+1 < p.items.size()) && (p.items[k+1].kind == Item::Kind::LIT)) + stop = p.items[k+1].text[0]; + size_t c0 = c; + c = capture_end(it.type.read, l, c, stop); + std::string t = l.substr(c0, c-c0); + while (!t.empty() && is_blank(t[t.size()-1])) + t.erase(t.size()-1); + Capture x; + if (!convert(it.type.read, t, x.value)) + return false; + x.item = ⁢ + x.from = c0; + x.to = c0 + t.size(); + caps.push(std::move(x)); + return true; + } + } + return false; + } + + /// Return the value of variable \a n as text, for {$n} in a pattern + std::string variable(const std::string& n) { + const Value* v = lookup(n); + if (v == nullptr) + fail("'" + n + "' is not defined yet"); + if ((v->depth == 0) && (v->type == ValueType::STRING)) + return v->strs[0]; + if ((v->depth == 0) && (v->type == ValueType::INT)) + return std::to_string(v->ints[0]); + fail("'" + n + "' must be a string or an integer"); + } + + /// Return where a capture of \a type starting at \a c of \a l ends + static size_t capture_end(ReadType type, const std::string& l, size_t c, char stop) { + switch (type) { + case ReadType::REST: + return l.size(); + case ReadType::WORD: + while ((c < l.size()) && !is_blank(l[c]) && (l[c] != stop)) + c++; + return c; + default: + // Numbers end where a following literal starts + return number_end(l, c, true, type != ReadType::INT); + } + } + + void exec(const Block& b) { + for (const std::unique_ptr& s : b) + exec(*s); + } + + void exec(const Stmt& s) { + spec_line = s.line; + switch (s.kind) { + case Stmt::Kind::COMMENT: + for (const std::string& w : s.words) in.comments.push(w); + break; + case Stmt::Kind::STOP: + for (const std::string& w : s.words) in.stops.push(w); + break; + case Stmt::Kind::TOKENS: in.add_split(s.chars); break; + case Stmt::Kind::SEPARATORS: in.add_separators(s.chars); break; + case Stmt::Kind::TRAILING: trailing = true; break; + case Stmt::Kind::PARAM: param(s); break; + case Stmt::Kind::READ: read(s); break; + case Stmt::Kind::LIST: list(s); break; + case Stmt::Kind::LINE: line(s); break; + case Stmt::Kind::FIND: find(s); break; + case Stmt::Kind::SKIP: skip_lines(s); break; + case Stmt::Kind::EXPECT: expect(s); break; + case Stmt::Kind::INDEX: index(s); break; + case Stmt::Kind::REPEAT: repeat(s); break; + case Stmt::Kind::SECTIONS: sections(s); break; + case Stmt::Kind::INSTANCES: instances(s); break; + case Stmt::Kind::TYPE: type_ranges[&s] = range(s.type); break; + case Stmt::Kind::LET: set(s.name, eval(*s.expr)); break; + case Stmt::Kind::IF: + exec(truth(eval(*s.expr)) ? s.body : s.orelse); + break; + case Stmt::Kind::CHECK: check(s); break; + case Stmt::Kind::EACH: break; + } + } + + /** + * \brief param x [= default] + * + * A given value has the type of the default. Without a default, it + * is an integer if possible, otherwise a float or a string. + */ + void param(const Stmt& s) { + std::string v; + long long int iv; + double fv; + Value d; + if (s.expr) + d = eval(*s.expr); + if (!params.get(s.name, v)) { + if (!s.expr) + fail("parameter '" + s.name + "' must be given"); + set(s.name, std::move(d)); + } else if (s.expr && (d.type == ValueType::STRING)) { + set(s.name, Value::string(v)); + } else if (parse_int(v, iv) && (!s.expr || (d.type == ValueType::INT))) { + set(s.name, Value::integer(iv)); + } else if (parse_float(v, fv) && (!s.expr || (d.type == ValueType::FLOAT))) { + set(s.name, Value::real(fv)); + } else if (!s.expr) { + set(s.name, Value::string(v)); + } else { + fail("parameter '" + s.name + "' must be " + (d.type == ValueType::INT ? "an integer" : "a number") + + ", not " + quote(v)); + } + } + + /// x: type and x[d1][d2]...: type + void read(const Stmt& s) { + context.push(Context(s.name)); + long long int o = offset(s); + Domain d(s.type, range(s.type)); + if (s.dims.empty()) { + Value v = value(d); + offset(v, o); + store(s, std::move(v)); + } else { + Seq dims; + for (const std::unique_ptr& e : s.dims) + dims.push(count(*e, "array size")); + if (s.type.read == ReadType::SYMBOL) { + store(s, symbols(s.type, dims[0], dims[1])); + } else if (s.expr) { + Value f = eval(*s.expr); + if ((f.depth > 0) || (f.type != ValueType::STRING)) + fail("matrix layout must be a string"); + store(s, layout(d, dims[0], dims[1], f.strs[0], o)); + } else { + Value v = array(d, dims); + offset(v, o); + store(s, std::move(v)); + } + } + context.pop(); + } + + /// list x: type until (eol | terminator) + void list(const Stmt& s) { + context.push(Context(s.name)); + Value v(ValueType::INT); + if (!spare.empty()) { + v = std::move(spare.back()); + spare.pop(); + } + v.clear_list(s.type.read == ReadType::FLOAT ? ValueType::FLOAT : + s.type.read == ReadType::WORD ? ValueType::STRING : ValueType::INT); + Domain d(s.type, range(s.type)); + try { + if (s.until_eol) + list_line(d, v); + else + list_until(s, d, v); + } catch (const ShapeError& e) { + fail(e, s.name); + } + v.lengths[0][0] = v.leaves(); + offset(v, offset(s)); + context.pop(); + store(s, std::move(v)); + } + /// Read the values of domain \a d on the rest of the line into \a v + void list_line(const Domain& d, Value& v) { + // The rest of the current line, or the next line if the current + // one is used up (skip_to_data() moves there) + if (!in.skip_to_line_data() && !in.skip_to_data()) + fail("expected a line of values, found end of input"); + while (in.skip_to_line_data()) + push(d, v); + } + /// Test whether the scalars \a x and \a y are numbers that are equal + static bool same_number(const Value& x, const Value& y) { + if ((x.type == ValueType::INT) && (y.type == ValueType::INT)) + return x.ints[0] == y.ints[0]; + if ((x.type == ValueType::STRING) || (y.type == ValueType::STRING)) + return false; + return x.as_double() == y.as_double(); + } + /// Read integers of domain \a d into \a v up to the terminator \a stop + void ints_until(const Domain& d, long long int stop, Value& v) { + for (;;) { + // The terminator need not be in the range + long long int x = int_scalar(d); + if (x == stop) + return; + if (!d.range.contains(x)) + outside(d, token_start, in.pos); + v.ints.push(x); + } + } + /// Read values of domain \a d into \a v up to the terminator of \a s + void list_until(const Stmt& s, const Domain& d, Value& v) { + if ((d.type.read == ReadType::INT) && (s.sentinel->kind == Expr::Kind::INT)) { + ints_until(d, s.sentinel->i, v); + return; + } + Value stop = eval(*s.sentinel); + if (stop.depth > 0) + fail("list terminator must be a single value"); + // Words end at the token spelling an integer terminator + if ((s.type.read == ReadType::WORD) && (stop.type == ValueType::INT)) + stop = Value::string(std::to_string(stop.ints[0])); + if ((s.type.read == ReadType::WORD) && (stop.type == ValueType::FLOAT)) + fail("a list of words needs a string or integer terminator"); + if ((d.type.read == ReadType::INT) && (stop.type == ValueType::INT)) { + ints_until(d, stop.ints[0], v); + return; + } + for (;;) { + // A string terminator is compared before the token is read as + // a value, so "EOF" can end a list of numbers + if (stop.type == ValueType::STRING) { + if (!in.skip_to_data()) + fail("expected " + quote(stop.strs[0]) + ", found end of input"); + if (in.peek() == stop.strs[0]) { + (void) in.token(); + return; + } + } + // The terminator need not be in the range + Value x = scalar(d); + if (same_number(x, stop)) + return; + if (!fits(d, x)) + outside(d, token_start, in.pos); + v.push(x); + } + } + + /// line "pattern": the rest of the current line, or the next line if + /// the current one is used up, must match + void line(const Stmt& s) { + if (match(s.pattern)) + return; + fail("expected a line matching \"" + s.pattern.source + "\"" + + (in.pos >= in.text.size() ? ", found end of input" : "")); + } + /// find "pattern": skip lines until one matches + void find(const Stmt& s) { + while (!match(s.pattern)) { + if (!in.skip_to_data()) + fail("no line matching \"" + s.pattern.source + "\" found"); + in.to_eol(); + } + } + /// skip e lines + void skip_lines(const Stmt& s) { + long long int n = count(*s.expr, "number of lines"); + for (long long int k=0; k 0 ? " or " : "") + quote(s.words[k]); + return r; + } + + /** + * \brief Test whether the instance being read may not be the + * selected one + * + * When selecting by name, the instance is the selected one if it has + * the name, and may not be while the name is not defined yet. + */ + bool may_be_skipped(void) const { + if (current == 0) + return false; + if (catalog) + return true; + if (!select) + return current != 1; + if (number > 0) + return current != number; + return instance_name(frames[instance_frame]) != want; + } + + /** + * \brief Reject the instance being read with error \a e, summed up by + * \a message + * + * Reading stops, unless the instance may not be the selected one: + * then the first rejection is recorded, and reading continues (see + * instances). + */ + void reject(const std::string& e, const std::string& message) { + if (!may_be_skipped()) + throw Error(e); + if (deferred_error.empty()) { + deferred_error = e; + deferred_message = message; + } + } + + /// check e "message" + void check(const Stmt& s) { + std::string e; + if (!holds(s, e)) + reject(e, s.message.empty() ? "check on line " + std::to_string(s.line) + " of the spec" : s.message); + } + + + /// Read instance \a n with the body \a b into a frame, and return it + Seq instance(const Block& b, int n) { + current = n; + deferred_error.clear(); + deferred_message.clear(); + context.push(Context("instance", n)); + frames.push(Seq()); + instance_frame = frames.size()-1; + try { + exec(b); + } catch (const Error& e) { + // The earlier rejection may explain the error + if (deferred_error.empty()) + throw; + throw Error(std::string(e.what()) + "\n after an earlier error in the instance: " + deferred_message); + } + Seq f = std::move(frames.back()); + frames.pop(); + context.pop(); + current = 0; + return f; + } + + /// Return the name of the instance in frame \a f, or empty + static std::string instance_name(const Seq& f) { + for (const Entry& e : f) + if ((e.name == "name") && (e.value.depth == 0)) { + if (e.value.type == ValueType::STRING) return e.value.strs[0]; + if (e.value.type == ValueType::INT) return std::to_string(e.value.ints[0]); + } + return std::string(); + } + + /** + * \brief Read the instances of a file and keep the selected one + * + * Each instance is read into a frame of its own, which is dropped + * unless the instance is the one wanted. The parameter \c instance + * selects an instance by number (from 1) if it is a number, and by + * the value of \c name otherwise. Without it, the file must contain + * a single instance. Reading stops after the selected instance. In a + * catalog, all instances are read and only their names and + * rejections are kept. + * + * A rejection (see reject) of an instance that is not the selected + * one does not stop reading, so that an invalid instance does not + * hide the ones after it. Other errors do, as the input cannot be + * read on. + */ + void instances(const Stmt& s) { + select = params.get("instance", want); + long long int k = 0; + bool numbered = select && parse_int(want, k); + if (numbered && (k < 1)) + fail("instance numbers start at 1"); + number = numbered ? k : -1; + int n = 0; + while (in.skip_to_data()) { + n++; + size_t start = in.pos; + Seq f = instance(s.body, n); + spec_line = s.line; + if (in.pos == start) + fail("an instance must read some input"); + std::string name = instance_name(f); + if (catalog) { + names.push(Value::string(name)); + failures.push(Value::string(deferred_message)); + } else if (select ? ((number > 0) ? (number == n) : (name == want)) : (n == 1)) { + // An instance selected by name may have been rejected before + // its name was known + if (!deferred_error.empty()) + throw Error(deferred_error); + for (Entry& e : f) + set(e.name, std::move(e.value)); + if (select) { + selected = true; + return; + } + } + } + instance_count = n; + if (catalog) { + // Keep the names only if every instance has one + bool all = (n > 0); + for (const std::string& v : names.strs) + all = all && !v.empty(); + if (all) + names.lengths[0][0] = n; + else + names = Value(); + bool failed = false; + for (const std::string& m : failures.strs) + failed = failed || !m.empty(); + if (failed) + failures.lengths[0][0] = n; + else + failures = Value(); + return; + } + if (n == 0) + fail("the file contains no instance"); + if (select) + fail("there is no instance " + quote(want) + " (the file has " + std::to_string(n) + " instance" + + (n == 1 ? "" : "s") + ")"); + if (n > 1) + fail("the file has " + std::to_string(n) + + " instances; select one with the parameter instance (a number from 1, or a name)"); + } + + /** + * \brief Repeat the body of \a s + * + * Each iteration runs in a frame of its own. Afterwards, the value of + * every name the body defines is added as a row to the name's column, + * and the frame is dropped, so that only one iteration's values exist + * at a time besides the columns. + */ + void repeat(const Stmt& s) { + Seq names; + declared(s.body, names); + long long int n = s.expr ? count(*s.expr, "repeat count") : -1; + Seq columns; + for (const Decl& d : names) + columns.push(Column(d.type, d.depth)); + // Where each record starts, for errors in checks + Value starts; + // The frame of an iteration, kept between iterations for its memory + Seq frame; + for (long long int r=0; (n < 0) || (r < n); r++) { + if (s.until_eof && !in.skip_to_data()) + break; + if (until(s)) + break; + if (s.has_until && !in.skip_to_data()) + fail("expected " + until_text(s) + " before end of input"); + context.push(Context("repeat", r+1, n)); + frames.push(std::move(frame)); + rows.push(r); + starts.ints.push(static_cast(in.next_data())); + size_t start = in.pos; + exec(s.body); + if (in.pos == start) { + if (n < 0) + fail("the body of a repeat until read no input"); + work(1); + } + rows.pop(); + Seq& f = frames.back(); + for (int i=0; ivalue); + } catch (const ShapeError& err) { + fail("'" + names[i].name + "' " + err.msg); + } + } + // A list read in each iteration then needs no new memory + for (Entry& e : f) + if ((e.value.depth > 0) && (spare.size() < max_spare)) + spare.push(std::move(e.value)); + frame = std::move(frames.back()); + frame.clear(); + frames.pop(); + context.pop(); + spec_line = s.line; + } + for (int i=0; i seen; + for (int k=0; k caps; + for (int k=0; (found < 0) && (k < s.arms.size()); k++) { + caps.clear(); + if (matches(s.arms[k].pattern, caps)) + found = k; + } + if (found < 0) { + if (s.skip_other) { + in.to_eol(); + continue; + } + std::string expected; + for (int k=0; (k < s.arms.size()) && (k < 6); k++) + expected += (k > 0 ? ", \"" : "\"") + s.arms[k].pattern.source + "\""; + if (s.arms.size() > 6) + expected += ", ..."; + fail("unexpected line; expected one of " + expected); + } + const Arm& a = s.arms[found]; + if (seen[found] > 0) { + std::string m = "a second line matching \"" + a.pattern.source + "\""; + reject(located(m + " (the first is on line " + std::to_string(seen[found]) + ")", at), m); + } else { + seen[found] = at.line; + } + take(caps); + context.push(Context("section \"" + a.pattern.source + "\"")); + exec(a.body); + context.pop(); + if (a.last) { + end = "the line matching \"" + a.pattern.source + "\""; + end_at = at; + break; + } + } + for (int k=0; k