commit 6bab8338f8ab4d49341a224246a5928533343721 Author: Ryan Date: Tue Dec 24 22:00:48 2024 +0100 Hello libcode-query diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c96e1ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +.bdep/ + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..81e5d42 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-query + +![Build status](https://code.helloryan.se/code/libcode-query/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-query repository. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..ae85f47 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-query + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..4d55b73 --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/query/ +} + +export $out_root/code/query/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/query/.gitignore b/code/query/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/query/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/code/query/boolean.cxx b/code/query/boolean.cxx new file mode 100644 index 0000000..57335b3 --- /dev/null +++ b/code/query/boolean.cxx @@ -0,0 +1,31 @@ +#include + +namespace code::query +{ + + boolean_t:: + boolean_t(source_location_t origin, bool value) + : origin_{move(origin)}, value_{value} + {} + + source_location_t const& + boolean_t:: + origin() const + { + return origin_; + } + + bool + boolean_t:: + value() const + { + return value_; + } + + string + to_string(boolean_t const& boolean) + { + return boolean.value() ? "true" : "false"; + } + +} // namespace code::query diff --git a/code/query/boolean.hxx b/code/query/boolean.hxx new file mode 100644 index 0000000..667ab87 --- /dev/null +++ b/code/query/boolean.hxx @@ -0,0 +1,32 @@ +#ifndef code__query__boolean_hxx_ +#define code__query__boolean_hxx_ + +#include +#include + +namespace code::query +{ + + class boolean_t + { + public: + boolean_t(source_location_t, bool); + + source_location_t const& + origin() const; + + bool + value() const; + + private: + source_location_t origin_; + bool value_; + + }; + + string + to_string(boolean_t const&); + +} // namespace code::query + +#endif diff --git a/code/query/buildfile b/code/query/buildfile new file mode 100644 index 0000000..70e5bd4 --- /dev/null +++ b/code/query/buildfile @@ -0,0 +1,62 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +./: lib{code-query}: libul{code-query} + +libul{code-query}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-query}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{code-query}: bin.whole = false +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{code-query}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{code-query}: bin.lib.version = "-$version.project_id" +else + lib{code-query}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/query/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/query/ + install.subdirs = true +} diff --git a/code/query/concepts.hxx b/code/query/concepts.hxx new file mode 100644 index 0000000..3d0b81c --- /dev/null +++ b/code/query/concepts.hxx @@ -0,0 +1,25 @@ +#ifndef code__query__concepts_hxx_ +#define code__query__concepts_hxx_ + +#include +#include + +#include + +namespace code::query +{ + + class expression_t; + + template + concept Expression = requires(E const& e) + { + { e.origin() } -> std::convertible_to; + + { to_string(e) } -> std::convertible_to; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/error.cxx b/code/query/error.cxx new file mode 100644 index 0000000..0214298 --- /dev/null +++ b/code/query/error.cxx @@ -0,0 +1,46 @@ +#include + +namespace code::query +{ + + warning_t:: + warning_t(source_location_t origin, string description) + : origin_{move(origin)}, + description_{move(description)} + {} + + source_location_t const& + warning_t:: + origin() const + { + return origin_; + } + + string const& + warning_t:: + description() const + { + return description_; + } + + error_t:: + error_t(source_location_t origin, string description) + : origin_{move(origin)}, + description_{move(description)} + {} + + source_location_t const& + error_t:: + origin() const + { + return origin_; + } + + string const& + error_t:: + description() const + { + return description_; + } + +} // namespace code::query diff --git a/code/query/error.hxx b/code/query/error.hxx new file mode 100644 index 0000000..e78f0ae --- /dev/null +++ b/code/query/error.hxx @@ -0,0 +1,46 @@ +#ifndef code__query__error_hxx_ +#define code__query__error_hxx_ + +#include +#include + +namespace code::query +{ + + class warning_t + { + public: + warning_t(source_location_t, string); + + source_location_t const& + origin() const; + + string const& + description() const; + + private: + source_location_t origin_; + string description_; + + }; + + class error_t + { + public: + error_t(source_location_t source, string); + + source_location_t const& + origin() const; + + string const& + description() const; + + private: + source_location_t origin_; + string description_; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/expression.cxx b/code/query/expression.cxx new file mode 100644 index 0000000..c233e82 --- /dev/null +++ b/code/query/expression.cxx @@ -0,0 +1,25 @@ +#include + +namespace code::query +{ + + source_location_t const& + expression_t:: + origin() const + { + return container_->origin_(); + } + + void + accept(expression_t const& e, visitor_t& v) + { + e.container_->accept_(v); + } + + string + to_string(expression_t const& e) + { + return e.container_->to_string_(); + } + +} // namespace code::query diff --git a/code/query/expression.hxx b/code/query/expression.hxx new file mode 100644 index 0000000..73adad3 --- /dev/null +++ b/code/query/expression.hxx @@ -0,0 +1,95 @@ +#ifndef code__query__expression_hxx_ +#define code__query__expression_hxx_ + +#include +#include +#include +#include +#include + +namespace code::query +{ + +class expression_t +{ +public: + template + expression_t(E e) + : container_{make_shared>(move(e))} + {} + + source_location_t const& + origin() const; + + friend + void + accept(expression_t const& e, visitor_t& v); + + friend + string + to_string(expression_t const& e); + +private: + struct abstract_t + { + virtual + ~abstract_t() noexcept = default; + + virtual + source_location_t const& + origin_() const = 0; + + virtual + void + accept_(visitor_t& v) const = 0; + + virtual + string + to_string_() const = 0; + + }; + + template + struct container_t + : abstract_t + { + explicit + container_t(E e) + : e{move(e)} + {} + + ~container_t() noexcept override + {} + + source_location_t const& + origin_() const override + { + return e.origin(); + } + + void + accept_(visitor_t& v) const override + { + if (auto c = dynamic_cast*>(&v); c) { + c->visit(e); + return; + } + + v.visit_default(); + } + + string + to_string_() const override + { + return to_string(e); + } + + E e; + }; + + shared_ptr container_; +}; + +} // namespace code::query + +#endif diff --git a/code/query/lexical-analyzer.cxx b/code/query/lexical-analyzer.cxx new file mode 100644 index 0000000..e5070bb --- /dev/null +++ b/code/query/lexical-analyzer.cxx @@ -0,0 +1,124 @@ +#include + +#include + +namespace code::query +{ + + lexical_analyzer_t:: + ~lexical_analyzer_t() noexcept = default; + + token_t + lexical_analyzer_t:: + peek() + { + if (!current_) + current_ = extract(); + + return *current_; + } + + void + lexical_analyzer_t:: + consume() + { + current_ = extract(); + } + + lexical_analyzer_t:: + lexical_analyzer_t() = default; + + token_t + lexical_analyzer_t:: + extract() + { + char_type c = peek_char(); + + while (is_whitespace(c)) { + consume_char(); + c = peek_char(); + } + + if (c == 0) + return token_t{token_type_t::end}; + + if (c == ':') { + consume_char(); + return token_t{token_type_t::colon}; + } + + if (c == '(') { + consume_char(); + return token_t{token_type_t::open_parens}; + } + + if (c == ')') { + consume_char(); + return token_t{token_type_t::close_parens}; + } + + if (c == '"') { + consume_char(); + + c = peek_char(); + + std::stringstream str; + + while (c != 0 && c != '"') { + str << (char)c; // FIXME: UTF-8 encode. + consume_char(); + c = peek_char(); + } + + consume_char(); + + return token_t{token_type_t::quoted_term, move(str.str())}; + } + + std::stringstream str; + + while (c != 0 && !is_whitespace(c) && c != '(' && c != ')' && c != ':') { + consume_char(); + str << (char)c; // FIXME: UFT-8 encode. + c = peek_char(); + } + + auto term = str.str(); + + if (term == "AND") + return token_t{token_type_t::logical_and}; + + if (term == "OR") + return token_t{token_type_t::logical_or}; + + if (term == "NOT") + return token_t{token_type_t::logical_not}; + + return token_t{token_type_t::simple_term, move(term)}; + } + + lexical_analyzer_t::char_type + lexical_analyzer_t:: + peek_char() + { + if (!next_char_) + next_char_ = extract_char(); + + return *next_char_; + } + + void + lexical_analyzer_t:: + consume_char() + { + next_char_ = extract_char(); + } + + bool + lexical_analyzer_t:: + is_whitespace(char_type c) + { + return c == ' ' || c == '\t' || c == '\v'; + } + +} // namespace code::query diff --git a/code/query/lexical-analyzer.hxx b/code/query/lexical-analyzer.hxx new file mode 100644 index 0000000..1c9c3b9 --- /dev/null +++ b/code/query/lexical-analyzer.hxx @@ -0,0 +1,94 @@ +#ifndef code__query__lexical_analyzer_hxx_ +#define code__query__lexical_analyzer_hxx_ + +#include +#include + +namespace code::query +{ + + class lexical_analyzer_t + { + public: + using char_type = uint32_t; + + virtual + ~lexical_analyzer_t() noexcept; + + token_t + peek(); + + void + consume(); + + protected: + lexical_analyzer_t(); + + lexical_analyzer_t(lexical_analyzer_t const&) = delete; + lexical_analyzer_t(lexical_analyzer_t&&) = delete; + + token_t + extract(); + + char_type + peek_char(); + + void + consume_char(); + + virtual + char_type + extract_char() = 0; + + lexical_analyzer_t& operator=(lexical_analyzer_t const&) = delete; + lexical_analyzer_t& operator=(lexical_analyzer_t&&) = delete; + + static + bool + is_whitespace(char_type); + + private: + optional next_char_; + optional current_; + + }; + + template + class basic_lexical_analyzer_t + : public lexical_analyzer_t + { + public: + using iterator = Iterator; + using end_iterator = EndIterator; + + basic_lexical_analyzer_t(iterator, end_iterator); + + basic_lexical_analyzer_t(basic_lexical_analyzer_t const&); + + basic_lexical_analyzer_t(basic_lexical_analyzer_t&&); + + ~basic_lexical_analyzer_t() noexcept override; + + basic_lexical_analyzer_t& + operator=(basic_lexical_analyzer_t const&) = delete; + + basic_lexical_analyzer_t& + operator=(basic_lexical_analyzer_t&&) = delete; + + protected: + char_type + extract_char() override; + + private: + iterator current_; + end_iterator end_; + + }; + + using string_lexical_analyzer_t = basic_lexical_analyzer_t; + +} // namespace code::query + +#include + +#endif diff --git a/code/query/lexical-analyzer.txx b/code/query/lexical-analyzer.txx new file mode 100644 index 0000000..bdf693b --- /dev/null +++ b/code/query/lexical-analyzer.txx @@ -0,0 +1,40 @@ +namespace code::query +{ + + template + basic_lexical_analyzer_t:: + basic_lexical_analyzer_t(iterator begin, end_iterator end) + : current_{move(begin)}, + end_{move(end)} + {} + + template + basic_lexical_analyzer_t:: + basic_lexical_analyzer_t(basic_lexical_analyzer_t const& other) + : current_{other.current_}, + end_{other.end_} + {} + + template + basic_lexical_analyzer_t:: + basic_lexical_analyzer_t(basic_lexical_analyzer_t&& other) + : current_{move(other.current_)}, + end_{move(other.end_)} + {} + + template + basic_lexical_analyzer_t:: + ~basic_lexical_analyzer_t() noexcept = default; + + template + basic_lexical_analyzer_t::char_type + basic_lexical_analyzer_t:: + extract_char() + { + if (current_ == end_) + return 0; + + return *current_++; + } + +} // namespace code::query diff --git a/code/query/logical-and.cxx b/code/query/logical-and.cxx new file mode 100644 index 0000000..fd26eee --- /dev/null +++ b/code/query/logical-and.cxx @@ -0,0 +1,56 @@ +#include + +#include + +namespace code::query +{ + + logical_and_t:: + logical_and_t(expression_t left, expression_t right) + : left_{move(left)}, + right_{move(right)} + {} + + logical_and_t:: + logical_and_t(source_location_t origin, + expression_t left, + expression_t right) + : origin_{move(origin)}, + left_{move(left)}, + right_{move(right)} + {} + + source_location_t const& + logical_and_t:: + origin() const + { + return origin_; + } + + expression_t const& + logical_and_t:: + left() const + { + return left_; + } + + expression_t const& + logical_and_t:: + right() const + { + return right_; + } + + string + to_string(logical_and_t const& e) + { + std::stringstream str; + + str << to_string(e.left()); + str << " AND "; + str << to_string(e.right()); + + return str.str(); + } + +} // namespace code::query diff --git a/code/query/logical-and.hxx b/code/query/logical-and.hxx new file mode 100644 index 0000000..7180c78 --- /dev/null +++ b/code/query/logical-and.hxx @@ -0,0 +1,39 @@ +#ifndef code__query__logical_and_hxx_ +#define code__query__logical_and_hxx_ + +#include +#include +#include + +namespace code::query +{ + + class logical_and_t + { + public: + logical_and_t(expression_t, expression_t); + + logical_and_t(source_location_t, expression_t, expression_t); + + source_location_t const& + origin() const; + + expression_t const& + left() const; + + expression_t const& + right() const; + + private: + source_location_t origin_; + expression_t left_; + expression_t right_; + + }; + + string + to_string(logical_and_t const); + +} // namespace code::query + +#endif diff --git a/code/query/logical-not.cxx b/code/query/logical-not.cxx new file mode 100644 index 0000000..490774b --- /dev/null +++ b/code/query/logical-not.cxx @@ -0,0 +1,43 @@ +#include + +#include + +namespace code::query +{ + + logical_not_t:: + logical_not_t(expression_t right) + : right_{move(right)} + {} + + logical_not_t:: + logical_not_t(source_location_t origin, expression_t right) + : origin_{move(origin)}, + right_{move(right)} + {} + + source_location_t const& + logical_not_t:: + origin() const + { + return origin_; + } + + expression_t const& + logical_not_t:: + right() const + { + return right_; + } + + string + to_string(logical_not_t const& e) + { + std::stringstream str; + + str << "NOT " << to_string(e.right()); + + return str.str(); + } + +} // namespace code::query diff --git a/code/query/logical-not.hxx b/code/query/logical-not.hxx new file mode 100644 index 0000000..a4d180a --- /dev/null +++ b/code/query/logical-not.hxx @@ -0,0 +1,34 @@ +#ifndef code__query__logical_not_t_hxx_ +#define code__query__logical_not_t_hxx_ + +#include +#include + +namespace code::query +{ + + class logical_not_t + { + public: + explicit + logical_not_t(expression_t); + + logical_not_t(source_location_t, expression_t); + + source_location_t const& + origin() const; + + expression_t const& + right() const; + + private: + source_location_t origin_; + expression_t right_; + }; + + string + to_string(logical_not_t const& e); + +} // namespace code::query + +#endif diff --git a/code/query/logical-or.cxx b/code/query/logical-or.cxx new file mode 100644 index 0000000..2912a9c --- /dev/null +++ b/code/query/logical-or.cxx @@ -0,0 +1,56 @@ +#include + +#include + +namespace code::query +{ + + logical_or_t:: + logical_or_t(expression_t left, expression_t right) + : left_{move(left)}, + right_{move(right)} + {} + + logical_or_t:: + logical_or_t(source_location_t origin, + expression_t left, + expression_t right) + : origin_{move(origin)}, + left_{move(left)}, + right_{move(right)} + {} + + source_location_t const& + logical_or_t:: + origin() const + { + return origin_; + } + + expression_t const& + logical_or_t:: + left() const + { + return left_; + } + + expression_t const& + logical_or_t:: + right() const + { + return right_; + } + + string + to_string(logical_or_t const& e) + { + std::stringstream str; + + str << to_string(e.left()); + str << " or "; + str << to_string(e.right()); + + return str.str(); + } + +} // namespace code::query diff --git a/code/query/logical-or.hxx b/code/query/logical-or.hxx new file mode 100644 index 0000000..603f3ba --- /dev/null +++ b/code/query/logical-or.hxx @@ -0,0 +1,39 @@ +#ifndef code__query__logical_or_hxx_ +#define code__query__logical_or_hxx_ + +#include +#include +#include + +namespace code::query +{ + + class logical_or_t + { + public: + logical_or_t(expression_t, expression_t); + + logical_or_t(source_location_t, expression_t, expression_t); + + source_location_t const& + origin() const; + + expression_t const& + left() const; + + expression_t const& + right() const; + + private: + source_location_t origin_; + expression_t left_; + expression_t right_; + + }; + + string + to_string(logical_or_t const); + +} // namespace code::query + +#endif diff --git a/code/query/match.cxx b/code/query/match.cxx new file mode 100644 index 0000000..c9b6bcc --- /dev/null +++ b/code/query/match.cxx @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace code::query +{ + + bool + match(query_t const& q, predicate_t const& p) + { + class matcher_t + : public visitor_t, + public basic_visitor_t, + public basic_visitor_t, + public basic_visitor_t, + public basic_visitor_t, + public basic_visitor_t, + public basic_visitor_t, + public basic_visitor_t + { + public: + static + bool + match(expression_t const& e, predicate_t const& p) + { + matcher_t m{p}; + accept(e, m); + return m.result; + } + + private: + explicit + matcher_t(predicate_t const& p) + : predicate_{p} + {} + + void + visit_default() override + { + result = false; + } + + void + visit(term_t const& e) override + { + result = predicate_(e); + } + + void + visit(tag_t const& e) override + { + result = predicate_(e); + } + + void + visit(boolean_t const& e) override + { + result = e.value(); + } + + void + visit(logical_and_t const& e) override + { + result = match(e.left(), predicate_) && match(e.right(), predicate_); + } + + void + visit(logical_or_t const& e) override + { + result = match(e.left(), predicate_) || match(e.right(), predicate_); + } + + void + visit(logical_not_t const& e) override + { + result = !match(e.right(), predicate_); + } + + void + visit(parenthesized_t const& e) override + { + result = match(e.expr(), predicate_); + } + + predicate_t const& predicate_; + bool result{}; + + }; + + return matcher_t::match(q.expr(), p); + } + +} // namespace code::query diff --git a/code/query/match.hxx b/code/query/match.hxx new file mode 100644 index 0000000..81ae5a1 --- /dev/null +++ b/code/query/match.hxx @@ -0,0 +1,22 @@ +#ifndef code__query__match_hxx_ +#define code__query__match_hxx_ + +#include + +#include +#include +#include + +namespace code::query +{ + + using predicate_t = function< + bool(variant const&) + >; + + bool + match(query_t const& q, predicate_t const& p); + +} // namespace code::query + +#endif diff --git a/code/query/parenthesized.cxx b/code/query/parenthesized.cxx new file mode 100644 index 0000000..5955788 --- /dev/null +++ b/code/query/parenthesized.cxx @@ -0,0 +1,43 @@ +#include + +#include + +namespace code::query +{ + + parenthesized_t:: + parenthesized_t(expression_t expr) + : expr_{move(expr)} + {} + + parenthesized_t:: + parenthesized_t(source_location_t origin, expression_t expr) + : origin_{move(origin)}, + expr_{move(expr)} + {} + + source_location_t const& + parenthesized_t:: + origin() const + { + return origin_; + } + + expression_t const& + parenthesized_t:: + expr() const + { + return expr_; + } + + string + to_string(parenthesized_t const& e) + { + std::stringstream str; + + str << '(' << to_string(e.expr()) << ')'; + + return str.str(); + } + +} // namespace code::query diff --git a/code/query/parenthesized.hxx b/code/query/parenthesized.hxx new file mode 100644 index 0000000..8ffa49e --- /dev/null +++ b/code/query/parenthesized.hxx @@ -0,0 +1,35 @@ +#ifndef code__query__parenthesized_hxx_ +#define code__query__parenthesized_hxx_ + +#include +#include + +namespace code::query +{ + + class parenthesized_t + { + public: + explicit + parenthesized_t(expression_t); + + parenthesized_t(source_location_t, expression_t); + + source_location_t const& + origin() const; + + expression_t const& + expr() const; + + private: + source_location_t origin_; + expression_t expr_; + + }; + + string + to_string(parenthesized_t const& e); + +} // namespace code::query + +#endif diff --git a/code/query/parse-context.cxx b/code/query/parse-context.cxx new file mode 100644 index 0000000..044e32f --- /dev/null +++ b/code/query/parse-context.cxx @@ -0,0 +1,38 @@ +#include + +namespace code::query +{ + + parse_context_t:: + parse_context_t() + {} + + vector const& + parse_context_t:: + warnings() const + { + return warnings_; + } + + vector const& + parse_context_t:: + errors() const + { + return errors_; + } + + void + parse_context_t:: + report_warning(warning_t w) + { + warnings_.emplace_back(move(w)); + } + + void + parse_context_t:: + report_error(error_t e) + { + errors_.emplace_back(move(e)); + } + +} // namespace code::query diff --git a/code/query/parse-context.hxx b/code/query/parse-context.hxx new file mode 100644 index 0000000..310e9ef --- /dev/null +++ b/code/query/parse-context.hxx @@ -0,0 +1,36 @@ +#ifndef code__query__parse_context_hxx_ +#define code__query__parse_context_hxx_ + +#include +#include +#include + +namespace code::query +{ + + class parse_context_t + { + public: + parse_context_t(); + + vector const& + warnings() const; + + vector const& + errors() const; + + void + report_warning(warning_t); + + void + report_error(error_t); + + private: + vector warnings_; + vector errors_; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/parse.cxx b/code/query/parse.cxx new file mode 100644 index 0000000..beb2a10 --- /dev/null +++ b/code/query/parse.cxx @@ -0,0 +1,17 @@ +#include + +#include +#include + +namespace code::query +{ + + optional + try_parse(string const& q, parse_context_t& context) + { + string_lexical_analyzer_t lexer{q.begin(), q.end()}; + + return syntactical_analyzer_t{lexer, context}.try_parse(); + } + +} // namespace code::query diff --git a/code/query/parse.hxx b/code/query/parse.hxx new file mode 100644 index 0000000..bab6f99 --- /dev/null +++ b/code/query/parse.hxx @@ -0,0 +1,16 @@ +#ifndef code__query__parse_hxx_ +#define code__query__parse_hxx_ + +#include +#include +#include + +namespace code::query +{ + + optional + try_parse(string const&, parse_context_t&); + +} // namespace code::query + +#endif diff --git a/code/query/query.cxx b/code/query/query.cxx new file mode 100644 index 0000000..178ba87 --- /dev/null +++ b/code/query/query.cxx @@ -0,0 +1,48 @@ +#include + +namespace code::query +{ + + query_t:: + query_t(expression_t e, + vector warnings, + vector errors) + : expr_{move(e)}, + warnings_{move(warnings)}, + errors_{move(errors)} + {} + + expression_t const& + query_t:: + expr() const + { + return expr_; + } + + vector const& + query_t:: + warnings() const + { + return warnings_; + } + + vector const& + query_t:: + errors() const + { + return errors_; + } + + void + accept(query_t const& q, visitor_t& v) + { + accept(q.expr(), v); + } + + string + to_string(query_t const& q) + { + return to_string(q.expr()); + } + +} // namespace code::query diff --git a/code/query/query.hxx b/code/query/query.hxx new file mode 100644 index 0000000..ff67d4c --- /dev/null +++ b/code/query/query.hxx @@ -0,0 +1,43 @@ +#ifndef code__query__query_hxx_ +#define code__query__query_hxx_ + +#include +#include +#include +#include + +namespace code::query +{ + + class query_t + { + public: + query_t(expression_t e, + vector warnings, + vector errors); + + expression_t const& + expr() const; + + vector const& + warnings() const; + + vector const& + errors() const; + + private: + expression_t expr_; + vector warnings_; + vector errors_; + + }; + + void + accept(query_t const& q, visitor_t& v); + + string + to_string(query_t const& q); + +} // namespace code::query + +#endif diff --git a/code/query/source-location.hxx b/code/query/source-location.hxx new file mode 100644 index 0000000..3ce74e1 --- /dev/null +++ b/code/query/source-location.hxx @@ -0,0 +1,19 @@ +#ifndef code__query__source_location_hxx_ +#define code__query__source_location_hxx_ + +#include + +namespace code::query +{ + + struct source_location_t + { + string name; + uint32_t row{}; + uint32_t column{}; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/syntactical-analyzer.cxx b/code/query/syntactical-analyzer.cxx new file mode 100644 index 0000000..8ce2ec0 --- /dev/null +++ b/code/query/syntactical-analyzer.cxx @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace code::query +{ + + syntactical_analyzer_t:: + syntactical_analyzer_t(lexical_analyzer_t& lexer, + parse_context_t& context) + : lexer_{lexer}, + context_{context} + {} + + lexical_analyzer_t& + syntactical_analyzer_t:: + lexer() + { + return lexer_; + } + + parse_context_t& + syntactical_analyzer_t:: + context() + { + return context_; + } + + optional + syntactical_analyzer_t:: + try_parse() + { + auto expr = try_parse_expression(); + + if (expr) { + auto last = lexer().peek(); + + if (last.type() != token_type_t::end) { + context().report_warning({{}, "trailing token at end of query"}); + } + + return query_t{ + *move(expr), + context().warnings(), + context().errors() + }; + } + + return nullopt; + } + + optional + syntactical_analyzer_t:: + try_parse_primary_expression() + { + optional expr; + + for (;;) { + auto t = lexer().peek(); + + if (t.type() == token_type_t::logical_not) { + lexer().consume(); + + auto rhs = try_parse_primary_expression(); + + if (!rhs) { + context().report_warning({{}, "expected expression after NOT"}); + break; + } + + if (expr) { + expr = logical_and_t{ + source_location_t{}, *expr, logical_not_t{{}, *rhs} + }; + } + else { + expr = logical_not_t{{}, *rhs}; + } + } + + else if (t.type() == token_type_t::simple_term) { + lexer().consume(); + + // If the next token is a ':' then this is a tag. + // + if (lexer().peek().type() == token_type_t::colon) { + lexer().consume(); + + auto identifier = *t.value(); + + // Next expect a term or quoted term. + // + t = lexer().peek(); + + if (t.type() != token_type_t::simple_term && + t.type() != token_type_t::quoted_term) { + context().report_warning({{}, "expected simple-term or quoted-term after tag" }); + break; + } + + lexer().consume(); + + // Construct the term + // + term_t term{ + {}, + t.type() == token_type_t::simple_term ? term_t::simple : term_t::quoted, + *t.value() + }; + + if (expr) { + expr = logical_and_t{ + {}, *expr, tag_t{{}, move(identifier), move(term)} + }; + } + else { + expr = tag_t{{}, move(identifier), move(term)}; + } + } + else if (expr) { + expr = logical_and_t{ + {}, *expr, term_t{{}, term_t::simple, *t.value()} + }; + } + else { + expr = term_t{{}, term_t::simple, *t.value()}; + } + } + + else if (t.type() == token_type_t::quoted_term) { + if (expr) { + expr = logical_and_t{ + {}, *expr, term_t{{}, term_t::quoted, *t.value()} + }; + } + else { + expr = term_t{{}, term_t::quoted, *t.value()}; + } + + lexer().consume(); + } + + else if (t.type() == token_type_t::open_parens) { + lexer().consume(); + + t = lexer().peek(); + + if (t.type() == token_type_t::close_parens) { + lexer().consume(); + continue; + } + + auto next_expr = try_parse_expression(); + + if (!next_expr) { + context().report_warning({{}, "expected expression inside parenthesis"}); + break; + } + + next_expr = parenthesized_t{{}, *next_expr}; + + if (expr) { + expr = logical_and_t{{}, *expr, *next_expr}; + } + else { + expr = next_expr; + } + + t = lexer().peek(); + + if (t.type() != token_type_t::close_parens) { + context().report_warning({{}, "expected end parenthesis"}); + break; + } + + lexer().consume(); + } + + else { + break; + } + } + + return expr; + } + + optional + syntactical_analyzer_t:: + try_parse_logical_and() + { + auto lhs = try_parse_primary_expression(); + + if (!lhs) + return nullopt; + + while (lexer().peek().type() == token_type_t::logical_and) { + lexer().consume(); + auto rhs = try_parse_primary_expression(); + + if (!rhs) { + context().report_warning({{}, "expected expression after AND"}); + return lhs; + } + + lhs = logical_and_t{{}, *lhs, *rhs}; + } + + return lhs; + } + + optional + syntactical_analyzer_t:: + try_parse_logical_or() + { + auto lhs = try_parse_logical_and(); + + if (!lhs) + return nullopt; + + while (lexer().peek().type() == token_type_t::logical_or) { + lexer().consume(); + auto rhs = try_parse_logical_and(); + + if (!rhs) + return nullopt; + + lhs = logical_or_t{{}, *lhs, *rhs}; + } + + return lhs; + } + + optional + syntactical_analyzer_t:: + try_parse_expression() + { + return try_parse_logical_or(); + } + +} // namespace code::query diff --git a/code/query/syntactical-analyzer.hxx b/code/query/syntactical-analyzer.hxx new file mode 100644 index 0000000..b4d7c11 --- /dev/null +++ b/code/query/syntactical-analyzer.hxx @@ -0,0 +1,48 @@ +#ifndef code__query__syntactical_analyzer_hxx_ +#define code__query__syntactical_analyzer_hxx_ + +#include +#include +#include +#include +#include + +namespace code::query +{ + + class syntactical_analyzer_t + { + public: + syntactical_analyzer_t(lexical_analyzer_t&, parse_context_t&); + + lexical_analyzer_t& + lexer(); + + parse_context_t& + context(); + + optional + try_parse(); + + private: + optional + try_parse_primary_expression(); + + optional + try_parse_logical_and(); + + optional + try_parse_logical_or(); + + optional + try_parse_expression(); + + private: + lexical_analyzer_t& lexer_; + parse_context_t& context_; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/tag.cxx b/code/query/tag.cxx new file mode 100644 index 0000000..a78a90a --- /dev/null +++ b/code/query/tag.cxx @@ -0,0 +1,48 @@ +#include + +namespace code::query +{ + + tag_t:: + tag_t(string identifier, term_t value) + : identifier_{move(identifier)}, + value_{move(value)} + {} + + tag_t:: + tag_t(source_location_t origin, + string identifier, + term_t value) + : origin_{move(origin)}, + identifier_{move(identifier)}, + value_{move(value)} + {} + + source_location_t const& + tag_t:: + origin() const + { + return origin_; + } + + string const& + tag_t:: + identifier() const + { + return identifier_; + } + + term_t const& + tag_t:: + value() const + { + return value_; + } + + string + to_string(tag_t const& e) + { + return e.identifier() + ":" + to_string(e.value()); + } + +} // namespace code::query diff --git a/code/query/tag.hxx b/code/query/tag.hxx new file mode 100644 index 0000000..96c6d7f --- /dev/null +++ b/code/query/tag.hxx @@ -0,0 +1,40 @@ +#ifndef code__query__tag_hxx_ +#define code__query__tag_hxx_ + +#include +#include +#include +#include + +namespace code::query +{ + + class tag_t + { + public: + tag_t(string, term_t); + + tag_t(source_location_t, string, term_t); + + source_location_t const& + origin() const; + + string const& + identifier() const; + + term_t const& + value() const; + + private: + source_location_t origin_; + + string identifier_; + term_t value_; + }; + + string + to_string(tag_t const& e); + +} // namespace imperium::contract::query + +#endif diff --git a/code/query/term.cxx b/code/query/term.cxx new file mode 100644 index 0000000..f520333 --- /dev/null +++ b/code/query/term.cxx @@ -0,0 +1,53 @@ +#include + +namespace code::query +{ + + term_t:: + term_t(term_type_t type, string value) + : type_{type}, + value_{move(value)} + {} + + term_t:: + term_t(source_location_t origin, + term_type_t type, + string value) + : origin_{move(origin)}, + type_{type}, + value_{move(value)} + {} + + source_location_t const& + term_t:: + origin() const + { + return origin_; + } + + term_t::term_type_t + term_t:: + type() const + { + return type_; + } + + string const& + term_t:: + value() const + { + return value_; + } + + string + to_string(term_t const& term) + { + if (term.type() == term_t::simple) + return term.value(); + + // fixme: escape term.value()? + // + return "\"" + term.value() + "\""; + } + +} // namespace code::query diff --git a/code/query/term.hxx b/code/query/term.hxx new file mode 100644 index 0000000..17429b1 --- /dev/null +++ b/code/query/term.hxx @@ -0,0 +1,45 @@ +#ifndef code__query__term_hxx_ +#define code__query__term_hxx_ + +#include +#include +#include + +namespace code::query +{ + + class term_t + { + public: + enum term_type_t + { + simple, + quoted + }; + + term_t(term_type_t, string); + + term_t(source_location_t, term_type_t, string); + + source_location_t const& + origin() const; + + term_type_t + type() const; + + string const& + value() const; + + private: + source_location_t origin_; + + term_type_t type_; + string value_; + }; + + string + to_string(term_t const&); + +} // namespace code::query + +#endif diff --git a/code/query/token.cxx b/code/query/token.cxx new file mode 100644 index 0000000..bd3d90f --- /dev/null +++ b/code/query/token.cxx @@ -0,0 +1,26 @@ +#include + +namespace code::query +{ + + token_t:: + token_t(token_type_t type, optional value) + : type_{type}, + value_{move(value)} + {} + + token_type_t + token_t:: + type() const + { + return type_; + } + + optional const& + token_t:: + value() const + { + return value_; + } + +} // namespace code::query diff --git a/code/query/token.hxx b/code/query/token.hxx new file mode 100644 index 0000000..403488d --- /dev/null +++ b/code/query/token.hxx @@ -0,0 +1,47 @@ +#ifndef code__query__token_hxx_ +#define code__query__token_hxx_ + +#include + +namespace code::query +{ + + enum class token_type_t + { + end, + + simple_term, + quoted_term, + + colon, + + logical_and, + logical_not, + logical_or, + + open_parens, + close_parens + + }; + + class token_t + { + public: + explicit + token_t(token_type_t, optional = nullopt); + + token_type_t + type() const; + + optional const& + value() const; + + private: + token_type_t type_; + optional value_; + + }; + +} // namespace code::query + +#endif diff --git a/code/query/types.hxx b/code/query/types.hxx new file mode 100644 index 0000000..fbc3a75 --- /dev/null +++ b/code/query/types.hxx @@ -0,0 +1,45 @@ +#ifndef code__query__types_hxx_ +#define code__query__types_hxx_ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace code::query +{ + + using std::forward; + using std::move; + + using std::int8_t; + using std::int16_t; + using std::int32_t; + using std::int64_t; + using std::uint8_t; + using std::uint16_t; + using std::uint32_t; + using std::uint64_t; + + using std::make_shared; + using std::shared_ptr; + using std::weak_ptr; + + using std::optional; + using std::nullopt; + + using std::vector; + using std::variant; + + using std::string; + using strings = vector; + + using std::function; + +} // namespace code::query + +#endif diff --git a/code/query/version.hxx.in b/code/query/version.hxx.in new file mode 100644 index 0000000..7ecd0bc --- /dev/null +++ b/code/query/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__query__version_hxx_ +#define code__query__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - code / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBCODE_QUERY_VERSION $libcode_query.version.project_number$ULL +#define LIBCODE_QUERY_VERSION_STR "$libcode_query.version.project$" +#define LIBCODE_QUERY_VERSION_ID "$libcode_query.version.project_id$" +#define LIBCODE_QUERY_VERSION_FULL "$libcode_query.version$" + +#define LIBCODE_QUERY_VERSION_MAJOR $libcode_query.version.major$ +#define LIBCODE_QUERY_VERSION_MINOR $libcode_query.version.minor$ +#define LIBCODE_QUERY_VERSION_PATCH $libcode_query.version.patch$ + +#define LIBCODE_QUERY_PRE_RELEASE $libcode_query.version.pre_release$ + +#define LIBCODE_QUERY_SNAPSHOT_SN $libcode_query.version.snapshot_sn$ULL +#define LIBCODE_QUERY_SNAPSHOT_ID "$libcode_query.version.snapshot_id$" + +#endif diff --git a/code/query/visitor.hxx b/code/query/visitor.hxx new file mode 100644 index 0000000..4027e9c --- /dev/null +++ b/code/query/visitor.hxx @@ -0,0 +1,39 @@ +#ifndef code__query__visitor_hxx_ +#define code__query__visitor_hxx_ + +namespace code::query +{ + + class visitor_t + { + public: + virtual + ~visitor_t() noexcept = default; + + virtual + void + visit_default() = 0; + + protected: + visitor_t() = default; + + }; + + template + class basic_visitor_t + { + public: + virtual + void + visit(V const&) = 0; + + protected: + basic_visitor_t() = default; + + ~basic_visitor_t() noexcept = default; + + }; + +} // namespace code::query + +#endif diff --git a/manifest b/manifest new file mode 100644 index 0000000..408ac09 --- /dev/null +++ b/manifest @@ -0,0 +1,11 @@ +: 1 +name: libcode-query +version: 0.1.0-a.0.z +language: c++ +summary: libcode-query C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..26fdbf2 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,2 @@ +: 1 +summary: libcode-query project repository diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}