#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 #include #include #include #include #include #include #include #include #include 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() 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(), the underlying value is boxed here. Value::as() 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; 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> 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()-restricted slots. std::shared_ptr 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 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 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() instead // since they need a name<->value table supplied by the caller. template struct TypeOf; // intentionally undefined for unsupported T #define CLIFORGE_INT_TYPE(CPP_T, KIND, NAME) \ template <> \ struct TypeOf { \ 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(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 { 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(tok, lbl, "float32")}; }; return t; } }; template <> struct TypeOf { 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(tok, lbl, "float64")}; }; return t; } }; template <> struct TypeOf { 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 { 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 { 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()` 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 TypeInfo makeChoiceTypeInfo(std::string typeName, const std::vector>& choices) { auto table = std::make_shared(); table->entries.reserve(choices.size()); for (const auto& [name, val] : choices) { if constexpr (std::is_enum_v) { table->entries.emplace_back(name, Scalar{EnumTag{static_cast(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 v) { Value out; out.data_ = std::move(v); return out; } bool hasValue() const { return !std::holds_alternative(data_); } bool isVector() const { return std::holds_alternative>(data_); } template T as() const { const Scalar& s = std::get(data_); if constexpr (std::is_enum_v) { return static_cast(std::get(s).value); } else { return std::get(s); } } template std::vector asVector() const { const auto& vec = std::get>(data_); std::vector out; out.reserve(vec.size()); for (const Scalar& s : vec) { if constexpr (std::is_enum_v) { out.push_back(static_cast(std::get(s).value)); } else { out.push_back(std::get(s)); } } return out; } private: std::variant> data_; }; } // namespace cliforge