From b857bc96ef374897475dfdbd0349df481cadcc93 Mon Sep 17 00:00:00 2001 From: Brett Date: Thu, 9 May 2024 13:51:25 -0400 Subject: [PATCH] template engine --- CMakeLists.txt | 2 +- include/blt/parse/templating.h | 270 ++++++++++++++++++++++++++++++++ include/blt/std/expected.h | 74 +++++---- src/blt/parse/templating.cpp | 226 ++++++++++++++++++++++++++ tests/include/blt_tests.h | 1 + tests/include/templating_test.h | 27 ++++ tests/src/template_test.cpp | 89 +++++++++++ 7 files changed, 654 insertions(+), 35 deletions(-) create mode 100644 include/blt/parse/templating.h create mode 100644 src/blt/parse/templating.cpp create mode 100644 tests/include/templating_test.h create mode 100644 tests/src/template_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 464b22a..8e04563 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.20) include(cmake/color.cmake) -set(BLT_VERSION 0.16.22) +set(BLT_VERSION 0.16.23) set(BLT_TEST_VERSION 0.0.1) set(BLT_TARGET BLT) diff --git a/include/blt/parse/templating.h b/include/blt/parse/templating.h new file mode 100644 index 0000000..814140a --- /dev/null +++ b/include/blt/parse/templating.h @@ -0,0 +1,270 @@ +#pragma once +/* + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BLT_TEMPLATING_H +#define BLT_TEMPLATING_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace blt +{ + template + class template_consumer_base_t + { + public: + explicit template_consumer_base_t(Storage storage): storage(std::move(storage)) + {} + + [[nodiscard]] Consumable next(size_t offset = 0) const + { + return storage[current_index + offset]; + } + + void advance(size_t offset = 1) + { + current_index += offset; + } + + [[nodiscard]] bool hasNext(size_t offset = 1) const + { + return (current_index + (offset - 1)) < storage.size(); + } + + [[nodiscard]] Consumable consume() + { + Consumable c = next(); + advance(); + return c; + } + + [[nodiscard]] size_t getCurrentIndex() const + { + return current_index; + } + + [[nodiscard]] size_t getPreviousIndex() const + { + return current_index - 1; + } + + protected: + size_t current_index = 0; + Storage storage; + }; + + enum class template_token_t + { + //STRING, // A string of characters not $ { or } + IDENT, // $ + CURLY_OPEN, // { + CURLY_CLOSE, // } + IF, // IF + ELSE, // ELSE + PAR_OPEN, // ( + PAR_CLOSE, // ) + OR, // || + AND, // && + XOR, // ^ + NOT, // ! + QUOTE, // " + SEMI, // ; + COMMA, // , + PERIOD, // . + FUNCTION, // ~ + STRING // variable name + }; + + namespace detail + { + inline const blt::hashmap_t identifiers = { + {"IF", template_token_t::IF}, + {"ELSE", template_token_t::ELSE} + }; + } + + inline std::string template_token_to_string(template_token_t token) + { + switch (token) + { + case template_token_t::IDENT: + return "[Template Identifier]"; + case template_token_t::CURLY_OPEN: + return "[Curly Open]"; + case template_token_t::CURLY_CLOSE: + return "[Curly Close]"; + case template_token_t::IF: + return "[IF]"; + case template_token_t::ELSE: + return "[ELSE]"; + case template_token_t::PAR_OPEN: + return "[Par Open]"; + case template_token_t::PAR_CLOSE: + return "[Par Close]"; + case template_token_t::OR: + return "[OR]"; + case template_token_t::AND: + return "[AND]"; + case template_token_t::XOR: + return "[XOR]"; + case template_token_t::NOT: + return "[NOT]"; + case template_token_t::QUOTE: + return "[QUOTE]"; + case template_token_t::FUNCTION: + return "[FUNC]"; + case template_token_t::STRING: + return "[STR]"; + case template_token_t::SEMI: + return "[SEMI]"; + case template_token_t::COMMA: + return "[COMMA]"; + case template_token_t::PERIOD: + return "[PERIOD]"; + } + } + + enum class template_tokenizer_failure_t + { + MISMATCHED_CURLY, + MISMATCHED_PAREN, + MISMATCHED_QUOTE, + }; + + enum class template_parser_failure_t + { + TOKENIZER_FAILURE, + NO_MATCHING_CURLY + }; + + struct template_token_data_t + { + template_token_t type; + size_t level; + std::string_view token; + size_t paren_level = 0; + + template_token_data_t(template_token_t type, size_t level, const std::string_view& token): type(type), level(level), token(token) + {} + + template_token_data_t(template_token_t type, size_t level, const std::string_view& token, size_t parenLevel): + type(type), level(level), token(token), paren_level(parenLevel) + {} + }; + + class template_char_consumer_t : public template_consumer_base_t + { + public: + explicit template_char_consumer_t(std::string_view statement): template_consumer_base_t(statement) + {} + + [[nodiscard]] std::string_view from(size_t begin, size_t end) + { + return std::string_view{&storage[begin], end - begin}; + } + }; + + class template_token_consumer_t : public template_consumer_base_t, template_token_data_t> + { + public: + explicit template_token_consumer_t(const std::vector& statement): template_consumer_base_t(statement) + {} + + std::string_view from_last(std::string_view raw_string) + { + if (current_index == 0) + return ""; + auto token = storage[getPreviousIndex()]; + auto len = (&token.token.back() - &raw_string.front()) - last_read_index; + auto str = std::string_view(&raw_string[last_read_index], len); + last_read_index += len; + return str; + } + + private: + size_t last_read_index = 0; + }; + + class template_parser_t + { + public: + using estring = blt::expected; + template_parser_t(blt::hashmap_t& substitutions, template_token_consumer_t& consumer): + substitutions(substitutions), consumer(consumer) + {} + + estring parse() + { + consumer.advance(2); + auto str = statement(); + if (!str) + return str; + // should never occur + if (consumer.hasNext() && consumer.next().type != template_token_t::CURLY_CLOSE) + return blt::unexpected(template_parser_failure_t::NO_MATCHING_CURLY); + consumer.advance(); + return str; + } + + private: + estring statement() + { + + } + + blt::hashmap_t& substitutions; + template_token_consumer_t& consumer; + }; + + class template_engine_t + { + public: + inline std::string& operator[](const std::string& key) + { + return substitutions[key]; + } + + inline std::string& operator[](std::string_view key) + { + return substitutions[key]; + } + + inline template_engine_t& set(std::string_view key, std::string_view replacement) + { + substitutions[key] = replacement; + return *this; + } + + static blt::expected, template_tokenizer_failure_t> process_string(std::string_view str); + + blt::expected evaluate(std::string_view str); + + private: + blt::hashmap_t substitutions; + }; + +} + +#endif //BLT_TEMPLATING_H diff --git a/include/blt/std/expected.h b/include/blt/std/expected.h index 737b02d..b2700af 100644 --- a/include/blt/std/expected.h +++ b/include/blt/std/expected.h @@ -75,7 +75,7 @@ namespace blt } template - inline friend constexpr bool operator==(const unexpected& x, const unexpected & y) + inline friend constexpr bool operator==(const unexpected& x, const unexpected& y) { return x.error() == y.error(); } @@ -122,31 +122,42 @@ namespace blt template inline static constexpr bool eight_insanity_v = std::is_constructible_v&> || std::is_constructible_v> || - std::is_constructible_v&> || std::is_constructible_v> || - std::is_convertible_v&, T> || std::is_convertible_v, T> || - std::is_convertible_v&, T> || std::is_convertible_v, T>; + std::is_constructible_v&> || std::is_constructible_v> || + std::is_convertible_v&, T> || std::is_convertible_v, T> || + std::is_convertible_v&, T> || std::is_convertible_v, T>; template inline static constexpr bool four_insanity_v = std::is_constructible_v, expected&> || std::is_constructible_v, expected> || - std::is_constructible_v, const expected&> || std::is_constructible_v, const expected>; - + std::is_constructible_v, const expected&> || std::is_constructible_v, const expected>; public: template, bool> = true> constexpr expected() noexcept: v(T()) {} - constexpr expected(const expected& copy) = delete; - - constexpr expected(expected&& move) noexcept: v(move ? std::move(*move) : std::move(move.error())) +// constexpr expected(const expected& copy) = delete; + constexpr expected(const expected& copy): expected::v(copy.v) {} + expected& operator=(const expected& copy) + { + v = copy.v; + } + + constexpr expected(expected&& move) noexcept: v(std::move(move.v)) + {} + + expected& operator=(expected&& move) + { + std::swap(v, move.v); + } + /* * (4)...(5) */ template, class GF = const G&, std::enable_if_t< (!std::is_convertible_v || !std::is_convertible_v) && (std::is_constructible_v || std::is_void_v) && - std::is_constructible_v && !eight_insanity_v < U, G>&& !four_insanity_v, bool> = true> + std::is_constructible_v && !eight_insanity_v && !four_insanity_v, bool> = true> constexpr explicit expected(const expected& other): v(other.has_value() ? std::forward(*other) : std::forward(other.error())) @@ -154,7 +165,7 @@ namespace blt template || !std::is_convertible_v) && (std::is_constructible_v || std::is_void_v) && - std::is_constructible_v && !eight_insanity_v < U, G>&& !four_insanity_v, bool> = true> + std::is_constructible_v && !eight_insanity_v && !four_insanity_v, bool> = true> constexpr explicit expected(expected&& other): v(other.has_value() ? std::forward(*other) : std::forward(other.error())) @@ -162,7 +173,7 @@ namespace blt template, class GF = const G&, std::enable_if_t< (std::is_convertible_v && std::is_convertible_v) && (std::is_constructible_v || std::is_void_v) && - std::is_constructible_v && !eight_insanity_v < U, G>&& !four_insanity_v, bool> = true> + std::is_constructible_v && !eight_insanity_v && !four_insanity_v, bool> = true> constexpr expected(const expected& other): v(other.has_value() ? std::forward(*other) : std::forward(other.error())) @@ -170,7 +181,7 @@ namespace blt template && std::is_convertible_v) && (std::is_constructible_v || std::is_void_v) && - std::is_constructible_v && !eight_insanity_v < U, G>&& !four_insanity_v, bool> = true> + std::is_constructible_v && !eight_insanity_v && !four_insanity_v, bool> = true> constexpr expected(expected&& other): v(other.has_value() ? std::forward(*other) : std::forward(other.error())) @@ -182,22 +193,22 @@ namespace blt */ template && - !std::is_same_v, void> && - !std::is_same_v, std::in_place_t> && - !std::is_same_v> && - std::is_constructible_v && - !std::is_same_v, unexpected> && - !std::is_same_v, expected>, bool> = true> + !std::is_same_v, void> && + !std::is_same_v, std::in_place_t> && + !std::is_same_v> && + std::is_constructible_v && + !std::is_same_v, unexpected> && + !std::is_same_v, expected>, bool> = true> constexpr explicit expected(U&& v): v(T(std::forward(v))) {} template && - !std::is_same_v, void> && - !std::is_same_v, std::in_place_t> && - !std::is_same_v> && - std::is_constructible_v && - !std::is_same_v, unexpected> && - !std::is_same_v, expected>, bool> = true> + !std::is_same_v, void> && + !std::is_same_v, std::in_place_t> && + !std::is_same_v> && + std::is_constructible_v && + !std::is_same_v, unexpected> && + !std::is_same_v, expected>, bool> = true> constexpr expected(U&& v): v(T(std::forward(v))) {} @@ -252,10 +263,6 @@ namespace blt constexpr explicit expected(unexpect_t, std::initializer_list il, Args&& ... args): v(E(il, std::forward(args)...)) {} - expected& operator=(const expected& copy) = delete; - - expected& operator=(expected&& move) = default; - [[nodiscard]] constexpr explicit operator bool() const noexcept { return std::holds_alternative(v); @@ -362,15 +369,14 @@ namespace blt }; template - class expected : expected + class expected : public expected { public: - using expected::expected; + using expected::expected; - constexpr expected(const expected& copy): expected::v(copy ? *copy : copy.error()) - {} + constexpr expected(const expected& copy) = delete; - expected& operator=(const expected& copy) = default; + expected& operator=(const expected& copy) = delete; }; } diff --git a/src/blt/parse/templating.cpp b/src/blt/parse/templating.cpp new file mode 100644 index 0000000..ff6ad0b --- /dev/null +++ b/src/blt/parse/templating.cpp @@ -0,0 +1,226 @@ +/* + * + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include "blt/std/logging.h" + +namespace blt +{ + + bool isNonStringNext(char c) + { + switch (c) + { + case '$': + case '{': + case '}': + case '(': + case ')': + case '"': + case '^': + case '!': + case '&': + case ';': + case ',': + case '.': + case '|': + return true; + default: + return false; + } + } + + blt::expected, template_tokenizer_failure_t> template_engine_t::process_string(std::string_view str) + { + std::vector tokens; + + template_char_consumer_t consumer(str); + + i64 start = -1; + size_t paren_level = 0; + size_t level = 0; + bool open = false; + while (consumer.hasNext()) + { + i64 current_start = static_cast(consumer.getCurrentIndex()); + char c = consumer.consume(); + switch (c) + { + case '$': + tokens.emplace_back(template_token_t::IDENT, level, consumer.from(current_start, current_start + 1)); + if (consumer.next() == '{') + { + paren_level = 0; + open = true; + } + continue; + case '{': + tokens.emplace_back(template_token_t::CURLY_OPEN, level, consumer.from(current_start, current_start + 1)); + if (open) + level++; + continue; + case '}': + tokens.emplace_back(template_token_t::CURLY_CLOSE, level, consumer.from(current_start, current_start + 1)); + if (open) + level--; + if (level == 0) + { + open = false; + if (paren_level != 0) + return blt::unexpected(template_tokenizer_failure_t::MISMATCHED_PAREN); + } + continue; + case '(': + tokens.emplace_back(template_token_t::PAR_OPEN, level, consumer.from(current_start, current_start + 1), paren_level); + paren_level++; + break; + case ')': + tokens.emplace_back(template_token_t::PAR_CLOSE, level, consumer.from(current_start, current_start + 1), paren_level); + paren_level--; + break; + case '"': + tokens.emplace_back(template_token_t::QUOTE, level, consumer.from(current_start, current_start + 1)); + // if we just encountered a quote, we need to consume characters until we find its matching quote + // only if we are currently inside a template though... + if (open) + { + current_start = static_cast(consumer.getCurrentIndex()); + while (consumer.hasNext() && consumer.next() != '"') + consumer.advance(); + if (!consumer.hasNext()) + return blt::unexpected(template_tokenizer_failure_t::MISMATCHED_QUOTE); + tokens.emplace_back(template_token_t::STRING, level, consumer.from(current_start, consumer.getCurrentIndex())); + consumer.advance(); + current_start = static_cast(consumer.getCurrentIndex()); + tokens.emplace_back(template_token_t::QUOTE, level, consumer.from(current_start, current_start + 1)); + } + break; + case '^': + tokens.emplace_back(template_token_t::XOR, level, consumer.from(current_start, current_start + 1)); + break; + case '!': + tokens.emplace_back(template_token_t::NOT, level, consumer.from(current_start, current_start + 1)); + break; + case ';': + tokens.emplace_back(template_token_t::SEMI, level, consumer.from(current_start, current_start + 1)); + break; + case ',': + tokens.emplace_back(template_token_t::COMMA, level, consumer.from(current_start, current_start + 1)); + break; + case '.': + tokens.emplace_back(template_token_t::PERIOD, level, consumer.from(current_start, current_start + 1)); + break; + case '~': + tokens.emplace_back(template_token_t::FUNCTION, level, consumer.from(current_start, current_start + 1)); + break; + case '|': + if (consumer.hasNext() && consumer.next() == '|') + { + consumer.advance(); + tokens.emplace_back(template_token_t::OR, level, consumer.from(current_start, current_start + 2)); + continue; + } + start = current_start; + break; + case '&': + if (consumer.hasNext() && consumer.next() == '&') + { + consumer.advance(); + tokens.emplace_back(template_token_t::AND, level, consumer.from(current_start, current_start + 2)); + continue; + } + start = current_start; + break; + default: + // do not add whitespace to anything + if (std::isspace(c)) + break; + if (start == -1) + start = current_start; + if (consumer.hasNext() && (isNonStringNext(consumer.next()) || std::isspace(consumer.next()))) + { + tokens.emplace_back(template_token_t::STRING, level, consumer.from(start, consumer.getCurrentIndex())); + start = -1; + } + break; + } + } + + if (start != -1) + tokens.emplace_back(template_token_t::STRING, level, consumer.from(start, consumer.getCurrentIndex())); + + for (auto& token : tokens) + { + if (token.type == template_token_t::STRING && detail::identifiers.contains(token.token)) + token.type = detail::identifiers.at(token.token); + } + + if (level != 0) + return unexpected(template_tokenizer_failure_t::MISMATCHED_CURLY); + + return tokens; + } + + blt::expected template_engine_t::evaluate(std::string_view str) + { + auto tokens = process_string(str); + + if (!tokens) + { + switch (tokens.error()) + { + case template_tokenizer_failure_t::MISMATCHED_CURLY: + BLT_ERROR("Mismatched curly braces"); + break; + case template_tokenizer_failure_t::MISMATCHED_PAREN: + BLT_ERROR("Mismatched parentheses"); + break; + case template_tokenizer_failure_t::MISMATCHED_QUOTE: + BLT_ERROR("Mismatched quotes"); + break; + } + return blt::unexpected(template_parser_failure_t::TOKENIZER_FAILURE); + } + + std::string return_str; + return_str.reserve(str.size()); + + template_token_consumer_t consumer{tokens.value()}; + + template_parser_t parser(substitutions, consumer); + + while (consumer.hasNext()) + { + while (consumer.hasNext(2)) + { + if (consumer.next().type == template_token_t::IDENT && consumer.next().type == template_token_t::CURLY_OPEN) + { + return_str += consumer.from_last(str); + break; + } + } + if (auto result = parser.parse()) + return_str += result.value(); + else + return result; + } + + return return_str; + } +} \ No newline at end of file diff --git a/tests/include/blt_tests.h b/tests/include/blt_tests.h index b5efd9c..bc32d43 100644 --- a/tests/include/blt_tests.h +++ b/tests/include/blt_tests.h @@ -23,6 +23,7 @@ #include #include #include +#include namespace blt::test { diff --git a/tests/include/templating_test.h b/tests/include/templating_test.h new file mode 100644 index 0000000..0d13c80 --- /dev/null +++ b/tests/include/templating_test.h @@ -0,0 +1,27 @@ +#pragma once +/* + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef BLT_TEMPLATING_TEST_H +#define BLT_TEMPLATING_TEST_H + +namespace blt::test +{ + void template_test(); +} + +#endif //BLT_TEMPLATING_TEST_H diff --git a/tests/src/template_test.cpp b/tests/src/template_test.cpp new file mode 100644 index 0000000..b5edc75 --- /dev/null +++ b/tests/src/template_test.cpp @@ -0,0 +1,89 @@ +/* + * + * Copyright (C) 2024 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include + +const std::string shader_test_string = R"(" +#version 300 es +precision mediump float; + +${LAYOUT_STRING} out vec4 FragColor; +in vec2 uv; +in vec2 pos; + +uniform sampler2D tex; + +vec4 linear_iter(vec4 i, vec4 p, float factor){ + return (i + p) * factor; +} + +void main() { + FragColor = texture(tex, uv); +} + +")"; + +void process_string(const std::string& str) +{ + BLT_DEBUG(str); + auto results = blt::template_engine_t::process_string(str); + if (results) + { + auto val = results.value(); + for (auto& v : val) + { + BLT_TRACE_STREAM << (blt::template_token_to_string(v.type)); + } + BLT_TRACE_STREAM << "\n"; + for (auto& v : val) + { + BLT_TRACE("{%s: %s}", blt::template_token_to_string(v.type).c_str(), std::string(v.token).c_str()); + } + } else + { + auto error = results.error(); + switch (error) + { + case blt::template_tokenizer_failure_t::MISMATCHED_CURLY: + BLT_ERROR("Tokenizer Failure: Mismatched curly"); + break; + case blt::template_tokenizer_failure_t::MISMATCHED_PAREN: + BLT_ERROR("Tokenizer Failure: Mismatched parenthesis"); + break; + case blt::template_tokenizer_failure_t::MISMATCHED_QUOTE: + BLT_ERROR("Tokenizer Failure: Mismatched Quotes"); + break; + } + + } + BLT_DEBUG("--------------------------"); +} + +namespace blt::test +{ + void template_test() + { + process_string(shader_test_string); + process_string("~hello"); + process_string("hello"); + process_string("hello ${WORLD}"); + process_string("layout (location = ${IF(LAYOUT_LOCATION) LAYOUT_LOCATION ELSE ~DISCARD})"); + } +} \ No newline at end of file