Skip to content

feat(sysiolib): add sysio::slug_name and make it an abigen builtin - #119

Merged
heifner merged 13 commits into
masterfrom
feature/slug-name-cdt-builtin
Sep 25, 2026
Merged

heifner merged 13 commits into
masterfrom
feature/slug-name-cdt-builtin

Conversation

@heifner

@heifner heifner commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Landing 3 of the slug_name carrier unification.

⚠️ MUST NOT MERGE BEFORE wire-sysio#619

This makes abigen stop describing slug_name — no typedef, no struct_def — which is only safe once the host knows the type intrinsically (#619's built_in_types entry).

Nothing breaks at the instant of merge: the committed .abis still carry a slug_name struct_def, so _is_type resolves 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 offers slug_name as neither builtin, typedef, struct, variant, enum, nor protobuf type, so _is_type (abi_serializer.cpp:401-411) misses on all six, validate() asserts invalid_type_inside_abi (:449), and set_abi propagates it (:228). get_table_rows builds a fresh abi_serializer per request (chain_plugin.cpp:2861, :3036), so it throws on every request — and validate() walks every struct field, so non-key fields fail like keys.

Scope: five contracts and all 103 pre-existing slug_name fields — reserv 48, uwrit 24, opreg 14, tokens 12, chains 5 — plus anything else building an abi_serializer over those ABIs, including JSON push_action.

The BE-key FC_ASSERT("Unsupported BE key type") is not the mechanism: it is caught and dlog'd (chain_plugin.cpp:2464-2474) with the default level at info, so alone it is silent and only degrades JSON key output to hex.

What changes

  • sysio/slug_name.hpp (new) — the traits, a basic_name-derived slug_name struct, and the _s literal. This is the packed registry-code identifier the depot keys chains::chains, tokens::tokens, tokens::chaintokens, reserv::reserves and uwrit::locksums on. It lived only in wire-sysio's contracts/sysio.opp.common as a hand-rolled struct with its own packing loop, and belongs beside name — both are basic_name instantiations.
  • 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.hpp reaches every contract TU via name.hpp, so an alias reachable from there would be a hard redefinition against every wire-sysio TU still including the old copy.

slug_name is a DERIVED STRUCT — the alias rationale below was wrong

An 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 second
basic_name_slug_name_traits struct, because add_struct records one base and iterates only declared
fields". That diagnosis of add_struct was exactly right. The conclusion drawn from it was not.

Both forms leak, and the alias leaks worse:

declaration abigen emits host reaction
using slug_name = basic_name<…> types: [slug_name -> basic_name_slug_name_traits] and the struct rejects the ABI: duplicate_abi_type_def_exception: type already exists 'slug_name'
struct slug_name : basic_name<…> slug_name (base, no fields) and the struct shadows the struct_def; the orphan base is noise
derived + the add_struct guard nothing clean

The builtins entry never protected the alias: abigen resolves an alias to the underlying template
before the builtin match can apply. sysio::name escapes only because it is a derived struct, which
is why slug_name is one now.

This was unreachable until wire-sysio#619 deleted the contracts' own hand-rolled slug_name, because
no contract had ever fed CDT's type to abigen. It surfaced as 523 of 762 contract tests failing the
first time they did.

add_struct skips a builtin

The base walk in add_struct runs unconditionally, before any builtin check applies to the derived
type, 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_type already skips builtins, so this is only reachable
where 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. name was
escaping by luck — no table is keyed on a bare name, and the moment one were, its base would leak
identically.

One validation algorithm, shared with the host

basic_name gains validity_error() — the single predicate both the constructor and is_valid_literal()
use — plus pack() and is_canonical(). validity_error and pack are token-identical with the
host-side fc::basic_name (same md5); only the throw mechanism differs (sysio::check with 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 uint64 a 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_REFLECT on the template

to_key's generic arm reflects rather than consulting operator<<, so SYSLIB_SERIALIZE does not help it and an unreflected basic_name writes a silent zero-byte key.

This is defensive, not a live fix. to_key has no callers — core/sysio/key_utils.hpp is included by nothing here. The near-miss worth knowing: sysio::multi_index is a shim over kv_multi_index.hpp, which includes kv_utils.hpp (be_key_stream) — one letter from key_utils.hpp, and a different file. Both multi_index and kv::table encode through be_key_stream and never reach to_key.

Declaring it on basic_name covers slug_name, an alias with no declaration of its own. It does not cover sysio::name, a derived class whose own CDT_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.txt registers 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 == 53413609783296 plus six more constants taken from fc::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_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 builtins entry and rebuilding: with it gone, this fixture is the only one of 70 to fail.
  • slug_name_to_key_writes_eight_bytes pins the CDT_REFLECT. Every other case here passes with the reflection deleted. It includes key_utils.hpp explicitly because nothing else does — itself why to_key had no coverage.

Verification

ctest -L unit_tests 32/32, toolchain suite 70/70. ctest counts binaries, not cases, so the deltas are inside them: slug_name_tests runs 6 where it ran 5, the toolchain suite 70 where it ran 69.

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
@heifner
heifner requested a review from a team September 15, 2026 20:55
…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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread libraries/sysiolib/core/sysio/slug_name.hpp
Comment thread plugins/sysio/gen.hpp
Comment thread plugins/sysio/abigen.hpp
Comment thread libraries/sysiolib/core/sysio/basic_name.hpp
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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/sysio/abigen.hpp
Comment thread libraries/sysiolib/core/sysio/slug_name.hpp
…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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/sysio/abigen.hpp Outdated
Comment thread plugins/sysio/abigen.hpp Outdated
Comment thread plugins/sysio/abigen.hpp Outdated
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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread libraries/sysiolib/core/sysio/basic_name.hpp Outdated
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
huangminghuang previously approved these changes Sep 22, 2026

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@heifner
heifner merged commit a00bd71 into master Sep 25, 2026
16 of 20 checks passed
@heifner
heifner deleted the feature/slug-name-cdt-builtin branch September 25, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants