template engine

v1
Brett 2024-05-09 13:51:25 -04:00
parent 943fb84211
commit b857bc96ef
7 changed files with 654 additions and 35 deletions

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
include(cmake/color.cmake) 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_TEST_VERSION 0.0.1)
set(BLT_TARGET BLT) set(BLT_TARGET BLT)

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef BLT_TEMPLATING_H
#define BLT_TEMPLATING_H
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <utility>
#include <blt/std/hashmap.h>
#include <blt/std/types.h>
#include <blt/std/expected.h>
#include <variant>
namespace blt
{
template<typename Storage, typename Consumable>
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<std::string_view, template_token_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<std::string_view, char>
{
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<std::vector<template_token_data_t>, template_token_data_t>
{
public:
explicit template_token_consumer_t(const std::vector<template_token_data_t>& 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<std::string, template_parser_failure_t>;
template_parser_t(blt::hashmap_t<std::string, std::string>& 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<std::string, std::string>& 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<std::vector<template_token_data_t>, template_tokenizer_failure_t> process_string(std::string_view str);
blt::expected<std::string, template_parser_failure_t> evaluate(std::string_view str);
private:
blt::hashmap_t<std::string, std::string> substitutions;
};
}
#endif //BLT_TEMPLATING_H

View File

@ -75,7 +75,7 @@ namespace blt
} }
template<typename E2> template<typename E2>
inline friend constexpr bool operator==(const unexpected& x, const unexpected <E2>& y) inline friend constexpr bool operator==(const unexpected& x, const unexpected<E2>& y)
{ {
return x.error() == y.error(); return x.error() == y.error();
} }
@ -130,23 +130,34 @@ namespace blt
inline static constexpr bool four_insanity_v = inline static constexpr bool four_insanity_v =
std::is_constructible_v<unexpected<E>, expected<U, G>&> || std::is_constructible_v<unexpected<E>, expected<U, G>> || std::is_constructible_v<unexpected<E>, expected<U, G>&> || std::is_constructible_v<unexpected<E>, expected<U, G>> ||
std::is_constructible_v<unexpected<E>, const expected<U, G>&> || std::is_constructible_v<unexpected<E>, const expected<U, G>>; std::is_constructible_v<unexpected<E>, const expected<U, G>&> || std::is_constructible_v<unexpected<E>, const expected<U, G>>;
public: public:
template<typename std::enable_if_t<std::is_default_constructible_v<T>, bool> = true> template<typename std::enable_if_t<std::is_default_constructible_v<T>, bool> = true>
constexpr expected() noexcept: v(T()) constexpr expected() noexcept: v(T())
{} {}
constexpr expected(const expected& copy) = delete; // constexpr expected(const expected& copy) = delete;
constexpr expected(const expected<T, E, true>& copy): expected<T, E, true>::v(copy.v)
constexpr expected(expected&& move) noexcept: v(move ? std::move(*move) : std::move(move.error()))
{} {}
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) * (4)...(5)
*/ */
template<class U, class G, class UF = std::add_lvalue_reference_t<const U>, class GF = const G&, std::enable_if_t< template<class U, class G, class UF = std::add_lvalue_reference_t<const U>, class GF = const G&, std::enable_if_t<
(!std::is_convertible_v<UF, T> || !std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) && (!std::is_convertible_v<UF, T> || !std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) &&
std::is_constructible_v<E, GF> && !eight_insanity_v < U, G>&& !four_insanity_v<U, G>, bool> = true> std::is_constructible_v<E, GF> && !eight_insanity_v<U, G> && !four_insanity_v<U, G>, bool> = true>
constexpr explicit expected(const expected<U, G>& other): constexpr explicit expected(const expected<U, G>& other):
v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error())) v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error()))
@ -154,7 +165,7 @@ namespace blt
template<class U, class G, class UF = U, class GF = G, std::enable_if_t< template<class U, class G, class UF = U, class GF = G, std::enable_if_t<
(!std::is_convertible_v<UF, T> || !std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) && (!std::is_convertible_v<UF, T> || !std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) &&
std::is_constructible_v<E, GF> && !eight_insanity_v < U, G>&& !four_insanity_v<U, G>, bool> = true> std::is_constructible_v<E, GF> && !eight_insanity_v<U, G> && !four_insanity_v<U, G>, bool> = true>
constexpr explicit expected(expected<U, G>&& other): constexpr explicit expected(expected<U, G>&& other):
v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error())) v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error()))
@ -162,7 +173,7 @@ namespace blt
template<class U, class G, class UF = std::add_lvalue_reference_t<const U>, class GF = const G&, std::enable_if_t< template<class U, class G, class UF = std::add_lvalue_reference_t<const U>, class GF = const G&, std::enable_if_t<
(std::is_convertible_v<UF, T> && std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) && (std::is_convertible_v<UF, T> && std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) &&
std::is_constructible_v<E, GF> && !eight_insanity_v < U, G>&& !four_insanity_v<U, G>, bool> = true> std::is_constructible_v<E, GF> && !eight_insanity_v<U, G> && !four_insanity_v<U, G>, bool> = true>
constexpr expected(const expected<U, G>& other): constexpr expected(const expected<U, G>& other):
v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error())) v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error()))
@ -170,7 +181,7 @@ namespace blt
template<class U, class G, class UF = U, class GF = G, std::enable_if_t< template<class U, class G, class UF = U, class GF = G, std::enable_if_t<
(std::is_convertible_v<UF, T> && std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) && (std::is_convertible_v<UF, T> && std::is_convertible_v<GF, E>) && (std::is_constructible_v<T, UF> || std::is_void_v<U>) &&
std::is_constructible_v<E, GF> && !eight_insanity_v < U, G>&& !four_insanity_v<U, G>, bool> = true> std::is_constructible_v<E, GF> && !eight_insanity_v<U, G> && !four_insanity_v<U, G>, bool> = true>
constexpr expected(expected<U, G>&& other): constexpr expected(expected<U, G>&& other):
v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error())) v(other.has_value() ? std::forward<UF>(*other) : std::forward<GF>(other.error()))
@ -252,10 +263,6 @@ namespace blt
constexpr explicit expected(unexpect_t, std::initializer_list<U> il, Args&& ... args): v(E(il, std::forward<Args>(args)...)) constexpr explicit expected(unexpect_t, std::initializer_list<U> il, Args&& ... args): v(E(il, std::forward<Args>(args)...))
{} {}
expected& operator=(const expected& copy) = delete;
expected& operator=(expected&& move) = default;
[[nodiscard]] constexpr explicit operator bool() const noexcept [[nodiscard]] constexpr explicit operator bool() const noexcept
{ {
return std::holds_alternative<T>(v); return std::holds_alternative<T>(v);
@ -362,15 +369,14 @@ namespace blt
}; };
template<typename T, typename E> template<typename T, typename E>
class expected<T, E, true> : expected<T, E, false> class expected<T, E, false> : public expected<T, E, true>
{ {
public: public:
using expected<T, E, false>::expected; using expected<T, E, true>::expected;
constexpr expected(const expected& copy): expected<T, E, false>::v(copy ? *copy : copy.error()) constexpr expected(const expected<T, E, false>& copy) = delete;
{}
expected& operator=(const expected& copy) = default; expected& operator=(const expected& copy) = delete;
}; };
} }

View File

@ -0,0 +1,226 @@
/*
* <Short Description>
* 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 <https://www.gnu.org/licenses/>.
*/
#include <blt/parse/templating.h>
#include <blt/std/string.h>
#include <cctype>
#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<std::vector<template_token_data_t>, template_tokenizer_failure_t> template_engine_t::process_string(std::string_view str)
{
std::vector<template_token_data_t> 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<i64>(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<i64>(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<i64>(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<std::string, template_parser_failure_t> 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;
}
}

View File

@ -23,6 +23,7 @@
#include <blt/parse/argparse.h> #include <blt/parse/argparse.h>
#include <utility_test.h> #include <utility_test.h>
#include <blt/std/utility.h> #include <blt/std/utility.h>
#include <templating_test.h>
namespace blt::test namespace blt::test
{ {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef BLT_TEMPLATING_TEST_H
#define BLT_TEMPLATING_TEST_H
namespace blt::test
{
void template_test();
}
#endif //BLT_TEMPLATING_TEST_H

View File

@ -0,0 +1,89 @@
/*
* <Short Description>
* 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 <https://www.gnu.org/licenses/>.
*/
#include <templating_test.h>
#include <string>
#include <blt/std/logging.h>
#include <blt/parse/templating.h>
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})");
}
}