Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 119 additions & 29 deletions libraries/sysiolib/core/sysio/basic_name.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
*/

#include "check.hpp"
#include "reflect.hpp"
#include "serialize.hpp"

#include <compare>
Expand Down Expand Up @@ -63,44 +64,107 @@ concept basic_name_traits =
{ Traits::bad_char_message } -> std::convertible_to<const char*>;
{ Traits::too_long_message } -> std::convertible_to<const char*>;
{ Traits::bad_final_symbol_message } -> std::convertible_to<const char*>;
{ Traits::not_normalized_message } -> std::convertible_to<const char*>;
Comment thread
huangminghuang marked this conversation as resolved.
}
&& Traits::max_len > 0
&& std::string_view{ Traits::alphabet }.size() > 0;

/// OPTIONAL traits members: the symbols a spelling may START with, and the
/// message to report when it does not. Traits that omit them accept any
/// alphabet character in the leading position, which is what `sysio::name`
/// wants. `slug_name` supplies them so that no legal code can be confused with
/// a decimal number - see `slug_name_traits::leading_alphabet`. Byte-identical
/// with the host-side fc::basic_name.
template <typename Traits>
concept basic_name_has_leading_alphabet = requires {
{ Traits::leading_alphabet } -> std::convertible_to<std::string_view>;
{ Traits::bad_leading_char_message } -> std::convertible_to<const char*>;
};

template <basic_name_traits Traits>
struct basic_name {
uint64_t value = 0;

constexpr basic_name() = default;
constexpr explicit basic_name( uint64_t v ) : value(v) {}

/// Per-character validated string constructor. sysio::check-throws on an
/// over-long string, an out-of-alphabet character, a final symbol too
/// wide for its (possibly narrowed) slot, or - for zero_terminates traits
/// only - the pad symbol embedded anywhere in the string (such a literal
/// would silently decode to just its prefix; rejecting it keeps the
/// literal and the canonical decoding in agreement). constexpr - so an
/// invalid `_n` / `_s` literal is a compile error.
/// Construct from a string. sysio::check-throws unless the input is the
/// canonical spelling of its own encoding - see validity_error(), the
/// SINGLE validation algorithm this type has, shared with
/// is_valid_literal() and byte-for-byte the same rules, in the same order,
/// as the host-side fc::basic_name. The two are meant to be diffable.
///
/// constexpr - sysio::check is not, so it is reached only on the failure
/// path: a valid `_n` / `_s` literal constant-evaluates, and an invalid one
/// is a compile error rather than a silent mis-encoding.
constexpr explicit basic_name( std::string_view str ) : value(0) {
// sysio::check is not constexpr - invoke it only on the failure path so
// a valid `_n` / `_s` literal still constant-evaluates (a bad one
// reaches check and is therefore a compile error).
if ( str.size() > static_cast<std::size_t>(Traits::max_len) )
sysio::check( false, Traits::too_long_message );
if ( const char* why = validity_error(str) )
sysio::check( false, why );
value = pack(str);
}

/// Non-validating encode - the constexpr path used by literals. Characters
/// outside the alphabet pack as symbol 0.
static constexpr uint64_t pack( std::string_view str ) {
uint64_t v = 0;
const int n = static_cast<int>(str.size());
for ( int i = 0; i < Traits::max_len && i < n; ++i ) {
const uint64_t sym = symbol( str[i] );
for ( int i = 0; i < Traits::max_len && i < n; ++i )
v |= (sym_of(str[i]) & width_mask(i)) << shift(i);
return v;
}

/// Is `str` a valid, canonical spelling? Delegates to validity_error so the
/// literal path and the throwing constructor can never disagree.
static constexpr bool is_valid_literal( std::string_view str ) {
return validity_error(str) == nullptr;
}

/// THE validation algorithm. Returns nullptr when `str` is a valid,
/// canonical spelling; otherwise the traits' message for the FIRST rule it
/// breaks. Rules 4-6 make pack() lossless, so to_string(pack(str)) IS str.
/// Identical to fc::basic_name::validity_error - keep the two in lock-step.
static constexpr const char* validity_error( std::string_view str ) {
// 1. length
if ( str.size() > static_cast<std::size_t>(Traits::max_len) )
return Traits::too_long_message;

// 2. leading symbol, for traits that restrict it
if constexpr ( basic_name_has_leading_alphabet<Traits> ) {
if ( !str.empty()
&& std::string_view{ Traits::leading_alphabet }.find( str[0] )
== std::string_view::npos )
return Traits::bad_leading_char_message;
}

for ( std::size_t i = 0; i < str.size(); ++i ) {
const std::size_t sym = alphabet.find( str[i] );

// 3. in the alphabet
if ( sym == std::string_view::npos )
return Traits::bad_char_message;

// 4. a zero-terminated alphabet has no INTERIOR pad: to_string() stops
// at the first symbol-0 slot, so such a spelling cannot round-trip.
if constexpr ( Traits::zero_terminates ) {
// sym == 0 is the pad/terminator slot. For zero_terminates traits,
// an interior pad would make to_string() truncate (e.g. "A\0B"
// decodes to "A"), so the input would not round-trip. Reject.
if ( sym == 0 )
sysio::check( false, Traits::bad_char_message );
return Traits::bad_char_message;
}
if ( sym > width_mask(i) )
sysio::check( false, Traits::bad_final_symbol_message );
value |= sym << shift(i);

// 5. the final slot may be narrower than `bits` (13 x 5 > 64 for name,
// leaving 4 bits), and pack() would silently truncate a symbol too
// wide for it.
if ( static_cast<uint64_t>(sym) > width_mask( static_cast<int>(i) ) )
return Traits::bad_final_symbol_message;
}

// 6. a non-zero-terminated alphabet strips TRAILING pads in to_string(),
// so a trailing pad cannot round-trip either.
if constexpr ( !Traits::zero_terminates ) {
if ( !str.empty() && str.back() == alphabet[0] )
return Traits::not_normalized_message;
}

return nullptr;
}

constexpr uint64_t to_uint64_t() const { return value; }
Expand All @@ -116,10 +180,10 @@ struct basic_name {
// slot; for name, symbol 0 ('.') is an ordinary interior character.
if ( Traits::zero_terminates && sym == 0 )
break;
s.push_back( character( sym ) );
s.push_back( char_of( sym ) );
}
if ( !Traits::zero_terminates ) {
const char pad = character(0);
const char pad = char_of(0);
while ( !s.empty() && s.back() == pad )
s.pop_back();
}
Expand All @@ -129,16 +193,14 @@ struct basic_name {
/// character -> symbol; sysio::check-throws on a character outside the
/// alphabet. (sysio::name re-exposes this as char_to_value.)
static constexpr uint64_t symbol( char c ) {
const std::string_view a = Traits::alphabet;
for ( std::size_t s = 0; s < a.size(); ++s )
if ( a[s] == c ) return static_cast<uint64_t>(s);
for ( std::size_t s = 0; s < alphabet.size(); ++s )
if ( alphabet[s] == c ) return static_cast<uint64_t>(s);
sysio::check( false, Traits::bad_char_message );
return 0; // unreachable
}
/// symbol -> character; out-of-range symbols decode as the pad (alphabet[0]).
static constexpr char character( uint64_t s ) {
const std::string_view a = Traits::alphabet;
return s < a.size() ? a[s] : a[0];
return s < alphabet.size() ? alphabet[s] : alphabet[0];
}

// Total order on the packed value. With MSB packing this matches the
Expand All @@ -150,20 +212,48 @@ struct basic_name {
friend constexpr bool operator==( basic_name a, basic_name b ) = default;

SYSLIB_SERIALIZE( basic_name, (value) )
// Bluegrass reflection, needed by CDT's to_key: its generic dispatches on is_floating_point /
// is_integral / is_enum and otherwise reflects, never consulting operator<<. Without this a basic_name
// reaching to_key reflects as invalid_fields and silently encodes a ZERO-BYTE key. Declared here rather
// than per-instantiation so every traits specialisation is covered.
CDT_REFLECT(value);

private:
/// `Traits::alphabet` as a view. The traits concept requires only that the
/// member be CONVERTIBLE to string_view, so every use binds here rather than
/// calling find/size/operator[] on the traits member and silently demanding
/// more of a policy than the concept declares. Mirrors fc::basic_name.
static constexpr std::string_view alphabet = Traits::alphabet;

// --- symbol width: minimal bits to index the alphabet ---
static constexpr int symbol_bits( std::size_t alphabet_size ) {
int b = 0;
while ( (std::size_t{1} << b) < alphabet_size ) ++b;
return b;
}
static constexpr int bits = symbol_bits( Traits::alphabet.size() );
static constexpr int bits = symbol_bits( alphabet.size() );
static constexpr int total_bits = Traits::max_len * bits < 64
? Traits::max_len * bits : 64;
static_assert( (Traits::max_len - 1) * bits < 64,
"basic_name: symbol layout does not fit in 64 bits" );

/// symbol -> character; out-of-range symbols decode as the pad (alphabet[0]).
/// The private counterpart to sym_of, matching the host-side fc::basic_name.
/// symbol()/character() below stay as this type's PUBLIC surface, which
/// sysio::name re-exposes as char_to_value.
static constexpr char char_of( uint64_t s ) {
return s < alphabet.size() ? alphabet[s] : alphabet[0];
}

/// character -> symbol, NON-throwing: any character outside the alphabet
/// maps to 0. Used by pack(), which is non-validating by contract; callers
/// that need rejection go through validity_error(). Mirrors fc's sym_of.
static constexpr uint64_t sym_of( char c ) {
for ( std::size_t s = 0; s < alphabet.size(); ++s )
if ( alphabet[s] == c ) return static_cast<uint64_t>(s);
return 0;
}

// --- Bit layout. Direction is set by Traits::packing. The final symbol
// absorbs any shortfall when max_len * bits > 64. ---
/// Bit offset of symbol i. MSB: symbol 0 occupies the highest bits and the
Expand Down
1 change: 1 addition & 0 deletions libraries/sysiolib/core/sysio/check.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
#pragma once

#include <cstdint>
#include <string>
#include <string_view>

Expand Down
9 changes: 7 additions & 2 deletions libraries/sysiolib/core/sysio/name.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ namespace sysio {
"string is too long to be a valid name";
static constexpr const char* bad_final_symbol_message =
"thirteenth character in name cannot be a letter that comes after j";
// A legal character in an illegal position (a trailing pad) leaves a
// spelling that does not round-trip through to_string(). Host wording.
static constexpr const char* not_normalized_message = "name is not properly normalized";
};

/**
Expand Down Expand Up @@ -213,8 +216,10 @@ namespace sysio {
CDT_REFLECT(value);
// name's own serialization: an exact-match operator<<(ds, const name&)
// must exist, else the generic bluegrass::meta field-iterator is chosen
// and rejects name as a non-aggregate. (basic_name has its own, used by
// slug_name, which is the alias type itself rather than a derived type.)
// and rejects name as a non-aggregate. The base's hidden friend does not
// serve a derived type -- reaching it needs a conversion, which loses to
// the exact-matching generic overload. Every derived basic_name needs its
// own; slug_name carries SYSLIB_SERIALIZE_DERIVED_EMPTY for this reason.
SYSLIB_SERIALIZE( name, (value) )
};

Expand Down
123 changes: 123 additions & 0 deletions libraries/sysiolib/core/sysio/slug_name.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#pragma once

#include "basic_name.hpp"
Comment thread
huangminghuang marked this conversation as resolved.
#include "name.hpp" // for sysio::detail::to_const_char_arr

#include <string_view>

namespace sysio {

/**
* @defgroup slug_name
* @ingroup core
* @ingroup types
* @brief Packed registry-code identifier
*/

/// Alphabet + length traits for the registry-code encoding: up to 8 symbols over [A-Z0-9_].
/// Drives sysio::basic_name.
///
/// Byte-identical with the host-side fc::slug_name, which instantiates fc::basic_name over the same
/// alphabet, length, terminator rule and packing direction. The two traits structs are the whole
/// specification — keep them diffable line for line.
struct slug_name_traits {
/// 8, not the 10 that would fill 64 bits: 8 symbols x 6 bits = 48, so every encoded value stays in
/// [0, 2^48) — under JS Number's 2^53 safe integer limit, letting TS consumers use `number` rather
/// than bigint.
static constexpr int max_len = 8;

/// Symbol 0 is the '\0' pad/terminator; 1-26 = A-Z, 27-36 = 0-9, 37 = '_'. Held as a named array so
/// the length comes from sizeof — a string_view built straight from the literal would stop at the
/// leading NUL.
static constexpr char alphabet_storage[] = "\0ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
static constexpr std::string_view alphabet{ alphabet_storage, sizeof(alphabet_storage) - 1 };

/// A code must START with a letter. This is what makes the host's string
/// carrier unambiguous: no legal code can be spelled like a number, so a
/// bare JSON string is always a code and never a decimal. Without it the
/// alphabet's digits make "7" both a valid code and a valid decimal, and
/// "1E3" / "0X10" additionally collide with JS numeric syntax. Digits and
/// '_' remain legal in every position after the first ("V1", "TRAIL_").
/// The empty string is unaffected - it is the zero sentinel.
static constexpr std::string_view leading_alphabet{ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" };

/// A symbol-0 slot TERMINATES the string, unlike name's '.' which is an ordinary interior character.
/// This is why a slug_name is not total over uint64: every value below 2^42 has a zero in the char[0]
/// slot and so decodes to the empty string.
static constexpr bool zero_terminates = true;

/// MSB-first: char[0] occupies bits [42..47]. This is what gives the packed value its grouping
/// property — a shared textual prefix is a shared leading bit prefix, so prefix-related codes are
/// contiguous in key order and retrievable as a range.
static constexpr basic_name_endianness packing = basic_name_endianness::MSB;

static constexpr const char* bad_char_message =
"character is not in allowed character set for slug_names ([A-Z0-9_])";
static constexpr const char* too_long_message = "string is too long to be a valid slug_name";
static constexpr const char* bad_leading_char_message =
"slug_name must start with a letter ([A-Z])";
static constexpr const char* bad_final_symbol_message =
"final character in slug_name does not fit its packed slot";
static constexpr const char* not_normalized_message =
"slug_name is not properly normalized";
};

/// Packed registry-code identifier — up to 8 symbols over [A-Z0-9_].
///
/// A DERIVED STRUCT, not an alias -- the same shape as sysio::name, and for the same reason.
///
/// abigen matches builtins on the namespace-stripped written spelling, and `slug_name` is in that
/// set (plugins/sysio/gen.hpp) -- but an ALIAS never reaches that match: abigen resolves it through
/// to the underlying template first and then emits BOTH a typedef
/// (`slug_name` -> `basic_name_slug_name_traits`) AND a struct_def for the instantiation. The host,
/// which knows `slug_name` intrinsically, then rejects the ABI outright:
/// duplicate_abi_type_def_exception: type already exists 'slug_name'
/// `sysio::name` avoids this only because it is a derived struct, so the builtin match applies to
/// the written name. slug_name follows it.
struct slug_name : basic_name<slug_name_traits> {
using base = basic_name<slug_name_traits>;
using base::base; // slug_name(uint64_t), slug_name(std::string_view)
constexpr slug_name() = default;
Comment thread
huangminghuang marked this conversation as resolved.

/// slug_name's own serialization, for the same reason sysio::name carries one: a DERIVED type
/// needs an EXACT-match operator on itself. The base's hidden friend takes `const basic_name&`,
/// so reaching it from a `slug_name` requires a derived-to-base conversion -- and the generic
/// class-template overload, which matches exactly, wins instead. That overload hands the type to
/// the bluegrass::meta field iterator, which rejects anything with a user-declared constructor:
/// "Types with user specified constructors are not supported"
/// Clang happens to tolerate it; GCC 13 does not, which matters because add_native_contract()
/// builds with the HOST compiler and its generated dispatcher deserializes action arguments
/// through this path. Forwarding to the base keeps the bytes identical to basic_name's.
SYSLIB_SERIALIZE_DERIVED_EMPTY( slug_name, base )

/// Does this value have a canonical spelling? A slug_name built from a RAW uint64 bypasses the
/// validating constructor -- and nothing validates on deserialization either, since the
/// reflected member is written directly -- so it can hold a value no spelling produces:
/// anything whose leading symbol slot is empty, or that uses one of the 26 unused symbol
/// values, or that sets any of bits 48-63. Such a value cannot round-trip.
///
/// This lives on slug_name and NOT on basic_name because it is meaningless for `name`: that
/// alphabet is exactly 2^5 with no gaps and its 13 symbols consume all 64 bits, so every
/// uint64 IS a canonical name and the predicate could never be false.
bool is_canonical() const {
const std::string text = to_string();
return is_valid_literal(text) && pack(text) == value;
}
};

} // namespace sysio

/**
* Compile-time slug_name literal: `"ETH"_s`, `"LIQSOL"_s`. Validation happens in basic_name's constexpr
* constructor — a character outside [A-Z0-9_], or more than 8 characters, fails the constant evaluation.
* Mirrors the shape of sysio::name's `_n` literal, including its global scope.
*/
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu-string-literal-operator-template"
template <typename T, T... Str>
inline constexpr sysio::slug_name operator""_s() {
constexpr auto x = sysio::slug_name{ std::string_view{ sysio::detail::to_const_char_arr<Str...>::value,
sizeof...(Str) } };
return x;
}
#pragma clang diagnostic pop
Loading
Loading