From d938c5985d73731fc12ca726892f7bc0fbc6b799 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 10:16:18 +0100 Subject: [PATCH 01/31] Add applicative parser --- src/base/CMakeLists.txt | 1 + src/base/applicative.cpp | 105 ++++++++++ src/base/applicative.h | 431 +++++++++++++++++++++++++++++++++++++++ tests/io/CMakeLists.txt | 1 + tests/io/applicative.cpp | 86 ++++++++ 5 files changed, 624 insertions(+) create mode 100644 src/base/applicative.cpp create mode 100644 src/base/applicative.h create mode 100644 tests/io/applicative.cpp diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index e0e1b89078..a43df93528 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -1,5 +1,6 @@ add_library( base + applicative.cpp cbor.cpp enumOptionsBase.cpp geometry.cpp diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp new file mode 100644 index 0000000000..c3f722ad0c --- /dev/null +++ b/src/base/applicative.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "applicative.h" +#include +#include +#include + +namespace parsers +{ + +// A parser that expects an exact string +Parser literal(std::string_view constant) { return Parser(constant); } + +Parser takeWhile(std::function f) +{ + Parser result( + [f](const auto input) -> parser_output + { + if (input.empty()) + return {}; + auto it = std::find_if(input.begin(), input.end(), [f](const auto x) { return !f(x); }); + if (it == input.begin()) + return {}; + return {{std::string_view(input.begin(), it), std::string_view(it, input.end())}}; + }); + return result; +} + +// A parser that accepts an integer greater than or equal to zero +Parser natural() +{ + auto result = digits(); + return result.map( + [](const auto terms) -> int + { + int total = 0; + for (auto term : terms) + total = 10 * total + (term - '0'); + return total; + }); +} + +// Take an optional minus sign and a whole number to create an integer +int nat2int(std::optional minus, int number) +{ + if (minus) + return -number; + return number; +} + +// A parser that accepts an integer +Parser integer() { return (maybe("-"_p) & natural()).apply(nat2int); } + +// Take the numbers before the decimal, some optional digits after the period, and an optional exponent, and return a double. +double nat2dbl(int before, std::optional after, std::optional exponent) +{ + double result = before; + if (after) + { + double magnitude = 1; + double total = 0; + for (auto digit : *after) + { + total = total * 10 + (digit - '0'); + magnitude *= 10; + } + if (result < 0) + total *= -1; + result += total / magnitude; + } + if (exponent) + { + result *= pow(10, *exponent); + } + return result; +} + +// A parser that accepts a real, floating point number +Parser real() +{ + auto result = integer() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> integer()); + return result.apply(nat2dbl); +} + +// A parser that accepts and amount of whitespace +Parser spaces() { return takeWhile(std::isspace); } +// A parse that accepts any amount of visible characters +Parser graphs() { return takeWhile(std::isgraph); } +// A parse that accepts any amount of alphanumeric characters +Parser alphanums() { return takeWhile(std::isalnum); } +// A parse that accepts any amount of letters +Parser alphas() { return takeWhile(std::isalpha); } +// A parse that accepts any amount of upper case letters +Parser uppers() { return takeWhile(std::isupper); } +// A parse that accepts any amount of lower case letters +Parser lowers() { return takeWhile(std::islower); } +// A parse that accepts any amount of punctuation case letters +Parser punctuations() { return takeWhile(std::ispunct); } +// A parser that accepts any amount of digit characters +Parser digits() { return takeWhile(std::isdigit); } + +// A quick wrapper for easily making parses from strings +Parser operator""_p(const char *text, size_t size) { return literal(std::string_view(text, size)); } +} // namespace parsers diff --git a/src/base/applicative.h b/src/base/applicative.h new file mode 100644 index 0000000000..6e2f966c4b --- /dev/null +++ b/src/base/applicative.h @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace parsers +{ + +// A concept to check if a type is a tuple. This is built in in +// C++23, be we can do this for now. +template +concept TupleLike = requires { std::tuple_size::value; }; + +// The simplest defintion of an applicative parser is a function that +// takes a string_view and, if the parse succeeds, returns the parsed +// value and the rest of the string_view. To make life simpler, we +// define the parser_output for the return type of the function. +// This *could* have further implications because there are more +// complicated parsers we could create. +template using parser_output = std::optional>; + +// This concept type checks that a given lambda matches the definition +// given in the paragraph above. +template +concept ApParse = requires(Lambda lam, std::string_view input) { + { lam(input) } -> std::convertible_to>; +}; + +// It's fully possible to just use the functions as the parsers, but, if we wrap them un a struct, we can use operator +// overloading to more easily combine smaller parsers into larger parsers. +template class Parser +{ + + public: + // Wrap a parser lambda in a structure + template + requires ApParse + Parser(Lambda lambda) : lambda_(lambda) + { + } + + // Create a parser that matches and exact string + template ::value>> + Parser(std::string_view constant) + : lambda_( + [constant](const std::string_view input) -> parser_output + { + if (input.starts_with(constant)) + return {{constant, input.substr(constant.size())}}; + else + return {}; + }) + { + } + + private: + // The actual function that we have wrapped. We need to wrap this + // in a std::function instead of using a lambda so that we don't + // have to handle the exact type of the bound lambda + std::function(std::string_view)> lambda_; + + public: + // Parse a string and, if possible, return the value and the remainder + parser_output parse(std::string_view input) const { return lambda_(input); }; + + // Create a new parser that takes the output of the old parser and + // passes it through a function + template auto map(Lambda f) -> Parser()))> + { + auto &method = lambda_; + Parser()))> result( + [method, f](const std::string_view input) -> parser_output()))> + { + auto first = method(input); + if (first) + { + auto &[body, remainder] = *first; + return {{f(body), remainder}}; + } + else + return {}; + }); + return result; + } + + // Same as map, but unpacks the tuple and passes the individual + // parts as arguments. This greatly simplifies creating mapping + // functions + template + requires(TupleLike) + auto apply(Lambda f) -> Parser()))> + { + auto &method = lambda_; + Parser()))> result( + [method, f](const std::string_view input) -> parser_output()))> + { + auto first = method(input); + if (first) + { + auto &[body, remainder] = *first; + return {{std::apply(f, body), remainder}}; + } + else + return {}; + }); + return result; + } + + // Insist that this parse is followed by another parse, but we + // ignore the output of that other parser + template Parser operator<<(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](const std::string_view input) -> parser_output + { + auto first = method(input); + if (first) + { + auto &[body, middle] = *first; + auto second = other.parse(middle); + if (second) + { + return {{body, std::get<1>(*second)}}; + } + else + { + return {}; + } + } + else + { + return {}; + } + }); + return result; + } + + // Confirm that this parser passes, but ignore its output and return the value of a subsequent parser + template Parser operator>>(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](const std::string_view input) -> parser_output + { + auto first = method(input); + if (first) + { + auto &[body, middle] = *first; + return other.parse(middle); + } + else + { + return {}; + } + }); + return result; + } + + // If this parser fails, try an alternate parser instead of + // immediately failing. + Parser operator|(Parser other) + { + auto &method = lambda_; + Parser result( + [method, other](const std::string_view input) -> parser_output + { + auto first = method(input); + return first ? first : other.parse(input); + }); + return result; + } + + // If this parser fails, try an alternate literal string parser + // instead of immediately failing. + template ::value>> + Parser operator|(std::string_view other) + { + return Parser(other) | *this; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(!TupleLike && !TupleLike) + auto operator&(Parser other) -> Parser> + { + auto &method = lambda_; + Parser> result( + [method, other](const std::string_view input) -> parser_output> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other.parse(middle); + if (second) + { + auto &[snd, final] = *second; + return {{{fst, snd}, final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(TupleLike && !TupleLike) + auto operator&(Parser other) -> Parser(), std::make_tuple(std::declval())))> + { + auto &method = lambda_; + Parser(), std::make_tuple(std::declval())))> result( + [method, other](const std::string_view input) + -> parser_output(), std::make_tuple(std::declval())))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other.parse(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(fst, std::make_tuple(snd)), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(!TupleLike && TupleLike) + auto operator&(Parser other) -> Parser()), std::declval()))> + { + auto &method = lambda_; + Parser()), std::declval()))> result( + [method, other](const std::string_view input) + -> parser_output()), std::declval()))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other.parse(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(std::make_tuple(fst), snd), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } + + // After confirming that this parser passes, apply a second parser + // on the remainder and collect both values. + template + requires(TupleLike && TupleLike) + auto operator&(Parser other) -> Parser(), std::declval()))> + { + auto &method = lambda_; + Parser(), std::declval()))> result( + [method, other]( + const std::string_view input) -> parser_output(), std::declval()))> + { + auto first = method(input); + if (first) + { + auto &[fst, middle] = *first; + auto second = other.parse(middle); + if (second) + { + auto &[snd, final] = *second; + return {{std::tuple_cat(fst, snd), final}}; + } + else + return {}; + } + else + return {}; + }); + return result; + } +}; + +// Create a parser that always succeeds and returns a constant value +template Parser pure(T constant) +{ + Parser result([constant](const std::string_view input) -> parser_output { return {{constant, input}}; }); + return result; +} + +// A parser that always fails +template +Parser null([](const auto x) -> parser_output { return {}; }); + +// Parser literal(std::string_view constant); + +// Modify a parser so that the parsed value is wrapped in a +// std::optional. If the parser would have failed, act as though +// the parse succeeded, but make the std::optional empty. +template Parser> maybe(Parser inner) +{ + Parser> result( + [inner](const std::string_view input) -> parser_output> + { + auto first = inner.parse(input); + if (first) + { + auto &[body, remainder] = *first; + return {{{body}, remainder}}; + } + else + { + std::optional empty; + return {{empty, input}}; + } + }); + return result; +} + +// Insist that a parser passes at least once, but collect as many +// parsed values as possible. +template Parser> some(Parser inner) +{ + Parser> result( + [inner](const std::string_view input) -> parser_output> + { + std::vector collection; + auto ctx = input; + while (!ctx.empty()) + { + auto trial = inner.parse(ctx); + if (trial) + { + auto &[body, remainder] = *trial; + collection.push_back(body); + ctx = remainder; + } + else + break; + } + + if (collection.empty()) + return {}; + return {{collection, ctx}}; + }); + return result; +} + +// A parser that expects an exact string +Parser literal(std::string_view constant); +// A parser that accepts any amount of digit characters +Parser digits(); + +// A parser that accepts an integer greater than or equal to zero +Parser natural(); + +// A parser that accepts an integer +Parser integer(); + +// A parser that accepts a real, floating point number +Parser real(); + +// A parser that accepts and amount of whitespace +Parser spaces(); + +// A parse that accepts any amount of visible characters +Parser graphs(); +// A parse that accepts any amount of alphanumeric characters +Parser alphanums(); +// A parse that accepts any amount of letters +Parser alphas(); +// A parse that accepts any amount of upper case letters +Parser uppers(); +// A parse that accepts any amount of lower case letters +Parser lowers(); +// A parse that accepts any amount of punctuation case letters +Parser punctuations(); + +// A quick wrapper for easily making parses from strings +Parser operator""_p(const char *text, size_t size); + +// A quick overload to easily ignore strings around a parser +template Parser operator<<(Parser body, std::string_view other) +{ + return body << Parser(other); +} + +// A quick overload to easily ignore strings around a parser +template Parser operator>>(std::string_view other, Parser body) +{ + return Parser(other) >> body; +} + +// A quick overload to easily require strings around a parser +template auto operator&(Parser self, std::string_view other) -> decltype(self & Parser(other)) +{ + return self & Parser(other); +} + +// A quick overload to easily require strings around a parser +template auto operator&(std::string_view other, Parser self) -> decltype(Parser(other) & self) +{ + return Parser(other) & self; +} + +}; // namespace parsers diff --git a/tests/io/CMakeLists.txt b/tests/io/CMakeLists.txt index 6ab8d5340d..299d86ea9b 100644 --- a/tests/io/CMakeLists.txt +++ b/tests/io/CMakeLists.txt @@ -1,3 +1,4 @@ +dissolve_add_test(SRC applicative.cpp) dissolve_add_test(SRC cbor.cpp) dissolve_add_test(SRC intraParameterParse.cpp) dissolve_add_test(SRC version.cpp) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp new file mode 100644 index 0000000000..f9c5c3dbfd --- /dev/null +++ b/tests/io/applicative.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "base/applicative.h" +#include + +namespace UnitTest +{ + +using namespace parsers; + +template void test_parser(std::string_view input, Parser parser, parser_output expected) +{ + auto result = parser.parse(input); + ASSERT_EQ(result, expected); +} +TEST(ApplicativeTest, BasicStrings) +{ + test_parser("Foo", "Fo"_p, {{"Fo", "o"}}); + test_parser("Foobar", "Foo"_p, {{"Foo", "bar"}}); +} +TEST(ApplicativeTest, Ignoring) +{ + test_parser("Foobar", "Foo"_p << "bar"_p, {{"Foo", ""}}); + test_parser("Foobar", "Foo" >> "bar"_p, {{"bar", ""}}); +} +TEST(ApplicativeTest, Joining) +{ + test_parser("Foobar", "Foo"_p & "bar", {{{"Foo", "bar"}, ""}}); + test_parser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{{1, 2, 3, 4}, "Foobar"}}); + test_parser("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {{{"Fo", "ob", "a", "r"}, ""}}); + test_parser("Foobar", ("Fo"_p & "ob") & "ar", {{{"Fo", "ob", "ar"}, ""}}); + test_parser("Foobar", "Fo" & ("ob"_p & "ar"), {{{"Fo", "ob", "ar"}, ""}}); + test_parser("Foobar", (pure(1) & pure(2)) & pure(3), {{{1, 2, 3}, "Foobar"}}); + test_parser("Foobar", pure(1) & (pure(2) & pure(3)), {{{1, 2, 3}, "Foobar"}}); +} + +TEST(ApplicativeTest, Choices) +{ + test_parser("Foo", "Foo"_p | "Bar", {{"Foo", ""}}); + test_parser("Bar", "Foo"_p | "Bar", {{"Bar", ""}}); + test_parser("Quux", "Foo"_p | "Bar", {}); +} + +TEST(ApplicativeTest, NaturalNumbers) +{ + test_parser("123foo", natural(), {{123, "foo"}}); + auto triplet = natural() & "," >> natural() & "," >> natural(); + test_parser("123,456,789", triplet, {{{123, 456, 789}, ""}}); + auto vecsum = triplet.apply([](const auto x, const auto y, const auto z) -> int { return x + y + z; }); + test_parser("123,456,789", vecsum, {{123 + 456 + 789, ""}}); +} + +TEST(ApplicativeTest, Optionals) +{ + test_parser("123,456", maybe(natural() << ",") & natural(), {{{{123}, 456}, ""}}); + test_parser("456", maybe(natural() << ",") & natural(), {{{std::nullopt, 456}, ""}}); + test_parser("-456", integer(), {{-456, ""}}); + test_parser("456", integer(), {{456, ""}}); +} + +TEST(ApplicativeTest, MultipleTerms) +{ + // Parse multiple terms + test_parser("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {{{123, 456, 789, 12}, ""}}); + // Completely fail the parse if no copies are present + test_parser("789", some(integer() << ","), {}); +} + +TEST(ApplicativeTest, RealNumbers) +{ + test_parser("-12.0543", real(), {{-12.0543, ""}}); + test_parser("1.02E-3", real(), {{1.02e-3, ""}}); + test_parser("-71.2e3", real(), {{-71.2e3, ""}}); +} + +TEST(ApplicativeTest, BasicParser) +{ + test_parser(" \t foo", spaces(), {{" \t ", "foo"}}); + test_parser("1qaz.QAZ foo", graphs(), {{"1qaz.QAZ", " foo"}}); + test_parser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), + {{{"1", "qaz", ".", "QAZ", " "}, "foo"}}); + + test_parser("\"Foo\"", "\"" >> alphas() << "\"", {{"Foo", ""}}); +} +} // namespace UnitTest From 7f8d043506bea9bb9e4352afd6b605f45a8ea43d Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 11:45:18 +0100 Subject: [PATCH 02/31] Add function call overload for parsers --- src/base/applicative.h | 20 +++++++++++--------- tests/io/applicative.cpp | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 6e2f966c4b..6c2b611e62 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -69,6 +69,8 @@ template class Parser public: // Parse a string and, if possible, return the value and the remainder parser_output parse(std::string_view input) const { return lambda_(input); }; + // Parse a string and, if possible, return the value and the remainder + parser_output operator()(std::string_view input) const { return lambda_(input); } // Create a new parser that takes the output of the old parser and // passes it through a function @@ -125,7 +127,7 @@ template class Parser if (first) { auto &[body, middle] = *first; - auto second = other.parse(middle); + auto second = other(middle); if (second) { return {{body, std::get<1>(*second)}}; @@ -154,7 +156,7 @@ template class Parser if (first) { auto &[body, middle] = *first; - return other.parse(middle); + return other(middle); } else { @@ -173,7 +175,7 @@ template class Parser [method, other](const std::string_view input) -> parser_output { auto first = method(input); - return first ? first : other.parse(input); + return first ? first : other(input); }); return result; } @@ -200,7 +202,7 @@ template class Parser if (first) { auto &[fst, middle] = *first; - auto second = other.parse(middle); + auto second = other(middle); if (second) { auto &[snd, final] = *second; @@ -230,7 +232,7 @@ template class Parser if (first) { auto &[fst, middle] = *first; - auto second = other.parse(middle); + auto second = other(middle); if (second) { auto &[snd, final] = *second; @@ -260,7 +262,7 @@ template class Parser if (first) { auto &[fst, middle] = *first; - auto second = other.parse(middle); + auto second = other(middle); if (second) { auto &[snd, final] = *second; @@ -290,7 +292,7 @@ template class Parser if (first) { auto &[fst, middle] = *first; - auto second = other.parse(middle); + auto second = other(middle); if (second) { auto &[snd, final] = *second; @@ -327,7 +329,7 @@ template Parser> maybe(Parser inner) Parser> result( [inner](const std::string_view input) -> parser_output> { - auto first = inner.parse(input); + auto first = inner(input); if (first) { auto &[body, remainder] = *first; @@ -353,7 +355,7 @@ template Parser> some(Parser inner) auto ctx = input; while (!ctx.empty()) { - auto trial = inner.parse(ctx); + auto trial = inner(ctx); if (trial) { auto &[body, remainder] = *trial; diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index f9c5c3dbfd..5fa92e5d1a 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -11,7 +11,7 @@ using namespace parsers; template void test_parser(std::string_view input, Parser parser, parser_output expected) { - auto result = parser.parse(input); + auto result = parser(input); ASSERT_EQ(result, expected); } TEST(ApplicativeTest, BasicStrings) From 8e71769d90c12a1d31e6bdee2bb69ce6ec094537 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 12:11:03 +0100 Subject: [PATCH 03/31] Allow exact tests --- tests/io/applicative.cpp | 50 ++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 5fa92e5d1a..e11c4301fa 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -3,17 +3,27 @@ #include "base/applicative.h" #include +#include namespace UnitTest { using namespace parsers; +using namespace std::literals; template void test_parser(std::string_view input, Parser parser, parser_output expected) { auto result = parser(input); - ASSERT_EQ(result, expected); + EXPECT_EQ(result, expected); } +template void test_exact(std::string_view input, Parser parser, T expected) +{ + auto result = parser(input); + ASSERT_TRUE(result); + EXPECT_EQ(std::get<1>(*result), ""); + EXPECT_EQ(std::get<0>(*result), expected); +} + TEST(ApplicativeTest, BasicStrings) { test_parser("Foo", "Fo"_p, {{"Fo", "o"}}); @@ -21,24 +31,24 @@ TEST(ApplicativeTest, BasicStrings) } TEST(ApplicativeTest, Ignoring) { - test_parser("Foobar", "Foo"_p << "bar"_p, {{"Foo", ""}}); - test_parser("Foobar", "Foo" >> "bar"_p, {{"bar", ""}}); + test_exact("Foobar", "Foo"_p << "bar"_p, "Foo"sv); + test_exact("Foobar", "Foo" >> "bar"_p, "bar"sv); } TEST(ApplicativeTest, Joining) { - test_parser("Foobar", "Foo"_p & "bar", {{{"Foo", "bar"}, ""}}); + test_exact("Foobar", "Foo"_p & "bar", {"Foo", "bar"}); test_parser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{{1, 2, 3, 4}, "Foobar"}}); - test_parser("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {{{"Fo", "ob", "a", "r"}, ""}}); - test_parser("Foobar", ("Fo"_p & "ob") & "ar", {{{"Fo", "ob", "ar"}, ""}}); - test_parser("Foobar", "Fo" & ("ob"_p & "ar"), {{{"Fo", "ob", "ar"}, ""}}); + test_exact("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {"Fo", "ob", "a", "r"}); + test_exact("Foobar", ("Fo"_p & "ob") & "ar", {"Fo", "ob", "ar"}); + test_exact("Foobar", "Fo" & ("ob"_p & "ar"), {"Fo", "ob", "ar"}); test_parser("Foobar", (pure(1) & pure(2)) & pure(3), {{{1, 2, 3}, "Foobar"}}); test_parser("Foobar", pure(1) & (pure(2) & pure(3)), {{{1, 2, 3}, "Foobar"}}); } TEST(ApplicativeTest, Choices) { - test_parser("Foo", "Foo"_p | "Bar", {{"Foo", ""}}); - test_parser("Bar", "Foo"_p | "Bar", {{"Bar", ""}}); + test_exact("Foo", "Foo"_p | "Bar", "Foo"sv); + test_exact("Bar", "Foo"_p | "Bar", "Bar"sv); test_parser("Quux", "Foo"_p | "Bar", {}); } @@ -46,32 +56,32 @@ TEST(ApplicativeTest, NaturalNumbers) { test_parser("123foo", natural(), {{123, "foo"}}); auto triplet = natural() & "," >> natural() & "," >> natural(); - test_parser("123,456,789", triplet, {{{123, 456, 789}, ""}}); + test_exact("123,456,789", triplet, {123, 456, 789}); auto vecsum = triplet.apply([](const auto x, const auto y, const auto z) -> int { return x + y + z; }); - test_parser("123,456,789", vecsum, {{123 + 456 + 789, ""}}); + test_exact("123,456,789", vecsum, 123 + 456 + 789); } TEST(ApplicativeTest, Optionals) { - test_parser("123,456", maybe(natural() << ",") & natural(), {{{{123}, 456}, ""}}); - test_parser("456", maybe(natural() << ",") & natural(), {{{std::nullopt, 456}, ""}}); - test_parser("-456", integer(), {{-456, ""}}); - test_parser("456", integer(), {{456, ""}}); + test_exact("123,456", maybe(natural() << ",") & natural(), {{123}, 456}); + test_exact("456", maybe(natural() << ",") & natural(), {std::nullopt, 456}); + test_exact("-456", integer(), -456); + test_exact("456", integer(), 456); } TEST(ApplicativeTest, MultipleTerms) { // Parse multiple terms - test_parser("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {{{123, 456, 789, 12}, ""}}); + test_exact("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {123, 456, 789, 12}); // Completely fail the parse if no copies are present test_parser("789", some(integer() << ","), {}); } TEST(ApplicativeTest, RealNumbers) { - test_parser("-12.0543", real(), {{-12.0543, ""}}); - test_parser("1.02E-3", real(), {{1.02e-3, ""}}); - test_parser("-71.2e3", real(), {{-71.2e3, ""}}); + test_exact("-12.0543", real(), -12.0543); + test_exact("1.02E-3", real(), 1.02e-3); + test_exact("-71.2e3", real(), -71.2e3); } TEST(ApplicativeTest, BasicParser) @@ -81,6 +91,6 @@ TEST(ApplicativeTest, BasicParser) test_parser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), {{{"1", "qaz", ".", "QAZ", " "}, "foo"}}); - test_parser("\"Foo\"", "\"" >> alphas() << "\"", {{"Foo", ""}}); + test_exact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"sv); } } // namespace UnitTest From 15b790d859a307e35c928cdec25f876347587fae Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 12:28:52 +0100 Subject: [PATCH 04/31] Enable exact parses --- src/base/applicative.h | 8 ++++++++ tests/io/applicative.cpp | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 6c2b611e62..bfa0cf41a5 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -71,6 +71,14 @@ template class Parser parser_output parse(std::string_view input) const { return lambda_(input); }; // Parse a string and, if possible, return the value and the remainder parser_output operator()(std::string_view input) const { return lambda_(input); } + // Parse a string and enforce that it parsed the entire input + std::optional exact(std::string_view input) const + { + auto result = lambda_(input); + if (result && std::get<1>(*result) == "") + return std::get<0>(*result); + return {}; + } // Create a new parser that takes the output of the old parser and // passes it through a function diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index e11c4301fa..c20b6cddd3 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -18,10 +18,9 @@ template void test_parser(std::string_view input, Parser parser, } template void test_exact(std::string_view input, Parser parser, T expected) { - auto result = parser(input); + auto result = parser.exact(input); ASSERT_TRUE(result); - EXPECT_EQ(std::get<1>(*result), ""); - EXPECT_EQ(std::get<0>(*result), expected); + EXPECT_EQ(*result, expected); } TEST(ApplicativeTest, BasicStrings) From 15439ba7f772440255a3bffd693a91c23db6df65 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 14:59:54 +0100 Subject: [PATCH 05/31] Rework double parsing --- src/base/CMakeLists.txt | 1 + src/base/applicative.cpp | 66 ++++++-------------------------------- src/base/applicative.h | 9 ------ src/base/parserLibrary.cpp | 53 ++++++++++++++++++++++++++++++ src/base/parserLibrary.h | 21 ++++++++++++ tests/io/applicative.cpp | 2 ++ 6 files changed, 87 insertions(+), 65 deletions(-) create mode 100644 src/base/parserLibrary.cpp create mode 100644 src/base/parserLibrary.h diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index a43df93528..7cee7f7115 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -1,6 +1,7 @@ add_library( base applicative.cpp + parserLibrary.cpp cbor.cpp enumOptionsBase.cpp geometry.cpp diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index c3f722ad0c..8f6e039ae3 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2026 Team Dissolve and contributors #include "applicative.h" + #include #include #include @@ -27,62 +28,6 @@ Parser takeWhile(std::function f) return result; } -// A parser that accepts an integer greater than or equal to zero -Parser natural() -{ - auto result = digits(); - return result.map( - [](const auto terms) -> int - { - int total = 0; - for (auto term : terms) - total = 10 * total + (term - '0'); - return total; - }); -} - -// Take an optional minus sign and a whole number to create an integer -int nat2int(std::optional minus, int number) -{ - if (minus) - return -number; - return number; -} - -// A parser that accepts an integer -Parser integer() { return (maybe("-"_p) & natural()).apply(nat2int); } - -// Take the numbers before the decimal, some optional digits after the period, and an optional exponent, and return a double. -double nat2dbl(int before, std::optional after, std::optional exponent) -{ - double result = before; - if (after) - { - double magnitude = 1; - double total = 0; - for (auto digit : *after) - { - total = total * 10 + (digit - '0'); - magnitude *= 10; - } - if (result < 0) - total *= -1; - result += total / magnitude; - } - if (exponent) - { - result *= pow(10, *exponent); - } - return result; -} - -// A parser that accepts a real, floating point number -Parser real() -{ - auto result = integer() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> integer()); - return result.apply(nat2dbl); -} - // A parser that accepts and amount of whitespace Parser spaces() { return takeWhile(std::isspace); } // A parse that accepts any amount of visible characters @@ -100,6 +45,15 @@ Parser punctuations() { return takeWhile(std::ispunct); } // A parser that accepts any amount of digit characters Parser digits() { return takeWhile(std::isdigit); } +// A parser that accepts any amount of digit characters +Parser inlines() +{ + return takeWhile([](const auto c) { return c != '\r' && c != '\n'; }); +} + +// A parser that accepts any amount of digit characters +Parser newlines() { return "\r\n"_p | "\n"_p; } + // A quick wrapper for easily making parses from strings Parser operator""_p(const char *text, size_t size) { return literal(std::string_view(text, size)); } } // namespace parsers diff --git a/src/base/applicative.h b/src/base/applicative.h index bfa0cf41a5..80126c306c 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -386,15 +386,6 @@ Parser literal(std::string_view constant); // A parser that accepts any amount of digit characters Parser digits(); -// A parser that accepts an integer greater than or equal to zero -Parser natural(); - -// A parser that accepts an integer -Parser integer(); - -// A parser that accepts a real, floating point number -Parser real(); - // A parser that accepts and amount of whitespace Parser spaces(); diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp new file mode 100644 index 0000000000..4bd071feb7 --- /dev/null +++ b/src/base/parserLibrary.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#include "base/parserLibrary.h" +#include + +namespace parsers +{ + +// A parser that accepts an integer greater than or equal to zero +Parser natural() +{ + auto result = digits(); + return result.map( + [](const auto terms) -> int + { + int total = 0; + for (auto term : terms) + total = 10 * total + (term - '0'); + return total; + }); +} + +// Take an optional minus sign and a whole number to create an integer +int nat2int(std::optional minus, int number) +{ + if (minus) + return -number; + return number; +} + +// A parser that accepts an integer +Parser integer() { return (maybe("-"_p) & natural()).apply(nat2int); } + +// Take the numbers before the decimal, some optional digits after the period, and an optional exponent, and return a double. +double nat2dbl(std::optional minus, std::string_view front, std::optional decimals, + std::optional, std::string_view>> exponent) +{ + auto basic = std::format("{}{}", minus.value_or(""), front); + if (decimals) + basic += std::format(".{}", *decimals); + if (exponent) + basic += std::format("e{}{}", std::get<0>(*exponent).value_or(""), std::get<1>(*exponent)); + return std::stod(basic); +} + +// A parser that accepts a real, floating point number +Parser real() +{ + auto result = maybe("-"_p) & digits() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> maybe("-"_p) & digits()); + return result.apply(nat2dbl); +} +} // namespace parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h new file mode 100644 index 0000000000..e3ecbd6cf7 --- /dev/null +++ b/src/base/parserLibrary.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026 Team Dissolve and contributors + +#pragma once + +#include "base/applicative.h" +#include "math/vector3.h" + +namespace parsers +{ + +// A parser that accepts an integer greater than or equal to zero +Parser natural(); + +// A parser that accepts an integer +Parser integer(); + +// A parser that accepts a real, floating point number +Parser real(); + +}; // namespace parsers diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index c20b6cddd3..3edafeeecf 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -2,6 +2,7 @@ // Copyright (c) 2026 Team Dissolve and contributors #include "base/applicative.h" +#include "base/parserLibrary.h" #include #include @@ -80,6 +81,7 @@ TEST(ApplicativeTest, RealNumbers) { test_exact("-12.0543", real(), -12.0543); test_exact("1.02E-3", real(), 1.02e-3); + test_exact("-3E-4", real(), -3e-4); test_exact("-71.2e3", real(), -71.2e3); } From 42bb8058a4f69ecc3b07e7735ae4e271f0b15100 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 14:59:54 +0100 Subject: [PATCH 06/31] Add Vector3 parser --- src/base/parserLibrary.cpp | 6 ++++++ src/base/parserLibrary.h | 1 + tests/io/applicative.cpp | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 4bd071feb7..c26e103fd7 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -50,4 +50,10 @@ Parser real() auto result = maybe("-"_p) & digits() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> maybe("-"_p) & digits()); return result.apply(nat2dbl); } + +Parser vector3() +{ + return (real() & spaces() >> real() & spaces() >> real()) + .apply([](double x, double y, double z) { return Vector3(x, y, z); }); +} } // namespace parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index e3ecbd6cf7..4838ad9244 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -18,4 +18,5 @@ Parser integer(); // A parser that accepts a real, floating point number Parser real(); +Parser vector3(); }; // namespace parsers diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 3edafeeecf..4a7c1b20f7 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -94,4 +94,6 @@ TEST(ApplicativeTest, BasicParser) test_exact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"sv); } + +TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } } // namespace UnitTest From 66833dd93d6c60392f5f5193d6ec6d299a73cbba Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 14:59:54 +0100 Subject: [PATCH 07/31] Add parser for StructureAtom --- src/base/parserLibrary.cpp | 7 +++++++ src/base/parserLibrary.h | 2 ++ tests/io/applicative.cpp | 3 +++ 3 files changed, 12 insertions(+) diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index c26e103fd7..1a47d09bf5 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -56,4 +56,11 @@ Parser vector3() return (real() & spaces() >> real() & spaces() >> real()) .apply([](double x, double y, double z) { return Vector3(x, y, z); }); } + +parsers::Parser>> structureAtom() +{ + auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real()); + return parser; +} + } // namespace parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index 4838ad9244..cf497b1bdb 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -19,4 +19,6 @@ Parser integer(); Parser real(); Parser vector3(); +Parser>> structureAtom(); + }; // namespace parsers diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 4a7c1b20f7..d2fb2c8a5c 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -96,4 +96,7 @@ TEST(ApplicativeTest, BasicParser) } TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } + +TEST(ApplicativeTest, StructureAtom) { test_exact("HW 1 2.5 -3e-4 5", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5}); } + } // namespace UnitTest From 16d33a0c1ac1e99220020ff8b6e0af6e1b414aa5 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Mon, 22 Jun 2026 17:52:06 +0100 Subject: [PATCH 08/31] Start parsing xyz structures --- src/base/applicative.h | 16 +++++++----- src/base/parserLibrary.cpp | 4 +-- src/nodes/importXYZStructure.cpp | 42 ++++++++++++++++++------------- src/nodes/importXYZStructure.h | 4 +-- src/nodes/importXYZTrajectory.cpp | 23 +++++++++++------ tests/io/applicative.cpp | 35 ++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 35 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 80126c306c..708c8e412e 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -389,18 +389,22 @@ Parser digits(); // A parser that accepts and amount of whitespace Parser spaces(); -// A parse that accepts any amount of visible characters +// A parser that accepts any amount of visible characters Parser graphs(); -// A parse that accepts any amount of alphanumeric characters +// A parser that accepts any amount of alphanumeric characters Parser alphanums(); -// A parse that accepts any amount of letters +// A parser that accepts any amount of letters Parser alphas(); -// A parse that accepts any amount of upper case letters +// A parser that accepts any amount of upper case letters Parser uppers(); -// A parse that accepts any amount of lower case letters +// A parser that accepts any amount of lower case letters Parser lowers(); -// A parse that accepts any amount of punctuation case letters +// A parser that accepts any amount of punctuation case letters Parser punctuations(); +// A parser that continues until a newline +Parser inlines(); +// A parser that matches newline characters +Parser newlines(); // A quick wrapper for easily making parses from strings Parser operator""_p(const char *text, size_t size); diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 1a47d09bf5..c7918fd01f 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -47,7 +47,7 @@ double nat2dbl(std::optional minus, std::string_view front, st // A parser that accepts a real, floating point number Parser real() { - auto result = maybe("-"_p) & digits() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> maybe("-"_p) & digits()); + auto result = maybe("-"_p) & digits() & maybe("." >> digits()) & maybe(("e"_p | "E"_p) >> maybe("-"_p | "+"_p) & digits()); return result.apply(nat2dbl); } @@ -59,7 +59,7 @@ Parser vector3() parsers::Parser>> structureAtom() { - auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real()); + auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real() << spaces()); return parser; } diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index 1dac55eb2e..3aa6f5d26c 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -2,6 +2,9 @@ // Copyright (c) 2026 Team Dissolve and contributors #include "nodes/importXYZStructure.h" +#include "base/parserLibrary.h" +#include +#include ImportXYZStructureNode::ImportXYZStructureNode(Graph *parentGraph) : Node(parentGraph) { @@ -31,32 +34,35 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() { structure_.clear(); - // Open file and check that we're OK to proceed importing from it - LineParser parser; - if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + std::ifstream infile{filePath_}; + if (!infile) return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); + std::ostringstream oss{}; + oss << infile.rdbuf(); + return read(oss.view(), structure_); - return read(parser, structure_); + // // Open file and check that we're OK to proceed importing from it + // LineParser parser; + // if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + // return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); + + // return read(parser, structure_); } // Read structure from the specified file parser -NodeConstants::ProcessResult ImportXYZStructureNode::read(LineParser &parser, Structure &structure) +NodeConstants::ProcessResult ImportXYZStructureNode::read(std::string_view input, Structure &structure) { - // Read natoms - if (parser.getArgsDelim() != LineParser::Success) - return NodeConstants::ProcessResult::Failed; - auto nAtoms = parser.argi(0); + using namespace parsers; + auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(input); - // Skip title - if (parser.skipLines(1) != LineParser::Success) + if (!xyz) return NodeConstants::ProcessResult::Failed; + auto rest = std::get<1>(*xyz); + auto &[nAtoms, atoms] = std::get<0>(*xyz); - for (auto n = 0; n < nAtoms; ++n) - { - if (parser.getArgsDelim() != LineParser::Success) - return NodeConstants::ProcessResult::Failed; - structure.addAtom(Elements::element(parser.argsv(0)), parser.arg3d(1), parser.hasArg(4) ? parser.argd(4) : 0.0); - } + for (auto &[elem, v, q] : atoms) + structure.addAtom(Elements::element(elem), v, q.value_or(0.0)); + assert(nAtoms == atoms.size()); return NodeConstants::ProcessResult::Success; -} \ No newline at end of file +} diff --git a/src/nodes/importXYZStructure.h b/src/nodes/importXYZStructure.h index 1cbe74e924..af52c527e5 100644 --- a/src/nodes/importXYZStructure.h +++ b/src/nodes/importXYZStructure.h @@ -39,5 +39,5 @@ class ImportXYZStructureNode : public Node public: // Read structure from the specified file parser - static NodeConstants::ProcessResult read(LineParser &parser, Structure &structure); -}; \ No newline at end of file + static NodeConstants::ProcessResult read(std::string_view, Structure &structure); +}; diff --git a/src/nodes/importXYZTrajectory.cpp b/src/nodes/importXYZTrajectory.cpp index ac308b7c86..6ef4f20540 100644 --- a/src/nodes/importXYZTrajectory.cpp +++ b/src/nodes/importXYZTrajectory.cpp @@ -40,26 +40,35 @@ NodeConstants::ProcessResult ImportXYZTrajectoryNode::process() { message("Reading XYZ trajectory file frame from '{}'...\n", filePath_); - // Open the file - LineParser parser; - if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + std::ifstream infile{filePath_}; + if (!infile) { error("Couldn't open trajectory file '{}'.\n", filePath_); return NodeConstants::ProcessResult::Failed; } + std::ostringstream oss{}; + oss << infile.rdbuf(); - // Seek to the next file position - parser.seekg(filePosition_); + // // Open the file + // LineParser parser; + // if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) + // { + // error("Couldn't open trajectory file '{}'.\n", filePath_); + // return NodeConstants::ProcessResult::Failed; + // } + + // // Seek to the next file position + // parser.seekg(filePosition_); structure_.clear(); // Get the frame read result - auto frameResult = ImportXYZStructureNode::read(parser, structure_); + auto frameResult = ImportXYZStructureNode::read(oss.view(), structure_); if (frameResult != NodeConstants::ProcessResult::Success) return frameResult; // Store the new trajectory file position - filePosition_ = parser.tellg(); + // filePosition_ = parser.tellg(); return NodeConstants::ProcessResult::Success; } diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index d2fb2c8a5c..d36818dee2 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -99,4 +99,39 @@ TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, TEST(ApplicativeTest, StructureAtom) { test_exact("HW 1 2.5 -3e-4 5", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5}); } +TEST(ApplicativeTest, XYZStructure) +{ + + std::ifstream infile{"xyz/c2so3.xyz"}; + ASSERT_TRUE(infile); + std::ostringstream oss{}; + oss << infile.rdbuf(); + auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(oss.view()); + // auto xyz = (maybe(spaces()) >> natural() ).parse(oss.view()); + + ASSERT_TRUE(xyz); + auto &[value, rest] = *xyz; + ASSERT_EQ(rest, ""); + auto &terms = std::get<1>(value); + EXPECT_EQ(terms.size(), std::get<0>(value)); + + std::vector> expected{ + + {"S", {0.010001, 0.000000, -0.000012}}, {"O", {1.465001, 0.000000, -0.000012}}, + {"O", {-0.475688, -1.371543, -0.000012}}, {"O", {-0.475688, 0.685772, 1.187779}}, + {"C", {-0.588181, 0.844607, -1.462914}}, {"C", {-0.079239, 0.124469, -2.712002}}, + {"H", {-1.678181, 0.844607, -1.462914}}, {"H", {-0.225364, 1.872451, -1.463546}}, + {"H", {-0.442057, -0.903375, -2.712634}}, {"H", {-0.443088, 0.638209, -3.601825}}, + {"H", {1.000760, 0.124469, -2.713254}}, + + }; + int index = 0; + for (auto &[elem, r] : expected) + { + EXPECT_EQ(elem, std::get<0>(terms[index])); + EXPECT_EQ(r, std::get<1>(terms[index])); + ++index; + } +} + } // namespace UnitTest From b47ea242c7dbae8e3cc24c29020cbe4ed370b3a2 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 10:23:23 +0100 Subject: [PATCH 09/31] Reworking with stream base --- src/base/applicative.cpp | 60 ++++++++++++----- src/base/applicative.h | 108 ++++++++++++++++-------------- src/base/parserLibrary.cpp | 4 +- src/base/parserLibrary.h | 2 +- src/nodes/importXYZStructure.cpp | 8 +-- src/nodes/importXYZStructure.h | 2 +- src/nodes/importXYZTrajectory.cpp | 7 +- tests/io/applicative.cpp | 46 +++++++------ 8 files changed, 136 insertions(+), 101 deletions(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index 8f6e039ae3..f4fb5b67c1 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -4,8 +4,8 @@ #include "applicative.h" #include -#include #include +#include namespace parsers { @@ -13,40 +13,66 @@ namespace parsers // A parser that expects an exact string Parser literal(std::string_view constant) { return Parser(constant); } -Parser takeWhile(std::function f) +Parser takeWhile(std::function f) { - Parser result( - [f](const auto input) -> parser_output + Parser result( + [f](auto &input) -> parser_output { - if (input.empty()) + if (input.eof()) return {}; - auto it = std::find_if(input.begin(), input.end(), [f](const auto x) { return !f(x); }); - if (it == input.begin()) + std::string result = ""; + while (f(input.peek())) + result.push_back(input.get()); + if (result.empty()) return {}; - return {{std::string_view(input.begin(), it), std::string_view(it, input.end())}}; + return {{result, input}}; }); return result; } // A parser that accepts and amount of whitespace -Parser spaces() { return takeWhile(std::isspace); } +Parser spaces() +{ + return takeWhile([](const char c) { return isspace(c); }); +} // A parse that accepts any amount of visible characters -Parser graphs() { return takeWhile(std::isgraph); } +Parser graphs() +{ + return takeWhile([](const char c) { return std::isgraph(c); }); +} // A parse that accepts any amount of alphanumeric characters -Parser alphanums() { return takeWhile(std::isalnum); } +Parser alphanums() +{ + return takeWhile([](const char c) { return std::isalnum(c); }); +} // A parse that accepts any amount of letters -Parser alphas() { return takeWhile(std::isalpha); } +Parser alphas() +{ + return takeWhile([](const char c) { return std::isalpha(c); }); +} // A parse that accepts any amount of upper case letters -Parser uppers() { return takeWhile(std::isupper); } +Parser uppers() +{ + return takeWhile([](const char c) { return std::isupper(c); }); +} // A parse that accepts any amount of lower case letters -Parser lowers() { return takeWhile(std::islower); } +Parser lowers() +{ + return takeWhile([](const char c) { return std::islower(c); }); +} // A parse that accepts any amount of punctuation case letters -Parser punctuations() { return takeWhile(std::ispunct); } +Parser punctuations() +{ + return takeWhile([](const char c) { return std::ispunct(c); }); +} // A parser that accepts any amount of digit characters -Parser digits() { return takeWhile(std::isdigit); } +Parser digits() +{ + return takeWhile([](const char c) { return std::isdigit(c); }); +} // A parser that accepts any amount of digit characters -Parser inlines() +Parser inlines() { return takeWhile([](const auto c) { return c != '\r' && c != '\n'; }); } diff --git a/src/base/applicative.h b/src/base/applicative.h index 708c8e412e..071a79d891 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -4,9 +4,12 @@ #pragma once #include +#include #include +#include #include #include +#include #include #include @@ -24,12 +27,12 @@ concept TupleLike = requires { std::tuple_size::value; }; // define the parser_output for the return type of the function. // This *could* have further implications because there are more // complicated parsers we could create. -template using parser_output = std::optional>; +template using parser_output = std::optional>; // This concept type checks that a given lambda matches the definition // given in the paragraph above. template -concept ApParse = requires(Lambda lam, std::string_view input) { +concept ApParse = requires(Lambda lam, std::istream input) { { lam(input) } -> std::convertible_to>; }; @@ -50,12 +53,12 @@ template class Parser template ::value>> Parser(std::string_view constant) : lambda_( - [constant](const std::string_view input) -> parser_output + [constant](std::istream &input) -> parser_output { - if (input.starts_with(constant)) - return {{constant, input.substr(constant.size())}}; - else - return {}; + for (auto c : constant) + if (c != input.get()) + return {}; + return {{constant, input}}; }) { } @@ -64,18 +67,18 @@ template class Parser // The actual function that we have wrapped. We need to wrap this // in a std::function instead of using a lambda so that we don't // have to handle the exact type of the bound lambda - std::function(std::string_view)> lambda_; + std::function(std::istream &)> lambda_; public: // Parse a string and, if possible, return the value and the remainder - parser_output parse(std::string_view input) const { return lambda_(input); }; + parser_output parse(std::istream &input) const { return lambda_(input); }; // Parse a string and, if possible, return the value and the remainder - parser_output operator()(std::string_view input) const { return lambda_(input); } + parser_output operator()(std::istream &input) const { return lambda_(input); } // Parse a string and enforce that it parsed the entire input - std::optional exact(std::string_view input) const + std::optional exact(std::istream &input) const { auto result = lambda_(input); - if (result && std::get<1>(*result) == "") + if (result && input.get() == -1) return std::get<0>(*result); return {}; } @@ -86,7 +89,7 @@ template class Parser { auto &method = lambda_; Parser()))> result( - [method, f](const std::string_view input) -> parser_output()))> + [method, f](std::istream &input) -> parser_output()))> { auto first = method(input); if (first) @@ -109,7 +112,7 @@ template class Parser { auto &method = lambda_; Parser()))> result( - [method, f](const std::string_view input) -> parser_output()))> + [method, f](std::istream &input) -> parser_output()))> { auto first = method(input); if (first) @@ -129,7 +132,7 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](const std::string_view input) -> parser_output + [method, other](std::istream &input) -> parser_output { auto first = method(input); if (first) @@ -137,13 +140,9 @@ template class Parser auto &[body, middle] = *first; auto second = other(middle); if (second) - { return {{body, std::get<1>(*second)}}; - } else - { return {}; - } } else { @@ -158,7 +157,7 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](const std::string_view input) -> parser_output + [method, other](std::istream &input) -> parser_output { auto first = method(input); if (first) @@ -167,9 +166,7 @@ template class Parser return other(middle); } else - { return {}; - } }); return result; } @@ -180,10 +177,15 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](const std::string_view input) -> parser_output + [method, other](std::istream &input) -> parser_output { + auto location = input.tellg(); auto first = method(input); - return first ? first : other(input); + if (first) + return first; + input.clear(); + input.seekg(location); + return other(input); }); return result; } @@ -204,7 +206,7 @@ template class Parser { auto &method = lambda_; Parser> result( - [method, other](const std::string_view input) -> parser_output> + [method, other](std::istream &input) -> parser_output> { auto first = method(input); if (first) @@ -233,7 +235,7 @@ template class Parser { auto &method = lambda_; Parser(), std::make_tuple(std::declval())))> result( - [method, other](const std::string_view input) + [method, other](std::istream &input) -> parser_output(), std::make_tuple(std::declval())))> { auto first = method(input); @@ -263,7 +265,7 @@ template class Parser { auto &method = lambda_; Parser()), std::declval()))> result( - [method, other](const std::string_view input) + [method, other](std::istream &input) -> parser_output()), std::declval()))> { auto first = method(input); @@ -293,8 +295,8 @@ template class Parser { auto &method = lambda_; Parser(), std::declval()))> result( - [method, other]( - const std::string_view input) -> parser_output(), std::declval()))> + [method, + other](std::istream &input) -> parser_output(), std::declval()))> { auto first = method(input); if (first) @@ -319,15 +321,13 @@ template class Parser // Create a parser that always succeeds and returns a constant value template Parser pure(T constant) { - Parser result([constant](const std::string_view input) -> parser_output { return {{constant, input}}; }); + Parser result([constant](std::istream &input) -> parser_output { return {{constant, input}}; }); return result; } // A parser that always fails template -Parser null([](const auto x) -> parser_output { return {}; }); - -// Parser literal(std::string_view constant); +Parser null([](const auto x) -> parser_output { return {}; }); // Modify a parser so that the parsed value is wrapped in a // std::optional. If the parser would have failed, act as though @@ -335,8 +335,9 @@ Parser null([](const auto x) -> parser_output { return {}; template Parser> maybe(Parser inner) { Parser> result( - [inner](const std::string_view input) -> parser_output> + [inner](std::istream &input) -> parser_output> { + auto location = input.tellg(); auto first = inner(input); if (first) { @@ -346,6 +347,8 @@ template Parser> maybe(Parser inner) else { std::optional empty; + input.clear(); + input.seekg(location); return {{empty, input}}; } }); @@ -357,26 +360,29 @@ template Parser> maybe(Parser inner) template Parser> some(Parser inner) { Parser> result( - [inner](const std::string_view input) -> parser_output> + [inner](std::istream &input) -> parser_output> { std::vector collection; - auto ctx = input; - while (!ctx.empty()) + auto location = input.tellg(); + while (!input.eof()) { - auto trial = inner(ctx); + auto trial = inner(input); if (trial) { - auto &[body, remainder] = *trial; + auto &[body, _] = *trial; collection.push_back(body); - ctx = remainder; + location = input.tellg(); } else + { + input.clear(); + input.seekg(location); break; + } } - if (collection.empty()) return {}; - return {{collection, ctx}}; + return {{collection, input}}; }); return result; } @@ -384,25 +390,25 @@ template Parser> some(Parser inner) // A parser that expects an exact string Parser literal(std::string_view constant); // A parser that accepts any amount of digit characters -Parser digits(); +Parser digits(); // A parser that accepts and amount of whitespace -Parser spaces(); +Parser spaces(); // A parser that accepts any amount of visible characters -Parser graphs(); +Parser graphs(); // A parser that accepts any amount of alphanumeric characters -Parser alphanums(); +Parser alphanums(); // A parser that accepts any amount of letters -Parser alphas(); +Parser alphas(); // A parser that accepts any amount of upper case letters -Parser uppers(); +Parser uppers(); // A parser that accepts any amount of lower case letters -Parser lowers(); +Parser lowers(); // A parser that accepts any amount of punctuation case letters -Parser punctuations(); +Parser punctuations(); // A parser that continues until a newline -Parser inlines(); +Parser inlines(); // A parser that matches newline characters Parser newlines(); diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index c7918fd01f..496e5dde54 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -57,9 +57,9 @@ Parser vector3() .apply([](double x, double y, double z) { return Vector3(x, y, z); }); } -parsers::Parser>> structureAtom() +parsers::Parser>> structureAtom() { - auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real() << spaces()); + auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real() << maybe(spaces())); return parser; } diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index cf497b1bdb..aee91246d9 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -19,6 +19,6 @@ Parser integer(); Parser real(); Parser vector3(); -Parser>> structureAtom(); +Parser>> structureAtom(); }; // namespace parsers diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index 3aa6f5d26c..6c51135754 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -37,9 +37,7 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() std::ifstream infile{filePath_}; if (!infile) return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); - std::ostringstream oss{}; - oss << infile.rdbuf(); - return read(oss.view(), structure_); + return read(infile, structure_); // // Open file and check that we're OK to proceed importing from it // LineParser parser; @@ -50,14 +48,14 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() } // Read structure from the specified file parser -NodeConstants::ProcessResult ImportXYZStructureNode::read(std::string_view input, Structure &structure) +NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace parsers; auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(input); if (!xyz) return NodeConstants::ProcessResult::Failed; - auto rest = std::get<1>(*xyz); + auto &rest = std::get<1>(*xyz); auto &[nAtoms, atoms] = std::get<0>(*xyz); for (auto &[elem, v, q] : atoms) diff --git a/src/nodes/importXYZStructure.h b/src/nodes/importXYZStructure.h index af52c527e5..02f9218d98 100644 --- a/src/nodes/importXYZStructure.h +++ b/src/nodes/importXYZStructure.h @@ -39,5 +39,5 @@ class ImportXYZStructureNode : public Node public: // Read structure from the specified file parser - static NodeConstants::ProcessResult read(std::string_view, Structure &structure); + static NodeConstants::ProcessResult read(std::istream &input, Structure &structure); }; diff --git a/src/nodes/importXYZTrajectory.cpp b/src/nodes/importXYZTrajectory.cpp index 6ef4f20540..8658302390 100644 --- a/src/nodes/importXYZTrajectory.cpp +++ b/src/nodes/importXYZTrajectory.cpp @@ -46,8 +46,7 @@ NodeConstants::ProcessResult ImportXYZTrajectoryNode::process() error("Couldn't open trajectory file '{}'.\n", filePath_); return NodeConstants::ProcessResult::Failed; } - std::ostringstream oss{}; - oss << infile.rdbuf(); + infile.seekg(filePosition_); // // Open the file // LineParser parser; @@ -63,12 +62,12 @@ NodeConstants::ProcessResult ImportXYZTrajectoryNode::process() structure_.clear(); // Get the frame read result - auto frameResult = ImportXYZStructureNode::read(oss.view(), structure_); + auto frameResult = ImportXYZStructureNode::read(infile, structure_); if (frameResult != NodeConstants::ProcessResult::Success) return frameResult; // Store the new trajectory file position - // filePosition_ = parser.tellg(); + filePosition_ = infile.tellg(); return NodeConstants::ProcessResult::Success; } diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index d36818dee2..9c00751576 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -4,6 +4,7 @@ #include "base/applicative.h" #include "base/parserLibrary.h" #include +#include #include namespace UnitTest @@ -12,22 +13,29 @@ namespace UnitTest using namespace parsers; using namespace std::literals; -template void test_parser(std::string_view input, Parser parser, parser_output expected) +template void test_parser(std::string_view input, Parser parser, std::optional expected) { - auto result = parser(input); - EXPECT_EQ(result, expected); + std::cout << "Base string:\t" << input << std::endl; + std::istringstream stream{std::string(input)}; + auto result = parser(stream); + if (!result) + EXPECT_FALSE(expected); + else + EXPECT_EQ(std::get<0>(*result), *expected); } template void test_exact(std::string_view input, Parser parser, T expected) { - auto result = parser.exact(input); + std::cout << "Base string:\t" << input << std::endl; + std::istringstream stream{std::string(input)}; + auto result = parser.exact(stream); ASSERT_TRUE(result); EXPECT_EQ(*result, expected); } TEST(ApplicativeTest, BasicStrings) { - test_parser("Foo", "Fo"_p, {{"Fo", "o"}}); - test_parser("Foobar", "Foo"_p, {{"Foo", "bar"}}); + test_parser("Foo", "Fo"_p, {"Fo"}); + test_parser("Foobar", "Foo"_p, {"Foo"}); } TEST(ApplicativeTest, Ignoring) { @@ -37,12 +45,12 @@ TEST(ApplicativeTest, Ignoring) TEST(ApplicativeTest, Joining) { test_exact("Foobar", "Foo"_p & "bar", {"Foo", "bar"}); - test_parser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{{1, 2, 3, 4}, "Foobar"}}); + test_parser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{1, 2, 3, 4}}); test_exact("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {"Fo", "ob", "a", "r"}); test_exact("Foobar", ("Fo"_p & "ob") & "ar", {"Fo", "ob", "ar"}); test_exact("Foobar", "Fo" & ("ob"_p & "ar"), {"Fo", "ob", "ar"}); - test_parser("Foobar", (pure(1) & pure(2)) & pure(3), {{{1, 2, 3}, "Foobar"}}); - test_parser("Foobar", pure(1) & (pure(2) & pure(3)), {{{1, 2, 3}, "Foobar"}}); + test_parser("Foobar", (pure(1) & pure(2)) & pure(3), {{1, 2, 3}}); + test_parser("Foobar", pure(1) & (pure(2) & pure(3)), {{1, 2, 3}}); } TEST(ApplicativeTest, Choices) @@ -54,7 +62,7 @@ TEST(ApplicativeTest, Choices) TEST(ApplicativeTest, NaturalNumbers) { - test_parser("123foo", natural(), {{123, "foo"}}); + test_parser("123foo", natural(), {123}); auto triplet = natural() & "," >> natural() & "," >> natural(); test_exact("123,456,789", triplet, {123, 456, 789}); auto vecsum = triplet.apply([](const auto x, const auto y, const auto z) -> int { return x + y + z; }); @@ -87,31 +95,29 @@ TEST(ApplicativeTest, RealNumbers) TEST(ApplicativeTest, BasicParser) { - test_parser(" \t foo", spaces(), {{" \t ", "foo"}}); - test_parser("1qaz.QAZ foo", graphs(), {{"1qaz.QAZ", " foo"}}); - test_parser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), - {{{"1", "qaz", ".", "QAZ", " "}, "foo"}}); + test_parser(" \t foo", spaces(), {" \t "}); + test_exact("HW", alphas(), "HW"s); + test_parser("1qaz.QAZ foo", graphs(), {"1qaz.QAZ"}); + test_parser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), {{"1", "qaz", ".", "QAZ", " "}}); - test_exact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"sv); + test_exact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"s); } TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } -TEST(ApplicativeTest, StructureAtom) { test_exact("HW 1 2.5 -3e-4 5", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5}); } +TEST(ApplicativeTest, StructureAtom) { test_exact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); } TEST(ApplicativeTest, XYZStructure) { std::ifstream infile{"xyz/c2so3.xyz"}; ASSERT_TRUE(infile); - std::ostringstream oss{}; - oss << infile.rdbuf(); - auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(oss.view()); + auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(infile); // auto xyz = (maybe(spaces()) >> natural() ).parse(oss.view()); ASSERT_TRUE(xyz); auto &[value, rest] = *xyz; - ASSERT_EQ(rest, ""); + EXPECT_TRUE(rest.eof()); auto &terms = std::get<1>(value); EXPECT_EQ(terms.size(), std::get<0>(value)); From b52d1b5aa62cc2cad0620d952ac0967ea8f97bb0 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 16:03:36 +0100 Subject: [PATCH 10/31] Start fixing up StructureAtom --- src/base/applicative.cpp | 5 +++++ src/base/applicative.h | 2 ++ src/base/parserLibrary.cpp | 2 +- src/nodes/importXYZStructure.cpp | 3 ++- tests/io/applicative.cpp | 6 ++++-- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index f4fb5b67c1..cbc85dc6b8 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -35,6 +35,11 @@ Parser spaces() { return takeWhile([](const char c) { return isspace(c); }); } +// A parser that accepts space and tab +Parser inline_spaces() +{ + return takeWhile([](const char c) { return c == ' ' || c == '\t'; }); +} // A parse that accepts any amount of visible characters Parser graphs() { diff --git a/src/base/applicative.h b/src/base/applicative.h index 071a79d891..fdc824e63d 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -394,6 +394,8 @@ Parser digits(); // A parser that accepts and amount of whitespace Parser spaces(); +// A parser that accepts space and tab +Parser inline_spaces(); // A parser that accepts any amount of visible characters Parser graphs(); diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 496e5dde54..d3bbe18d02 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -59,7 +59,7 @@ Parser vector3() parsers::Parser>> structureAtom() { - auto parser = alphas() & spaces() >> vector3() << spaces() & maybe(real() << maybe(spaces())); + auto parser = alphas() & inline_spaces() >> vector3() << inline_spaces() & maybe(real() << maybe(inline_spaces())); return parser; } diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index 6c51135754..a330f65e57 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -51,7 +51,8 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace parsers; - auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(input); + auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << newlines())) + .parse(input); if (!xyz) return NodeConstants::ProcessResult::Failed; diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 9c00751576..e420124c0b 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -112,12 +112,14 @@ TEST(ApplicativeTest, XYZStructure) std::ifstream infile{"xyz/c2so3.xyz"}; ASSERT_TRUE(infile); - auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom())).parse(infile); + auto xyz = + (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) + .parse(infile); // auto xyz = (maybe(spaces()) >> natural() ).parse(oss.view()); ASSERT_TRUE(xyz); auto &[value, rest] = *xyz; - EXPECT_TRUE(rest.eof()); + EXPECT_EQ(rest.get(), -1); auto &terms = std::get<1>(value); EXPECT_EQ(terms.size(), std::get<0>(value)); From 15dbe673096bb5ccf979c1ea32fb5020bd5f8658 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 16:03:36 +0100 Subject: [PATCH 11/31] No required final newline in Structure --- src/nodes/importXYZStructure.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index a330f65e57..c46e2fc4bf 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -51,8 +51,9 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace parsers; - auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << newlines())) - .parse(input); + auto xyz = + (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) + .parse(input); if (!xyz) return NodeConstants::ProcessResult::Failed; From 7c174af36eb52c385a6e8bf21036538270e66a8c Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 16:03:36 +0100 Subject: [PATCH 12/31] Get structure import working --- src/base/applicative.h | 9 +++++---- src/base/parserLibrary.cpp | 2 +- src/nodes/importXYZStructure.cpp | 14 ++++---------- src/nodes/importXYZTrajectory.cpp | 13 ------------- src/nodes/importXYZTrajectory.h | 4 ++-- tests/io/applicative.cpp | 24 +++++++++++++++++++++++- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index fdc824e63d..1a19e33b48 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -22,8 +22,8 @@ template concept TupleLike = requires { std::tuple_size::value; }; // The simplest defintion of an applicative parser is a function that -// takes a string_view and, if the parse succeeds, returns the parsed -// value and the rest of the string_view. To make life simpler, we +// takes a stream and, if the parse succeeds, returns the parsed +// value and the rest of the stream. To make life simpler, we // define the parser_output for the return type of the function. // This *could* have further implications because there are more // complicated parsers we could create. @@ -36,8 +36,9 @@ concept ApParse = requires(Lambda lam, std::istream input) { { lam(input) } -> std::convertible_to>; }; -// It's fully possible to just use the functions as the parsers, but, if we wrap them un a struct, we can use operator -// overloading to more easily combine smaller parsers into larger parsers. +// It's fully possible to just use the functions as the parsers, but, +// if we wrap them in a struct, we can use operator overloading to +// more easily combine smaller parsers into larger parsers. template class Parser { diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index d3bbe18d02..903101801d 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -59,7 +59,7 @@ Parser vector3() parsers::Parser>> structureAtom() { - auto parser = alphas() & inline_spaces() >> vector3() << inline_spaces() & maybe(real() << maybe(inline_spaces())); + auto parser = alphas() & inline_spaces() >> vector3() & maybe(inline_spaces() >> real() << maybe(inline_spaces())); return parser; } diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index c46e2fc4bf..d555f925b3 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -38,28 +38,22 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() if (!infile) return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); return read(infile, structure_); - - // // Open file and check that we're OK to proceed importing from it - // LineParser parser; - // if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) - // return error("Couldn't open file '{}' for loading XYZ data.\n", filePath_); - - // return read(parser, structure_); } // Read structure from the specified file parser NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace parsers; - auto xyz = - (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) - .parse(input); + auto xyz = (maybe(inline_spaces()) >> natural() << newlines() & + inlines() >> newlines() >> some(structureAtom() << maybe(inline_spaces()) << maybe(newlines()))) + .parse(input); if (!xyz) return NodeConstants::ProcessResult::Failed; auto &rest = std::get<1>(*xyz); auto &[nAtoms, atoms] = std::get<0>(*xyz); + structure.clear(); for (auto &[elem, v, q] : atoms) structure.addAtom(Elements::element(elem), v, q.value_or(0.0)); assert(nAtoms == atoms.size()); diff --git a/src/nodes/importXYZTrajectory.cpp b/src/nodes/importXYZTrajectory.cpp index 8658302390..0e0e91a72a 100644 --- a/src/nodes/importXYZTrajectory.cpp +++ b/src/nodes/importXYZTrajectory.cpp @@ -48,19 +48,6 @@ NodeConstants::ProcessResult ImportXYZTrajectoryNode::process() } infile.seekg(filePosition_); - // // Open the file - // LineParser parser; - // if ((!parser.openInput(filePath_)) || (!parser.isFileGoodForReading())) - // { - // error("Couldn't open trajectory file '{}'.\n", filePath_); - // return NodeConstants::ProcessResult::Failed; - // } - - // // Seek to the next file position - // parser.seekg(filePosition_); - - structure_.clear(); - // Get the frame read result auto frameResult = ImportXYZStructureNode::read(infile, structure_); if (frameResult != NodeConstants::ProcessResult::Success) diff --git a/src/nodes/importXYZTrajectory.h b/src/nodes/importXYZTrajectory.h index 25bebaf1b9..3feeb7e2b2 100644 --- a/src/nodes/importXYZTrajectory.h +++ b/src/nodes/importXYZTrajectory.h @@ -28,7 +28,7 @@ class ImportXYZTrajectoryNode : public Node // File path std::string filePath_; // Last read file position - std::streampos filePosition_; + std::streampos filePosition_ = 0; // Structure Structure structure_; @@ -38,4 +38,4 @@ class ImportXYZTrajectoryNode : public Node protected: // Perform processing NodeConstants::ProcessResult process() override; -}; \ No newline at end of file +}; diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index e420124c0b..3285ed54e0 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -105,7 +105,11 @@ TEST(ApplicativeTest, BasicParser) TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } -TEST(ApplicativeTest, StructureAtom) { test_exact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); } +TEST(ApplicativeTest, StructureAtom) +{ + test_exact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); + test_exact("He 0.5 0.5 0.5", structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); +} TEST(ApplicativeTest, XYZStructure) { @@ -142,4 +146,22 @@ TEST(ApplicativeTest, XYZStructure) } } +TEST(ApplicativeTest, Helium) +{ + + std::ifstream infile{"xyz/voxelDensity-helium.xyz"}; + ASSERT_TRUE(infile); + auto xyz = + (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) + .parse(infile); + ASSERT_TRUE(xyz); + auto &[value, rest] = *xyz; + EXPECT_EQ(rest.get(), -1); + auto &terms = std::get<1>(value); + EXPECT_EQ(terms.size(), std::get<0>(value)); + int index = 0; + for (auto &[elem, r, q] : terms) + EXPECT_EQ(elem, "He"); +} + } // namespace UnitTest From 6837977ef4163ae750e81407ee116fbe759d6d07 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 18:16:23 +0100 Subject: [PATCH 13/31] Simplify apply on parsers --- src/base/applicative.h | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 1a19e33b48..6d20e96a4f 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -111,20 +111,7 @@ template class Parser requires(TupleLike) auto apply(Lambda f) -> Parser()))> { - auto &method = lambda_; - Parser()))> result( - [method, f](std::istream &input) -> parser_output()))> - { - auto first = method(input); - if (first) - { - auto &[body, remainder] = *first; - return {{std::apply(f, body), remainder}}; - } - else - return {}; - }); - return result; + return map([f](const T tup) { return std::apply(f, tup); }); } // Insist that this parse is followed by another parse, but we From 2aab03c65f7991711fc2bada77f660f8a05edee5 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Tue, 23 Jun 2026 18:25:45 +0100 Subject: [PATCH 14/31] Remove explicit call to template type in lambda --- src/base/applicative.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 6d20e96a4f..ebca898305 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -334,10 +334,9 @@ template Parser> maybe(Parser inner) } else { - std::optional empty; input.clear(); input.seekg(location); - return {{empty, input}}; + return {{{}, input}}; } }); return result; From 9fc22d50b0196b07ca538142e109571f6b56972e Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 10:24:53 +0100 Subject: [PATCH 15/31] Explicit nullopt in null parsers --- src/base/applicative.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index ebca898305..12ec8f1421 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -315,7 +315,7 @@ template Parser pure(T constant) // A parser that always fails template -Parser null([](const auto x) -> parser_output { return {}; }); +Parser null([](const auto x) -> parser_output { return std::nullopt; }); // Modify a parser so that the parsed value is wrapped in a // std::optional. If the parser would have failed, act as though From 026c85c316d4791960b293ed30ca5985346fba89 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 10:59:36 +0100 Subject: [PATCH 16/31] Turn null parser into a function --- src/base/applicative.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 12ec8f1421..86f6c483f6 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -314,8 +314,11 @@ template Parser pure(T constant) } // A parser that always fails -template -Parser null([](const auto x) -> parser_output { return std::nullopt; }); +template Parser null() +{ + Parser result = ([](const auto x) -> parser_output { return std::nullopt; }); + return result; +} // Modify a parser so that the parsed value is wrapped in a // std::optional. If the parser would have failed, act as though From afd5c27180ae83340d1b3ca21e18542efdbb14ee Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:18:26 +0000 Subject: [PATCH 17/31] Update src/base/applicative.cpp Co-authored-by: Tristan Youngs --- src/base/applicative.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index cbc85dc6b8..886d4f0978 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -76,7 +76,7 @@ Parser digits() return takeWhile([](const char c) { return std::isdigit(c); }); } -// A parser that accepts any amount of digit characters +// A parser that continues until a newline Parser inlines() { return takeWhile([](const auto c) { return c != '\r' && c != '\n'; }); From 2d04d178acef78b5b42fffe577ae890f864d3600 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:18:35 +0000 Subject: [PATCH 18/31] Update src/base/applicative.cpp Co-authored-by: Tristan Youngs --- src/base/applicative.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index 886d4f0978..eeda39c852 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -82,7 +82,7 @@ Parser inlines() return takeWhile([](const auto c) { return c != '\r' && c != '\n'; }); } -// A parser that accepts any amount of digit characters +// A parser that matches newline characters Parser newlines() { return "\r\n"_p | "\n"_p; } // A quick wrapper for easily making parses from strings From ae822a5de171aee1517ea771cde5706761b019be Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:18:52 +0000 Subject: [PATCH 19/31] Update tests/io/applicative.cpp Co-authored-by: Tristan Youngs --- tests/io/applicative.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 3285ed54e0..7f4763a719 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -113,7 +113,6 @@ TEST(ApplicativeTest, StructureAtom) TEST(ApplicativeTest, XYZStructure) { - std::ifstream infile{"xyz/c2so3.xyz"}; ASSERT_TRUE(infile); auto xyz = From 8918bdfb34184cfc192b32028a0bfa0985ff417e Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:19:16 +0000 Subject: [PATCH 20/31] Update tests/io/applicative.cpp Co-authored-by: Tristan Youngs --- tests/io/applicative.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 7f4763a719..32009ed637 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -136,7 +136,7 @@ TEST(ApplicativeTest, XYZStructure) {"H", {1.000760, 0.124469, -2.713254}}, }; - int index = 0; + auto index = 0; for (auto &[elem, r] : expected) { EXPECT_EQ(elem, std::get<0>(terms[index])); From 1709810a5d3904500d88356b5ba2097ba08d71f1 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:19:53 +0000 Subject: [PATCH 21/31] Update src/base/applicative.h Co-authored-by: Tristan Youngs --- src/base/applicative.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index 86f6c483f6..a5704d81bb 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -382,7 +382,7 @@ Parser literal(std::string_view constant); // A parser that accepts any amount of digit characters Parser digits(); -// A parser that accepts and amount of whitespace +// A parser that accepts any amount of whitespace Parser spaces(); // A parser that accepts space and tab Parser inline_spaces(); From 2df5259074810612fc56ebd0f126218b096ea91a Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:20:04 +0000 Subject: [PATCH 22/31] Update tests/io/applicative.cpp Co-authored-by: Tristan Youngs --- tests/io/applicative.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 32009ed637..6d67c43867 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -147,7 +147,6 @@ TEST(ApplicativeTest, XYZStructure) TEST(ApplicativeTest, Helium) { - std::ifstream infile{"xyz/voxelDensity-helium.xyz"}; ASSERT_TRUE(infile); auto xyz = From 06e44a1a605eed7e09c3db3f726e7364525d8508 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:20:26 +0000 Subject: [PATCH 23/31] Update src/base/applicative.h Co-authored-by: Tristan Youngs --- src/base/applicative.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/applicative.h b/src/base/applicative.h index a5704d81bb..7752a3c0d5 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -21,7 +21,7 @@ namespace parsers template concept TupleLike = requires { std::tuple_size::value; }; -// The simplest defintion of an applicative parser is a function that +// The simplest definition of an applicative parser is a function that // takes a stream and, if the parse succeeds, returns the parsed // value and the rest of the stream. To make life simpler, we // define the parser_output for the return type of the function. From f5c63fb918734b58e504d1755383e82d324b32da Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 13:21:21 +0000 Subject: [PATCH 24/31] Update src/nodes/importXYZStructure.cpp Co-authored-by: Tristan Youngs --- src/nodes/importXYZStructure.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index d555f925b3..f3f081e190 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -40,7 +40,7 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() return read(infile, structure_); } -// Read structure from the specified file parser +// Read structure from the specified input stream NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace parsers; From b292355ae30aa6eba35d39a5f79782002d09b547 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 14:10:14 +0100 Subject: [PATCH 25/31] Rename Parsers namespace --- src/base/applicative.cpp | 4 ++-- src/base/applicative.h | 4 ++-- src/base/parserLibrary.cpp | 6 +++--- src/base/parserLibrary.h | 4 ++-- src/nodes/importXYZStructure.cpp | 2 +- tests/io/applicative.cpp | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index eeda39c852..6097c6dc65 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -7,7 +7,7 @@ #include #include -namespace parsers +namespace Parsers { // A parser that expects an exact string @@ -87,4 +87,4 @@ Parser newlines() { return "\r\n"_p | "\n"_p; } // A quick wrapper for easily making parses from strings Parser operator""_p(const char *text, size_t size) { return literal(std::string_view(text, size)); } -} // namespace parsers +} // namespace Parsers diff --git a/src/base/applicative.h b/src/base/applicative.h index 7752a3c0d5..9365b0d2b0 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -13,7 +13,7 @@ #include #include -namespace parsers +namespace Parsers { // A concept to check if a type is a tuple. This is built in in @@ -431,4 +431,4 @@ template auto operator&(std::string_view other, Parser self) -> return Parser(other) & self; } -}; // namespace parsers +}; // namespace Parsers diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 903101801d..5336fca650 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -4,7 +4,7 @@ #include "base/parserLibrary.h" #include -namespace parsers +namespace Parsers { // A parser that accepts an integer greater than or equal to zero @@ -57,10 +57,10 @@ Parser vector3() .apply([](double x, double y, double z) { return Vector3(x, y, z); }); } -parsers::Parser>> structureAtom() +Parser>> structureAtom() { auto parser = alphas() & inline_spaces() >> vector3() & maybe(inline_spaces() >> real() << maybe(inline_spaces())); return parser; } -} // namespace parsers +} // namespace Parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index aee91246d9..9da5906e29 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -6,7 +6,7 @@ #include "base/applicative.h" #include "math/vector3.h" -namespace parsers +namespace Parsers { // A parser that accepts an integer greater than or equal to zero @@ -21,4 +21,4 @@ Parser real(); Parser vector3(); Parser>> structureAtom(); -}; // namespace parsers +}; // namespace Parsers diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index f3f081e190..4051435031 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -43,7 +43,7 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() // Read structure from the specified input stream NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { - using namespace parsers; + using namespace Parsers; auto xyz = (maybe(inline_spaces()) >> natural() << newlines() & inlines() >> newlines() >> some(structureAtom() << maybe(inline_spaces()) << maybe(newlines()))) .parse(input); diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 6d67c43867..9f8baa8105 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -10,7 +10,7 @@ namespace UnitTest { -using namespace parsers; +using namespace Parsers; using namespace std::literals; template void test_parser(std::string_view input, Parser parser, std::optional expected) From 2ae7f2059d72224f18b6a974acf389988d0da3e6 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 14:22:58 +0100 Subject: [PATCH 26/31] Assort Pr suggestions --- src/base/applicative.cpp | 2 +- src/base/applicative.h | 37 ++++++++-------- src/nodes/importXYZTrajectory.h | 2 +- tests/io/applicative.cpp | 77 ++++++++++++++++----------------- 4 files changed, 58 insertions(+), 60 deletions(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index 6097c6dc65..cab084a242 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -16,7 +16,7 @@ Parser literal(std::string_view constant) { return Parser takeWhile(std::function f) { Parser result( - [f](auto &input) -> parser_output + [f](auto &input) -> ParserOutput { if (input.eof()) return {}; diff --git a/src/base/applicative.h b/src/base/applicative.h index 9365b0d2b0..d000a8aeeb 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -27,13 +27,13 @@ concept TupleLike = requires { std::tuple_size::value; }; // define the parser_output for the return type of the function. // This *could* have further implications because there are more // complicated parsers we could create. -template using parser_output = std::optional>; +template using ParserOutput = std::optional>; // This concept type checks that a given lambda matches the definition // given in the paragraph above. template concept ApParse = requires(Lambda lam, std::istream input) { - { lam(input) } -> std::convertible_to>; + { lam(input) } -> std::convertible_to>; }; // It's fully possible to just use the functions as the parsers, but, @@ -54,7 +54,7 @@ template class Parser template ::value>> Parser(std::string_view constant) : lambda_( - [constant](std::istream &input) -> parser_output + [constant](std::istream &input) -> ParserOutput { for (auto c : constant) if (c != input.get()) @@ -68,13 +68,13 @@ template class Parser // The actual function that we have wrapped. We need to wrap this // in a std::function instead of using a lambda so that we don't // have to handle the exact type of the bound lambda - std::function(std::istream &)> lambda_; + std::function(std::istream &)> lambda_; public: // Parse a string and, if possible, return the value and the remainder - parser_output parse(std::istream &input) const { return lambda_(input); }; + ParserOutput parse(std::istream &input) const { return lambda_(input); }; // Parse a string and, if possible, return the value and the remainder - parser_output operator()(std::istream &input) const { return lambda_(input); } + ParserOutput operator()(std::istream &input) const { return lambda_(input); } // Parse a string and enforce that it parsed the entire input std::optional exact(std::istream &input) const { @@ -90,7 +90,7 @@ template class Parser { auto &method = lambda_; Parser()))> result( - [method, f](std::istream &input) -> parser_output()))> + [method, f](std::istream &input) -> ParserOutput()))> { auto first = method(input); if (first) @@ -120,7 +120,7 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](std::istream &input) -> parser_output + [method, other](std::istream &input) -> ParserOutput { auto first = method(input); if (first) @@ -145,7 +145,7 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](std::istream &input) -> parser_output + [method, other](std::istream &input) -> ParserOutput { auto first = method(input); if (first) @@ -165,7 +165,7 @@ template class Parser { auto &method = lambda_; Parser result( - [method, other](std::istream &input) -> parser_output + [method, other](std::istream &input) -> ParserOutput { auto location = input.tellg(); auto first = method(input); @@ -194,7 +194,7 @@ template class Parser { auto &method = lambda_; Parser> result( - [method, other](std::istream &input) -> parser_output> + [method, other](std::istream &input) -> ParserOutput> { auto first = method(input); if (first) @@ -224,7 +224,7 @@ template class Parser auto &method = lambda_; Parser(), std::make_tuple(std::declval())))> result( [method, other](std::istream &input) - -> parser_output(), std::make_tuple(std::declval())))> + -> ParserOutput(), std::make_tuple(std::declval())))> { auto first = method(input); if (first) @@ -254,7 +254,7 @@ template class Parser auto &method = lambda_; Parser()), std::declval()))> result( [method, other](std::istream &input) - -> parser_output()), std::declval()))> + -> ParserOutput()), std::declval()))> { auto first = method(input); if (first) @@ -283,8 +283,7 @@ template class Parser { auto &method = lambda_; Parser(), std::declval()))> result( - [method, - other](std::istream &input) -> parser_output(), std::declval()))> + [method, other](std::istream &input) -> ParserOutput(), std::declval()))> { auto first = method(input); if (first) @@ -309,14 +308,14 @@ template class Parser // Create a parser that always succeeds and returns a constant value template Parser pure(T constant) { - Parser result([constant](std::istream &input) -> parser_output { return {{constant, input}}; }); + Parser result([constant](std::istream &input) -> ParserOutput { return {{constant, input}}; }); return result; } // A parser that always fails template Parser null() { - Parser result = ([](const auto x) -> parser_output { return std::nullopt; }); + Parser result = ([](const auto x) -> ParserOutput { return std::nullopt; }); return result; } @@ -326,7 +325,7 @@ template Parser null() template Parser> maybe(Parser inner) { Parser> result( - [inner](std::istream &input) -> parser_output> + [inner](std::istream &input) -> ParserOutput> { auto location = input.tellg(); auto first = inner(input); @@ -350,7 +349,7 @@ template Parser> maybe(Parser inner) template Parser> some(Parser inner) { Parser> result( - [inner](std::istream &input) -> parser_output> + [inner](std::istream &input) -> ParserOutput> { std::vector collection; auto location = input.tellg(); diff --git a/src/nodes/importXYZTrajectory.h b/src/nodes/importXYZTrajectory.h index 3feeb7e2b2..7b842f0072 100644 --- a/src/nodes/importXYZTrajectory.h +++ b/src/nodes/importXYZTrajectory.h @@ -28,7 +28,7 @@ class ImportXYZTrajectoryNode : public Node // File path std::string filePath_; // Last read file position - std::streampos filePosition_ = 0; + std::streampos filePosition_{0}; // Structure Structure structure_; diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 9f8baa8105..070ff6694f 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -13,7 +13,7 @@ namespace UnitTest using namespace Parsers; using namespace std::literals; -template void test_parser(std::string_view input, Parser parser, std::optional expected) +template void testParser(std::string_view input, Parser parser, std::optional expected) { std::cout << "Base string:\t" << input << std::endl; std::istringstream stream{std::string(input)}; @@ -23,7 +23,7 @@ template void test_parser(std::string_view input, Parser parser, else EXPECT_EQ(std::get<0>(*result), *expected); } -template void test_exact(std::string_view input, Parser parser, T expected) +template void testExact(std::string_view input, Parser parser, T expected) { std::cout << "Base string:\t" << input << std::endl; std::istringstream stream{std::string(input)}; @@ -34,81 +34,81 @@ template void test_exact(std::string_view input, Parser parser, TEST(ApplicativeTest, BasicStrings) { - test_parser("Foo", "Fo"_p, {"Fo"}); - test_parser("Foobar", "Foo"_p, {"Foo"}); + testParser("Foo", "Fo"_p, {"Fo"}); + testParser("Foobar", "Foo"_p, {"Foo"}); } TEST(ApplicativeTest, Ignoring) { - test_exact("Foobar", "Foo"_p << "bar"_p, "Foo"sv); - test_exact("Foobar", "Foo" >> "bar"_p, "bar"sv); + testExact("Foobar", "Foo"_p << "bar"_p, "Foo"sv); + testExact("Foobar", "Foo" >> "bar"_p, "bar"sv); } TEST(ApplicativeTest, Joining) { - test_exact("Foobar", "Foo"_p & "bar", {"Foo", "bar"}); - test_parser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{1, 2, 3, 4}}); - test_exact("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {"Fo", "ob", "a", "r"}); - test_exact("Foobar", ("Fo"_p & "ob") & "ar", {"Fo", "ob", "ar"}); - test_exact("Foobar", "Fo" & ("ob"_p & "ar"), {"Fo", "ob", "ar"}); - test_parser("Foobar", (pure(1) & pure(2)) & pure(3), {{1, 2, 3}}); - test_parser("Foobar", pure(1) & (pure(2) & pure(3)), {{1, 2, 3}}); + testExact("Foobar", "Foo"_p & "bar", {"Foo", "bar"}); + testParser("Foobar", (pure(1) & pure(2)) & (pure(3) & pure(4)), {{1, 2, 3, 4}}); + testExact("Foobar", ("Fo"_p & "ob") & ("a" & "r"_p), {"Fo", "ob", "a", "r"}); + testExact("Foobar", ("Fo"_p & "ob") & "ar", {"Fo", "ob", "ar"}); + testExact("Foobar", "Fo" & ("ob"_p & "ar"), {"Fo", "ob", "ar"}); + testParser("Foobar", (pure(1) & pure(2)) & pure(3), {{1, 2, 3}}); + testParser("Foobar", pure(1) & (pure(2) & pure(3)), {{1, 2, 3}}); } TEST(ApplicativeTest, Choices) { - test_exact("Foo", "Foo"_p | "Bar", "Foo"sv); - test_exact("Bar", "Foo"_p | "Bar", "Bar"sv); - test_parser("Quux", "Foo"_p | "Bar", {}); + testExact("Foo", "Foo"_p | "Bar", "Foo"sv); + testExact("Bar", "Foo"_p | "Bar", "Bar"sv); + testParser("Quux", "Foo"_p | "Bar", {}); } TEST(ApplicativeTest, NaturalNumbers) { - test_parser("123foo", natural(), {123}); + testParser("123foo", natural(), {123}); auto triplet = natural() & "," >> natural() & "," >> natural(); - test_exact("123,456,789", triplet, {123, 456, 789}); + testExact("123,456,789", triplet, {123, 456, 789}); auto vecsum = triplet.apply([](const auto x, const auto y, const auto z) -> int { return x + y + z; }); - test_exact("123,456,789", vecsum, 123 + 456 + 789); + testExact("123,456,789", vecsum, 123 + 456 + 789); } TEST(ApplicativeTest, Optionals) { - test_exact("123,456", maybe(natural() << ",") & natural(), {{123}, 456}); - test_exact("456", maybe(natural() << ",") & natural(), {std::nullopt, 456}); - test_exact("-456", integer(), -456); - test_exact("456", integer(), 456); + testExact("123,456", maybe(natural() << ",") & natural(), {{123}, 456}); + testExact("456", maybe(natural() << ",") & natural(), {std::nullopt, 456}); + testExact("-456", integer(), -456); + testExact("456", integer(), 456); } TEST(ApplicativeTest, MultipleTerms) { // Parse multiple terms - test_exact("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {123, 456, 789, 12}); + testExact("123, 456, 789,012", some(integer() << maybe(","_p << maybe(spaces()))), {123, 456, 789, 12}); // Completely fail the parse if no copies are present - test_parser("789", some(integer() << ","), {}); + testParser("789", some(integer() << ","), {}); } TEST(ApplicativeTest, RealNumbers) { - test_exact("-12.0543", real(), -12.0543); - test_exact("1.02E-3", real(), 1.02e-3); - test_exact("-3E-4", real(), -3e-4); - test_exact("-71.2e3", real(), -71.2e3); + testExact("-12.0543", real(), -12.0543); + testExact("1.02E-3", real(), 1.02e-3); + testExact("-3E-4", real(), -3e-4); + testExact("-71.2e3", real(), -71.2e3); } TEST(ApplicativeTest, BasicParser) { - test_parser(" \t foo", spaces(), {" \t "}); - test_exact("HW", alphas(), "HW"s); - test_parser("1qaz.QAZ foo", graphs(), {"1qaz.QAZ"}); - test_parser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), {{"1", "qaz", ".", "QAZ", " "}}); + testParser(" \t foo", spaces(), {" \t "}); + testExact("HW", alphas(), "HW"s); + testParser("1qaz.QAZ foo", graphs(), {"1qaz.QAZ"}); + testParser("1qaz.QAZ foo", digits() & lowers() & punctuations() & uppers() & spaces(), {{"1", "qaz", ".", "QAZ", " "}}); - test_exact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"s); + testExact("\"Foo\"", "\"" >> alphas() << "\"", "Foo"s); } -TEST(ApplicativeTest, Vector) { test_exact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } +TEST(ApplicativeTest, Vector) { testExact("1 2.5 -3e-1", vector3(), Vector3(1, 2.5, -3e-1)); } TEST(ApplicativeTest, StructureAtom) { - test_exact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); - test_exact("He 0.5 0.5 0.5", structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); + testExact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); + testExact("He 0.5 0.5 0.5", structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); } TEST(ApplicativeTest, XYZStructure) @@ -118,7 +118,6 @@ TEST(ApplicativeTest, XYZStructure) auto xyz = (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) .parse(infile); - // auto xyz = (maybe(spaces()) >> natural() ).parse(oss.view()); ASSERT_TRUE(xyz); auto &[value, rest] = *xyz; @@ -157,7 +156,7 @@ TEST(ApplicativeTest, Helium) EXPECT_EQ(rest.get(), -1); auto &terms = std::get<1>(value); EXPECT_EQ(terms.size(), std::get<0>(value)); - int index = 0; + auto index = 0; for (auto &[elem, r, q] : terms) EXPECT_EQ(elem, "He"); } From f59e75c08db8342409e95d50aa81ee5a3b775574 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 14:30:26 +0100 Subject: [PATCH 27/31] Rename inlineSpaces --- src/base/applicative.cpp | 2 +- src/base/applicative.h | 2 +- src/base/parserLibrary.cpp | 2 +- src/nodes/importXYZStructure.cpp | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/base/applicative.cpp b/src/base/applicative.cpp index cab084a242..4030614e44 100644 --- a/src/base/applicative.cpp +++ b/src/base/applicative.cpp @@ -36,7 +36,7 @@ Parser spaces() return takeWhile([](const char c) { return isspace(c); }); } // A parser that accepts space and tab -Parser inline_spaces() +Parser inlineSpaces() { return takeWhile([](const char c) { return c == ' ' || c == '\t'; }); } diff --git a/src/base/applicative.h b/src/base/applicative.h index d000a8aeeb..d7f1a786b5 100644 --- a/src/base/applicative.h +++ b/src/base/applicative.h @@ -384,7 +384,7 @@ Parser digits(); // A parser that accepts any amount of whitespace Parser spaces(); // A parser that accepts space and tab -Parser inline_spaces(); +Parser inlineSpaces(); // A parser that accepts any amount of visible characters Parser graphs(); diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 5336fca650..9db1724be3 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -59,7 +59,7 @@ Parser vector3() Parser>> structureAtom() { - auto parser = alphas() & inline_spaces() >> vector3() & maybe(inline_spaces() >> real() << maybe(inline_spaces())); + auto parser = alphas() & inlineSpaces() >> vector3() & maybe(inlineSpaces() >> real() << maybe(inlineSpaces())); return parser; } diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index 4051435031..ae5ccfdccb 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -44,8 +44,8 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace Parsers; - auto xyz = (maybe(inline_spaces()) >> natural() << newlines() & - inlines() >> newlines() >> some(structureAtom() << maybe(inline_spaces()) << maybe(newlines()))) + auto xyz = (maybe(inlineSpaces()) >> natural() << newlines() & + inlines() >> newlines() >> some(structureAtom() << maybe(inlineSpaces()) << maybe(newlines()))) .parse(input); if (!xyz) From 09d8bcc549ddc1f3e806e569c109dda27635da6b Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 14:34:07 +0100 Subject: [PATCH 28/31] Sort cmake file --- src/base/CMakeLists.txt | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 7cee7f7115..90b0642706 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -1,29 +1,29 @@ add_library( base applicative.cpp - parserLibrary.cpp cbor.cpp - enumOptionsBase.cpp - geometry.cpp - lineParser.cpp - lock.cpp - messenger.cpp - outputHandler.cpp - sysFunc.cpp - timer.cpp - units.cpp - version.cpp enumOption.h - enumOptionsBase.h enumOptions.h + enumOptionsBase.cpp + enumOptionsBase.h + geometry.cpp geometry.h + lineParser.cpp lineParser.h + lock.cpp lock.h + messenger.cpp messenger.h + outputHandler.cpp outputHandler.h + parserLibrary.cpp + sysFunc.cpp sysFunc.h + timer.cpp timer.h + units.cpp units.h + version.cpp version.h ) From 49739935b86cec59d520971cbba8786601290e8c Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Wed, 24 Jun 2026 15:18:58 +0100 Subject: [PATCH 29/31] Move more Structure parsing code into node --- src/base/parserLibrary.cpp | 6 ------ src/base/parserLibrary.h | 1 - src/nodes/importXYZStructure.cpp | 19 ++++++++++++++++--- src/nodes/importXYZStructure.h | 5 +++++ tests/io/applicative.cpp | 13 +++++-------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 9db1724be3..4cfa197632 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -57,10 +57,4 @@ Parser vector3() .apply([](double x, double y, double z) { return Vector3(x, y, z); }); } -Parser>> structureAtom() -{ - auto parser = alphas() & inlineSpaces() >> vector3() & maybe(inlineSpaces() >> real() << maybe(inlineSpaces())); - return parser; -} - } // namespace Parsers diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index 9da5906e29..40a5d92322 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -19,6 +19,5 @@ Parser integer(); Parser real(); Parser vector3(); -Parser>> structureAtom(); }; // namespace Parsers diff --git a/src/nodes/importXYZStructure.cpp b/src/nodes/importXYZStructure.cpp index ae5ccfdccb..4313b2e30b 100644 --- a/src/nodes/importXYZStructure.cpp +++ b/src/nodes/importXYZStructure.cpp @@ -44,9 +44,7 @@ NodeConstants::ProcessResult ImportXYZStructureNode::process() NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, Structure &structure) { using namespace Parsers; - auto xyz = (maybe(inlineSpaces()) >> natural() << newlines() & - inlines() >> newlines() >> some(structureAtom() << maybe(inlineSpaces()) << maybe(newlines()))) - .parse(input); + auto xyz = structureBlock().parse(input); if (!xyz) return NodeConstants::ProcessResult::Failed; @@ -60,3 +58,18 @@ NodeConstants::ProcessResult ImportXYZStructureNode::read(std::istream &input, S return NodeConstants::ProcessResult::Success; } + +Parsers::Parser>> ImportXYZStructureNode::structureAtom() +{ + using namespace Parsers; + auto parser = alphas() & inlineSpaces() >> vector3() & maybe(inlineSpaces() >> real() << maybe(inlineSpaces())); + return parser; +} + +Parsers::Parser>>>> +ImportXYZStructureNode::structureBlock() +{ + using namespace Parsers; + return maybe(inlineSpaces()) >> natural() << newlines() & + inlines() >> newlines() >> some(structureAtom() << maybe(inlineSpaces()) << maybe(newlines())); +} diff --git a/src/nodes/importXYZStructure.h b/src/nodes/importXYZStructure.h index 02f9218d98..1838396920 100644 --- a/src/nodes/importXYZStructure.h +++ b/src/nodes/importXYZStructure.h @@ -3,6 +3,7 @@ #pragma once +#include "base/applicative.h" #include "classes/structure.h" #include "nodes/node.h" @@ -40,4 +41,8 @@ class ImportXYZStructureNode : public Node public: // Read structure from the specified file parser static NodeConstants::ProcessResult read(std::istream &input, Structure &structure); + static Parsers::Parser>> structureAtom(); + + static Parsers::Parser>>>> + structureBlock(); }; diff --git a/tests/io/applicative.cpp b/tests/io/applicative.cpp index 070ff6694f..7660ec5357 100644 --- a/tests/io/applicative.cpp +++ b/tests/io/applicative.cpp @@ -3,6 +3,7 @@ #include "base/applicative.h" #include "base/parserLibrary.h" +#include "nodes/importXYZStructure.h" #include #include #include @@ -107,17 +108,15 @@ TEST(ApplicativeTest, Vector) { testExact("1 2.5 -3e-1", vector3(), Vector3(1, 2 TEST(ApplicativeTest, StructureAtom) { - testExact("HW 1 2.5 -3e-4 5.6", structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); - testExact("He 0.5 0.5 0.5", structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); + testExact("HW 1 2.5 -3e-4 5.6", ImportXYZStructureNode::structureAtom(), {"HW", Vector3(1, 2.5, -3e-4), 5.6}); + testExact("He 0.5 0.5 0.5", ImportXYZStructureNode::structureAtom(), {"He", Vector3(0.5, 0.5, 0.5), {}}); } TEST(ApplicativeTest, XYZStructure) { std::ifstream infile{"xyz/c2so3.xyz"}; ASSERT_TRUE(infile); - auto xyz = - (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) - .parse(infile); + auto xyz = ImportXYZStructureNode::structureBlock().parse(infile); ASSERT_TRUE(xyz); auto &[value, rest] = *xyz; @@ -148,9 +147,7 @@ TEST(ApplicativeTest, Helium) { std::ifstream infile{"xyz/voxelDensity-helium.xyz"}; ASSERT_TRUE(infile); - auto xyz = - (maybe(spaces()) >> natural() << spaces() & inlines() >> newlines() >> some(structureAtom() << maybe(newlines()))) - .parse(infile); + auto xyz = ImportXYZStructureNode::structureBlock().parse(infile); ASSERT_TRUE(xyz); auto &[value, rest] = *xyz; EXPECT_EQ(rest.get(), -1); From 17dc164715f7fd3175f08f432b4e2d876d3a64c6 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Fri, 26 Jun 2026 10:56:55 +0000 Subject: [PATCH 30/31] Update src/base/parserLibrary.h Co-authored-by: Tristan Youngs --- src/base/parserLibrary.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/base/parserLibrary.h b/src/base/parserLibrary.h index 40a5d92322..25772cc015 100644 --- a/src/base/parserLibrary.h +++ b/src/base/parserLibrary.h @@ -18,6 +18,7 @@ Parser integer(); // A parser that accepts a real, floating point number Parser real(); +// A parser that accepts a 3-vector of floating point numbers Parser vector3(); }; // namespace Parsers From e4006cc264c1564399014b23ab20e84c3800ceaf Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Fri, 26 Jun 2026 10:57:01 +0000 Subject: [PATCH 31/31] Update src/base/parserLibrary.cpp Co-authored-by: Tristan Youngs --- src/base/parserLibrary.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/base/parserLibrary.cpp b/src/base/parserLibrary.cpp index 4cfa197632..fe5202978f 100644 --- a/src/base/parserLibrary.cpp +++ b/src/base/parserLibrary.cpp @@ -51,6 +51,7 @@ Parser real() return result.apply(nat2dbl); } +// A parser that accepts a 3-vector of floating point numbers Parser vector3() { return (real() & spaces() >> real() & spaces() >> real())