feat(sysiolib): add sysio::slug_name and make it an abigen builtin - #119
Conversation
slug_name is the packed registry-code identifier the depot keys its v6 registry
tables on, and it lived only in wire-sysio's contracts/sysio.opp.common as a
hand-rolled struct. It belongs beside name: both are basic_name instantiations
over their own traits, and the abigen builtins entry that lets a field carry the
bare `slug_name` ABI type name has to live in this repo regardless.
The builtins entry is what stops the alias emitting a typedef. abi_serializer
resolves typedefs BEFORE its builtin lookup, so an alias without the entry would
silently revert every slug field to `{"value": N}` with no error anywhere.
CDT_REFLECT moves onto basic_name itself rather than the instantiation: to_key's
generic reflects instead of consulting operator<<, so a basic_name reaching it
without reflection encodes a zero-byte key. Declaring it on the template covers
name, slug_name and any future traits at once.
Change-Id: I2c0e4d00126b576877c38c91464a24302ef2d971
…ltin Both changes this commit's parent made were untestable by its own suite: deleting either left all 35 unit tests and all 69 toolchain tests green. - tests/toolchain/abigen-pass/slug_name_builtin: pins that a sysio::slug_name field reaches the ABI as the bare builtin on both paths that can leak it — an action field type and a kv table's key_types — with no struct_def and no typedef. Verified by removing the gen.hpp builtins entry and rebuilding: abigen then emits `types: [slug_name -> basic_name_slug_name_traits]` plus that struct, while the field type and key_types still read "slug_name", which is why the failure is silent. With the entry removed this fixture is the only one of 70 that fails. - slug_name_to_key_writes_eight_bytes: pins CDT_REFLECT(value) on basic_name. to_key's generic arm reflects rather than consulting operator<<, so an unreflected basic_name writes a zero-byte key with no diagnostic. Every other case in the file exercises the type and passes with the reflection deleted. Note the test must include key_utils.hpp explicitly — nothing else does, which is itself why to_key had no coverage. Change-Id: I7f8eb5884a1f50e01767b00ef64cbfdfa0872d20
The new files were wrapped at 87-92 columns while CLAUDE.md sets 120 for new code and the neighbouring headers run past 300. Comment prose only; no declaration, constant or test assertion changed. slug_name_tests 6/6, ctest unit_tests 32/32, toolchain 70/70. Change-Id: I21097253e8b5e531c5edc89c09c4ea2b2ed6b60a
Mirrors the host-side rule in fc::slug_name_traits: no legal code can be spelled like a number, which is what makes the host's string carrier unambiguous. Digits and '_' stay legal after the first position. leading_alphabet / bad_leading_char_message are OPTIONAL traits members, so sysio::name is unaffected. Change-Id: Ife81282e7a14f18b2c815ecf52d46631fece004b
basic_name gains validity_error() -- the single predicate both the constructor and is_valid_literal() use -- plus pack() and is_canonical(). Token-identical with the host-side fc::basic_name; only the throw mechanism differs, so the two stay diffable. slug_name becomes a derived struct like name. An alias never reaches abigen's builtin match: abigen resolves it to the underlying template and emits a typedef plus a struct_def for the instantiation, which the host -- knowing slug_name intrinsically -- rejects as a duplicate type definition. abigen: add_struct skips a type the ABI knows intrinsically, so a builtin's base is no longer described as an orphan struct_def no field references. Change-Id: Ie00ff09437eecc0482c42e88f4018a5379426aec
…dt-builtin Change-Id: I8dba8a895362d0ef39ac4ebe279a945cf067c8af
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at head 5dba0330. wire-sysio#619 correctly fixes the host-side ABI/JSON support, so that dependency is not itself a finding. The issues below remain in CDT's generated ABI and public type implementation. I reproduced the ABI cases with packaged merge-base/HEAD compilers and the native serialization case with GCC 13 versus Clang 18.
A derived basic_name needs an EXACT-match operator<</>>. The base's hidden friend
takes `const basic_name&`, so reaching it from slug_name needs a derived-to-base
conversion and loses to the generic class overload -- which hands the type to
bluegrass's field iterator and is rejected under GCC ("Types with user specified
constructors are not supported"). Clang tolerates it, so CI masked it;
add_native_contract() builds with the HOST compiler, so a GCC-native contract
taking a slug_name action parameter would not compile.
abigen gains the two fixes the same builtin spelling exposed: a struct whose name
collides with a builtin is now a diagnostic instead of a silent drop that leaves
the action unusable, and a table keyed DIRECTLY on a builtin publishes that one
leaf instead of an empty key shape no host can decode.
Change-Id: I94a2202c61b631fca227ca2fd2c7a6adf255ea49
huangminghuang
left a comment
There was a problem hiding this comment.
I accepted the rationale for keeping not_normalized_message unconditional. The exact serializer, action-method collision, and direct-key findings from the previous review are resolved. Two current-head issues remain below.
…ions before the type short-circuit check.hpp declares its intrinsics with uint32_t/uint64_t but did not include <cstdint>, so a TU whose FIRST include is <sysio/slug_name.hpp> reached it through basic_name.hpp before anything supplied the fixed-width types and failed on host GCC. Every test includes <sysio/sysio.hpp> first, which masked it. add_type() short-circuits on any type whose translated spelling is builtin, so a contract-declared record or enum named after one was never described and never reached add_struct's guard: the ABI named the field with the builtin's spelling while the dispatcher kept the declaration's real layout. The check now runs before that short-circuit and is declaration-aware -- sysio:: declares the builtins that are real types and std:: is where string comes from, so a colliding spelling outside both is the author's own. Change-Id: Ibe20a0ef241c01abf0ce418d0dfe292abaf45c9f
huangminghuang
left a comment
There was a problem hiding this comment.
The two findings from the previous review are fixed: the public header now compiles standalone under GCC 13, and direct record/enum collisions are diagnosed. I continue to accept the not_normalized_message rationale. The new collision guard still has three reproducible bypasses that produce the same missing-type ABI mismatch, detailed below.
A user type whose name collides with an ABI builtin has always been dropped
silently. `struct name { uint64_t id; uint32_t flags; }` used as an action
parameter compiles clean on the PRE-PR compiler and emits `payload: name` with
no struct for it -- identical to what was reported for slug_name. The condition
belongs to the builtin set, not to this type, and no other builtin diagnoses it.
The guard added here tried to, and produced a regression each round: typedef
aliases, explicit nested containers and nested namespaces all bypassed it while
the pre-PR compiler handled them correctly. Reverting leaves slug_name behaving
exactly like sysio::name, which is the bar this PR should meet. The serializer,
the direct-key leaf and the check.hpp include are unaffected.
Change-Id: I426c9a29d4b2eb6cfbebc01968a3f682e9a28778
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 0eb9eb9d. I accept reverting and deferring the builtin-name collision diagnostic; the three prior guard findings are resolved. The exact serializer, direct builtin KV-key shape, and self-contained header fixes remain valid. One separate public-template regression remains below.
The traits concept requires only convertible-to-string_view, but validity_error used find()/operator[] on the traits member directly and the symbol-width derivation used size(). One private binding now serves every use, as the concept already does for leading_alphabet. basic_name_tests adds a policy whose alphabet is only convertible: it fails to compile at all three sites without the binding. Change-Id: Id7c2e203fbd16246047530f3442c38043898b797
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 9b3bbeb8. The convertible-alphabet regression is fixed comprehensively by normalizing every use through a constexpr std::string_view, with coverage for a policy exposing no find, size, or subscript operation. The serializer, direct builtin KV-key, and self-contained-header fixes remain valid. I continue to accept deferring builtin-name collision handling to a separate all-builtins change. Linux, package, and required checks pass; the documented wire-sysio#619 merge-order dependency remains. No remaining findings.
is_canonical is meaningless on `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 can never be false. It belongs to slug_name's encoding, which leaves 26 symbol values unused, terminates on symbol 0, and never reads bits 48-63. Two static_asserts pin the shape, because it drifted silently once: this repo derived slug_name so abigen would match the builtin, while wire-sysio kept an alias, and is_canonical sat on the base they share. wire-sysio carries the same pair, so either side drifting is a compile error. ctest 35/35. Change-Id: If500b02a829b2a4c92f258852cc0199198d625f3
They asserted the two properties that had already drifted, which is not where the next drift will be, and the has_is_canonical concept existed only to feed them — public surface with no runtime value. Both properties are enforced by use anyway: deriving slug_name is what makes abigen match the builtin, and is_canonical only compiles against the derived type. ctest 35/35. Change-Id: I8600293d940d0e6860142bceee4ccad214fe426c
The action wrapper is named after the method, so a method named after a builtin yields a struct validate_struct drops; the action's type then names the builtin and the host reads its layout instead of the parameter list. Restores the wrapper-path diagnostic and its fixture, reverted with the leaky declaration guard in 0eb9eb9; this path has no alias or nested route around it. Change-Id: If39429a8adb8479c71b2131f7ad8d291ac12ba2c
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 3b340d0f97338a1a96215d60a4ff1ac70a6163c0 using the open-code-review delegate workflow. No remaining actionable findings.
The action-method collision is now diagnosed before its synthetic ABI wrapper can be silently dropped. I rebuilt the current ABI-generation plugin and passed seven focused checks: the original reproduction, the committed failure fixture, collisions with name and asset, preservation of an explicit action name when the C++ method has a safe name, and both slug ABI fixtures. The library files are unchanged from the prior review, whose three affected unit-test binaries and GCC serialization/canonicality checks passed. The uniform traits requirement remains accepted.
Linux build/tests pass. Package verification stopped on an Ubuntu dependency download (libexpat1, HTTP 404), so that check remains unverified; macOS CI is still running. Full integration tests were not rerun locally. The documented merge-order dependency on Wire-Network/wire-sysio#619 remains in force.
Landing 3 of the
slug_namecarrier unification.This makes abigen stop describing
slug_name— no typedef, no struct_def — which is only safe once the host knows the type intrinsically (#619'sbuilt_in_typesentry).Nothing breaks at the instant of merge: the committed
.abis still carry aslug_namestruct_def, so_is_typeresolves it through the struct table. The failure arrives with the next contract-artifact regeneration on a host without #619, and then it is a hard throw. A regenerated ABI offersslug_nameas neither builtin, typedef, struct, variant, enum, nor protobuf type, so_is_type(abi_serializer.cpp:401-411) misses on all six,validate()assertsinvalid_type_inside_abi(:449), andset_abipropagates it (:228).get_table_rowsbuilds a freshabi_serializerper request (chain_plugin.cpp:2861,:3036), so it throws on every request — andvalidate()walks every struct field, so non-key fields fail like keys.Scope: five contracts and all 103 pre-existing
slug_namefields —reserv48,uwrit24,opreg14,tokens12,chains5 — plus anything else building anabi_serializerover those ABIs, including JSONpush_action.The BE-key
FC_ASSERT("Unsupported BE key type")is not the mechanism: it is caught anddlog'd (chain_plugin.cpp:2464-2474) with the default level atinfo, so alone it is silent and only degrades JSON key output to hex.What changes
sysio/slug_name.hpp(new) — the traits, abasic_name-derivedslug_namestruct, and the_sliteral. This is the packed registry-code identifier the depot keyschains::chains,tokens::tokens,tokens::chaintokens,reserv::reservesanduwrit::locksumson. It lived only in wire-sysio'scontracts/sysio.opp.commonas a hand-rolled struct with its own packing loop, and belongs besidename— both arebasic_nameinstantiations.plugins/sysio/gen.hpp—"slug_name"in the abigen builtins set.basic_name.hpp—CDT_REFLECT(value)on the template.Nothing in CDT includes the new header:
basic_name.hppreaches every contract TU vianame.hpp, so an alias reachable from there would be a hard redefinition against every wire-sysio TU still including the old copy.slug_nameis a DERIVED STRUCT — the alias rationale below was wrongAn earlier revision of this description argued for an alias, predicting that a derived
struct slug_name : basic_name<…>"would emit a base-carrying struct with zero fields plus a secondbasic_name_slug_name_traitsstruct, becauseadd_structrecords one base and iterates only declaredfields". That diagnosis of
add_structwas exactly right. The conclusion drawn from it was not.Both forms leak, and the alias leaks worse:
using slug_name = basic_name<…>types: [slug_name -> basic_name_slug_name_traits]and the structduplicate_abi_type_def_exception: type already exists 'slug_name'struct slug_name : basic_name<…>slug_name(base, no fields) and the structadd_structguardThe builtins entry never protected the alias: abigen resolves an alias to the underlying template
before the builtin match can apply.
sysio::nameescapes only because it is a derived struct, whichis why
slug_nameis one now.This was unreachable until wire-sysio#619 deleted the contracts' own hand-rolled
slug_name, becauseno contract had ever fed CDT's type to abigen. It surfaced as 523 of 762 contract tests failing the
first time they did.
add_structskips a builtinThe base walk in
add_structruns unconditionally, before any builtin check applies to the derivedtype, so a builtin's base was described as an orphan struct_def that no field references — five ABIs
carried
basic_name_slug_name_traits.add_typealready skips builtins, so this is only reachablewhere a caller force-adds a struct: today the kv-key path, which adds a table's key struct so clients
can reference it. Correct for a composite key, wrong for one that is already a builtin.
namewasescaping by luck — no table is keyed on a bare
name, and the moment one were, its base would leakidentically.
One validation algorithm, shared with the host
basic_namegainsvalidity_error()— the single predicate both the constructor andis_valid_literal()use — plus
pack()andis_canonical().validity_errorandpackare token-identical with thehost-side
fc::basic_name(same md5); only the throw mechanism differs (sysio::checkwith the traits'message vs
throw_invalid, which also names the offending input).This gives the contracts the primitive they had no way to express before: "is this raw
uint64a code?"is now
slug_name::is_valid_literal, which is what wire-sysio#619's proto-boundary validation calls.CDT is deliberately stricter than upstream CDT here — it exists only to interface with wire-sysio,
so the host's rules are the contract. In particular a trailing pad is rejected (upstream CDT accepts it,
upstream Spring rejects it; the two upstreams disagree, so "match upstream" could not settle it).
CDT_REFLECTon the templateto_key's generic arm reflects rather than consultingoperator<<, soSYSLIB_SERIALIZEdoes not help it and an unreflectedbasic_namewrites a silent zero-byte key.This is defensive, not a live fix.
to_keyhas no callers —core/sysio/key_utils.hppis included by nothing here. The near-miss worth knowing:sysio::multi_indexis a shim overkv_multi_index.hpp, which includeskv_utils.hpp(be_key_stream) — one letter fromkey_utils.hpp, and a different file. Bothmulti_indexandkv::tableencode throughbe_key_streamand never reachto_key.Declaring it on
basic_namecoversslug_name, an alias with no declaration of its own. It does not coversysio::name, a derived class whose ownCDT_REFLECT(value)(name.hpp:213) hides the base's — so that one stays in effect and is not redundant.Tests
tests/unit/slug_name_tests.cpp, 6 cases, registered in both places:tests/unit/builds it,tests/CMakeLists.txtregisters it with ctest. CDT tests are not globbed, so building without registering yields a test that compiles and never runs.The centrepiece is byte identity with the host —
slug_name{"LIQSOL"}.value == 53413609783296plus six more constants taken fromfc::slug_name's output. The two implementations live in separate toolchains and agree only because their traits agree, so these constants are the encoding's only mechanical guard: a failure means the traits diverged, so do not edit them. Also pinned: the traits policy, the 2^42 floor, and prefix grouping.Two guards cover the lines the suite could not otherwise see:
abigen-pass/slug_name_builtinpins that asysio::slug_namefield reaches the ABI as the bare builtin on both paths that can leak it — an action field type and a kv table'skey_types— with no struct_def and no typedef. Verified by removing the builtins entry and rebuilding: with it gone, this fixture is the only one of 70 to fail.slug_name_to_key_writes_eight_bytespins theCDT_REFLECT. Every other case here passes with the reflection deleted. It includeskey_utils.hppexplicitly because nothing else does — itself whyto_keyhad no coverage.Verification
ctest -L unit_tests32/32, toolchain suite 70/70. ctest counts binaries, not cases, so the deltas are inside them:slug_name_testsruns 6 where it ran 5, the toolchain suite 70 where it ran 69.