Add project files.
This commit is contained in:
1 parent
9c60d1b47c
commit
40d55e00f2
11 files changed
+2099
No files matched your search
@@ -0,0 +1,353 @@
|
||||
#pragma once
|
||||
//
|
||||
// value.hpp
|
||||
//
|
||||
// The type system. A `Scalar` is a closed variant over every primitive
|
||||
// type the engine understands. A `Value` wraps either a single Scalar
|
||||
// (for ordinary parameters/options) or a vector of them (for variadic
|
||||
// parameters/options). `TypeInfo` bundles a display name with a parser:
|
||||
// for the built-in scalar types this is a *plain stateless function
|
||||
// pointer* (no heap allocation, unlike std::function), and only the
|
||||
// rarer enum/choice-restricted case pays for a one-time, shared, heap
|
||||
// allocation to hold its name table.
|
||||
//
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace cliforge {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Exceptions
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Every distinct reason a run-time parse can fail. Exposed on CliError so
|
||||
// callers (and Engine internally, for its own error-selection logic) can
|
||||
// branch on *why* something failed, not just read a free-text message.
|
||||
enum class ErrorKind {
|
||||
Generic,
|
||||
UnknownOption, // a flag/option name isn't recognized for this command
|
||||
TooManyArguments, // more positional values than the command declares
|
||||
MissingArgument, // a required positional value (or option value) was never given
|
||||
MissingValue, // an option/flag was named but its value token is missing
|
||||
TypeMismatch, // a token couldn't convert to the slot's declared type
|
||||
};
|
||||
|
||||
// Base of every error the engine raises while registering commands or
|
||||
// parsing user input. Engine::run() catches CliError and prints it
|
||||
// nicely instead of letting it escape as an unhandled exception.
|
||||
class CliError : public std::runtime_error {
|
||||
public:
|
||||
explicit CliError(const std::string& msg, ErrorKind kind = ErrorKind::Generic)
|
||||
: std::runtime_error(msg), kind_(kind) {}
|
||||
ErrorKind kind() const noexcept { return kind_; }
|
||||
|
||||
private:
|
||||
ErrorKind kind_;
|
||||
};
|
||||
|
||||
// A token couldn't be converted to the type a slot expects -- always
|
||||
// ErrorKind::TypeMismatch.
|
||||
class ParseError : public CliError {
|
||||
public:
|
||||
explicit ParseError(const std::string& msg) : CliError(msg, ErrorKind::TypeMismatch) {}
|
||||
};
|
||||
|
||||
// Something is wrong with how a command was *built* (mismatched arity
|
||||
// between declared slots and the bound function, duplicate names, a
|
||||
// choices<T>() call whose T doesn't match the declared type, an option
|
||||
// bound to a non-optional type, an action() parameter type that doesn't
|
||||
// match what was declared, etc). These fire at program startup (many of
|
||||
// them at compile time via static_assert), not at end-user parse time.
|
||||
struct RegistrationError : CliError {
|
||||
explicit RegistrationError(const std::string& msg) : CliError(msg, ErrorKind::Generic) {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Enum storage
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Custom enums aren't a native Scalar alternative (there are infinitely
|
||||
// many enum types), so when a slot is restricted to an enum via
|
||||
// choices<T>(), the underlying value is boxed here. Value::as<T>() knows
|
||||
// to unbox + static_cast back to the caller's real enum type.
|
||||
struct EnumTag {
|
||||
long long value;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Scalar: the closed set of representable primitive values
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
using Scalar = std::variant<int8_t, uint8_t, int16_t, uint16_t, int32_t,
|
||||
uint32_t, int64_t, uint64_t, float, double,
|
||||
std::string, char, bool, EnumTag>;
|
||||
|
||||
enum class Kind {
|
||||
I8, U8, I16, U16, I32, U32, I64, U64, F32, F64, Str, Char, Bool, Enum
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// TypeInfo: how to parse + describe one type
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Holds the name table for an enum/choice-restricted slot. Built once at
|
||||
// registration time (a single, amortized allocation via shared_ptr);
|
||||
// parsing against it never allocates beyond the error path.
|
||||
struct ChoiceTable {
|
||||
std::vector<std::pair<std::string, Scalar>> entries;
|
||||
|
||||
Scalar parse(std::string_view token, std::string_view label) const {
|
||||
for (const auto& [name, val] : entries) {
|
||||
if (name == token) return val;
|
||||
}
|
||||
std::string allowed;
|
||||
for (size_t i = 0; i < entries.size(); ++i) {
|
||||
if (i) allowed += ", ";
|
||||
allowed += entries[i].first;
|
||||
}
|
||||
throw ParseError("type mismatch for '" + std::string(label) + "': got '" +
|
||||
std::string(token) + "', expected one of: " + allowed);
|
||||
}
|
||||
};
|
||||
|
||||
struct TypeInfo {
|
||||
Kind kind{};
|
||||
std::string displayName;
|
||||
// Stateless parser for built-in scalar types -- a plain function
|
||||
// pointer costs nothing to store or copy, unlike std::function.
|
||||
Scalar (*parsePlain)(std::string_view token, std::string_view label) = nullptr;
|
||||
// Populated only for choices<T>()-restricted slots.
|
||||
std::shared_ptr<const ChoiceTable> choiceTable;
|
||||
|
||||
bool valid() const { return parsePlain != nullptr || choiceTable != nullptr; }
|
||||
|
||||
Scalar parse(std::string_view token, std::string_view label) const {
|
||||
return choiceTable ? choiceTable->parse(token, label) : parsePlain(token, label);
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <typename Int>
|
||||
Int parseInteger(std::string_view token, std::string_view label, std::string_view typeName) {
|
||||
Int out{};
|
||||
auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), out);
|
||||
if (ec != std::errc{} || ptr != token.data() + token.size()) {
|
||||
throw ParseError("type mismatch for '" + std::string(label) + "': got '" +
|
||||
std::string(token) + "', expected " + std::string(typeName));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename Float>
|
||||
Float parseFloating(std::string_view token, std::string_view label, std::string_view typeName) {
|
||||
Float out{};
|
||||
auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), out);
|
||||
if (ec != std::errc{} || ptr != token.data() + token.size()) {
|
||||
throw ParseError("type mismatch for '" + std::string(label) + "': got '" +
|
||||
std::string(token) + "', expected " + std::string(typeName));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::string toLower(std::string_view s) {
|
||||
std::string out(s);
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Maps a C++ type T to its Kind + a ready-to-use TypeInfo. Specialized
|
||||
// below for every built-in type; enums go through choices<T>() instead
|
||||
// since they need a name<->value table supplied by the caller.
|
||||
template <typename T>
|
||||
struct TypeOf; // intentionally undefined for unsupported T
|
||||
|
||||
#define CLIFORGE_INT_TYPE(CPP_T, KIND, NAME) \
|
||||
template <> \
|
||||
struct TypeOf<CPP_T> { \
|
||||
static TypeInfo get() { \
|
||||
TypeInfo t; \
|
||||
t.kind = Kind::KIND; \
|
||||
t.displayName = NAME; \
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar { \
|
||||
return Scalar{detail::parseInteger<CPP_T>(tok, lbl, NAME)}; \
|
||||
}; \
|
||||
return t; \
|
||||
} \
|
||||
};
|
||||
|
||||
CLIFORGE_INT_TYPE(int8_t, I8, "int8")
|
||||
CLIFORGE_INT_TYPE(uint8_t, U8, "uint8")
|
||||
CLIFORGE_INT_TYPE(int16_t, I16, "int16")
|
||||
CLIFORGE_INT_TYPE(uint16_t, U16, "uint16")
|
||||
CLIFORGE_INT_TYPE(int32_t, I32, "int32")
|
||||
CLIFORGE_INT_TYPE(uint32_t, U32, "uint32")
|
||||
CLIFORGE_INT_TYPE(int64_t, I64, "int64")
|
||||
CLIFORGE_INT_TYPE(uint64_t, U64, "uint64")
|
||||
#undef CLIFORGE_INT_TYPE
|
||||
|
||||
template <>
|
||||
struct TypeOf<float> {
|
||||
static TypeInfo get() {
|
||||
TypeInfo t;
|
||||
t.kind = Kind::F32;
|
||||
t.displayName = "float32";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar {
|
||||
return Scalar{detail::parseFloating<float>(tok, lbl, "float32")};
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct TypeOf<double> {
|
||||
static TypeInfo get() {
|
||||
TypeInfo t;
|
||||
t.kind = Kind::F64;
|
||||
t.displayName = "float64";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar {
|
||||
return Scalar{detail::parseFloating<double>(tok, lbl, "float64")};
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct TypeOf<std::string> {
|
||||
static TypeInfo get() {
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Str;
|
||||
t.displayName = "string";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view) -> Scalar {
|
||||
return Scalar{std::string(tok)};
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct TypeOf<char> {
|
||||
static TypeInfo get() {
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Char;
|
||||
t.displayName = "char";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar {
|
||||
if (tok.size() != 1) {
|
||||
throw ParseError("type mismatch for '" + std::string(lbl) + "': got '" +
|
||||
std::string(tok) + "', expected a single char");
|
||||
}
|
||||
return Scalar{tok[0]};
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct TypeOf<bool> {
|
||||
static TypeInfo get() {
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Bool;
|
||||
t.displayName = "bool";
|
||||
t.parsePlain = [](std::string_view tok, std::string_view lbl) -> Scalar {
|
||||
std::string lower = detail::toLower(tok);
|
||||
if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") {
|
||||
return Scalar{true};
|
||||
}
|
||||
if (lower == "false" || lower == "0" || lower == "no" || lower == "off") {
|
||||
return Scalar{false};
|
||||
}
|
||||
throw ParseError("type mismatch for '" + std::string(lbl) + "': got '" +
|
||||
std::string(tok) + "', expected bool (true/false, 1/0, yes/no, on/off)");
|
||||
};
|
||||
return t;
|
||||
}
|
||||
};
|
||||
|
||||
// Builds a TypeInfo for a `choices<T>()` restricted slot: only the
|
||||
// listed string keys are accepted, each mapped to a T. Works both for
|
||||
// genuine C++ enums (boxed into EnumTag) and for restricting a built-in
|
||||
// type (e.g. a string option limited to a fixed set of values). The
|
||||
// choice table is converted to type-erased Scalars once, here, so
|
||||
// nothing downstream needs to be templated on T again.
|
||||
template <typename T>
|
||||
TypeInfo makeChoiceTypeInfo(std::string typeName,
|
||||
const std::vector<std::pair<std::string, T>>& choices) {
|
||||
auto table = std::make_shared<ChoiceTable>();
|
||||
table->entries.reserve(choices.size());
|
||||
for (const auto& [name, val] : choices) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
table->entries.emplace_back(name, Scalar{EnumTag{static_cast<long long>(val)}});
|
||||
} else {
|
||||
table->entries.emplace_back(name, Scalar{val});
|
||||
}
|
||||
}
|
||||
TypeInfo t;
|
||||
t.kind = Kind::Enum;
|
||||
t.displayName = std::move(typeName);
|
||||
t.choiceTable = std::move(table);
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Value: what actually flows out of the parser and into bound functions
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
class Value {
|
||||
public:
|
||||
Value() = default;
|
||||
|
||||
static Value ofScalar(Scalar s) {
|
||||
Value v;
|
||||
v.data_ = std::move(s);
|
||||
return v;
|
||||
}
|
||||
static Value ofVector(std::vector<Scalar> v) {
|
||||
Value out;
|
||||
out.data_ = std::move(v);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool hasValue() const { return !std::holds_alternative<std::monostate>(data_); }
|
||||
bool isVector() const { return std::holds_alternative<std::vector<Scalar>>(data_); }
|
||||
|
||||
template <typename T>
|
||||
T as() const {
|
||||
const Scalar& s = std::get<Scalar>(data_);
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
return static_cast<T>(std::get<EnumTag>(s).value);
|
||||
} else {
|
||||
return std::get<T>(s);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<T> asVector() const {
|
||||
const auto& vec = std::get<std::vector<Scalar>>(data_);
|
||||
std::vector<T> out;
|
||||
out.reserve(vec.size());
|
||||
for (const Scalar& s : vec) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
out.push_back(static_cast<T>(std::get<EnumTag>(s).value));
|
||||
} else {
|
||||
out.push_back(std::get<T>(s));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<std::monostate, Scalar, std::vector<Scalar>> data_;
|
||||
};
|
||||
|
||||
} // namespace cliforge
|
||||
Reference in new issue
Block a user