diff --git a/.circleci/config.yml b/.circleci/config.yml index 2efd6c1..6363daf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -62,7 +62,9 @@ jobs: - run: name: Install Required Tools command: | - apk add --no-cache bash git g++ make cmake clang py-pip shellcheck shfmt grep npm + apk add --no-cache \ + bash boost-dev boost-static git g++ make cmake \ + clang py-pip shellcheck shfmt grep npm pip install cpplint npm install -g markdownlint-cli @@ -83,6 +85,7 @@ jobs: mkdir build cd build cmake .. -DPOST_TAG_SYSTEM_BUILD_TESTING=ON \ + -DPOST_TAG_SYSTEM_BUILD_CLI=ON \ -DPOST_TAG_SYSTEM_ENABLE_ALLWARNINGS=ON cmake --build . @@ -105,7 +108,7 @@ jobs: - run: name: Install Required Tools - command: apk add --no-cache bash git g++ make cmake + command: apk add --no-cache bash boost-dev boost-static git g++ make cmake - run: name: Build @@ -113,6 +116,7 @@ jobs: mkdir build cd build cmake .. -DPOST_TAG_SYSTEM_BUILD_TESTING=ON \ + -DPOST_TAG_SYSTEM_BUILD_CLI=ON \ -DPOST_TAG_SYSTEM_ENABLE_ALLWARNINGS=ON cmake --build . @@ -170,6 +174,19 @@ jobs: cmakeDir=$(dir -1 | findstr -i cmake-*) echo "export PATH=\"$(pwd)/$cmakeDir/bin:$PATH\"" >> $BASH_ENV + - run: + name: Install Boost headers and build libraries + command: | + boostURL="https://dl.bintray.com/boostorg/release/1.72.0/source/boost_1_72_0.zip" + curl -L --output boost.zip $boostURL + unzip -q boost.zip + boostDir=$(dir -1 | findstr -i boost_*) + echo "export BOOST_ROOT=\"$(pwd)/$boostDir\"" >> $BASH_ENV + grep BOOST_ $BASH_ENV + cd $boostDir + ./bootstrap.bat + ./b2 --with-program_options + - run: name: Build command: scripts/buildLibraryResources.sh @@ -182,6 +199,22 @@ jobs: - store_artifacts: path: ./LibraryResources/ + - run: + name: Build + command: | + rm -rf build + mkdir -p build + cd build + cmake .. \ + -DPOST_TAG_SYSTEM_ENABLE_ALLWARNINGS=ON \ + -DPOST_TAG_SYSTEM_BUILD_CLI=ON \ + -DPOST_TAG_SYSTEM_CLI_STATIC_BUILD=ON \ + -DCMAKE_BUILD_TYPE=Release + cmake --build . --config Release + + - store_artifacts: + path: ./build/CLI/Release + workflows: version: 2 build-and-test: diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d15be6a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +build/ +*.xcodeproj/ diff --git a/CLI/CMakeLists.txt b/CLI/CMakeLists.txt new file mode 100644 index 0000000..4cae27e --- /dev/null +++ b/CLI/CMakeLists.txt @@ -0,0 +1,39 @@ +option(POST_TAG_SYSTEM_CLI_STATIC_BUILD "Build command-line interface as a statically linked executable." OFF) + +if(POST_TAG_SYSTEM_CLI_STATIC_BUILD) + set(Boost_USE_STATIC_LIBS ON) +endif() + +find_package(Boost 1.67 + COMPONENTS program_options + REQUIRED) + +set(_link_libraries + PostTagSystem + ${Boost_LIBRARIES} + ) + +add_executable(wolfram-postproject + main.cpp arguments.cpp + files/PostTagFileReader.cpp files/PostTagFileWriter.cpp + files/PostTagCribFile.cpp files/PostTagInitFile.cpp + files/PostTagResultFile.cpp + ) + +target_include_directories(wolfram-postproject PRIVATE + ${Boost_INCLUDE_DIR}) + +target_compile_options(wolfram-postproject PRIVATE + ${POST_TAG_SYSTEM_COMPILE_OPTIONS}) + +target_compile_definitions(wolfram-postproject PRIVATE + POST_TAG_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR} + POST_TAG_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR} + POST_TAG_VERSION_PATCH=${CMAKE_PROJECT_VERSION_PATCH} + ) + +if(POST_TAG_SYSTEM_CLI_STATIC_BUILD) + target_link_options(wolfram-postproject PUBLIC "-static") +endif() + +target_link_libraries(wolfram-postproject ${_link_libraries}) diff --git a/CLI/arguments.cpp b/CLI/arguments.cpp new file mode 100644 index 0000000..b35b55b --- /dev/null +++ b/CLI/arguments.cpp @@ -0,0 +1,122 @@ +#include "arguments.hpp" + +#include +#include + +namespace po = boost::program_options; + +auto validator_uint_greater_equal(const char* const option_name, uint64_t min) { + return [option_name, min](auto n) { + if (n < min) { + throw po::validation_error( + po::validation_error::invalid_option_value, option_name, std::to_string(static_cast(n))); + } + }; +} + +auto validator_uint_between(const char* const option_name, uint64_t min, uint64_t max) { + return [option_name, min, max](auto n) { + if (n < min || n > max) { + throw po::validation_error( + po::validation_error::invalid_option_value, option_name, std::to_string(static_cast(n))); + } + }; +} + +void validate_option_existence(const po::variables_map& args, const char* const option_name) { + if (args.count(option_name) == 0) { + // TODO(jessef): better exception + throw po::validation_error(po::validation_error::at_least_one_value_required, option_name); + } +} + +void mode_validate_chase(const po::variables_map& args) { + validate_option_existence(args, "initsize"); + validate_option_existence(args, "initstart"); + validate_option_existence(args, "initcount"); +} + +void mode_validate_pounce(const po::variables_map& args) { validate_option_existence(args, "initfile"); } + +po::variables_map parse_arguments(int argc, char** argv) { + po::options_description general_options("General options"); + po::options_description chase_options("Chase-mode options"); + po::options_description pounce_options("Pounce-mode options"); + + // clang-format off + general_options.add_options() + ("help,h", po::bool_switch(), + "Print help text") + ("version,v", po::bool_switch(), + "Print program version") + ("chase,c", po::bool_switch(), + "Chase mode (range search)") + ("pounce,p", po::bool_switch(), + "Pounce mode (file search)") + ("outfile,o", po::value()->default_value("./output.postresult")->value_name("path.postresult"), + "Path to output file") + ("maxsize,x", po::value()->default_value(static_cast(1e9), "10^9")->value_name("size"), + "Maximum tape length to evaluate each initial condition to (0 = no limit)") + ("maxoutsize,z", po::value()->value_name("size"), + "Maximum tape length to include in output file entries (0 = never write final tapes; omit for no limit)") + ("maxsteps,m", po::value()->default_value(static_cast(1e10), "10^10")->value_name("steps"), + "Maximum number of steps to evaluate each initial condition to (0 = no limit)") + ("timeout,t", po::value()->default_value(0)->value_name("secs"), + "Total execution time constraint (seconds) (0 = no limit)"); + + chase_options.add_options() + ("cribfile,f", po::value()->value_name("path.postcrib"), + "Path to crib file (list of known sequences)") + ("initsize,l", po::value()->value_name("size") + ->notifier(validator_uint_between("initsize", 1, 64)), + "Size of initial condition tapes") + ("initstart,s", po::value()->value_name("start"), + "Starting initial condition tape (as decimal integer)") + ("initcount,n", po::value()->default_value(1)->value_name("count") + ->notifier(validator_uint_greater_equal("initcount", 1)), + "Number of initial condition tapes to evaluate") + ("initoffset,e", po::value()->default_value(0)->value_name("offset"), + "Shifts starting condition by offset * count (for use with zero-indexed array jobs)"); + + pounce_options.add_options() + ("initfile,i", po::value()->value_name("path.postinit"), + "Path to file of initial conditions"); + // clang-format on + + po::options_description all_options; + all_options.add(general_options).add(chase_options).add(pounce_options); + + po::variables_map args; + po::store(po::command_line_parser(argc, argv).options(all_options).run(), args); + po::notify(args); + + if (args["help"].as()) { + std::cout << all_options << "\n"; + return args; + } + + if (args["version"].as()) { +#if defined(POST_TAG_VERSION_MAJOR) && defined(POST_TAG_VERSION_MINOR) && defined(POST_TAG_VERSION_PATCH) + printf("wolfram-postproject v%u.%u.%u =^._.^=\n", + POST_TAG_VERSION_MAJOR, + POST_TAG_VERSION_MINOR, + POST_TAG_VERSION_PATCH); +#else + printf("wolfram-postproject unknown version =^._.^=\n"); +#endif + + return args; + } + + if (args["chase"].as() && args["pounce"].as()) { + throw std::runtime_error("Only one of --chase or --pounce may be specified"); + } else if (args["chase"].as()) { + mode_validate_chase(args); + } else if (args["pounce"].as()) { + mode_validate_pounce(args); + } else { + throw std::runtime_error("One of --chase or --pounce must be specified"); + } + + return args; +} diff --git a/CLI/arguments.hpp b/CLI/arguments.hpp new file mode 100644 index 0000000..183c0da --- /dev/null +++ b/CLI/arguments.hpp @@ -0,0 +1,8 @@ +#ifndef CLI_ARGUMENTS_HPP_ +#define CLI_ARGUMENTS_HPP_ + +#include "boost/program_options.hpp" + +boost::program_options::variables_map parse_arguments(int argc, char** argv); + +#endif // CLI_ARGUMENTS_HPP_ diff --git a/CLI/files/PostTagCribFile.cpp b/CLI/files/PostTagCribFile.cpp new file mode 100644 index 0000000..f9c440d --- /dev/null +++ b/CLI/files/PostTagCribFile.cpp @@ -0,0 +1,45 @@ +#include "PostTagCribFile.hpp" + +#include "boost/format.hpp" + +using PostTagSystem::TagState; + +PostTagCribFile PostTagCribFileReader::read_file() { + uint8_t file_magic = read_u8(); + PostTagFileMagic format_magic = CribFileMagic; + + if (file_magic != format_magic) { + throw std::runtime_error((boost::format("File magic number 0x%X did not match expected 0x%X") % + static_cast(file_magic) % static_cast(format_magic)) + .str()); + } + + uint8_t version = read_u8(); + switch (version) { + case Version1: + return read_file_V1(); + + default: + throw std::runtime_error( + (boost::format("Unsupported file version %u") % static_cast(version)).str()); + } +} + +PostTagCribFile PostTagCribFileReader::read_file_V1() { + PostTagCribFile file; + file.version = Version1; + file.checkpoint_count = read_u64(); + file.checkpoints = read_checkpoints(file.checkpoint_count); + + return file; +} + +std::vector PostTagCribFileReader::read_checkpoints(uint64_t checkpoint_count) { + std::vector checkpoints(checkpoint_count); + for (size_t i = 0; i < checkpoint_count; i++) { + checkpoints[i].headState = read_u8(); + checkpoints[i].tape = read_prefixed_bits(); + } + + return checkpoints; +} diff --git a/CLI/files/PostTagCribFile.hpp b/CLI/files/PostTagCribFile.hpp new file mode 100644 index 0000000..ce7f928 --- /dev/null +++ b/CLI/files/PostTagCribFile.hpp @@ -0,0 +1,30 @@ +#ifndef CLI_FILES_POSTTAGCRIBFILE_HPP_ +#define CLI_FILES_POSTTAGCRIBFILE_HPP_ + +#include +#include + +#include "PostTagFile.hpp" +#include "PostTagFileReader.hpp" +#include "TagState.hpp" + +struct PostTagCribFile { + PostTagFileVersion version; + + uint64_t checkpoint_count; + std::vector checkpoints; +}; + +class PostTagCribFileReader : public PostTagFileReader { + public: + using PostTagFileReader::PostTagFileReader; + + PostTagCribFile read_file(); + + private: + PostTagCribFile read_file_V1(); + + std::vector read_checkpoints(uint64_t checkpoint_count); +}; + +#endif // CLI_FILES_POSTTAGCRIBFILE_HPP_ diff --git a/CLI/files/PostTagFile.hpp b/CLI/files/PostTagFile.hpp new file mode 100644 index 0000000..bc1b0f1 --- /dev/null +++ b/CLI/files/PostTagFile.hpp @@ -0,0 +1,8 @@ +#ifndef CLI_FILES_POSTTAGFILE_HPP_ +#define CLI_FILES_POSTTAGFILE_HPP_ + +enum PostTagFileVersion : uint8_t { Version1 = 1 }; + +enum PostTagFileMagic : uint8_t { CribFileMagic = 'C', InitFileMagic = 'I', ResultFileMagic = 'R' }; + +#endif // CLI_FILES_POSTTAGFILE_HPP_ diff --git a/CLI/files/PostTagFileReader.cpp b/CLI/files/PostTagFileReader.cpp new file mode 100644 index 0000000..fb24b12 --- /dev/null +++ b/CLI/files/PostTagFileReader.cpp @@ -0,0 +1,74 @@ +#include "PostTagFileReader.hpp" + +uint8_t PostTagFileReader::read_u8() { + uint8_t byte = static_cast(get()); + + if (eof()) { + throw std::logic_error("Unexpected EOF"); + } else { + return byte; + } +} + +uint16_t PostTagFileReader::read_u16() { + uint16_t n = 0; + n |= (read_u8() << 0); + n |= (read_u8() << 8); + + return n; +} + +uint32_t PostTagFileReader::read_u32() { + uint32_t n = 0; + n |= (read_u16() << 0); + n |= (read_u16() << 16); + + return n; +} + +uint64_t PostTagFileReader::read_u64() { + uint64_t n = 0; + n |= (read_u32() << 0); + n |= (static_cast(read_u32()) << 32); + + return n; +} + +std::vector PostTagFileReader::read_bits(uint64_t bit_count) { + std::vector bits(bit_count, false); + + uint64_t full_bytes = bit_count / 8; + uint8_t remainder_bits = bit_count % 8; + + // process all the full bytes + for (size_t byte_index = 0; byte_index < full_bytes; byte_index++) { + uint8_t byte = read_u8(); + for (size_t bit = 0; bit < 8; bit++) { + bits[(byte_index * 8) + bit] = (byte >> (7 - bit)) & 1; + } + } + + // process the single partial byte at the end, if applicable + if (remainder_bits > 0) { + uint8_t last_byte = read_u8(); + for (size_t bit = 0; bit < remainder_bits; bit++) { + bits[(full_bytes * 8) + bit] = (last_byte >> (7 - bit)) & 1; + } + } + + return bits; +} + +std::vector PostTagFileReader::read_prefixed_bits() { return read_bits(read_u64()); } + +std::vector PostTagFileReader::read_bits_u64(uint8_t bit_count) { + std::vector bits(bit_count, false); + uint64_t bits_dec = read_u64(); + + for (int8_t bit = (bit_count - 1); bit >= 0; bit--) { + bits[bit] = bits_dec & 1; + bits_dec >>= 1; + } + + return bits; +} diff --git a/CLI/files/PostTagFileReader.hpp b/CLI/files/PostTagFileReader.hpp new file mode 100644 index 0000000..0f5d50f --- /dev/null +++ b/CLI/files/PostTagFileReader.hpp @@ -0,0 +1,25 @@ +#ifndef CLI_FILES_POSTTAGFILEREADER_HPP_ +#define CLI_FILES_POSTTAGFILEREADER_HPP_ + +#include +#include + +class PostTagFileReader : public std::ifstream { + public: + using std::ifstream::ifstream; + + uint8_t read_u8(); + uint16_t read_u16(); // all multi-byte values are stored in little-endian format + uint32_t read_u32(); + uint64_t read_u64(); + + std::vector read_bits(uint64_t bit_count); + + // bit sequence prefixed by a uint_64 indicating the # of bits + std::vector read_prefixed_bits(); + + // sequence of <= 64 bits written as a 64-bit decimal integer + std::vector read_bits_u64(uint8_t bit_count); +}; + +#endif // CLI_FILES_POSTTAGFILEREADER_HPP_ diff --git a/CLI/files/PostTagFileWriter.cpp b/CLI/files/PostTagFileWriter.cpp new file mode 100644 index 0000000..a933879 --- /dev/null +++ b/CLI/files/PostTagFileWriter.cpp @@ -0,0 +1,67 @@ +#include "PostTagFileWriter.hpp" + +void PostTagFileWriter::write_u8(uint8_t n) { + put(n); + + if (bad()) { + throw std::logic_error("Failed to write to file"); + } +} + +void PostTagFileWriter::write_u16(uint16_t n) { + write_u8(0xFF & (n >> 0)); + write_u8(0xFF & (n >> 8)); +} + +void PostTagFileWriter::write_u32(uint32_t n) { + write_u16(0xFFFF & (n >> 0)); + write_u16(0xFFFF & (n >> 16)); +} + +void PostTagFileWriter::write_u64(uint64_t n) { + write_u32(0xFFFFFFFF & (n >> 0)); + write_u32(0xFFFFFFFF & (n >> 32)); +} + +void PostTagFileWriter::write_bits(const std::vector& bits) { + uint64_t full_bytes = bits.size() / 8; + uint8_t remainder_bits = bits.size() % 8; + + // process all the full bytes + for (size_t byte_index = 0; byte_index < full_bytes; byte_index++) { + uint8_t byte = 0; + for (size_t bit = 0; bit < 8; bit++) { + byte |= bits[(byte_index * 8) + bit] << (7 - bit); + } + write_u8(byte); + } + + // process the single partial byte at the end, if applicable + if (remainder_bits > 0) { + uint8_t last_byte = 0; + for (size_t bit = 0; bit < remainder_bits; bit++) { + last_byte |= bits[(full_bytes * 8) + bit] << (7 - bit); + } + write_u8(last_byte); + } +} + +void PostTagFileWriter::write_prefixed_bits(const std::vector& bits) { + write_u64(bits.size()); + write_bits(bits); +} + +void PostTagFileWriter::write_bits_u64(const std::vector& bits) { + uint64_t bits_dec = 0; + + if (bits.size() > 64) { + throw std::logic_error("Too many bits passed to write_bits_u4"); + } + + for (uint8_t bit = 0; bit < bits.size(); bit++) { + bits_dec <<= 1; + bits_dec |= bits[bit] & 1; + } + + write_u64(bits_dec); +} diff --git a/CLI/files/PostTagFileWriter.hpp b/CLI/files/PostTagFileWriter.hpp new file mode 100644 index 0000000..22fbafd --- /dev/null +++ b/CLI/files/PostTagFileWriter.hpp @@ -0,0 +1,25 @@ +#ifndef CLI_FILES_POSTTAGFILEWRITER_HPP_ +#define CLI_FILES_POSTTAGFILEWRITER_HPP_ + +#include +#include + +class PostTagFileWriter : public std::ofstream { + public: + using std::ofstream::ofstream; + + void write_u8(uint8_t n); + void write_u16(uint16_t n); // all multi-byte values are stored in little-endian format + void write_u32(uint32_t n); + void write_u64(uint64_t n); + + void write_bits(const std::vector& bits); + + // bit sequence prefixed by a uint_64 indicating the # of bits + void write_prefixed_bits(const std::vector& bits); + + // sequence of <= 64 bits written as a 64-bit decimal integer + void write_bits_u64(const std::vector& bits); +}; + +#endif // CLI_FILES_POSTTAGFILEWRITER_HPP_ diff --git a/CLI/files/PostTagInitFile.cpp b/CLI/files/PostTagInitFile.cpp new file mode 100644 index 0000000..e1afccd --- /dev/null +++ b/CLI/files/PostTagInitFile.cpp @@ -0,0 +1,48 @@ +#include "PostTagInitFile.hpp" + +#include "boost/format.hpp" + +using PostTagSystem::TagState; + +PostTagInitFile PostTagInitFileReader::read_file() { + uint8_t file_magic = read_u8(); + PostTagFileMagic format_magic = InitFileMagic; + + if (file_magic != format_magic) { + throw std::runtime_error((boost::format("File magic number 0x%X did not match expected 0x%X") % + static_cast(file_magic) % static_cast(format_magic)) + .str()); + } + + uint8_t version = read_u8(); + switch (version) { + case Version1: + return read_file_V1(); + + default: + throw std::runtime_error( + (boost::format("Unsupported file version %u") % static_cast(version)).str()); + } +} + +PostTagInitFile PostTagInitFileReader::read_file_V1() { + PostTagInitFile file; + file.version = Version1; + file.state_count = read_u64(); + file.states = read_states(file.state_count); + + return file; +} + +std::vector PostTagInitFileReader::read_states(uint64_t state_count) { + std::vector states(state_count); + for (size_t i = 0; i < state_count; i++) { + uint8_t state_header = read_u8(); + states[i].headState = (state_header & 0b11000000) >> 6; + + uint8_t tape_length = (state_header & 0b00111111) + 1; + states[i].tape = read_bits(tape_length); + } + + return states; +} diff --git a/CLI/files/PostTagInitFile.hpp b/CLI/files/PostTagInitFile.hpp new file mode 100644 index 0000000..a59cfac --- /dev/null +++ b/CLI/files/PostTagInitFile.hpp @@ -0,0 +1,30 @@ +#ifndef CLI_FILES_POSTTAGINITFILE_HPP_ +#define CLI_FILES_POSTTAGINITFILE_HPP_ + +#include +#include + +#include "PostTagFile.hpp" +#include "PostTagFileReader.hpp" +#include "TagState.hpp" + +struct PostTagInitFile { + PostTagFileVersion version; + + uint64_t state_count; + std::vector states; +}; + +class PostTagInitFileReader : public PostTagFileReader { + public: + using PostTagFileReader::PostTagFileReader; + + PostTagInitFile read_file(); + + private: + PostTagInitFile read_file_V1(); + + std::vector read_states(uint64_t state_count); +}; + +#endif // CLI_FILES_POSTTAGINITFILE_HPP_ diff --git a/CLI/files/PostTagResultFile.cpp b/CLI/files/PostTagResultFile.cpp new file mode 100644 index 0000000..b1ed70e --- /dev/null +++ b/CLI/files/PostTagResultFile.cpp @@ -0,0 +1,66 @@ +#include "PostTagResultFile.hpp" + +#include "TagState.hpp" +#include "boost/format.hpp" + +using PostTagSystem::PostTagSearcher; + +void PostTagResultFileWriter::write_file(const PostTagResultFile& file) { + // write magic number and version + write_u8(PostTagFileMagic::ResultFileMagic); + write_u8(file.version); + + switch (file.version) { + case Version1: + return write_file_V1(file); + + default: + throw std::runtime_error( + (boost::format("Unsupported file version %u") % static_cast(file.version)).str()); + } +} + +void PostTagResultFileWriter::write_file_V1(const PostTagResultFile& file) { + write_u64(file.result_count); + + for (const PostTagSearcher::EvaluationResult& result : file.results) { + // include the final state if it's not too big and the reason isn't one where it's irrelevant + bool write_final_state = (result.finalTapeLength <= file.biggest_tape_to_write) && (result.finalTapeLength > 0); + switch (result.conclusionReason) { + case PostTagSearcher::ConclusionReason::InvalidInput: + case PostTagSearcher::ConclusionReason::NotEvaluated: + write_final_state = false; + break; + + default: + break; + } + + write_result(result, write_final_state); + } +} + +void PostTagResultFileWriter::write_result(const PostTagSearcher::EvaluationResult& result, bool write_final_state) { + // write initial state + uint8_t initial_state_header = 0; + initial_state_header |= (result.initialState.headState & 0b11) << 6; // bits 0-1: initial head state + initial_state_header |= (result.initialState.tape.size() - 1) & 0b111111; // bits 2-7: initial tape length minus 1 + write_u8(initial_state_header); + + write_bits(result.initialState.tape); + + uint8_t result_header = 0; + result_header |= (static_cast(result.conclusionReason) & 0b11111) << 3; // bits 0-4: conclusion reason + result_header |= (result.finalState.headState & 0b11) << 1; // bits 5-6: final head state + result_header |= (write_final_state & 1) << 0; // bit 7: whether or not final state follows + write_u8(result_header); + + write_u64(result.eventCount); + write_u64(result.maxTapeLength); + write_u64(result.finalTapeLength); + + if (write_final_state) { + // no need to write a prefix since we already wrote the final length + write_bits(result.finalState.tape); + } +} diff --git a/CLI/files/PostTagResultFile.hpp b/CLI/files/PostTagResultFile.hpp new file mode 100644 index 0000000..757f388 --- /dev/null +++ b/CLI/files/PostTagResultFile.hpp @@ -0,0 +1,38 @@ +#ifndef CLI_FILES_POSTTAGRESULTFILE_HPP_ +#define CLI_FILES_POSTTAGRESULTFILE_HPP_ + +#include +#include +#include + +#include "PostTagFile.hpp" +#include "PostTagFileWriter.hpp" +#include "PostTagSearcher.hpp" + +struct PostTagResultFile { + PostTagFileVersion version; + + uint64_t result_count; + const std::vector& results; + + uint64_t biggest_tape_to_write = std::numeric_limits::max(); + + PostTagResultFile(PostTagFileVersion v, const std::vector& r) + : version(v), results(r) { + result_count = results.size(); + } +}; + +class PostTagResultFileWriter : public PostTagFileWriter { + public: + using PostTagFileWriter::PostTagFileWriter; + + void write_file(const PostTagResultFile& file); + + private: + void write_file_V1(const PostTagResultFile& file); + + void write_result(const PostTagSystem::PostTagSearcher::EvaluationResult& result, bool write_final_state); +}; + +#endif // CLI_FILES_POSTTAGRESULTFILE_HPP_ diff --git a/CLI/main.cpp b/CLI/main.cpp new file mode 100644 index 0000000..5fc1764 --- /dev/null +++ b/CLI/main.cpp @@ -0,0 +1,170 @@ +#include +#include +#include + +#include "PostTagSearcher.hpp" +#include "arguments.hpp" +#include "boost/format.hpp" +#include "files/PostTagCribFile.hpp" +#include "files/PostTagInitFile.hpp" +#include "files/PostTagResultFile.hpp" + +using boost::program_options::variables_map; +using PostTagSystem::PostTagSearcher; + +PostTagSearcher::EvaluationParameters get_eval_parameters(const variables_map& args) { + PostTagSearcher::EvaluationParameters eval_params; + + auto max_size = args["maxsize"].as(); + if (max_size > 0) { + eval_params.maxTapeLength = max_size; + std::cout << boost::format("Maximum tape size: %.6g\n") % static_cast(max_size); + } else { + std::cout << "Maximum tape size: unlimited\n"; + } + + auto max_steps = args["maxsteps"].as(); + if (max_steps > 0) { + eval_params.maxEventCount = max_steps; + std::cout << boost::format("Maximum step count: %.6g\n") % static_cast(max_steps); + } else { + std::cout << "Maximum step count: unlimited\n"; + } + + auto timeout = args["timeout"].as(); + if (timeout > 0) { + eval_params.groupTimeConstraintNs = timeout * static_cast(1e9); // seconds to ns + std::cout << boost::format("Total evaluation time limit: %u seconds\n") % timeout; + } else { + std::cout << "Total evaluation time limit: unlimited\n"; + } + + if (args.count("cribfile")) { + auto crib_file_path = args["cribfile"].as(); + PostTagCribFileReader crib_file_reader(crib_file_path, std::ios::binary); + if (!crib_file_reader.is_open()) { + throw std::runtime_error("Failed to open crib file '" + crib_file_path + "' for reading"); + } + + PostTagCribFile crib_file = crib_file_reader.read_file(); + + eval_params.checkpoints = std::move(crib_file.checkpoints); + + std::cout << boost::format("Loaded %u checkpoint(s) from crib file '%s'\n") % crib_file.checkpoint_count % + crib_file_path; + } else { + std::cout << "No crib file specified; not loading checkpoints\n"; + } + + // TODO(jessef): check that outfile can be opened prior to running the evaluation + + return eval_params; +} + +PostTagInitFile get_initial_states_from_file(const variables_map& args) { + auto init_file_path = args["initfile"].as(); + PostTagInitFileReader init_file_reader(init_file_path, std::ios::binary); + if (!init_file_reader.is_open()) { + throw std::runtime_error("Failed to open init file '" + init_file_path + "' for reading"); + } + + PostTagInitFile init_file = init_file_reader.read_file(); + + std::cout << boost::format("Loaded %u initial conditions from file '%s'\n") % init_file.state_count % init_file_path; + + return init_file; +} + +int main(int argc, char** argv) { + variables_map args; + try { + args = parse_arguments(argc, argv); + } catch (const std::exception& err) { + std::cout << "Input error: " << err.what() << "\n"; + return 1; + } + + if (args["help"].as() || args["version"].as()) { + return 0; + } + + auto chaseMode = args["chase"].as(); + auto pounceMode = args["pounce"].as(); + if (!(chaseMode || pounceMode)) { + std::cout << "Input error: No valid mode specified\n"; + } + + PostTagSearcher searcher; + PostTagSearcher::EvaluationParameters eval_params; + + try { + eval_params = get_eval_parameters(args); + std::cout << "\n"; + } catch (const std::exception& err) { + std::cout << "Input error: " << err.what() << "\n"; + return 1; + } + + std::vector results; + + if (chaseMode) { + auto tape_length = static_cast(args["initsize"].as()); + auto start = args["initstart"].as(); + auto count = args["initcount"].as(); + auto offset = args["initoffset"].as(); + + // allows several jobs of the same size to be run + // at different offsets from the starting point + start += count * offset; + + std::cout << boost::format("Evaluating %u initial condition tapes, starting at %u...\n") % count % start; + std::cout << "----------------\n"; + + try { + results = searcher.evaluateRange(tape_length, start, start + count, eval_params); + } catch (const std::exception& err) { + std::cout << "Evaluation error: " << err.what() << "\n"; + return 1; + } + + } else if (pounceMode) { + PostTagInitFile init_file; + try { + init_file = get_initial_states_from_file(args); + } catch (const std::exception& err) { + std::cout << "Input error: " << err.what() << "\n"; + return 1; + } + + std::cout << boost::format("Evaluating %u initial condition tapes...\n") % init_file.state_count; + std::cout << "----------------\n"; + + try { + results = searcher.evaluateGroup(init_file.states, eval_params); + } catch (const std::exception& err) { + std::cout << "Evaluation error: " << err.what() << "\n"; + return 1; + } + } + + std::cout << boost::format("Evaluation finished with %u results\n") % results.size(); + + auto result_file_path = args["outfile"].as(); + + PostTagResultFileWriter result_file_writer(result_file_path, std::ios::binary); + if (!result_file_writer.is_open()) { + std::cout << boost::format("Output error: Failed to open output file '%s' for writing\n") % result_file_path; + return 1; + } + + PostTagResultFile result_file(Version1, results); + if (args.count("maxoutsize")) { + result_file.biggest_tape_to_write = args["maxoutsize"].as(); + } + + result_file_writer.write_file(result_file); + + std::cout << boost::format("Wrote results to '%s'\n") % result_file_path; + + return 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e695cb..0091000 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ project(PostTagSystem message(STATUS "${PROJECT_NAME} version: ${PROJECT_VERSION}") option(POST_TAG_SYSTEM_BUILD_TESTING "Enable cpp testing." OFF) +option(POST_TAG_SYSTEM_BUILD_CLI "Build command-line interface." OFF) include(GNUInstallDirs) # Define CMAKE_INSTALL_xxx: LIBDIR, INCLUDEDIR set(PostTagSystem_export_file "${PROJECT_BINARY_DIR}/PostTagSystemTargets.cmake") @@ -54,6 +55,7 @@ endif() message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") message(STATUS "POST_TAG_SYSTEM_BUILD_TESTING: ${POST_TAG_SYSTEM_BUILD_TESTING}") +message(STATUS "POST_TAG_SYSTEM_BUILD_CLI: ${POST_TAG_SYSTEM_BUILD_CLI}") message(STATUS "POST_TAG_SYSTEM_COMPILE_OPTIONS: ${POST_TAG_SYSTEM_COMPILE_OPTIONS}") set(libPostTagSystem_headers @@ -131,6 +133,10 @@ if(POST_TAG_SYSTEM_BUILD_TESTING) add_subdirectory(libPostTagSystem/test) endif() +if(POST_TAG_SYSTEM_BUILD_CLI) + add_subdirectory(CLI) +endif() + # INSTALL set(install_cmake_dir "${CMAKE_INSTALL_LIBDIR}/cmake/PostTagSystem") diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd0b30b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM alpine:3.12.1 as build + +RUN apk add --no-cache \ + bash boost-dev boost-static git g++ make cmake \ + clang py-pip shellcheck shfmt grep npm \ + && pip install cpplint \ + && npm install -g markdownlint-cli + +COPY . /working +WORKDIR /working + +# lint and build +RUN ./lint.sh \ + && mkdir build \ + && cd build \ + && cmake .. -DPOST_TAG_SYSTEM_BUILD_TESTING=ON \ + -DPOST_TAG_SYSTEM_BUILD_CLI=ON \ + -DPOST_TAG_SYSTEM_CLI_STATIC_BUILD=ON \ + -DPOST_TAG_SYSTEM_ENABLE_ALLWARNINGS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + && cmake --build . --config Release + +FROM alpine:3.12.1 + +COPY --from=build /working/build/CLI/wolfram-postproject /usr/bin/wolfram-postproject + +ENTRYPOINT ["/usr/bin/wolfram-postproject"] diff --git a/FileFormats/CribFile.wl b/FileFormats/CribFile.wl new file mode 100644 index 0000000..bbf4e97 --- /dev/null +++ b/FileFormats/CribFile.wl @@ -0,0 +1,96 @@ +BeginPackage["PostTagFileFormats`CribFile`"] + +Begin["`Private`"] + +Needs["PostTagFileFormats`Utility`"] + +$MagicNumber = "C" +$CurrentFormatVersion = 1 + +Export::"posttagcrib-invalid" = "The supplied crib data is invalid." +Export::"posttagcrib-unsupportedversion" = "Crib file version `1` is not supported." + +(* ::Section:: *) +(* Write a crib file to disk by opening a stream *) + +writeCribFile[path_String, data_List, ___] := Module[{stream, result}, + stream = OpenWrite[path, BinaryFormat -> True]; + result = writeCribFile[stream, data]; + Close[stream]; + Return[result] +] + +(* ::Section:: *) +(* Write a crib file to a stream and delegate to the appropriate version handler *) + +writeCribFile[ + stream_OutputStream, + cribs:{Rule[{__Integer}, _Integer] ..}, + ___ +] := Block[{$ByteOrdering = -1}, Module[{ + version = $CurrentFormatVersion +}, + BinaryWrite[stream, $MagicNumber, "Character8"]; + BinaryWrite[stream, version, "UnsignedInteger8"]; + + Return@writeCribFileBody[version, stream, cribs]; +]] + +writeCribFile[ + stream_OutputStream, + data_, + ___ +] := ( + Message[Export::"posttagcrib-invalid"]; + Return[$Failed] +) + +(* ::Section:: *) +(* Unknown file version *) + +writeCribFileBody[version_, ___] := ( + Message[Export::"posttagcrib-unsupportedversion", version]; + Return[$Failed] +) + +(* ::Section:: *) +(* File version 1 *) + +(* ::Subsection:: *) +(* Write a crib file (starting after magic and version) *) + +writeCribFileBody[version: 1, stream_OutputStream, cribs_List] := ( + BinaryWrite[stream, Length[cribs], "UnsignedInteger64"]; + Scan[ + writeCribFileEntry[version, stream, #] &, + cribs + ] +) + +(* ::Subsection:: *) +(* Write a single crib entry *) + +writeCribFileEntry[ + version: 1, + stream_OutputStream, + Rule[tape_List, headState_Integer] +] := ( + BinaryWrite[stream, headState, "UnsignedInteger8"]; + BinaryWrite[stream, Length[tape], "UnsignedInteger64"]; + + PostTagFileFormats`Utility`writePackedBits[stream, tape]; +) + +(* ::Section:: *) +(* Register export converter *) + +ImportExport`RegisterExport[ + "PostTagCrib", + writeCribFile, + "FunctionChannels" -> {"Streams"}, + "BinaryFormat" -> True +] + +End[] + +EndPackage[] diff --git a/FileFormats/InitFile.wl b/FileFormats/InitFile.wl new file mode 100644 index 0000000..3508761 --- /dev/null +++ b/FileFormats/InitFile.wl @@ -0,0 +1,105 @@ +BeginPackage["PostTagFileFormats`InitFile`"] + +Begin["`Private`"] + +Needs["PostTagFileFormats`Utility`"] + +$MagicNumber = "I" +$CurrentFormatVersion = 1 + +Export::"posttaginit-invalid" = "The supplied init data is invalid." +Export::"posttaginit-unsupportedversion" = "Init file version `1` is not supported." + +(* ::Section:: *) +(* Write an init file to disk by opening a stream *) + +writeInitFile[path_String, data_List, ___] := Module[{stream, result}, + stream = OpenWrite[path, BinaryFormat -> True]; + result = writeInitFile[stream, data]; + Close[stream]; + Return[result] +] + +(* ::Section:: *) +(* Write an init file to a stream and delegate to the appropriate version handler *) + +writeInitFile[ + stream_OutputStream, + inits:{Repeated@Rule[ + {__Integer}?(Length[#] <= 64 &), + _Integer + ]}, + ___ +] := Block[{$ByteOrdering = -1}, Module[{ + version = $CurrentFormatVersion +}, + BinaryWrite[stream, $MagicNumber, "Character8"]; + BinaryWrite[stream, version, "UnsignedInteger8"]; + + Return@writeInitFileBody[version, stream, inits]; +]] + +writeInitFile[ + stream_OutputStream, + data_, + ___ +] := ( + Message[Export::"posttaginit-invalid"]; + Return[$Failed] +) + +(* ::Section:: *) +(* Unknown file version *) + +writeInitFileBody[version_, ___] := ( + Message[Export::"posttaginit-unsupportedversion", version]; + Return[$Failed] +) + +(* ::Section:: *) +(* File version 1 *) + +(* ::Subsection:: *) +(* Write an init file (starting after magic and version) *) + +writeInitFileBody[version: 1, stream_OutputStream, inits_List] := ( + BinaryWrite[stream, Length[inits], "UnsignedInteger64"]; + Scan[ + writeInitFileEntry[version, stream, #] &, + inits + ] +) + +(* ::Subsection:: *) +(* Write a single init entry *) + +writeInitFileEntry[ + version: 1, + stream_OutputStream, + Rule[tape_List, headState_Integer] +] := ( + BinaryWrite[ + stream, + BitOr[ + BitShiftLeft[BitAnd[headState, 2^^00000011], 6], + BitAnd[Length[tape] - 1, 2^^00111111] + ], + "UnsignedInteger8" + ]; + + PostTagFileFormats`Utility`writePackedBits[stream, tape]; +) + +(* ::Section:: *) +(* Register export converter *) + +ImportExport`RegisterExport[ + "PostTagInit", + writeInitFile, + "FunctionChannels" -> {"Streams"}, + "BinaryFormat" -> True +] + +End[] + +EndPackage[] diff --git a/FileFormats/PostTagFileFormats.wl b/FileFormats/PostTagFileFormats.wl new file mode 100644 index 0000000..cacb4e9 --- /dev/null +++ b/FileFormats/PostTagFileFormats.wl @@ -0,0 +1,11 @@ +BeginPackage["PostTagFileFormats`"] +Begin["`Private`"] + +Get["Utility`"] + +Get["CribFile`"] +Get["InitFile`"] +Get["ResultFile`"] + +End[] +EndPackage[] diff --git a/FileFormats/README.md b/FileFormats/README.md new file mode 100644 index 0000000..06cf77f --- /dev/null +++ b/FileFormats/README.md @@ -0,0 +1,149 @@ +# File formats + +- Diagrams generated with http://www.luismg.com/protocol/ +- All integers stored in little-endian order + +## Crib file (list of known sequence checkpoints for "chase" evaluation) + +### Version 1 + +- Magic number: `0x43` (`'C'`) +- Version: `0x01` +- Checkpoint count: `u64` + - Number of checkpoint sequences stored in the file +- Checkpoint sequence (repeating) + - Head state: `u8` + - Bit count: `u64` + - Number of bits to follow + - Bits: packed bit array, padded at the end with zeroes to the nearest 8-bit boundary + +Diagram shows a sequence of 19 bits followed by one of 16 bits. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Magic ['C'] | Version [0x01]| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +| Checkpoint count | ++ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | Head state | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +| Bit count | ++ +-+-+-+-+-+-+-+-+ +| | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Bits | Padding | Head state | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +| Bit count | ++ +-+-+-+-+-+-+-+-+ +| | Bits | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++-+-+-+-+-+-+-+-+ +``` + +`Magic ['C']:8,Version [0x01]:8,Checkpoint count:64,Head state:8,Bit count:64,Bits:19,Padding:5,Head state:8,Bit count:64,Bits:16` + + +## Init file (list of initial states for "pounce" evaluation) + +### Version 1 + +- Magic number: `0x49` (`'I'`) +- Version: `0x01` +- State count: `u64` + - Number of initial states stored in the file +- Initial state (repeating) + - State header: `u8` + - Bits 0-1: Head state as unsigned 2-bit integer (0 to 3) + - Bits 2-7: Bit count (number of significant bits in following `u64`) + - Unsigned 6-bit integer; zero-indexed (`0` means 1 bit; `63` means 64 bits) + - Bits: packed bit array of up to 64 bits, padded at the end with zeroes to the nearest 8-bit boundary + - Number of significant bits indicated by bits 2-7 of the state header + +Diagram shows a single initial state of 25 bits. + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Magic ['I'] | Version [0x01]| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +| State count | ++ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | HS| Bit count | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Bits | Padding | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +`Magic ['I']:8,Version [0x01]:8,State count:64,HS:2,Bit count:6,Bits:25,Padding:7` + + +## Result file (list of initial states and corresponding evaluation results) + +### Version 1 + +- Magic number: `0x52` (`'R'`) +- Version: `0x01` +- Result count: `u64` + - Number of results stored in the file +- Result (repeating) + - Initial state header: `u8` + - Bits 0-1: Head state as unsigned 2-bit integer (0 to 3) + - Bits 2-7: Bit count (number of significant bits in following `u64`) + - Unsigned 6-bit integer; zero-indexed (`0` means 1 bit; `63` means 64 bits) + - Initial state bits: packed bit array of up to 64 bits, padded at the end with zeroes to the nearest 8-bit boundary + - Number of significant bits indicated by bits 2-7 of the initial state header + - Result header: `u8` + - Bits 0-4: Conclusion reason as unsigned 5-bit integer (0 to 31) + - `0`: Shouldn't happen (uninitialized enum?) + - `1`: Invalid input + - `2`: Evaluation terminated + - `3`: Evaluation reached cycle + - `4`: Evaluation reached known checkpoint + - `5`: Maximum tape length exceeded + - `6`: Maximum event count exceeded + - `7`: Time constraint exceeded + - `8`: Not evaluated (previous evaluation hit time constraint) + - Bits 5-6: Final head state as unsigned 2-bit integer (0 to 3) + - Bit 7: Whether final state is present + - Event count: `u64` + - Max. intermediate tape size reached: `u64` + - Final tape size: `u64` + - Final state bits: packed bit array, padded at the end with zeroes to the nearest 8-bit boundary + - Present only if indicated by bit 7 of the result header + - Number of significant bits indicated by the final tape length field + +Diagram shows a single result with a 23-bit initial state and a 32-bit final state + +``` + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Magic ['R'] | Version [0x01]| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +| Result count | ++ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | HS| Bit count | | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Initial state bits | Padding | Reason | HS|F| ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++ Event count + +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++ Max. intermediate size + +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| | ++ Final size + +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Final state bits | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +`Magic ['R']:8,Version [0x01]:8,Result count:64,HS:2,Bit count:6,Initial state bits:23,Padding:9,Reason:5,HS:2,F:1,Event count:64,Max. intermediate size:64,Final size:64,Final state bits:32` diff --git a/FileFormats/ResultFile.wl b/FileFormats/ResultFile.wl new file mode 100644 index 0000000..e205f6c --- /dev/null +++ b/FileFormats/ResultFile.wl @@ -0,0 +1,142 @@ +BeginPackage["PostTagFileFormats`ResultFile`"] + +Begin["`Private`"] + +Needs["PostTagFileFormats`Utility`"] + +$MagicNumber = "R" + +$ConclusionReasons = <| + 0 -> "FileFormatError", + 1 -> "InvalidInput", + 2 -> "Terminated", + 3 -> "ReachedCycle", + 4 -> "ReachedKnownCheckpoint", + 5 -> "MaxTapeLengthExceeded", + 6 -> "MaxEventCountExceeded", + 7 -> "TimeConstraintExceeded", + 8 -> "NotEvaluated" +|> + +(* ::Section:: *) +(* Read a result file from disk by opening a stream *) + +readResultFile[path_String, ___] := Module[{stream, result}, + stream = OpenRead[path, BinaryFormat -> True]; + result = readResultFile[stream]; + Close[stream]; + Return[result] +] + +(* ::Section:: *) +(* Read a result file from a stream and delegate to the appropriate version handler *) + +readResultFile[stream_InputStream, ___] := Block[{$ByteOrdering = -1}, Module[{ + magic, version +}, + magic = BinaryRead[stream, "Character8"]; + If[magic =!= $MagicNumber, + Return@Failure["BadMagic", <| + "MessageTemplate" -> "Invalid magic number encountered", + "MagicNumber" -> magic + |>] + ]; + + version = BinaryRead[stream, "UnsignedInteger8"]; + + Return@readResultFileBody[version, stream]; +]] + +(* ::Section:: *) +(* Unknown file version *) + +readResultFileBody[version_, ___] := Failure["UnsupportedVersion", <| + "MessageTemplate" -> "File version `1` is not supported", + "MessageParameters" -> {version} +|>] + +(* ::Section:: *) +(* File version 1 *) + +(* ::Subsection:: *) +(* Read a result file (starting after magic and version) *) + +readResultFileBody[version: 1, stream_InputStream] := Module[{resultCount}, + resultCount = BinaryRead[stream, "UnsignedInteger64"]; + Return@Table[ + readResultFileResult[version, stream], + resultCount + ] +] + +(* ::Subsection:: *) +(* Read a single result entry *) + +readResultFileResult[version: 1, stream_InputStream] := Module[{ + result = <||>, + initialState = <||>, + finalState = <||>, + + initialStateHeaderByte, + + resultHeaderByte, + conclusionReasonNumber, + finalStateTapePresent +}, + initialStateHeaderByte = BinaryRead[stream, "UnsignedInteger8"]; + + initialState["HeadState"] = BitShiftRight[BitAnd[initialStateHeaderByte, 2^^11000000], 6]; + initialState["TapeSize"] = 1 + BitAnd[initialStateHeaderByte, 2^^00111111]; + initialState["Tape"] = PostTagFileFormats`Utility`readPackedBits[ + stream, + initialState["TapeSize"] + ]; + result["InitialState"] = initialState; + + { + resultHeaderByte, + result["EventCount"], + result["MaxTapeSize"], + finalState["TapeSize"] + } = BinaryRead[stream, { + "UnsignedInteger8", + "UnsignedInteger64", + "UnsignedInteger64", + "UnsignedInteger64" + }]; + + conclusionReasonNumber = BitShiftRight[BitAnd[resultHeaderByte, 2^^11111000], 3]; + result["Reason"] = Lookup[ + $ConclusionReasons, + conclusionReasonNumber, + Missing["Unknown", conclusionReasonNumber] + ]; + + finalState["HeadState"] = BitShiftRight[BitAnd[resultHeaderByte, 2^^00000110], 1]; + finalStateTapePresent = BitAnd[resultHeaderByte, 2^^00000001] === 1; + + If[finalStateTapePresent, + finalState["Tape"] = PostTagFileFormats`Utility`readPackedBits[ + stream, + finalState["TapeSize"] + ] + ]; + + result["FinalState"] = finalState; + + Return[result] +] + +(* ::Section:: *) +(* Register import converter *) + +ImportExport`RegisterImport[ + "PostTagResult", + readResultFile, + "FunctionChannels" -> {"Streams"}, + "BinaryFormat" -> True +] + +End[] + +EndPackage[] diff --git a/FileFormats/SampleFiles/crib1.postcrib b/FileFormats/SampleFiles/crib1.postcrib new file mode 100644 index 0000000..46c854e Binary files /dev/null and b/FileFormats/SampleFiles/crib1.postcrib differ diff --git a/FileFormats/SampleFiles/init1.postinit b/FileFormats/SampleFiles/init1.postinit new file mode 100644 index 0000000..d5d162b Binary files /dev/null and b/FileFormats/SampleFiles/init1.postinit differ diff --git a/FileFormats/SampleFiles/result1.postresult b/FileFormats/SampleFiles/result1.postresult new file mode 100644 index 0000000..fc48b67 Binary files /dev/null and b/FileFormats/SampleFiles/result1.postresult differ diff --git a/FileFormats/SampleFiles/result2.postresult b/FileFormats/SampleFiles/result2.postresult new file mode 100644 index 0000000..051f8cd Binary files /dev/null and b/FileFormats/SampleFiles/result2.postresult differ diff --git a/FileFormats/Utility.wl b/FileFormats/Utility.wl new file mode 100644 index 0000000..01ce1ed --- /dev/null +++ b/FileFormats/Utility.wl @@ -0,0 +1,31 @@ +BeginPackage["PostTagFileFormats`Utility`"] + +PostTagFileFormats`Utility`readPackedBits +PostTagFileFormats`Utility`writePackedBits + +Begin["`Private`"] + +readPackedBits[ + stream_InputStream, + bitCount_Integer +] := Take[ + Flatten@IntegerDigits[ + BinaryReadList[stream, "UnsignedInteger8", Ceiling[bitCount / 8]], + 2, + 8 + ], + bitCount +] + +writePackedBits[ + stream_OutputStream, + bits_List +] := BinaryWrite[ + stream, + FromDigits[#, 2] & /@ Partition[bits, 8, 8, 1, 0], + "UnsignedInteger8" +] + +End[] + +EndPackage[] diff --git a/libPostTagSystem/PostTagSearcher.hpp b/libPostTagSystem/PostTagSearcher.hpp index da6891b..b59d0d6 100644 --- a/libPostTagSystem/PostTagSearcher.hpp +++ b/libPostTagSystem/PostTagSearcher.hpp @@ -12,16 +12,19 @@ class PostTagSearcher { public: PostTagSearcher(); - enum class ConclusionReason { - InvalidInput, - Terminated, - ReachedCycle, - ReachedKnownCheckpoint, - MaxTapeLengthExceeded, - MaxEventCountExceeded, - TimeConstraintExceeded, - NotEvaluated, - MergedWithAnotherInit + enum class ConclusionReason : uint8_t { + InvalidInput = 1, + Terminated = 2, + ReachedCycle = 3, + ReachedKnownCheckpoint = 4, + MaxTapeLengthExceeded = 5, + MaxEventCountExceeded = 6, + TimeConstraintExceeded = 7, + NotEvaluated = 8, + MergedWithAnotherInit = 9, + + // result file spec requires the reason to fit in 5 bits + MAX_DO_NOT_EXCEED = 31 }; struct EvaluationResult { diff --git a/lint.sh b/lint.sh index 57b2657..189cd07 100755 --- a/lint.sh +++ b/lint.sh @@ -13,6 +13,11 @@ lsfilesOptions=( ':(exclude)Dependencies/*' ':(exclude)libPostTagSystem/WolframHeaders/*' ':(exclude)*.xcodeproj/*' # Xcode manages these automatically + + # data files + ':(exclude)*.postcrib' + ':(exclude)*.postinit' + ':(exclude)*.postresult' ) mapfile -t filesToLint < <(LC_ALL=C comm -13 <(git ls-files --deleted) <(git ls-files "${lsfilesOptions[@]}"))