fix: a bare [[sysio::table]] phantom entry, and --use-rt never reaching the linker - #117
fix: a bare [[sysio::table]] phantom entry, and --use-rt never reaching the linker#117heifner wants to merge 25 commits into
[[sysio::table]] phantom entry, and --use-rt never reaching the linker#117Conversation
Found while answering "what does a ported multi_index contract actually have to
change?" -- the answer turned out to include an ABI defect nobody had noticed.
A bare [[sysio::table]] names no table. The struct name stood in so the entry had
something, but that placeholder was then emitted ALONGSIDE the entry produced by
the multi_index / kv::table instantiation. Porting a stock Antelope contract --
`struct [[eosio::table]] account` beside `multi_index<"accounts"_n, account>`,
which is the idiom every ported contract arrives with -- produced two ABI tables
for one:
('account', 15140) <- placeholder; nothing ever writes to this table_id
('accounts', 25660) <- the real table
so get_table_rows for the struct's name described a table with no rows. Giving
the attribute an explicit name avoided it, which is why in-tree fixtures that do
so were unaffected.
The placeholder is now dropped when an instantiation of that struct exists, and
kept when none does -- a struct annotated but never instantiated has nothing else
to name it.
Two existing fixtures pinned the phantom and are regenerated: kv_key_types loses
('kvrow', 53512) and singleton_contract loses ('out_of_class3', 8120). Each diff
is one removal and nothing else. bare_table_attr pins the fix in both directions
-- one entry for the instantiated struct, a kept entry for an uninstantiated one
-- and fails when the drop is reverted.
Also, two examples that never compiled, because examples/ is not in the build
graph:
multi_index_example, multi_index_large -- postfix `itr++` on a KV iterator,
deleted since the KV port.
multi_index_large -- its row struct is nested in the contract, carries default
member initializers, and the table is a data member. A default member
initializer is parsed in a complete-class context, so while the compiler is
inside the enclosing class the row type is not yet default-constructible and
kv_multi_index's static_assert fires, naming the row type rather than the
initializers. Removed the initializers; any one of the three would do.
Note for a follow-up, not fixed here: `cdt-cpp --use-rt` is accepted but never
reaches the link -- cdt-cpp.cpp.in has no reference to use_rt -- so a float128
contract cannot be built in one step. `cdt-ld --use-rt` works.
ctest 33/33 with integration on, 16/16 integration suites, toolchain 43/43, and
44/44 wire-sysio artifacts byte-identical.
GetLdDefaults -- which turns --use-rt into -lsf -- runs only under ONLY_LD, so it is cdt-ld that acts on the flag. cdt-cpp parses the same options header, so it ACCEPTED --use-rt without complaint and then never passed it to the cdt-ld it invokes. A long double / float128 contract could not be built in one step: the link failed on librt's undefined f128_lt / f128_eq, and the diagnostic told the user to pass the flag they had just passed. cdt-cpp already forwards the flags it does not act on itself -- -fquery, -fquery-server, -fquery-client, --allow-names -- in the same block. --use-rt was simply missing from that list. Found while getting examples/multi_index_large to build: it uses long double, so it could not be built without pre-compiling to an object and linking with cdt-ld separately. Only the negative case had a test. build-fail/long_double_no_use_rt pinned that omitting the flag fails with a hint, so the hint stayed correct while the flag itself did nothing. build-pass/long_double_use_rt is the other half, sharing the negative fixture's body so the pair differs only by the flag; removing the forwarding again fails it and nothing else (43/44). Opt-in behaviour is unchanged: without --use-rt the link still fails, which is what keeps softfloat out of contracts that do not need it. ctest 33/33 with integration on, 16/16 integration suites, toolchain 44/44, and 44/44 wire-sysio artifacts byte-identical.
| // twice -- once under the ROW STRUCT's name, whose table_id nothing ever writes | ||
| // to, so get_table_rows for it returned nothing. A struct annotated but never | ||
| // instantiated keeps its placeholder, since nothing else can name it. | ||
| if (unnamed_table_types.count(t.type) && |
There was a problem hiding this comment.
[P1] Suppression fails across translation units
This check only sees tables generated during the current Clang invocation. If another source includes the bare-table row without instantiating the multi_index alias, its descriptor still contains the phantom table, and the final descriptor merge retains both names. I reproduced this with a two-source contract: the PR head emitted both accounts and account. Please carry placeholder provenance into the descriptors and prune after link-wide merging, or use an equivalent link-wide mechanism, and add a multi-source regression test.
There was a problem hiding this comment.
Reproduced and fixed in 73d487b8e. Your shape was the one I had not tried — my own two-source attempt put the alias in the shared header, where both TUs instantiate it and the phantom stays suppressed. Moving the alias into one source only:
tables: [('accounts', 25660), ('account', 15140)]
You are right that this cannot be decided per-invocation, and I have stopped trying. abigen now marks the placeholder rather than suppressing it — unconditionally, so every TU emits the same descriptor and the merge is not order-dependent — and cdt-codegen prunes after the link-wide merge, beside the table_id collision check that already runs on the final ABI.
bare_table_xtu is the regression: the struct in a header, the alias and contract in one source, a second source that only includes the header. Its second TU lives in a subdirectory so the suite does not discover it as a test of its own, and it is passed through compile_flags along with an explicit -contract, since auto-detection needs a single source.
The provenance flag rides in the .desc. I had talked myself out of that on a misreading of CLAUDE.md — it says treat such a change as breaking until proven otherwise, which is a burden of proof, not a veto, and .desc is an internal artifact produced and consumed by one toolchain version and never committed. The thing that actually needs proving is the emitted ABI, and that is unchanged: 44/44 wire-sysio artifacts byte-identical.
| long double f128 = 0.0L; | ||
| uint64_t u64; | ||
| uint128_t u128; | ||
| double f64; |
There was a problem hiding this comment.
[P1] f64 is now serialized uninitialized
Removing this initializer leaves newly emplaced rows with an indeterminate f64: both set and mod accept an f64 argument, but neither assigns it to the row. kv_multi_index::emplace then serializes the object and reads f64 for the secondary index. Please assign u.f64 = f64 and row.f64 = f64, or otherwise retain explicit initialization.
There was a problem hiding this comment.
You are right, and it is worse than a style regression — thank you for catching it. Neither set nor mod ever assigned f64, so the initializer was covering a pre-existing omission; removing it left an indeterminate double being serialized and used as the "byf" secondary key. That is undefined behaviour producing a nondeterministic index key, in a toolchain whose first stated requirement is deterministic output.
The deeper mistake was choosing the wrong workaround. The guide I wrote for #111 lists three ways past the complete-class problem and calls constructing-the-table-in-the-action the smallest change; I took the third — dropping initializers — the only one with runtime semantics, because it was the smallest diff.
Fixed in 73d487b8e: initializers restored, the table constructed per action instead of held as a member (which is what actually defers the instantiation past the class body), and both actions now assign the f64 they accept. All six fields are written on emplace, all five on modify.
| // has something, but it is a PLACEHOLDER: the table's real name comes from the | ||
| // multi_index / kv::table instantiation that uses this struct, if there is one. | ||
| t.name = t.type; | ||
| unnamed_table_types.insert(t.type); |
There was a problem hiding this comment.
[P2] Track the exact placeholder declaration instead of an ABI type-name string
t.type is an unqualified ABI name, so this set conflates distinct declarations that share that name. I reproduced a bare ns1::row plus an explicitly named table over schema-compatible ns2::row; once the generated table for the first type existed, this predicate silently removed the explicit archive table for the second. Please track per-entry or declaration provenance so only the placeholder created from the bare attribute can be removed.
There was a problem hiding this comment.
Fixed, and by construction rather than by a better key. The prune only ever removes entries that abigen marked as placeholders, so an explicitly named table can never be removed by a row struct that happens to share its ABI type name — which is what your ns1::row / ns2::row case exercised.
Checked directly: a bare ns1::row under multi_index<"live"_n, ...> beside an explicitly named ns2::row under multi_index<"archive"_n, ...> now gives [('archive', 24526), ('live', 30214)] — both survive, correctly named.
Worth noting the underlying ambiguity you point at is still there and is not mine to fix here: two distinct C++ types sharing an unqualified name both serialise as one structs entry, so the ABI cannot tell them apart regardless. The prune no longer depends on telling them apart.
| // twice -- once under the ROW STRUCT's name, whose table_id nothing ever writes | ||
| // to, so get_table_rows for it returned nothing. A struct annotated but never | ||
| // instantiated keeps its placeholder, since nothing else can name it. | ||
| if (unnamed_table_types.count(t.type) && |
There was a problem hiding this comment.
[P1] Preserve kv_key metadata when suppressing the phantom entry
The early continue drops the entire annotated ctable descriptor, including metadata supplied by sysio::kv_key. In a base-versus-head fixture using a bare table with sysio::kv_key("abi_key"), the real kv::table entry changed from the logical overridden key_names value to the physical key field name. That silently changes the ABI used by clients and SHiP key decoding. Please merge the annotated metadata into the generated table before suppressing only the phantom entry, and cover this combination with a regression test.
There was a problem hiding this comment.
Confirmed and fixed. kv_key is resolved into key_names/key_types on the ctables path (abigen.hpp:315), so dropping that entire entry took the override with it.
The prune now folds key metadata into the survivor before discarding the placeholder, rather than dropping the entry whole. bare_table_kv_key pins it — a bare attribute plus [[sysio::kv_key("abi_key")]] over a kv::table<"realname"_n, phys_key, val>:
realname 46377 key_names=['account_id']
the logical override, not the physical raw_id. Removing the prune fails it.
…cation
Three review findings, all against the same mistake: suppressing the placeholder
while compiling one translation unit.
A bare [[sysio::table]] names no table, so abigen emits an entry named after the
ROW STRUCT. The real name comes from the multi_index / kv::table that
instantiates it, which no single Clang invocation can see. Deciding there was
wrong three ways:
* it missed a second TU. A source that sees the annotated row without any
table over it kept its placeholder, and the descriptor merge restored it --
reproduced with the struct in a header and the alias in one of two sources,
giving back ('accounts', 25660) AND ('account', 15140);
* it keyed on the unqualified ABI type name, so two structs sharing one could
remove each other's entries -- including an explicitly named table;
* it dropped the whole annotated entry, and with it the key_names/key_types
that [[sysio::kv_key]] resolves there, so a real kv::table fell back from
the logical override to the physical key field.
abigen now MARKS the placeholder instead -- unconditionally, so every TU emits
the same descriptor and the merge is not order-dependent -- and cdt-codegen
prunes after the link-wide merge, beside the table_id collision check that
already runs on the final ABI. Only marked entries are ever removed, so an
explicitly named table cannot be. A placeholder with no real table survives, so
a struct annotated but never instantiated keeps its entry. Key metadata is
folded into the survivor rather than discarded. "__placeholder" never reaches
the emitted .abi.
The flag rides in the .desc, which is an internal build artifact -- produced and
consumed by one toolchain version, never committed -- so there is no format
compatibility to keep. The emitted ABI is what CLAUDE.md asks be proven, and it
is: 44/44 wire-sysio artifacts byte-identical, with the fixtures below pinning
the rest.
Tests: bare_table_xtu is the two-source case, its second TU in a subdirectory so
the suite does not discover it as a test of its own; bare_table_kv_key pins that
a kv_key override survives the prune as key_names ["account_id"], not the
physical "raw_id". Removing the prune fails six fixtures including both.
examples/multi_index_large: restoring the earlier commit's default member
initializers, which I should not have removed. set() and mod() never assigned
f64 -- the initializer was covering a pre-existing omission -- so dropping it
left an indeterminate double being serialized and used as the "byf" secondary
key: undefined behaviour producing a nondeterministic key. The table is now
constructed per action instead of held as a member, which is what defers the
complete-class problem, and both actions assign the f64 they accept.
ctest 33/33 with integration on, 16/16 integration suites, toolchain 46/46.
| // Superseded by a real entry for the same row type? | ||
| const ojson* real = nullptr; | ||
| for (const auto& other : abi["tables"].array_range()) { | ||
| if (!is_placeholder(other) && other["type"] == tbl["type"]) { |
There was a problem hiding this comment.
[P1] A marked entry can also represent a real same-name instantiation
abigen::to_json inserts the annotated ctable before auto-detected tables in a set keyed by table name. With a bare account row instantiated as both multi_index<"account"_n, account> and multi_index<"accounts"_n, account>, the exact-head descriptor therefore contains a marked account entry that also carries the real account table metadata, plus an unmarked accounts entry. This loop treats accounts as proof that the marked entry is superseded and deletes the live account table. I reproduced the final ABI containing only accounts, although the contract writes both tables. Multiple table names over one row type are already used in exclude_from_abi.hpp, so the descriptor needs to preserve whether a marked entry was also matched by a real same-name instantiation; add a two-table regression fixture.
There was a problem hiding this comment.
Reproduced and it is the worst of the four — the contract writes two tables and the ABI described one:
multi_index<"account"_n, account> // same name as the row struct
multi_index<"accounts"_n, account>
-> tables: [('accounts', 25660)]
Your diagnosis is exact: the by-name set merges the placeholder with the same-named instantiation, so the marked entry is the live table, and the prune then reads accounts as proof it was superseded.
Taken with the other three, I have retired the provenance approach rather than patch it — 160d393bf. A marked entry cannot be told apart from a real one by anything that survives into the descriptor, and making it sound needs a qualified declaration identity in both entry kinds plus descriptor versioning. No entry is emitted for a bare attribute at all now: each instantiation emits its own, which is where the name, table_id and key layout come from anyway, and nothing is removed later so nothing can be removed wrongly.
bare_table_two_names is your case, expecting both ('account', 15140) and ('accounts', 25660).
| if (real) { | ||
| // Carry key metadata across rather than discarding it with the entry. | ||
| for (const char* k : {"key_names", "key_types"}) { | ||
| if (has_content(tbl, k) && !has_content(*real, k)) |
There was a problem hiding this comment.
[P1] The kv_key fold remains incorrect and the new fixture bypasses it
A supported nested override still fails at the exact head: put abi_key { account_id } inside the bare-attributed value row, use phys_key { raw_id } as K, and instantiate kv::table<"realname"_n, phys_key, val>. add_table resolves the nested override, while add_kv_table searches only the enclosing context and emits the physical metadata. The descriptor has marked val with key_names=[account_id] and real realname with [raw_id]; this fill-only condition refuses to apply the explicit override, then deletes the marked entry, so the final ABI contains [raw_id]. The committed fixture declares abi_key as a sibling, which add_kv_table already resolves, so it passes without exercising this branch. Explicit kv_key metadata must override auto-derived metadata, with a nested-key regression test.
There was a problem hiding this comment.
Confirmed, and the fixture criticism lands hardest — mine declared abi_key as a sibling, which add_kv_table already resolves, so it passed without touching the branch. That is the exact failure I have been flagging in other code this week.
Reproduced with the nested form: key_names=['raw_id'], the physical key.
Fixed at the source rather than in the fold. add_kv_table searched only the enclosing context while add_table, reading the same attribute, searched nested types first and then the enclosing one. They now search in the same order. Two things came with it: the override struct is inserted into kv_key_structs so validate_struct cannot prune it, as the other path already did for its own; and a kv_key naming a struct that resolves to nothing now warns instead of silently falling back to the physical key.
kv_key_nested pins the nested case, and bare_table_kv_key stays for the sibling one.
| // Superseded by a real entry for the same row type? | ||
| const ojson* real = nullptr; | ||
| for (const auto& other : abi["tables"].array_range()) { | ||
| if (!is_placeholder(other) && other["type"] == tbl["type"]) { |
There was a problem hiding this comment.
[P2] Matching on the unqualified ABI type still conflates distinct declarations
The prior identity issue is only partially fixed. I reproduced an uninstantiated bare ns1::row beside an explicitly named ns2::row table called archive, using identical schemas so both legitimately map to ABI type row. The descriptor contains both entries, but this comparison mistakes archive for an instantiation of ns1::row and removes the bare row entry. The final ABI contains only archive, contradicting the stated rule that an uninstantiated placeholder survives. Please carry a hidden qualified declaration identity or equivalent provenance in both annotated and auto-detected entries instead of deciding from the lossy emitted type string.
There was a problem hiding this comment.
Reproduced — [('archive', 24526)], the uninstantiated ns1::row deleted, which contradicted the guarantee I had claimed for that design. That is what decided it: the one case provenance was chosen to preserve, it did not preserve.
Rather than carry a qualified identity through both entry kinds, nothing is removed at all now. bare_table_namespaces keeps your case, and its comment says the expected result — archive alone — holds because ns1::row is never instantiated and so contributes no table, not because anything was matched and deleted.
You are right that the underlying ambiguity remains: two declarations sharing an unqualified name still collapse to one ABI type. Nothing now depends on telling them apart.
| // entry does not, so [[sysio::kv_key]] on a bare-attributed row is not lost. | ||
| if (abi.has_key("tables")) { | ||
| const auto is_placeholder = [](const ojson& t) { | ||
| return t.has_key("__placeholder") && t["__placeholder"].as<bool>(); |
There was a problem hiding this comment.
[P2] Pre-marker descriptors are silently treated as real after an incremental toolchain upgrade
The .desc schema has no version or invalidation stamp, so a normal incremental build can mix an old object/descriptor with a TU rebuilt by this CDT. For example, an old a.cpp.desc contains the unmarked bare account phantom; a newly rebuilt z.cpp.desc contains the marked placeholder plus real accounts. Sorted merging keeps the old unmarked account, this predicate classifies it as real, and the final ABI again contains both account and accounts. Contract build rules do not inherently recompile every unchanged object when the compiler at the same path is upgraded. Please version/reject incompatible descriptors or otherwise force pre-marker descriptors to regenerate.
There was a problem hiding this comment.
Fixed by removal — there is no marker any more, so there is nothing for a stale descriptor to be missing. Your point stands on its own though: .desc has no version or invalidation stamp, and an incremental toolchain upgrade can mix descriptors from two compilers. That is a live hazard for any future descriptor change, and I did not want a fix that depended on it.
While regenerating fixtures this surfaced something separate, which I have left for its own PR rather than widening this one: singleton_contract loses ('tbl_config', 8868). That entry was itself wrong — named after the row struct, at an id the singleton does not store under — and removing it exposes that no sysio::singleton has ever produced an ABI table entry. The visitor matches "singleton", but sysio::singleton is an alias for kv_singleton, so the specialization name never matches. Six singletons in that fixture, zero entries, before this change and after. Fixing it would ADD entries to every contract using a singleton, so it wants its own verification.
toolchain 49/49, ctest 33/33 with integration on, 16/16 integration suites, 44/44 wire-sysio artifacts byte-identical. Restoring the placeholder fails 6 of the 49.
Four more findings against the provenance approach, all reproduced, and together
they retire it: a marked entry cannot be told apart from a real one by anything
that survives into the descriptor.
* the by-name set merges the placeholder with an instantiation that shares the
struct's name, so the marked entry IS sometimes the live table. With
multi_index<"account"_n> and multi_index<"accounts"_n> over one row, the
prune read `accounts` as proof and deleted `account` -- the contract wrote
two tables and the ABI described one;
* the emitted `type` string cannot distinguish two declarations sharing an
unqualified name, so an uninstantiated ns1::row was deleted by ns2::row's
`archive` -- the very case provenance was chosen to protect;
* the key-metadata fold was fill-only, so an explicit override could not
replace auto-derived metadata;
* .desc carries no version, so an incremental toolchain upgrade mixes marked
and unmarked descriptors and the unmarked phantom reads as real.
Making it sound needs a qualified declaration identity carried in both entry
kinds plus descriptor versioning. That is a lot of machinery in the ABI path to
preserve one case -- a struct annotated but never instantiated -- which measures
at zero occurrences in wire-sysio, and which the provenance form did not
actually preserve.
So no entry is emitted for a bare attribute. Each instantiation emits its own,
which is where the name, the table_id and the key layout come from anyway.
Nothing is removed later, so nothing can be removed wrongly.
Behaviour change, deliberate and pinned: a struct annotated but never
instantiated no longer contributes a table. It described one nothing could read
or write.
Also fixed, since this makes add_kv_table the only path that resolves it:
[[sysio::kv_key]] naming a struct declared INSIDE the value row was invisible
there -- it searched only the enclosing context, while add_table, reading the
same attribute, searched nested types first. The kv::table path silently used
the physical key's field names instead of the override. Both now search in the
same order, the override struct is protected from validate_struct as the other
path protects its own, and a name that resolves to nothing warns instead of
falling back in silence.
Tests, one per case, all failing when the placeholder is restored (6 of 49):
bare_table_attr (bare + instantiated, and an uninstantiated one contributing
nothing), bare_table_xtu (the two-source case), bare_table_two_names (two tables
over one row), bare_table_namespaces (two declarations sharing an ABI type
name), bare_table_kv_key (sibling override) and kv_key_nested (nested override).
singleton_contract loses ('tbl_config', 8868). That entry was itself wrong --
named after the row struct, at an id the singleton does not store under. It
exposes a separate pre-existing gap: the table visitor matches "singleton", but
sysio::singleton is an alias for kv_singleton, so no singleton has ever produced
an ABI table entry. Six in that fixture, none before this change or after. Left
for its own PR, since fixing it ADDS entries to every contract using a
singleton.
ctest 33/33 with integration on, 16/16 integration suites, toolchain 49/49, and
44/44 wire-sysio artifacts byte-identical.
sysio::singleton is an alias template over kv_singleton (singleton.hpp), and an
alias template has no specialization of its own -- the AST only ever holds the
aliased one. The table visitor tested for the name "singleton", so it never
matched, and no singleton has ever produced an ABI table entry: not through a
typedef, not through a using alias, not as a data member. sysio::multi_index is
the same alias shape over kv_multi_index, which WAS on that list, which is why
the gap stayed invisible -- a contract mixing the two saw its tables described
and its singletons omitted. kv_singleton joins the list, under the
kv_multi_index key layout it actually stores with.
Making singletons reach that branch exposed three defects already sitting in it,
each reproduced from source that compiles today.
A row type that is not a class crashed the compiler. singleton<"cfg"_n,
uint64_t> is ordinary contract code, and asking a scalar for its CXXRecordDecl
gives null -- which add_table then read a name off, for `clang frontend command
failed with exit code 139` and no diagnostic at all. The row is carried as a
QualType now. The same null sits on the kv::table path, where a scalar KEY type
still crashes on master; a key type's fields ARE the table's key layout, so that
one is refused with a diagnostic rather than described.
The ABI named types it did not declare. add_struct ran only when the row type
carried [[sysio::table]], but defined_in_contract() admits a table whose row type
carries no annotation at all -- which is how an ordinary singleton is written,
tests/unit/test_contracts/kv_singleton_tests.cpp included. The chain refuses that
document outright: invalid_type_inside_abi on t: ("config", "config"). Separately
the published `type` was the C++ RECORD name rather than the ABI one, so a
std::string row read `basic_string` and a checksum256 row `fixed_bytes`, neither
of which any ABI declares. Both paths publish get_type() now, the same spelling
add_type declares, so the two agree by construction; a row type that still cannot
be resolved is an error at the declaration rather than a set_abi failure later.
[[sysio::table("name")]] renames the table published over a row struct -- it has
to, because a _i-named table's raw is a DJB2 hash and the readable name lives
only in the annotation. It was applied where each instantiation was added, before
the rest of the translation unit was known, and that is where neither of the two
conditions it must respect is knowable. Both failures are the same failure, since
abi_table orders by NAME ALONE: two entries given one name collapse to whichever
the set reached first, and the survivor's `type` then describes one table while
its table_id addresses the other.
* a row struct backing two tables gave both entries the annotation's name. The
contract wrote two tables, the ABI described one. Live in this repo:
kv_global_tests.cpp declares 9 kv::global tables over 2 annotated rows, and
the ABI described 2;
* renaming onto a name another row struct's table already held collapsed those
two instead. [[sysio::table("alpha")]] a_row used as multi_index<"bravo"_n,
a_row>, beside multi_index<"alpha"_n, b_row>, published one `alpha` carrying
a_row's layout at b_row's table_id -- worse than the missing table, because a
client decoding by table_id got the wrong shape.
Resolved in to_json() now, where every instantiation is known: applied when the
struct backs exactly one table AND the target name is free, otherwise dropped
with a warning naming the condition that failed. Dropped rather than published,
because an annotation naming none of the live tables is a table under a table_id
nothing writes to -- the phantom this branch removes, reached by a third route.
It is also where [[sysio::kv_key]] was resolved, and that describes the ROW
rather than one table over it, so the key metadata goes to every table over the
struct instead of being lost with the entry.
Tests. singleton_decl_forms covers typedef and using over both sysio::singleton
and kv_singleton, on both admission paths, plus a data member with no alias, an
_i-named singleton, an unannotated row struct, and row types that are not classes
(name, uint64_t, std::string, checksum256). named_table_attr covers the rename,
annotated-but-never-instantiated, two tables over one named row, a rename onto an
occupied name, and kv_key metadata surviving a dropped annotation.
kv_table_builtin_row covers a kv::table over a builtin value type. Two new
abigen-fail cases pin the diagnostics: undescribable_row_type and
kv_table_scalar_key. singleton_contract.abi gains the four singleton entries it
should always have described.
Nine mutations, each failing only its own fixtures: kv_singleton off the visitor
list; eager renaming; publishing the superseded annotation; renaming onto an
occupied name; dropping the superseded annotation's key metadata; declaring the
row only when annotated; publishing the C++ record name as the ABI type;
dropping the row-type describability check; dropping the kv::table scalar-key
guard.
Also removed a dead std::set<CXXRecordDecl*> that nothing read, and folded the
open-coded DJB2 string hash into djbh_hash_string beside djbh_hash_raw.
Known limitation, unchanged by this commit and verified identical on the branch
tip: the rename is resolved per translation unit, so a named row struct in a
shared header that only one TU instantiates makes the two descriptors disagree on
table_id and ABIMerger refuses the link. Fixing that needs instantiation
provenance in .desc or merge semantics that can fill in an absent table_id.
ctest 33/33 with integration on, toolchain 54/54, and every artifact
wire-sysio's contracts/ tree produces -- 21 wasm and 21 abi -- byte-identical to
the branch tip's, with no abigen diagnostic.
A [[sysio::table("name")]] whose table another translation unit instantiates
refused the link:
Error, ABI structs malformed : cfg already defined
Only an instantiation carries a table's real table_id -- it comes from the
template parameter. A TU that sees the annotation and no instantiation derived
one from the annotation STRING instead, which is right only when the parameter
spells the same name. When it does not, the two descriptors disagreed on cfg's
id, and table_is_same treats a difference as a different table.
A regression from two correct changes meeting: #115 made the annotation's name
win, so both TUs emit an entry called `cfg`, and #112 made table_is_same compare
table_id strictly. Built against the plugin from before #115, the same case links
and produces ('cfg', 28383) beside ('cfgtbl', 49879) -- the phantom plus the real
table.
The guess is gone. It was dead in every case where it could be checked: a bound
annotation's entry takes the instantiation's id in to_json, so the derived value
was always overwritten. Where it could not be checked, it was wrong. Absence is
now not a difference in the merge, and the descriptor that has the real id
supplies it, the same way key_names and secondary_indexes are already filled in.
Two ABIs lose a table_id: kv_key_types' `custom` and named_table_attr's
`declared`, both annotated and never instantiated. Neither describes a table
anything writes to, and the id was a guess about where it would land.
named_table_xtu pins it, with the annotation-only source named to sort FIRST --
.desc files merge in sorted order, so it is the accumulator and the real id has
to be filled in rather than merely kept. Three mutations, each failing only that
fixture or its own: restoring the guess, comparing table_id strictly again, and
dropping table_id from the merge fold.
Not fixed: two TUs each instantiating a DIFFERENT table over one annotated row
still refuse the link, since both ids are real and the count is only knowable
link-wide. That needs the row struct's identity in .desc.
ctest 33/33 with integration on, toolchain 55/55, wire-sysio 42/42 unchanged.
…ptor
The annotation renames one table, and only into a name no other table holds.
Both are link-wide facts, and abigen checked them per translation unit, where
neither is knowable. Each TU decided on its partial view, the descriptors
disagreed, and the merge refused the link:
Error, ABI structs malformed : cfg already defined
Two shapes, both from a named row struct in a shared header: two TUs each
instantiating a table over it, and one TU renaming onto a name another TU's table
holds. The same source in ONE translation unit produced the right answer all
along, which is what made it a knowledge problem rather than a rule problem.
abigen no longer applies the annotation. Tables go into the descriptor under
their template parameter, each carrying the QUALIFIED name of its row struct, and
the annotation is recorded beside them in ____table_annotations. cdt-codegen
applies the rule after the merge, with every descriptor in hand, and strips both
keys. Matching on the qualified name also fixes an identity bug the per-TU code
had: ns1::row and ns2::row both serialise as `row`, so matching on the ABI type
conflated them and refused both renames.
.desc gains two keys. It is an internal artifact -- written by the plugin, read
by cdt-codegen, same build, same toolchain, never committed -- so there is no
compatibility surface. The one hazard is a stale descriptor after an in-place
toolchain upgrade, which the build graph already allows for any change, since the
plugin is not a declared dependency of the codegen step.
Also guarded, since cdt-codegen can now see them: an annotation that would
declare a table under a name already taken, and two renames onto one target.
Both would have put duplicate names in one ABI.
Table order: descriptors emit in name order and the merge appends, so the array
follows what a table was CALLED in its descriptor. A rename breaks that, so the
array is sorted when this pass renames or declares something, and left alone when
it does not -- which is why no wire-sysio artifact moves.
Tests: named_table_split (both cross-TU shapes in one contract),
named_table_namespaces (two annotated rows sharing an unqualified name),
named_table_attr gains the declared-name-already-taken case. Five mutations, each
failing only its own fixtures: unqualified row matching, renaming without the
link-wide count, renaming onto an occupied name, publishing a declared table over
a taken name, and dropping the annotations from the merge.
ctest 33/33 with integration on, toolchain 57/57, wire-sysio 42/42 unchanged.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 4349eacd. The previous unused-annotation/live-table collision is fixed. The inline findings below remain reproducible against binaries built from this exact head.
| // that struct -- including the ones the annotation could not name. | ||
| if (a.has_key("key_names") && !a["key_names"].empty()) { | ||
| for (std::size_t i : idx) { | ||
| abi["tables"][i]["key_names"] = a["key_names"]; |
There was a problem hiding this comment.
[P1] Preserve the scoped-table key prefix when applying kv_key metadata
This replaces the complete key layout, including the scope:name prefix that add_kv_table(..., scoped=true) already added. At this head, two kv::scoped_tables over a row with [[sysio::kv_key("logical_key")]] produce descriptors with [scope, account_id], but the final ABI contains only [account_id]. Clients therefore encode a different key from the runtime. Please preserve the existing scope prefix when applying the logical fields (and cover the scoped + superseded-annotation case).
There was a problem hiding this comment.
Reproduced (desc [scope, account_id] → abi [account_id]), and it was never confined to kv::scoped_table.
The annotation describes the row's logical key; whether a table carries a scope in front is a property of the table, which is why add_kv_table composes [scope] + logical. The prefix is kept now unless the annotation supplies one itself — a [[sysio::kv_key]] with no argument is [scope][primary_key].
The wider case is already in the repo: named_table_attr's first and second are kv_multi_index tables over a row with an override, and their committed layout had the same scope missing. Fixed with it. kv_key_scoped pins your case.
| if (override_key) { | ||
| key_source = override_key; | ||
| // Protect it from validate_struct, as the other path does for its own. | ||
| kv_key_structs.insert(kv_key_name); |
There was a problem hiding this comment.
[P1] Emit the selected kv_key type closure
This protects the override from pruning but never emits the override struct or calls add_type for its fields, unlike the named-annotation path. A bare [[sysio::table, sysio::kv_key("abi_key")]] whose abi_key contains a custom logical_id builds with key_types:["logical_id"], while neither abi_key nor logical_id appears in structs. The resulting ABI has an unresolved key type, so query-key decoding fails.
There was a problem hiding this comment.
Confirmed — protecting a struct from pruning does nothing when it was never added. Your shape reproduces: key_types: ["logical_id"] beside structs: [phys_key, test, val]. The struct is emitted and its fields run through add_type now, as add_table's identical branch always did.
Worth adding: the bare attribute is what makes it reachable. add_table returns as soon as it sees an unnamed [[sysio::table]], so the override never reaches the branch that would have declared it and add_kv_table is the only path left resolving the attribute — which is why the named fixtures never showed it.
kv_key_type_closure pins it; bare_table_kv_key and kv_table_explicit_key gain the struct they had been naming. Also guarded wrap_decl(nullptr) for a non-class V.
| auto table_decl = clang_wrapper::wrap_decl(table_type); | ||
| if ((table_decl.isSysioTable() && ag.is_sysio_contract(table_decl, ag.get_contract_name())) || defined_in_contract(owner)) { | ||
| if ((table_decl.isSysioTable() && ag.is_sysio_contract(table_decl, ag.get_contract_name())) || | ||
| defined_in_contract(owner)) { |
There was a problem hiding this comment.
[P1] Treat direct contract fields as ownership of the specialization
defined_in_contract() only matches the specialization itself or a TypedefNameDecl; it never recognizes a FieldDecl whose type is that specialization. An exact-head contract with directly used members singleton<"scalar"_n, uint64_t> scalar; and singleton<"plain"_n, plain_row> plain; builds successfully but emits tables: []. The new fixture's member is admitted only because its row is annotated, so it misses direct scalar and unannotated-row members.
There was a problem hiding this comment.
Confirmed at 4349eacd — both your cases give tables: [], a contract that builds clean and describes none of its state. Matched on DeclaratorDecl now, so a static data member counts too.
You are right about the fixture: member_inst's row is annotated, so it was admitted by the other arm of the || and could never tell whether the member itself was recognised — it documented the gap instead of catching it. singleton_decl_forms gains scalar_inst and plain_inst, neither row annotated and neither named by an alias.
| // Reported against the contract class: `d` is an implicit | ||
| // specialization, so its location is the template's definition inside | ||
| // sysiolib, which tells the author nothing about their own source. | ||
| CDT_CHECK_ERROR(ag.abi_can_describe_row(row_type), "abigen_error", |
There was a problem hiding this comment.
[P1] Translate ABI-representable container singleton payloads
singleton<"values"_n, std::vector<uint64_t>> is valid runtime contract code, but this new discovery path now hard-fails abigen because the canonical specialization is rendered as vector. The ABI serializer accepts the corresponding uint64[] spelling, so this is representable. The fail fixture currently codifies a compatibility regression; please translate canonical containers (or otherwise retain enough sugar) rather than rejecting them.
There was a problem hiding this comment.
Agreed — the fail fixture was pinning a limitation of the translation, not of the ABI. Retired.
A template argument is canonical and translate_type()'s container branches need a TemplateSpecializationType, so the same std::vector<uint64_t> gave uint64[] as an action parameter and the record name vector as a row. The guard then caught vector, correctly, and reported the row rather than the missing sugar. Rebuilding the sugar puts both on one path: uint64[], uint64?, pair_uint64_uint64[], uint64[4], row[].
Three limits, each of which cost a defect to find — my first attempt at this shipped two of them, and a pre-push review caught them:
- Not
variantortuple. Their canonical arguments are one Pack, whichget_template_argument()does not handle: it reachesCDT_INTERNAL_ERRORand terminates the compiler. Off the whitelist they keep the old path and are refused with a diagnostic —abigen-fail/variant_rowpins that, since turning a diagnostic into an abort is the opposite of what this PR is for. - Leading arguments only. The container helpers in
gen.hppdecide by counting<ingetAsString(), so a vector rebuilt with its allocator printedvector<unsigned long, allocator<unsigned long>>, read as nested, and reachedadd_struct()— which synthesised a struct over an__vector_base_…the document never declares. - One level. Nesting is left canonical and refused (
abigen-fail/nested_container_row). The machinery that namesB_vector_uint64_Eruns off that same printed form and declares the typedef the name needs; a half-resugared type slipped past it and publishedB_vector_uint64_E[]withtypesempty.
validate_struct also strips the suffix, or the struct behind row[] is pruned. fixed_bytes<7> keeps the guard honest.
| o["key_types"].push_back(kt); | ||
| if (t.table_id != 0) | ||
| o["table_id"] = t.table_id; | ||
| // Descriptor-only; cdt-codegen strips it. See abi_table::row. |
There was a problem hiding this comment.
[P1] Distinguish a known zero table_id from an absent ID
The preceding t.table_id != 0 check omits real ID zero, although the hash returns an unrestricted uint16_t. Valid names "rzy2"_n and "s3hm"_n both compute to zero; a contract containing both builds and emits both entries without IDs. Final collision validation then skips both (and, because its secondary loop is nested under has_key("table_id"), skips their secondary IDs too), even though both live tables alias runtime ID 0. Track presence separately and serialize a known zero.
There was a problem hiding this comment.
Confirmed — the pair builds and links silently, both entries shipping with no table_id, which is exactly what that check exists to prevent. Presence is tracked separately from value; absence still has to be representable, since a table an annotation declares carries no id, so it is a flag rather than another sentinel.
Your second point is fixed with it: the secondary-index loop was nested under the table's own id, so a table without one took its indexes out of the check too — reachable through any declared table, no zero required.
table_id_zero and abigen-fail/table_id_zero_collision.
| std::string loc; ///< source location of the annotated struct, for diagnostics | ||
| std::vector<std::string> key_names; ///< resolved [[sysio::kv_key]] override, if any | ||
| std::vector<std::string> key_types; | ||
| bool operator<(const abi_table_annotation& a) const { return name < a.name; } |
There was a problem hiding this comment.
[P2] Do not discard same-TU duplicate annotation targets
Because this set comparator uses only name, two different rows carrying [[sysio::table("alias")]] collapse before link-wide resolution. With physical tables first and second, exact-head output silently renames only the first to alias, leaves the second raw, and emits no collision diagnostic; swapping declaration order changes which row owns alias. Keep (name,row) entries (or diagnose failed insertion) so the resolver can reject the ambiguity deterministically.
There was a problem hiding this comment.
Confirmed: ['alias', 'second'] at 4349eacd — first renamed, second kept its parameter, nothing reported. Keyed on (name, row) both survive, and they are refused together, each warning naming the other row.
Chasing it across translation units turned up a second half. The merge identifies annotations by name like every other section, so two differing records under one name were Error, ABI structs malformed : xshared already defined — a dead link rather than a diagnostic. It needs a header per TU: in a shared header the per-TU set drops one of the pair first and the merge has nothing to reconcile, which is how my first attempt at the fixture passed on the broken code. Annotations merge by (name, row) now.
| << "': another table in this contract is already called '" << name | ||
| << "', so it keeps its own table parameter as its ABI name\n"; | ||
| } else { | ||
| taken.erase(abi["tables"][idx.front()]["name"].as<std::string>()); |
There was a problem hiding this comment.
[P2] Resolve the complete rename graph before mutating taken
This decision is order-dependent because a later successful rename can free a name that an earlier annotation was refused. For physical alpha/bravo, with annotations alpha -> bravo and bravo -> charlie, processing bravo first yields {alpha,charlie}, while processing charlie first frees bravo and yields {bravo,charlie}. Cross-TU annotation order follows descriptor merge order, so identical declarations can produce different ABIs. Preflight the whole rename graph or resolve to a deterministic fixed point.
There was a problem hiding this comment.
Confirmed, and the order is the merge's. Two descriptors, alpha/a_row asking for bravo and bravo/b_row asking for charlie, at 4349eacd:
merge order p,q -> ['alpha', 'charlie']
merge order q,p -> ['bravo', 'charlie']
In one TU it is not order-dependent — the set sorts by name — so it takes the merge to supply the order, meaning the answer turned on filenames.
Resolved to a fixed point now: refuse the ambiguities, then sweep until a sweep changes nothing. A target held by a table that is itself renamed away comes free, so the chain reaches {bravo, charlie} from either end; targets are unique by then, so nothing races. A cycle never comes free and gets the ordinary occupied-name warning — the ABI cannot express the swap either.
named_table_rename_chain, in one TU and split across two.
| // id, so a TU that sees a [[sysio::table("name")]] without one omits it, and | ||
| // the TU that does instantiate the table supplies it below. | ||
| compatible("table_id") && | ||
| compatible("____row") && |
There was a problem hiding this comment.
[P2] Copy ____row when accepting a rowless descriptor
This declares a missing ____row compatible, but the richness-copy list below contains only table_id, key arrays, and secondary indexes. If a stale/older rowless descriptor is merged first, the current descriptor's row marker is discarded; resolution then treats the surviving annotation as uninstantiated and emits a phantom declaration beside the physical table. Reversing merge order renames correctly. Add ____row to the fill list (and ideally version/invalidate the descriptor schema).
There was a problem hiding this comment.
Confirmed and fixed — ____row was the one key compatible() forgives the absence of that the fill list did not restore. At 4349eacd:
rowless first -> [('cfg', None), ('cfgtbl', 49879)]
marker first -> [('cfg', 49879)]
One thing worth passing on: cdt-codegen sorts its descriptor list, so merge order is filename order and passing --desc-file the other way round changes nothing. My first draft of the test passed both cases on the broken code because of it.
On versioning I have not acted, and I would rather say so than leave it implied: it is a general hazard, not specific to this key, and adding a compatibility surface here would ship one with a single caller and no way to exercise the incompatible path. tests/unit/abimerge_tests.sh pins the instance instead, with hand-written descriptors — the toolchain tester builds everything with one toolchain and can never produce the mixture.
| t.type = get_type(row); | ||
| // Same rule add_kv_table uses: the template parameter is the name, and a | ||
| // [[sysio::table("name")]] on the row struct may rename it link-wide, in cdt-codegen. | ||
| t.name = name_to_string(name); |
There was a problem hiding this comment.
[P2] Do not publish decoded hash text for non-record _i rows
A scalar row has no declaration on which to place the annotation used to recover an _i literal's source spelling. At this head, singleton<"singleton_builtin_long_name"_i, uint64_t> emits the ABI table name z4pypstb1k13h (with the correct ID 296), so name-based discovery and generated clients cannot address it by the declared name. Preserve the literal spelling through another channel or diagnose this combination instead of publishing a fabricated name.
There was a problem hiding this comment.
Fixed by recovering the spelling rather than diagnosing — it is still in the AST and I had not looked.
A specialization over such a row reaches the ABI only through the contract member naming it, and that member's TypeSourceInfo holds the literal as typed. Every member is tried, since one written through an alias declared outside the class carries no arguments of its own.
Two corrections a pre-push review forced, both worth stating because the first is worse than the bug:
- The recovered text now has to hash back to the parameter. Adjacent literals are spliced by the compiler and not by the lexer, so
"con" "cat"_iread ascat— a plausible name sitting atconcat'stable_id, which is worse than the garbage it replaced. A mismatch falls back to the decoded raw: unusable, but visibly so. - The gate was "the row is not a class" standing in for "the row cannot carry
[[sysio::table]]".std::string,sysio::checksum256and every container row are classes the author does not own, and all of them still published decoded hashes. Gated on the attribute now.
That gate is also what keeps the annotation path exclusive: a row carrying the annotation is admitted by it alone, so a TU that never names the specialization would recover nothing and two descriptors would call one table different things. _n is untouched — there the raw is the name.
…lar names Three ways the new singleton path published nothing, or published a name nothing could use. A DATA MEMBER admitted no table. defined_in_contract() matched the specialization itself or a TypedefNameDecl, so `singleton<"cfg"_n, uint64_t> cfg;` -- the most direct way a contract holds one, needing no alias -- was not recognised as owning it. A member over a scalar, or over a row struct carrying no annotation, emitted nothing at all while the contract built clean. Members whose row IS annotated were admitted by the other arm of the caller's ||, which is what kept the hole narrow enough to survive the fixture: singleton_decl_forms documented the gap as intended behaviour. A CONTAINER row was rejected outright. `singleton<"values"_n, std::vector<uint64_t>>` is code the runtime stores and a client decodes, but a template argument read with getTemplateArgs()[i].getAsType() is canonical, and translate_type()'s container branches match only a TemplateSpecializationType -- so the identical type translated to `uint64[]` in an action parameter and to the record name `vector` as a row. The describability guard then caught `vector`, correctly, and reported the row rather than the missing sugar. Restoring the sugar puts both spellings on one path; the guard is unchanged in kind, and `fixed_bytes<7>` is what still trips it. Resugaring is confined to the templates the ABI spells for itself. std::string canonicalises to basic_string<char, char_traits<char>, allocator<char>> and reaches `string` only through the alias table at the foot of translate_type(); resugaring it produced `basic_string_int8_char_traits_char__allocator_char_` for the ABI's most common builtin. The argument of a resugared type is canonical again, so nesting is resugared at that boundary too -- without it, std::vector<std::vector<uint8_t>> handed add_struct() an implicitly-instantiated class with no definition, which asserts inside CXXRecordDecl::data() and takes the compiler with it. An `_i` NAME over a non-class row was fabricated. The raw is a DJB2 hash, and #115 recovers the readable name from [[sysio::table("name")]] on the row struct -- but a scalar has nowhere to carry one, so name_to_string() decoded the hash and published `z4pypstb1k13h` at the correct table_id 296: a live table addressable by id and by nothing else. The spelling is still in the AST, on the TypeSourceInfo of the member that names the specialization, and that is where it now comes from. `_n` is untouched -- there the raw IS the name. validate_struct keeps the struct a container row's element names: the chain resolves `row[]` by stripping the suffix, and so must the retention check, or the document names a type it does not declare. Fixtures: singleton_decl_forms gains a scalar member, an unannotated-row member and an `_i` scalar; singleton_container_row covers vector, bytes, nested vector, optional, map, pair and a struct element; undescribable_row_type moves to fixed_bytes<7>, having pinned a limitation of the translation rather than of the ABI. Toolchain 58/58. Every new table_id checked against an independent reimplementation of compute_table_id, `_i` hash path included.
…refix Two halves of the same attribute, each losing something on the way out. The override was PROTECTED but never DECLARED. add_kv_table put its name into kv_key_structs -- the set validate_struct consults before pruning a struct -- and stopped there, never emitting the struct and never running its fields through add_type. Keeping an entry in a set it never joined does nothing, so a key field of a contract type left the ABI naming something the document does not define: key_types ["logical_id"] beside structs [phys_key, test, val], and query key decoding with nothing to resolve. add_table's identical branch has always done both. A bare attribute is what makes it reachable at all: add_table returns as soon as it sees an unnamed [[sysio::table]], since there is no table for it to describe, so the override never reaches the branch that would have declared it and add_kv_table is the only path that still resolves the attribute. The override also replaced the PHYSICAL key rather than the logical one. [[sysio::kv_key]] describes the row; whether a table carries a `scope` ahead of the key fields is a property of the table -- kv::scoped_table prepends one, kv::table does not, and kv_multi_index stores [scope:8B BE][pk:8B BE]. add_kv_- table composes [scope] + logical for exactly that reason, and then cdt-codegen replaced the whole array with the logical fields alone. Two scoped tables whose descriptors read [scope, account_id] were published as [account_id], so a client encoded eight bytes less than the runtime writes -- and got no error, just no rows. The prefix is kept now, unless the annotation supplies one itself: a [[sysio::kv_key]] with no argument IS [scope][primary_key], and prepending to that would describe two scopes. named_table_attr moves with it, and shows the defect was never confined to scoped tables: `first` and `second` are kv_multi_index tables over a row with an override, and their committed key layout had the same scope missing. While here: add_kv_table asked wrap_decl(nullptr) for an attribute when a kv::table's value type is not a class. The row-type check that reports such a V runs after this point, so a scalar V arrived first. Fixtures: kv_key_type_closure (bare attribute, override with a contract-typed field), kv_key_scoped (two kv::scoped_tables, so the rename is refused and the key fold is all that still applies). Toolchain 60/60.
Two ways the same annotations produced different ABIs depending on the order
they were read in.
A RENAME FREES the name its table was holding, and the next rename is entitled
to it -- but each decision was taken against a `taken` set the previous decision
had already mutated. With physical tables `alpha` and `bravo` and annotations
alpha->bravo, bravo->charlie, taking `bravo` first gives {bravo, charlie} and
taking `alpha` first refuses the second and gives {alpha, charlie}. Both are
self-consistent; only one uses the names the contract asked for. Cross-TU that
order is descriptor merge order, so which ABI a build produced came down to
which .desc the link read first.
The set is resolved to a fixed point now: refuse the ambiguities, then sweep
until a sweep changes nothing. A request waits for its target to be free, and a
target held by a table that is itself renamed away does come free. Targets are
unique by then, so no two requests race for one name, and a cycle never comes
free -- refused with the ordinary occupied-name warning, which is the honest
answer, since the ABI cannot hold the swap either.
TWO ROWS ASKING FOR ONE NAME was decided by declaration order and never
reported. abi_table_annotation ordered by `name` alone, so the second never
entered the set: one row was renamed, the other kept its table parameter in
silence, and swapping the declarations swapped the winner. Ordered by
(name, row) both survive, and the resolver refuses them together -- each warning
naming the other row, because an author reading one of them needs to know what
it collided with.
Cross-TU that case was not silent but fatal. The merge identifies annotations by
`name`, as it does every other section, so two differing records under one name
were a malformed ABI -- "already defined", a dead link rather than a diagnostic.
The same source produced a warning or a failed link depending only on whether
the two rows shared a translation unit. Annotations merge by (name, row) now.
Deciding link-wide questions link-wide is why they are carried rather than
applied where they are found; this was the last place that rule was not
followed.
The key-metadata fold moves ahead of every naming decision, since it depends on
none of them.
Fixtures: named_table_rename_chain and named_table_duplicate_target, each
covering its shape both within one translation unit and split across two.
Toolchain 62/62.
compute_table_id truncates a DJB2 hash to uint16_t, so zero is one of the values
it returns -- "rzy2"_n reaches it, and so does "s3hm"_n. The descriptor wrote
the key only `if (t.table_id != 0)`, which reads a real id as a missing one: the
entry shipped without a table_id, and everything downstream keyed on
has_key("table_id") skipped it. The collision check most of all, so a contract
writing two tables into runtime id 0 built and linked without a word -- the
exact outcome that check exists to prevent. Presence is tracked separately now.
Absence still has to be representable, since only an instantiation carries an id
and a table an annotation declares has none, which is why this is a flag and not
a sentinel.
That same guard nested the secondary-index loop under the table's own id, so a
table without one took its indexes out of the check too -- reachable with no
zero involved at all, through any declared table. The loop stands on its own.
____row joins the merge fill list. table_is_same() accepts a descriptor that
lacks it -- it has to, since a TU seeing a [[sysio::table]] without its
instantiation has no row to record -- but the list that copies the richer value
back in did not include it, so a rowless entry arriving first discarded the
marker the other side carried. The resolver then read the annotation as
uninstantiated and declared a phantom beside the real table, and reversing the
merge order renamed correctly: the signature of the bug. Anything compatible()
forgives the absence of has to be filled back in.
Descriptors are internal and never committed, but an incremental build can still
mix vintages -- the plugin is not a declared dependency of the codegen step, so
an in-place toolchain upgrade leaves unchanged objects with descriptors an older
abigen wrote. abimerge_tests.sh stands in for that with two hand-written
descriptors merged in both orders, driven through `cdt-codegen --finalize`,
because the toolchain tester builds everything with one toolchain and so can
never produce the mixture itself.
Fixtures: table_id_zero (the id is emitted), table_id_zero_collision (the pair
is refused), abimerge_tests (both merge orders). Toolchain 64/64.
Both new tests passed against the broken code, for two different reasons. Found by building the toolchain at 4349eac and running them against it, which is a step worth keeping in the habit: a regression test that has never failed has not been tested. cdt-codegen SORTS its descriptor list before merging, so that ABI output does not depend on directory iteration order. Merge order is therefore filename order, and passing --desc-file twice in the other sequence changes nothing -- abimerge_tests was running the same merge twice and calling it two orders. The descriptors go in per-case subdirectories with ordering prefixes now. Against the base toolchain the rowless-first case fails exactly as reported: rowless first -> [('cfg', None), ('cfgtbl', 49879)] phantom beside the real table marker first -> [('cfg', 49879)] named_table_duplicate_target put its cross-TU pair in a SHARED header, where the per-TU annotation set drops one of the two before the merge is ever reached: both descriptors then record the same survivor and the merge has nothing to reconcile. So the fixture exercised the set comparator twice and the merge not at all, and it built clean at 4349eac. Split into a header per translation unit, it reproduces the dead link: Error, ABI structs malformed : xshared already defined The s_* pair stays in the shared header, since that is the comparator's own case. Both fixtures say which half they are for. Toolchain 64/64, ctest 34/34.
Self-review of the review fixes.
`resugar_specialization` guarded against TemplateSpecializationType,
ElaboratedType and TypedefType before its dyn_cast to RecordType. None of the
three IS a RecordType, so the dyn_cast already rejected them and the guard never
fired. Gone, with a line saying why sugar falls out on its own.
`merge_table_annotations` took its argument by value -- a full copy of the other
document -- and materialised an empty array on both sides so it could index them
unconditionally, which meant mutating the merger's own `abi` member to read it.
It takes a const reference and skips a side that has no annotations.
`contract_member_naming` existed to hand the `_i` recovery the decl that names a
specialization, and `defined_in_contract` was rewritten in terms of it. Only one
caller ever wanted the decl, and it wanted more than one.
That last is a behaviour fix, not only a tidy-up. Taking the FIRST member that
names the specialization loses the name whenever a contract holds it through an
alias declared outside the class:
using outer_alias = singleton<"singleton_outer_alias_name"_i, uint64_t>;
class c { outer_alias inst; // TypedefTypeLoc: no arguments
using in_class = singleton<"singleton_outer_alias_name"_i, uint64_t>; };
The member writes `outer_alias` and keeps no arguments to read, so the recovery
gave up on a name the declaration two lines below spells in full -- publishing
`b1xrr4kjycdsj` at the right id. Every member naming the specialization is tried
now, and the first that yields a name wins. singleton_decl_forms carries the
shape, member deliberately ahead of the alias; reverting to first-match-only
fails it.
Toolchain 64/64, ctest 34/34.
Found by compiling every example, which nothing in the build graph does.
Its two row structs sit at namespace scope carrying [[sysio::table("...")]] and
nothing else. abigen records an annotation only for a struct it can tie to the
contract, and namespace scope gives it nothing to tie -- so neither annotation
reached the descriptor, and neither table could be renamed. Adding the
[[sysio::contract]] the fixtures already use on namespace-scope rows
(named_table_split_aux/row.hpp) is all it needed:
('31v1bqlusjbf5', 18592) -> ('user_preferences', 18592)
('1.izgmfjru5dh', 62237) -> ('feature_flags', 62237)
Both ids checked against an independent reimplementation of compute_table_id.
`user_preferences` also carries its [[sysio::kv_key]] override's field names now
-- ['user', 'category'] -- which is the same closure the toolchain fix in this
branch declares.
The example described no tables at all before this branch: its tables are held
as contract DATA MEMBERS, which defined_in_contract() did not recognise, so
nothing was emitted to be named. Fixing that is what made the missing annotation
visible. Nothing in the toolchain changes here -- a namespace-scope annotated
row still needs to say which contract it belongs to.
A pre-push review of this branch found six, two of which I had shipped as crashes or silent corruption in the very code meant to remove them. `variant` and `tuple` on the resugar whitelist ABORTED the compiler. Their canonical arguments are held as a single Pack, which get_template_argument() does not handle -- it reaches CDT_INTERNAL_ERROR and terminates. This PR ships abigen-fail/kv_table_scalar_key to turn a crash into a diagnostic and introduced a new one in the same family. Off the list: a canonical variant row keeps the old path and is refused where the author can see it. Resugaring carried the ALLOCATOR, and the container helpers in gen.hpp decide by counting '<' in getAsString(). A rebuilt vector printed `vector<unsigned long, allocator<unsigned long>>`, read as a nested container, and reached add_struct() -- which synthesised a struct named after the allocator over an `__vector_base_...` the document never declares. Only the leading arguments the ABI spelling uses are kept now. Nesting is left canonical, and refused, rather than half-supported. The machinery that names `B_vector_uint64_E` runs off the same printed form and declares the typedef the name needs; a half-resugared type slipped past it and published `B_vector_uint64_E[]` with `types` empty -- a document the chain refuses at set_abi. abigen-fail/nested_container_row pins the refusal, and singleton_container_row drops the nested case it only passed by way of the uint8-to-bytes shortcut. The `_i` recovery could publish a name that was WRONG rather than absent. Adjacent string literals are spliced by the compiler and not by the lexer, so `"con" "cat"_i` read as `cat` -- a plausible name at concat's table_id, worse than the garbage it replaced. The recovered spelling must now hash back to the template parameter; a mismatch falls back to the decoded raw, unusable but visibly so. Its gate was the wrong predicate. "The row is not a class" was standing in for "the row cannot carry [[sysio::table]]", and std::string, sysio::checksum256 and every container are classes the contract author does not own -- all published decoded hashes. Gated on the attribute itself now, which is also what keeps the annotation path exclusive: a row that carries the annotation is admitted by it alone, so a TU that never names the specialization would recover nothing and two descriptors would call one table different things. A table dropped by a name collision said nothing. abi_table is ordered by name, so a second table under a name already taken vanished inside the plugin, before any descriptor or any cdt-codegen check could see it -- newly reachable once an `_i` name could equal an `_n` one. Repeated instantiations of one table stay silent; a differing table_id, row or type is reported with both. The scoped-prefix guard misfired on an override field named `scope`, suppressing the physical prefix and publishing a key eight bytes short with its first element mistyped -- the same failure the guard exists to prevent. A layout that already ends with the override is left alone, which needs no name match at all. Also: a duplicated copy of resolve_table_annotations' doc comment left by the rewrite, and a decoded hash quoted in singleton_decl_forms that belonged to a different string. Toolchain 65/65, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain built at 4349eac. All eight examples compile.
Mutating each fix and rerunning the suite found that five of the six changed nothing any test could see. That is the same criticism this branch has already taken twice, so it is worth stating plainly: a fix is not finished when it works, it is finished when reverting it fails something. _i hash check off -> singleton_decl_forms (was: nothing) _i gate on class again -> singleton_decl_forms (was: nothing) table collision unreported -> table_name_collision (was: nothing) ends_with_override off -> kv_key_scoped (was: nothing) nested guard off -> nested_map_row (was: nothing) variant/tuple on whitelist -> variant_row (was: nothing) singleton_decl_forms gains `_i` over std::string and checksum256 -- classes the author cannot annotate, which the old class-based gate skipped -- and a spliced `"con" "cat"_i`, whose source text hashes to something other than its parameter and must therefore NOT be published as `cat`. kv_key_scoped gains an override whose own first field is called `scope`, which is what the name-matching prefix guard mistook for the physical prefix. table_name_collision pins both halves: two different tables asking for one name are reported with both table_ids and rows, and a table instantiated twice stays silent. The nesting guard needed two attempts to pin. A vector of vectors is refused whether or not the guard is there -- arguments are not resugared, so the inner stays canonical -- so it never discriminated. A MAP of vectors does: without the guard it publishes `pair_uint64_vector_unsigned_long_long_[]` with `types` empty. It also needed its own fixture, because an abigen-fail test is satisfied by any non-zero exit carrying the expected text, so the row that fails regardless was masking the row that stopped failing. One mutation still changes nothing: keeping the allocator in the rebuilt sugar. With nesting refused, no ABI-visible decision reaches the extra argument, so it is belt-and-braces rather than load-bearing -- said here rather than dressed up as coverage. Toolchain 68/68, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain built at 4349eac.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 7c422b38. The prior findings' targeted fixes now pass, and exact-head validation passed all 68 toolchain tests, both ABI-merger tests, and all three changed examples. The inline cases below remain reproducible; several generate ABIs the chain cannot load or key layouts that do not match storage, so I cannot approve this head.
| // Keep the table's own prefix -- unless the annotation already supplies one. A | ||
| // [[sysio::kv_key]] with no argument IS the standard [scope][primary_key], and | ||
| // prepending to that would describe two scopes. | ||
| if (t.has_key("key_names") && scoped(t["key_names"]) && !scoped(r.ann["key_names"]) && |
There was a problem hiding this comment.
[P1] Keep physical scope independent of logical field names
For a kv_multi_index row whose kv_key override is { name scope; }, this treats that logical field as though the annotation already supplied the physical scope prefix. At this head the ABI becomes key_names:["scope"] / key_types:["name"]; it must be physical scope:name followed by logical scope:name, i.e. two entries. The query key is otherwise eight bytes shorter than storage. Please preserve scope from table-kind/provenance rather than inferring it from a field name; the new scoped_table fixture passes only because add_kv_table already composed its suffix.
There was a problem hiding this comment.
Agreed, and gone further than asked: the fold is deleted.
You are right that the prefix must not be inferred from a field name, and once that is accepted there is nothing left for the fold to do. Every table's own path already knows its layout — add_kv_table resolves the attribute and composes [scope] + logical, add_table takes it from the table kind — so re-deriving it from the annotation was reconstructing something already correct, by the one means that cannot be made correct.
A kv_multi_index now publishes ['scope','primary_key'] / ['name','uint64'], which is what it stores. The annotation still carries the metadata for a table it declares, since that one has no instantiation to take a layout from.
| return false; | ||
| const std::size_t off = t["key_names"].size() - r.ann["key_names"].size(); | ||
| for (std::size_t k = 0; k < r.ann["key_names"].size(); ++k) | ||
| if (t["key_names"][off + k] != r.ann["key_names"][k]) |
There was a problem hiding this comment.
[P1] Include key types in applied-override detection
This suffix comparison can match a physical field and a logical override solely because their names coincide. With logical { name primary_key; }, the existing physical primary_key:uint64 is considered already applied, so the final ABI remains [name,uint64] instead of [name,name]. That changes how clients represent and encode the key. Compare both key_names and key_types (including lengths), not names alone.
There was a problem hiding this comment.
Confirmed — ['scope','primary_key'] / ['name','uint64'] where the logical key is {name primary_key}, so the type is wrong as well as the position.
Fixed by the same deletion as the sibling finding: the suffix comparison existed only to spot an override the table had already applied, and with the fold gone there is nothing to spot. Neither helper survives.
| for (const auto& v : _abi.variants) | ||
| if (v.name == type) return true; | ||
| for (const auto& e : _abi.enums) | ||
| if (e.name == type) return true; |
There was a problem hiding this comment.
[P1] Retain enum definitions referenced by container rows
This accepts singleton<"enums"_n, std::vector<state>> because state is present in _abi.enums, but the later enum-pruning check compares the table's full state[] spelling directly with state. Exact-head output therefore contains table type state[] and no enums section, and chain ABI validation rejects the unresolved type. Apply the same element/suffix normalization when deciding which enums to emit, and cover an enum-container row.
There was a problem hiding this comment.
Reproduced — table type state[] with enums: [].
Not fixed, though; container rows are no longer described at all. A table row is a struct the contract declares, and std::vector<state> is refused with a diagnostic naming the row.
That is the rule upstream has always had — its add_table() takes the row's CXXRecordDecl and dereferences it — and three rounds of findings on this branch were the argument for keeping it: a scalar row crashed the compiler, containers needed sugar reconstruction that mis-declared the nested ones, variant/tuple aborted on a canonical argument pack, and binary_extension produced a type the chain refuses. Each is one struct away from a shape that works. Narrowing removed 687 lines.
abigen-fail/row_must_be_a_struct covers seven shapes including this one; docs/abi-tables.md writes the rule down.
| inline clang::QualType resugar_specialization( const clang::QualType& type ) { | ||
| static const std::map<std::string, unsigned> abi_spelled = { | ||
| {"vector", 1}, {"set", 1}, {"deque", 1}, {"list", 1}, {"optional", 1}, | ||
| {"binary_extension", 1}, {"ignore", 1}, {"pb", 1}, |
There was a problem hiding this comment.
[P1] Do not admit a binary extension as a root table type
singleton<"ext"_n, binary_extension<uint64_t>> now succeeds and emits table type uint64$. The chain only removes $ while validating struct fields; table types go directly through _is_type, so set_abi rejects this document. binary_extension is a trailing-field marker, not a valid top-level row spelling. Please reject this row shape during abigen (or otherwise emit a representation the chain can actually validate).
There was a problem hiding this comment.
Confirmed, and refused now rather than represented — binary_extension is one of the seven shapes in abigen-fail/row_must_be_a_struct. You are right that it is a trailing-field marker and not a row spelling; the same is now true of every non-struct row. See the sibling finding for the reasoning and docs/abi-tables.md for the rule.
| auto table_decl = clang_wrapper::wrap_decl(table_type); | ||
| if ((table_decl.isSysioTable() && ag.is_sysio_contract(table_decl, ag.get_contract_name())) || defined_in_contract(owner)) { | ||
| if ((table_decl.isSysioTable() && ag.is_sysio_contract(table_decl, ag.get_contract_name())) || | ||
| defined_in_contract(owner)) { |
There was a problem hiding this comment.
[P1] Include table specializations used only inside actions
A valid action that directly constructs and writes singleton<"local"_n, uint64_t> has no class alias/member and no row annotation. The visitor reaches this specialization, but defined_in_contract() scans only contract_class->decls(), so this condition rejects it and the exact-head ABI has tables:[] despite live state. Please associate local declarations with their enclosing contract method (or diagnose unsupported use) rather than silently omitting the table.
There was a problem hiding this comment.
Confirmed, and left as a documented limitation rather than fixed.
defined_in_contract() reads the contract class's own declarations, which is exactly what upstream does — so this is not a divergence a porting contract would hit. Your example is also narrower than it looks now: the row must be a struct, and a local table over an annotated struct row is admitted by the annotation. What remains unseen is a local table over an unannotated row.
Detecting it means either walking method bodies in a pre-pass or warning on every unadmitted specialization in the TU, which would fire on other contracts' tables and on library code. For a shape with two one-line fixes — declare it in the class, or annotate the row — that is not a trade I want to make. Both are in docs/abi-tables.md, which now says plainly that a table declared only as a local, over an unannotated row, is not described.
| /// under one name is a contract that writes two tables and describes one. | ||
| void insert_table( abi_table t, const clang::SourceLocation& loc ) { | ||
| const auto [it, inserted] = _abi.tables.insert(t); | ||
| if (inserted || (it->table_id == t.table_id && it->type == t.type && it->row == t.row)) |
There was a problem hiding this comment.
[P1] Compare key and index metadata before declaring tables identical
Two kv::table<"same"_n, K, uint64_t> specializations with different key structs have the same name, ID, type, and empty row marker, so this returns silently. I reproduced K1{id:uint64} followed by K2{owner:name}: compilation emits no warning and the ABI keeps only the first key layout. That is exactly the silent collapse this helper is meant to catch. Include key_names, key_types, and secondary_indexes in compatibility, or merge only demonstrably equivalent metadata.
There was a problem hiding this comment.
Confirmed and fixed — it compared identity and not what the entry describes, so two kv::tables over one row differing only in the key struct matched on all four fields and the published layout came down to declaration order.
Key names, key types and secondary indexes are compared now, and the diagnostic names what differs rather than just that something did:
warning: two different tables are both called 'same': table_id 6122 over 'c::row' keyed on (id),
and table_id 6122 over 'c::row' keyed on (owner); the ABI can describe only one, and the second
is not described
table_name_collision pins it, and pins that one table instantiated in two actions stays silent.
|
|
||
| // The table parameter is the name. A [[sysio::table("name")]] on V may still rename it, | ||
| // link-wide, in cdt-codegen; `row` is what pairs the two up. | ||
| t.name = name_to_string(name); |
There was a problem hiding this comment.
[P1] Recover _i spelling on the kv::table path too
kv::table<"long_table_name"_i, key, uint64_t> cannot carry a row annotation, and this still decodes the DJB2 raw as a Wire name. Exact-head output calls the table d4ntagmflkvng rather than long_table_name, leaving generated clients unable to address the intended name. Pass the written _i spelling into add_kv_table as the singleton/global path now does, or reject the unrepresentable case.
There was a problem hiding this comment.
Not fixed by recovery — the source-text recovery is deleted, on both paths.
You had already shown it reads the lexer's tokens rather than the compiler's, and your other finding on escapes closed the argument: "con" "cat"_i recovered cat and "config\x31"_i recovered nothing. An _i name comes from [[sysio::table("name")]] on the row struct, as #115 established, and the row rule guarantees there is always a struct to put it on.
Your specific case is refused outright now for a different reason — its row is uint64_t. With a struct row and the annotation, kv::table<"long_table_name"_i, key, row> publishes long_table_name, which hash_id_table and examples/hash_id_example both cover.
| return {}; | ||
|
|
||
| bool invalid = false; | ||
| llvm::StringRef text = clang::Lexer::getSourceText( |
There was a problem hiding this comment.
[P2] Read the evaluated literal bytes, not raw source text
Raw text neither decodes escapes nor reliably covers concatenated/raw literals. singleton<"config\x31"_i, uint64_t> compiles here but publishes ...h1giniby.g instead of config1; the checked-in adjacent-literal case likewise intentionally falls back to a decoded hash. The AST StringLiteral already has the cooked bytes that produced the template value. Use those and verify the hash, or emit an error instead of a successful ABI with an unusable name.
There was a problem hiding this comment.
Confirmed — ...h1giniby.g rather than config1, and you are right that the cooked StringLiteral bytes are the correct source.
The recovery is gone instead. Reading source text was the wrong mechanism, and it existed only to name tables whose rows cannot carry an annotation — rows that are no longer supported. _i names come from the annotation, which is exact and needs nothing recovered. The hash check I added last round goes with it.
| if (!side->has_key("____table_annotations")) | ||
| continue; | ||
| for (const auto& a : (*side)["____table_annotations"].array_range()) | ||
| if (seen.insert(key_of(a)).second) |
There was a problem hiding this comment.
[P1] Merge duplicate annotation metadata instead of keeping the first
The same (name,row) can legitimately be empty in one descriptor and resolved in another: e.g. a shared row names a forward-declared logical_key, one TU only sees the declaration, and another completes it as { uint64_t account_id; }. This seen set discards whichever record sorts second. I reproduced the final table keeping physical keys when the empty descriptor sorts first and getting account_id when the rich descriptor sorts first. Fill missing key metadata per field and reject conflicting populated records, as table merging already does.
There was a problem hiding this comment.
Confirmed and fixed. Key metadata is filled per field now, and two populated records that disagree are refused the way table merging refuses them, rather than one of them being kept because it sorted first.
unit/abimerge_tests.sh pins it in both orders, with the empty descriptor first and the resolved one first. It needed a per-case directory to do that: cdt-codegen sorts its descriptor list, so merge order is filename order and passing --desc-file the other way round changes nothing.
| for (bool progress = true; progress; ) { | ||
| progress = false; | ||
| for (auto& r : reqs) { | ||
| if (!r.pending || taken.count(r.name)) |
There was a problem hiding this comment.
[P2] Apply satisfiable rename cycles atomically
With physical alpha/A annotated to bravo and physical bravo/B annotated to alpha, both requests remain pending because each desired name is currently taken. Yet the requested final mapping is unambiguous and ABI-valid: alpha/B and bravo/A, with unique names and the original IDs. Exact-head output warns and ignores both annotations. Build and validate the complete target mapping first, then apply unique-target SCCs/permutations together rather than requiring a transiently free name.
There was a problem hiding this comment.
Confirmed — both refused, and you are right that the mapping is unambiguous and ABI-valid.
Left refused, deliberately. Applying it needs the complete target mapping built and validated, then SCCs applied together — real machinery for a contract that renames alpha to bravo and bravo to alpha in one link. The warning says the name is taken, and giving one of them a different name resolves it.
Recorded under "Not fixed here" rather than left implied. If a real contract turns up wanting it I would rather add it then, with that contract as the fixture.
Three review rounds had been spent making abigen describe every row type C++ allows. Stepping back and comparing with upstream Antelope CDT says not to: its add_table() takes the row's CXXRecordDecl and dereferences it, so a non-struct row was never supported there either. Keeping the same rule means a contract ported from another Antelope chain behaves here exactly as it did there, and it is the rule that makes an ABI self-describing -- a table's rows are named fields, not a bare scalar whose shape lives only in the C++. Net -687 lines. REFUSED, with one diagnostic naming the row: a scalar, std::string, sysio::checksum256, fixed_bytes<N>, any container, variant, tuple, and binary_extension. Each was one line of contract code away from a shape that works, and the support cost was steep: a scalar row crashed the compiler, containers needed template-sugar reconstruction that mis-declared nested ones, variant and tuple aborted on a canonical argument pack, and binary_extension produced a `uint64$` table type the chain refuses at set_abi. Gone with it: resugar_specialization, abi_row_element, abi_can_describe_row and six fixtures. `_i` NAMES keep working the way #115 established -- the readable name comes from [[sysio::table("name")]] on the row struct, and the row rule guarantees there is always a struct to put it on. The source-text recovery added last round is gone: it read the lexer's tokens rather than the compiler's, so a spliced `"con" "cat"_i` recovered `cat` and an escape recovered neither, and it existed only to serve rows that are no longer supported. The [[sysio::kv_key]] FOLD onto instantiated tables is gone. Each table's own path already knows its key layout -- add_kv_table resolves the attribute and composes [scope] + logical, add_table takes it from the table kind -- and re-deriving it from the annotation meant reconstructing physical structure from FIELD NAMES. That got both halves wrong: a logical key whose first field is called `scope` suppressed the physical one, and a suffix compared by name alone read a logical `primary_key:name` as a physical `primary_key:uint64` already applied. The annotation still carries the metadata for a table it DECLARES, which has no instantiation to take a layout from. Two fixes rather than deletions. A table-name collision now compares everything an entry describes, not just its identity: two kv::tables over one row differing only in their key struct matched on id, type and row, so the published key layout came down to declaration order. And annotation metadata merges per field instead of first-wins -- a [[sysio::kv_key]] naming a struct one TU sees only forward-declared resolves to nothing there, and keeping whichever record sorted first published the physical key or the logical one depending on filenames. Legacy multi_index and singleton stay first-class: they are the transition path for contracts coming from other Antelope chains, and singletons vanishing was a Wire regression -- upstream eosio::singleton is a real class template, while sysio::singleton became an alias over kv_singleton that the visitor never matched. docs/abi-tables.md writes the rule down with examples of every supported form, the naming rules for `_n` and `_i`, and the diagnostics. Linked from the KV storage guide. singleton_contract wraps its `sysio::name` payloads in a struct -- it is a pre-existing fixture whose singletons described nothing at all before this branch, so the rule reaches them for the first time here. Toolchain 63/63, ctest 34/34. wire-sysio: 38/38 artifacts byte-identical to a toolchain built at 4349eac -- it uses no singleton, no `_i`, no [[sysio::kv_key]] and no container row, so none of this reaches it. All eight examples compile.
Change of direction: narrowed to one ruleStepping back from the last round rather than working through it. Three rounds of findings had gone into making abigen describe every row type C++ allows, and comparing with upstream Antelope CDT says not to — its A table row is a struct the contract declares. A scalar, Narrowing removed 687 lines net, and closed most of the round by deletion rather than by fixing:
One deliberate break: Toolchain 63/63, ctest 34/34, eleven mutations each failing only its own fixtures. wire-sysio: 38/38 artifacts byte-identical to a toolchain built at Thanks for the depth on these — the last two rounds are what made the case for narrowing rather than continuing. |
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at cf21f64. The previous scope/type, row-shape, table-collision, and annotation-merge fixes pass their exact-head tests, but the two inline cases still produce incorrect ABIs. The previously reported action-local table omission also remains reproducible for a supported struct row (cdt-cpp exits 0 with tables: []); documenting that limitation does not make the generated ABI complete, so I cannot approve this head.
| return x.name != y.name ? x.name < y.name : x.row < y.row; | ||
| }); | ||
|
|
||
| // [[sysio::kv_key]] is NOT applied to instantiated tables here. Each table's own path |
There was a problem hiding this comment.
[P1] Preserve merged kv_key metadata for live tables
After merge_table_annotations() enriches an annotation from another TU, this block deliberately ignores it whenever by_row contains an instantiated table. Exact-head two-TU repro: the instantiating TU sees logical_key only forward-declared and emits table keys raw_id:uint64 plus an empty rows annotation; a second TU completes it as logical_key { name owner; } and emits owner:name. Finalization renames the table to rows but leaves key_names:["raw_id"] / key_types:["uint64"], silently dropping the explicit logical schema. The new merger test has tables:[] on both sides, so it only covers annotation-declared tables. Preserve enough table-kind/provenance to apply merged overrides to live kv::table/scoped_table entries without reintroducing scope inference from field names.
There was a problem hiding this comment.
Reproduced exactly — the merged annotation carries ['owner'], the live table keeps ['raw_id'], and the rename lands on top of the wrong layout.
Fixed by refusing rather than repairing: an unresolvable [[sysio::kv_key]] is now an error in the translation unit that instantiates the table.
That TU is the one whose key layout reaches the ABI, so it is the one that has to resolve the override, and it was already warning twice that it could not:
warning: kv_key struct 'logical_key' not found; the physical key's field names will be used in the ABI
It just wasn't loud enough to stop a build that then published a layout the author did not ask for.
I went that way rather than preserving provenance because the alternative is to carry the physical key's shape through the descriptor and reason about which leading fields are physical — which is exactly the machinery removed this round, after it got that question wrong twice (a logical field named scope suppressing the physical one, and a suffix compared by name alone). A forward-declared key struct completed in a different translation unit is a missing include, and the fix belongs there.
abigen-fail/kv_key_not_visible pins it, and docs/abi-tables.md states the rule.
| t.type = get_type(row); | ||
| // Same rule add_kv_table uses: the template parameter is the name, and a | ||
| // [[sysio::table("name")]] on the row struct may rename it link-wide, in cdt-codegen. | ||
| // That annotation is the only channel a `_i` name has -- the raw is a DJB2 hash and |
There was a problem hiding this comment.
[P2] Cook annotation literals before using them as _i names
This assumes the row annotation is an exact naming channel, but optional C++11 attribute arguments are still extracted from one raw lexer token in sysio_attrs.cpp. At this head, matching annotation/template pairs using "config\\x31" compile successfully to the literal ABI name config\\x31 at the table ID of cooked config1; adjacent "con" "catenated_long_table" emits con at the ID of the full concatenated string. Both tables are therefore mislabeled despite providing the required annotation. Parse the evaluated StringLiteral (or reject a name/ID mismatch), and cover escapes plus adjacent literals.
There was a problem hiding this comment.
Confirmed, and it lands on a claim I made in this thread — I told you the annotation was an exact channel, and it is not. Reproduced both:
[[sysio::table("config\x31")]] -> 'config\x31' at id 62258 (= cooked config1)
[[sysio::table("con" "catenated_long_table")]] -> 'con' at id 34466 (= the concatenation)
Chasing it turned up a third, from the other end of the same pipeline: the argument is encoded into an AnnotateAttr as sysio_table(arg) and split back out on [\s,]+, so [[sysio::table("has space")]] published has. Truncated by the encoding rather than by the lexer, and silent for as long as the attribute has existed.
Refused rather than cooked, though — config\x31 is not a name we want to carry. The argument must be one plain string literal, and the name letters, digits and underscore. These travel out through the ABI into wire-sysio, SHiP and Hyperion, and a name worth having is one that can be written plainly. All 88 annotation names in wire-sysio and all 52 in this repo already satisfy it, so nothing existing moves. _i still lifts the 13-character limit — the name is hashed, not encoded — which is the whole point of it.
compile-fail/attr_arg_not_plain covers the escape, the splice and the whitespace; abigen-fail/table_name_charset covers the rest. docs/abi-tables.md says it, and the sentence claiming the channel is exact is gone.
Worth noting this is pre-existing — sysio_attrs.cpp is otherwise untouched by this branch — but it became load-bearing the moment the annotation became the only naming channel for an _i table, so it belongs here.
…ible
Two findings from review, both closed by refusing rather than repairing.
TABLE NAMES ARE READ LITERALLY, and were not checked. A C++11-spelled
attribute's argument is not parsed into an Expr, so it is taken from the source
text -- the token's own spelling, not the compiler's cooked value -- and then
encoded into an AnnotateAttr as `sysio_table(arg)` and split back out on [\s,]+.
Three ways that named a table something the author did not write, all silent:
[[sysio::table("config\x31")]] published `config\x31` at cooked config1's id
[[sysio::table("con" "cat")]] published `con` at the concatenation's id
[[sysio::table("has space")]] published `has`, truncated by the encoding
The first two are the compiler cooking a literal the reader does not; the third
is the encoding. This is pre-existing -- sysio_attrs.cpp is otherwise untouched
by this branch -- but it became load-bearing when the annotation became the only
naming channel for an `_i` table, so it is fixed here.
Refused, not cooked. The argument must be one plain string literal, and the name
letters, digits and underscore. These names leave the toolchain in the ABI and
are read by wire-sysio, SHiP and Hyperion; a name worth having is one that can
be written plainly. All 88 annotation names in wire-sysio and all 52 in this
repo already satisfy it. `_i` still lifts the 13-character limit -- the name is
hashed, not encoded -- which is what it is for.
A KV_KEY OVERRIDE THAT CANNOT BE RESOLVED is now an error where the table is
instantiated, not a warning. That TU's key layout is the one that reaches the
ABI, and falling back to the physical key advertised field names the author did
not ask for. No later pass can repair it: a second TU that sees the struct
completed enriches only the ANNOTATION during the merge, while the live table
keeps the physical names -- reported by huangminghuang, and reproduced with a
forward-declared key struct. Reconstructing the override onto an instantiated
table afterwards means carrying the physical key's shape through the descriptor
and reasoning about which leading fields are physical, which is the machinery
this branch removed for getting that question wrong twice. An include is the
fix, in the TU that is missing it.
Fixtures: compile-fail/attr_arg_not_plain (escape, splice, whitespace),
abigen-fail/table_name_charset, abigen-fail/kv_key_not_visible. Both rules are
in docs/abi-tables.md.
Toolchain 66/66, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain
built at 4349eac.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed at 5c680470. The prior escaped/adjacent-literal and cross-TU fallback repros now fail cleanly. Exact-head validation passed all 66 toolchain fixtures, all 4 ABI-merger cases, and all 8 examples. The inline cases below remain reproducible: two still accept named _i tables while silently discarding the requested name, one rejects a key definition that normal C++ lookup can see, and one breaks the existing dotted table-name alphabet. The previously reported action-local table omission also still produces tables: [], so I cannot approve this head.
| /* Hyperion, and a name worth having is one you can write plainly. */ \ | ||
| auto ArgLoc = Begin.getLocWithOffset(1); \ | ||
| Token Tok, Next; \ | ||
| bool one_plain_literal = \ |
There was a problem hiding this comment.
[P1] Do not turn macro-expanded named attributes into bare ones
This validation is skipped when the whole attribute comes from a macro, because the expansion range ends at the macro invocation and the probe above does not see (. Since sysio::table has zero required arguments, that path leaves Str empty and emits a bare sysio_table annotation instead of diagnosing it. At exact 5c680470, both #define NAMED_TABLE [[sysio::table("macro_table_name")]] and a function-like NAMED_TABLE(name) compile successfully with an _i singleton, but the ABI names the table ijqvau4wut5hi (the decoded hash) rather than macro_table_name. If the macro spelling cannot be recovered under the new plain-literal policy, reject it rather than silently dropping its argument.
There was a problem hiding this comment.
Confirmed — ijqvau4wut5hi instead of macro_table_name, the argument dropped and the attribute read as bare.
Supported rather than rejected, in the end. I went looking for whether the spelling was recoverable before deciding, and it is: the expansion range ends at the invocation, but the argument is still sitting at the spelling location inside the macro body. So the probe falls back there when the expansion has nothing, and an object-like macro now carries its name through:
#define NAMED_TABLE [[sysio::table("macro_table_name")]] -> ('macro_table_name', 43674)
Your function-like case is the one that genuinely cannot be recovered — at the macro's own location the argument is the parameter, not the caller's literal — so that is refused, as you asked, with a diagnostic that says a macro parameter is the problem rather than complaining about a literal the author did in fact write.
abigen-pass/table_name_forms and compile-fail/attr_arg_from_macro.
| // wire-sysio, SHiP and Hyperion, none of which should have to carry whatever the | ||
| // attribute happened to be written with. | ||
| t.name = table_name.str(); | ||
| CDT_CHECK_ERROR(!t.name.empty() && |
There was a problem hiding this comment.
[P1] Validate an explicit empty name before treating it as bare
This nonempty check is unreachable for [[sysio::table("")]]: the empty argument is encoded identically to a bare attribute, and the earlier if (table_name.empty()) return; exits first. An exact-head _i singleton using that annotation compiles with exit 0 and publishes sqwcza1ug5eid at table ID 31425 instead of rejecting the missing readable name. Preserve whether an argument was supplied (or reject the empty literal in the attribute handler) so the intended validation cannot be bypassed.
There was a problem hiding this comment.
Confirmed — an empty argument encodes identically to no argument, so if (table_name.empty()) return; fired first and the validation was unreachable.
Rejected in the attribute handler, which is the last point where "empty" and "absent" are still distinguishable. It joins compile-fail/attr_arg_not_plain with the escape, the splice and the whitespace cases, since they all come down to the same thing: the argument is read as text, so it has to be text that survives being read.
| // later pass can repair it -- another TU that happens to see the struct | ||
| // completed enriches only the ANNOTATION, while the live table keeps the | ||
| // physical names. The override has to be visible here. | ||
| CDT_CHECK_ERROR(false, "abigen_error", val_decl->getLocation(), |
There was a problem hiding this comment.
[P1] Search enclosing scopes before reporting a key invisible
The new hard error relies on the lookup above, but that lookup checks only types nested in the value row and direct declarations in the row's immediate context. A complete namespace-scope logical_key is normally visible from a contract-nested row (an in-class alias and static_assert(sizeof(logical_key)) both compile), yet exact-head abigen exits 255 here for [[sysio::kv_key("logical_key")]]. A control with the row at namespace scope succeeds and emits owner:uint64. Use semantic lookup or walk parent declaration contexts before concluding that the definition is absent from the instantiating TU.
There was a problem hiding this comment.
Confirmed, and this one is mine — the error I added an hour ago turned a survivable miss into a build failure on working code.
The lookup checked types nested in the row and the row's immediate DeclContext and stopped, so a row nested in the contract class could not see a namespace-scope struct. Harmless while the miss was a warning that fell back to the physical key; not harmless as an error.
It walks every enclosing scope out to the translation unit now, and both paths that read the attribute share one lookup rather than keeping two copies in step. Your exact shape is in abigen-pass/table_name_forms — namespace-scope logical_key, row nested in the class — publishing rows keyed on owner.
| t.name = table_name.str(); | ||
| CDT_CHECK_ERROR(!t.name.empty() && | ||
| std::all_of(t.name.begin(), t.name.end(), [](unsigned char c) { | ||
| return std::isalnum(c) || c == '_'; |
There was a problem hiding this comment.
[P2] Preserve valid dotted table names
This predicate rejects ., even though _n names explicitly support the .12345a-z alphabet, the existing singleton_contract fixture emits tables such as smpl.conf5, and wire-sysio defines table_def::name as a free-form string. Exact head now rejects [[sysio::table("foo.bar")]] paired with singleton<"foo.bar"_n, ...>, a previously accepted and unambiguous spelling that round-trips through the annotation encoding. Restrict characters that actually break the encoding, but retain . so existing annotated Wire names continue to compile.
There was a problem hiding this comment.
Agreed — I picked the C++ identifier charset when the relevant alphabet is the name one. . is in .12345a-z, singleton_contract already publishes smpl.conf5, and it round-trips the encoding without trouble. Letters, digits, underscore and dot now; abigen-pass/table_name_forms carries foo.bar alongside its _n parameter.
…oo soon
Four from review, two of them damage from the previous commit. Turning warnings
into errors and adding a charset rule both need the underlying lookup and
alphabet to be right first, and I shipped them without checking either.
DOTS ARE PART OF THE ALPHABET. `_n` names are `.12345a-z`, singleton_contract
already publishes `smpl.conf5`, and a dot round-trips the annotation encoding
without trouble -- so restricting a table name to the C++ identifier charset
rejected a spelling that has always been valid. Letters, digits, underscore and
dot.
THE KV_KEY LOOKUP STOPPED AT THE ROW'S IMMEDIATE CONTEXT, so a row nested in the
contract class could not see a struct at namespace scope -- one C++ finds
without difficulty. Survivable while the miss was a warning that fell back to
the physical key; a build failure once it became the error it should have been.
It walks every enclosing scope out to the translation unit now, and both paths
that read the attribute share one lookup.
A NAMED ATTRIBUTE INSIDE A MACRO lost its argument. The expansion range ends at
the macro invocation, so the probe for `(` found nothing, the attribute read as
bare, and the name was dropped: `#define NAMED_TABLE [[sysio::table("x")]]`
published the decoded hash of its `_i` parameter. The argument is still at the
SPELLING location, inside the macro body, so that is where it is read from when
the expansion has nothing. A function-like macro still cannot supply one -- at
its own location the argument is the parameter, not the caller's literal -- and
is refused with a diagnostic saying so, rather than silently dropped.
AN EMPTY ARGUMENT encodes identically to no argument, so `[[sysio::table("")]]`
read as bare and every later check agreed; the validation meant for a written
name was unreachable. Refused in the attribute handler, where the difference
between "empty" and "absent" still exists.
Fixtures: abigen-pass/table_name_forms covers the macro, the dotted name and the
namespace-scope kv_key struct; compile-fail/attr_arg_from_macro covers the
function-like macro; the empty argument joins compile-fail/attr_arg_not_plain.
docs/abi-tables.md gains the alphabet and the macro rules.
Toolchain 68/68, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain
built at 4349eac.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 4fcc2b77. The four cases raised on 5c680470 are fixed: object-like macro names, direct C++11 empty-name rejection, dotted names, and namespace-scope kv_key lookup now behave as intended. Exact-head validation passed the 68/68 toolchain suite, 4/4 ABI-merge suite, and all eight examples.
I still cannot approve because the three exact-head repros below compile successfully while publishing the wrong table name or key schema. The previously discussed action-local unannotated table omission is also unchanged (tables: []) and remains listed under “Not fixed.”
| if (Lexer::getSourceText(CharSourceRange(SourceRange(Begin), true), SM, LangOpts) == "(") { \ | ||
| Str = Lexer::getSourceText(CharSourceRange(SourceRange(Begin.getLocWithOffset(1)), true), SM, LangOpts); \ | ||
| auto opens = [&](SourceLocation L) { \ | ||
| return Lexer::getSourceText(CharSourceRange(SourceRange(L), true), SM, LangOpts) == "("; \ |
There was a problem hiding this comment.
[P1] Skip trivia before deciding the attribute is bare
An attribute argument clause may be separated from the attribute token by whitespace or comments, but this tests only the immediate source character. At this head, [[sysio::table /* legal trivia */ ("configuration_store")]] compiles and is recorded as bare sysio_table; with singleton<"configuration_store"_i, ...>, the ABI silently publishes the decoded name sqwcza1ug5eid instead of configuration_store. Please locate the next preprocessing token (while preserving expansion/spelling handling) before deciding that no argument was supplied, and cover the trivia form with a regression.
There was a problem hiding this comment.
Confirmed — sqwcza1ug5eid instead of configuration_store. I probed the next source character for ( where the question is the next token.
Before fixing it I checked whether the source-text path needs to exist at all, since it is the origin of this whole family. It does: clang parses arguments for the GNU spelling — getNumArgs() is 1 with a cooked StringLiteral — and not for the C++11 spelling, which reports 0 even for a plain [[sysio::table("x")]], and still reports 0 with a required argument declared. So the form every contract writes has to be read from source, and the answer is to read it properly rather than keep adjusting it.
It asks for the next token now, which skips whitespace and comments by construction, and the argument and its closing paren come from the same scan — the macro fallback and the one-plain-literal rule ride on it instead of on offset arithmetic. abigen-pass/table_name_forms carries the trivia spelling.
| /* An empty argument encodes identically to no argument at all, so every later */ \ | ||
| /* check reads it as a bare attribute and the validation meant for a written */ \ | ||
| /* name is never reached. Refused here, where the difference is still visible. */ \ | ||
| if (Str.size() <= 2) { \ |
There was a problem hiding this comment.
[P1] Apply empty-name rejection to the GNU spelling
This check is confined to the C++11 source-text branch. __attribute__((sysio_table(""))) takes the Attr.getNumArgs() > 0 path, leaves Str empty, skips this check, and is later encoded as bare sysio_table. Exact-head cdt-cpp exits 0 and an _i singleton again publishes the decoded hash rather than rejecting the empty name. Please track whether an argument was syntactically supplied across both spellings and perform the empty check after both parsing paths.
There was a problem hiding this comment.
Confirmed. The check sat inside the C++11 branch, and the GNU spelling takes the parsed-Expr path, so it never reached it.
Both paths record whether an argument was syntactically supplied now, and the emptiness check runs once after they converge — beside the charset check, which is the same kind of question and was already in the right place. compile-fail/attr_arg_not_plain covers both spellings.
| if (!ctx) | ||
| return nullptr; | ||
| for (auto* d : ctx->decls()) | ||
| if (auto* r = llvm::dyn_cast<clang::CXXRecordDecl>(d)) |
There was a problem hiding this comment.
[P1] Honor C++ shadowing when resolving kv_key
This direct-declaration scan ignores aliases and then continues into parent scopes. In an exact-head repro, a global struct logical_key { name wrong_outer_owner; }; is shadowed inside the contract by using logical_key = actual_key;; normal C++ lookup (and a static_assert) resolves the alias, but this helper skips it and selects the global record. The build succeeds while the ABI publishes wrong_outer_owner:name instead of intended_inner_id:uint64. Please use semantic lookup, or at minimum stop at nearer declarations and resolve aliases, because climbing past a shadowing declaration can silently emit the wrong key schema.
There was a problem hiding this comment.
Confirmed, and this is the one worth the most words back: wrong_outer_owner instead of intended_inner_id, with the build clean.
You offered semantic lookup or, at minimum, stopping at nearer declarations and resolving aliases. Semantic lookup is not available — abigen runs as an ASTConsumer in HandleTranslationUnit, and Sema is gone by then — so I have taken the second, but stated it as a rule rather than left it as an approximation:
the row, then the enclosing class, then each enclosing namespace; a struct or an alias to one; and the first scope that declares the name decides, complete struct or error.
The reason for making it a rule rather than a better approximation is your diagnosis: what caused the wrong schema was not missing aliases as such, it was climbing past a nearer declaration in search of something usable. A hand-rolled walk will never match C++ lookup — using-declarations, inline namespaces, dependent scopes are all out of reach — so anything it cannot resolve is now refused by name rather than guessed at, and the diagnostics say what was looked for, where, and what would satisfy it:
[[sysio::kv_key("nowhere_key")]]: no struct named 'nowhere_key' was found in the row, the
enclosing class, or any enclosing namespace. The key struct must be declared in one of those
-- as a struct or an alias to one -- and be complete in this translation unit
abigen-pass/kv_key_resolution pins all four sites including your shadowed alias, and docs/abi-tables.md states the rule. Nothing existing is precluded: every kv_key struct in this repo resolves from the row, the contract class or a namespace, and wire-sysio uses the attribute nowhere.
Three from review, all in validation added over the last two commits, and all
one mistake: hand-rolling what the compiler already does, then patching the
hand-rolled version per finding.
I checked whether the lexing half has a structural escape before fixing it.
It does not. Clang parses arguments for the GNU spelling -- `getNumArgs()` is 1
with a cooked StringLiteral -- and not for the C++11 spelling, which reports 0
even for `[[sysio::table("x")]]`, and still reports 0 with a required argument
declared. So the source-text path stays for the form every contract writes, and
the answer is to read it properly rather than to keep adjusting it.
TOKENS, NOT CHARACTERS. The probe for `(` tested the immediate source
character, so legal trivia between the attribute name and its argument read as
a bare attribute and dropped the name: `[[sysio::table /* c */ ("x")]]`
published the decoded hash of its `_i` parameter. It asks for the next TOKEN
now, which skips whitespace and comments by construction, and the argument and
its closing paren are read the same way. The macro fallback and the
one-plain-literal rule ride on the same scan instead of on offset arithmetic.
THE EMPTY-NAME CHECK SAT IN ONE BRANCH. `__attribute__((sysio_table("")))`
takes the parsed-Expr path, which never reached it, and was encoded as bare.
Both paths now record whether an argument was syntactically supplied, and the
check runs once after they converge -- beside the charset check, which is the
same kind of question.
KV_KEY RESOLUTION IS A STATED RULE. The scan matched only direct struct
declarations, so an alias was invisible: a contract-local `using logical_key =
actual_key;` was skipped, the walk climbed past it into the namespace, and a
global struct of the same name was chosen instead -- publishing another table's
key schema with the build clean. Climbing past a nearer declaration is what
makes an approximation dangerous.
So it does not approximate. The struct is looked for in the row, the enclosing
class, then each enclosing namespace; it may be a struct or an alias to one; and
the first scope declaring the name decides, complete struct or error. Sema is
gone by the time abigen runs, so using-declarations, inline namespaces and
dependent scopes are out of reach -- and are refused by name rather than
guessed at. The diagnostics say what was looked for, where, and what would
satisfy it.
Nothing existing is precluded: every kv_key struct in this repo is nested in its
row, in the contract class, or at namespace scope, and wire-sysio uses the
attribute nowhere.
Fixtures: abigen-pass/kv_key_resolution covers all four resolution sites
including the shadowed alias; table_name_forms gains the trivia spelling;
attr_arg_not_plain gains the GNU empty name. docs/abi-tables.md states the
resolution rule.
Toolchain 69/69, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain
built at 4349eac, all eight examples compiling.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 5f37c2ae. The three exact repros from my prior review are fixed: direct trivia preserves the name, GNU sysio_table("") is rejected, and the contract-local alias wins over the global record. Exact-head validation passed 69/69 toolchain tests, 4/4 ABI-merge tests, and all eight examples.
I still cannot approve. The inline cases below include three paths that compile cleanly while publishing the wrong table name or key schema, plus one documented alias form whose result depends on an unrelated instantiation. Two of the newly edited regression tests also pass against the exact broken 4fcc2b77 toolchain and therefore do not pin their fixes.
Non-blocking cleanup: the plugin build now emits -Wcomment at sysio_attrs.cpp:61, and the PR description still has the pre-fix character class/test count.
| Str = Lexer::getSourceText(CharSourceRange(SourceRange(Begin.getLocWithOffset(1)), true), SM, LangOpts); \ | ||
| auto AttrRange = SM.getExpansionRange(Attr.getRange()); \ | ||
| auto opening = [&](SourceLocation NameLoc) -> std::optional<Token> { \ | ||
| auto t = Lexer::findNextToken(NameLoc, SM, LangOpts); \ |
There was a problem hiding this comment.
[P1] Do not treat macro-produced argument clauses as bare
findNextToken sees the unexpanded source token here, so a macro that supplies the argument clause is missed. At this head, #define TABLE_ARGS ("configuration_store") followed by [[sysio::table TABLE_ARGS]] compiles with no diagnostic, but an _i singleton publishes sqwcza1ug5eid instead of configuration_store. The same happens when a macro supplies only ( or an empty macro sits before a direct (. Since the optional-argument path interprets a missing open as bare, please either inspect the expanded tokens or diagnose an unrecoverable macro-produced clause; it must not silently discard a supplied name.
| saw_name = false; | ||
| if (!ctx) | ||
| return nullptr; | ||
| for (auto* d : ctx->decls()) { |
There was a problem hiding this comment.
[P1] Search all fragments of an enclosing namespace
ctx->decls() only visits the current NamespaceDecl fragment. In an exact repro, app::logical_key is defined in one namespace app {} block and the contract in a later block, with a conflicting global logical_key. This scan misses the intended namespace record, climbs to the translation unit, and cdt-cpp exits 0 while the ABI publishes the global wrong_global_owner:name schema instead of intended_namespace_id:uint64. Please perform lookup across all redeclarations/fragments of each enclosing namespace.
| continue; | ||
| if (const auto* t = alias->getUnderlyingType().getTypePtrOrNull()) | ||
| r = t->getAsCXXRecordDecl(); | ||
| } else { |
There was a problem hiding this comment.
[P1] Stop lookup on every nearer declaration of the name
The stated rule says unsupported forms are errors rather than guesses, but this branch ignores every same-named declaration except a record or typedef. For example, an enclosing namespace with using keydefs::logical_key; and a conflicting global struct compiles successfully and publishes the global wrong_global_owner:name schema; a nearer enum or static data member has the same result. When any nearer declaration has this name but cannot be resolved under the supported rule, set saw_name and stop with the promised diagnostic instead of continuing outward.
| continue; | ||
| } | ||
| saw_name = true; | ||
| if (r && r->isCompleteDefinition()) |
There was a problem hiding this comment.
[P2] Do not make alias support depend on unrelated instantiation
A visible template<class T> struct key_template { T templated_id; }; using logical_key = key_template<uint64_t>; is a concrete alias to a defined struct, but abigen exits 255 here because Clang has not yet materialized the specialization. Adding an unrelated static_assert(sizeof(logical_key) > 0) makes the same contract pass and emits templated_id:uint64. Please materialize/resolve the visible specialization before testing completeness, or explicitly exclude this form from the documented alias support; ABI generation should not depend on an unrelated ODR-use.
| SYSLIB_SERIALIZE(macro_row, (v)) | ||
| }; | ||
|
|
||
| struct [[sysio::table /* legal trivia between name and argument */ ("foo.bar")]] dotted_row { |
There was a problem hiding this comment.
[P2] Make this regression distinguish named from bare
This case uses an _n name, so if the attribute regresses to bare the fallback decoder still produces exactly foo.bar; the expected ABI is unchanged. I ran this current test against the exact broken 4fcc2b77 toolchain and it still passed 1/1, while an isolated long _i trivia case reproduced the wrong decoded name. Put the trivia on a long _i-named row (or otherwise assert the annotation) so removing the token fix changes the expected output.
|
|
||
| // The GNU spelling takes the other parse path entirely -- clang hands it a cooked | ||
| // StringLiteral -- so an empty check confined to the C++11 branch never saw it. | ||
| struct __attribute__((sysio_table(""))) gnu_emptied { |
There was a problem hiding this comment.
[P2] Isolate the GNU empty-name regression
This fixture already has the C++11 empty-name declaration, which emits the identical may not be empty substring, and the test runner checks only that the substring appears once. Consequently this current test passes against the exact 4fcc2b77 toolchain even though that toolchain accepts the GNU declaration. Split the GNU case into its own compile-fail fixture (or assert distinct occurrence/location data) so it actually guards the other parse path.
Net -655 lines. Thirteen of the last thirteen review findings were on validation added after review, and none on the seven defects this PR is for. Each hardening step created surface, the holes in it were found, and I hardened again -- ending at namespace fragments, using-declarations and template specializations Clang has not materialized, which is a name-lookup implementation and not something worth writing here. sysio_attrs.cpp goes back to master. The plain-literal rule, the empty-argument check, the macro spelling fallback and the token probe are gone. Reading the annotation as source text is how it has always worked, and the ways that can disagree with the compiler -- an uncooked escape, adjacent literals truncated to the first, whitespace lost in the encoding -- are pre-existing and were not what this PR set out to fix. THE NAME CHARSET STAYS: letters, digits, underscore and dot. It is the one enforcement worth its weight, because these names leave the toolchain in the ABI and are read by wire-sysio, SHiP and Hyperion, and it happens to catch the escape case as well, since a backslash is not in the set. `.` is part of the `_n` alphabet, which is why it is in there. The kv_key lookup goes back to a simple scan -- types nested in the row, then the row's enclosing context -- and a miss goes back to a WARNING that falls back to the physical key. Round 2's fix stays, which is the part that mattered: both paths reading the attribute search in the same order, so a key struct declared inside the value row is no longer invisible to the kv::table path. Making that miss an error is what made the lookup's precision matter. As a warning it had been approximately right for years and nobody minded; as a build failure, every gap in a hand-rolled scope walk became a P1. The error is gone, and with it the aliases, shadowing rules and completeness semantics that were only there to hold it up. Five fixtures go with the code they tested. table_name_forms keeps the dotted name, which is the charset rule's other side. docs/abi-tables.md now says what is enforced and what is merely how to write it. Toolchain 65/65, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain built at 4349eac.
Reverting the validation I added; keeping one ruleThirteen of the last thirteen findings were on validation added after review, and none on the seven defects this PR is for. That is a treadmill I built: each hardening step created surface, you correctly found the holes, and I hardened again — arriving at namespace fragments, using-declarations and unmaterialised template specializations. That is a name-lookup implementation, and it does not belong in this PR. Net −655 lines.
One rule stays: a table name is letters, digits, underscore and dot. These names leave the toolchain in the ABI and are read by wire-sysio, SHiP and Hyperion, so what can appear in them is worth constraining. The Five fixtures go with the code they tested. What remains under review is the PR's actual subject: the bare-table phantom, singletons reaching the ABI at all, Toolchain 65/65, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain built at Thank you for the persistence on these — the pattern in them is what made the case for cutting rather than continuing. |
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 60e9834a. The scope reduction removes the prior newly added validation and lookup regressions, and exact-head validation passed 65/65 toolchain tests, 4/4 ABI-merger tests, and all eight examples. Ubuntu CI is green; macOS Arm64 is still running.
I still cannot approve because the inline cases produce mislabeled or incomplete ABIs.
Please also refresh the PR description for this head: it still says non-plain table names and invisible kv_key overrides are errors, gives a character class without dot, and reports 66 toolchain tests. The revert also left implementation comments at abigen.hpp:469-470 and abigen.hpp:1365-1381 describing behavior that no longer exists.
| declared.push_back(std::move(t)); | ||
| } else { | ||
| taken.erase(abi["tables"][r.target]["name"].as<std::string>()); | ||
| abi["tables"][r.target]["name"] = r.ann["name"]; |
There was a problem hiding this comment.
[P1] Validate annotations before the link-wide rename
The resolver now trusts r.ann["name"] even though the C++11 attribute handler can deliver truncated or uncooked source text. In an exact two-TU repro, a shared row uses [[sysio::table("configuration_" "store")]]; one TU only declares it and another instantiates multi_index<"configuration_store"_i, row>. Base fails on mismatched descriptors, but this head exits 0 and emits name:"configuration_" with table_id:31425, the ID of the full name, so clients cannot discover the requested table. Macro-token and whitespace variants do the same. Cook the literal or reject spellings that cannot round-trip before applying the link-wide rename.
There was a problem hiding this comment.
Reproduced the head behaviour exactly — configuration_ published at 31425, the full name's id.
I could not reproduce base failing, though, and that matters for what to do about it. In my two-TU construction — shared header declaring the annotated row, one TU instantiating multi_index<"configuration_store"_i, row>, the other only including it — base emits configuration_ too, with no error. If you have a construction where base fails on mismatched descriptors, I would like it, because that is the difference between a regression and a pre-existing wart.
As it stands I am leaving it, and want to be plain about why rather than quietly skipping it. sysio_attrs.cpp is now byte-identical to master: reading the annotation as source text, with the truncation that implies, is how it has always worked. I added a plain-literal check for exactly this and then reverted it, along with the empty-argument check, the macro fallback and the token probe, because that validation became the entire subject of review — thirteen consecutive findings, none of them on the seven defects this PR is for. Re-adding a piece of it reopens that surface.
What did stay is the charset — letters, digits, underscore and dot — which is the constraint worth enforcing, since these names are read downstream by wire-sysio, SHiP and Hyperion. It catches the escape spelling, since a backslash is not in the set. It does not catch adjacent literals, because configuration_ is a perfectly ordinary name; distinguishing it from an intended one needs the literal cooked, which is the surface I removed.
docs/abi-tables.md says to write the name as a plain literal and why. If you can show base failing here, I will reconsider on the grounds that it is a regression rather than an inherited limitation.
There was a problem hiding this comment.
I rebuilt all four relevant tools from the exact base 2fe0822b and reproduced the failure. The distinction from a TU that literally only includes the header is that the declaration-only TU also causes abigen to emit a descriptor:
// row.hpp
struct [[sysio::table("configuration_" "store"),
sysio::contract("adjacentxtu")]] configuration_row {
uint64_t id;
uint64_t primary_key() const { return id; }
SYSLIB_SERIALIZE(configuration_row, (id))
};
// main.cpp: contract action body
sysio::multi_index<"configuration_store"_i, configuration_row> rows(
get_self(), get_self().value);
// a_declares_only.cpp
#include "row.hpp"
[[sysio::action]] void other(uint64_t value) { (void)value; }Exact command:
cdt-cpp main.cpp "-abigen_output=''" -contract=adjacentxtu a_declares_only.cppBase exits 255 with Error, ABI structs malformed : configuration_ already defined; its two descriptors contain configuration_ at IDs 6649 and 31425. This head exits 0 and publishes configuration_ at 31425. I independently reran the same construction today. Macro-token and whitespace variants show the same base-failure-to-head-success transition.
So I agree that reading the source spelling is inherited, but the resolver converting the resulting descriptor disagreement into a successful mislabeled ABI is introduced here.
There was a problem hiding this comment.
You are right, and my earlier reply was wrong for a reason worth stating: I had been comparing against 4349eacd, an intermediate commit inside this branch, rather than the merge-base 2fe0822b. Every "base does this too" I have written in this review was measured against mid-PR code. Rebuilt at the real base, your construction reproduces exactly — base produces no ABI, head exits 0 with configuration_ at 31425.
Refused now rather than cooked. Chasing it also turned up a spelling that is not a corner case at all:
struct [[sysio::table("user_preferences_"
"history")]] pref_row { … };
multi_index<"user_preferences_history"_i, pref_row> t(…);_i exists because the name is too long for _n, and a name long enough to need _i is exactly a name long enough to wrap across two lines. That published user_preferences_ at table_id 32944 — the id of the full name — so the name addressed nothing and the id was unreachable by name.
One string literal is what the reader and the compiler are guaranteed to agree on, and every sysio attribute in the tree already takes exactly one, so nothing legal is precluded. compile-fail/attr_arg_one_literal uses the wrapped-name spelling, since that is the one someone will actually write.
| // An annotation may still become a table in cdt-codegen, and a table naming a type | ||
| // the document does not declare is refused by the chain. | ||
| for( const auto& ta : _abi.table_annotations ) { | ||
| if (as.name == _translate_type(ta.type)) |
There was a problem hiding this comment.
[P1] Keep kv_key dependencies for annotation-only tables
[[sysio::table("declared"), sysio::kv_key("logical_key")]] is explicitly retained here even without an instantiation, but its key dependencies are not. validate_struct() checks kv_key_structs only inside the instantiated-table loop; when this annotation is the only table, that loop is empty. An exact-head repro builds successfully and publishes key_types:["logical_id"] while types is empty; base emits logical_id -> uint64, and Wire's key codec reports Unsupported BE key type: logical_id for the head ABI. Adding an unrelated ordinary table makes the typedef and key struct reappear. Preserve kv_key_structs independently of the actual-table loop and add an annotation-only typedef-key regression.
There was a problem hiding this comment.
Confirmed and fixed. The kv_key_structs check sat inside the loop over instantiated tables and never depended on the loop variable, so with an annotation-declared table as the only table the loop never ran:
tables : [('declared', key_types ['logical_id'])]
structs: [lone_row, test] types: [] <- both pruned
Hoisted out of the loop — the override is a dependency of the attribute, not of any one table. named_table_attr's declared row carries a kv_key with a typedef key now, since it is already the annotation-only case, so removing the hoist fails it.
| return; | ||
|
|
||
| const auto row_of = [](const ojson& t) { | ||
| return t.has_key("____row") ? t["____row"].as<std::string>() : std::string{}; |
There was a problem hiding this comment.
[P2] Give internal-linkage rows a TU-unique identity
____row is only the textual qualified name, which is not unique across translation units. In an exact two-TU repro, each TU defines a different namespace { struct row }, annotates it as first/second, and instantiates tables one/two; both markers become (anonymous namespace)::row. This groups the unrelated tables, emits two row type of 2 warnings, and ignores both annotations. Base emits first and second; with _i parameters the head leaves decoded hash labels. Use a stable TU-unique identity, such as a source-qualified ID or USR, for internal-linkage declarations.
There was a problem hiding this comment.
Confirmed and fixed — that marker exists to name the same type in every translation unit, and for an anonymous-namespace struct it does not.
Qualified with the main file for internal-linkage declarations now, which is what makes it an identity rather than a printed name. Your case gives first and second with no warnings; named_table_internal_linkage pins it across two TUs.
One note on the comparison: in my construction base emits one and two as well, not first and second — I could not get base to apply either annotation for anonymous-namespace rows. The fix stands regardless, since the grouping was wrong on its own terms.
…an one type Two defects in mechanisms this PR introduced, plus comments the revert left behind describing behaviour that no longer exists. A KV_KEY OVERRIDE IS A DEPENDENCY OF THE ANNOTATION, not of any one table. validate_struct() consulted kv_key_structs from inside its loop over instantiated tables -- a condition that never depended on the loop variable -- so a contract whose only table is one an annotation DECLARES had no instantiated table to iterate, and the key struct and the types its fields name were pruned out from under it. The entry published key_types naming something the document does not define, and the chain's key codec had nothing to resolve; adding any unrelated table made both reappear. Hoisted out of the loop. ____ROW HAS TO NAME THE SAME TYPE IN EVERY TRANSLATION UNIT, since that is what it is for -- matching an annotation to the tables over its row after the merge. A struct in an anonymous namespace does not: it is a distinct type per TU and prints as `(anonymous namespace)::row` in all of them. Two unrelated rows were grouped as one, each annotation looked like it named two tables, both were refused with a warning, and both tables kept their raw parameters. The identity is qualified with the main file for internal-linkage declarations now, which is what makes it an identity. Fixtures: named_table_attr's `declared` gains a kv_key override with a typedef key, since it is already the annotation-only table; named_table_internal_linkage covers the anonymous-namespace pair across two translation units, and expects `first` and `second` with no warnings. Also removed: an orphaned doc comment for the `_i` source recovery that no longer exists, and a comment describing a scope walk that was reverted. Toolchain 66/66, ctest 34/34, wire-sysio 38/38 byte-identical to a toolchain built at 4349eac.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 139f6fc7. The production kv_key closure fix works in isolated exact-head probes, and the anonymous-namespace fixture is sensitive to the parent implementation. Exact-head validation passed 38/38 abigen-pass and 14/14 abigen-fail cases; both CI platforms are green.
I still cannot approve. The prior annotation-name finding remains a PR-introduced base-failure-to-head-success regression; I replied there with the exact construction and descriptor IDs. The two additional current-head issues are inline.
| if (decl->isInAnonymousNamespace()) { | ||
| const auto& sm = decl->getASTContext().getSourceManager(); | ||
| id += "@"; | ||
| id += sm.getFilename(sm.getLocForStartOfFile(sm.getMainFileID())).str(); |
There was a problem hiding this comment.
[P1] Use a declaration-stable row identity
The main-file suffix is neither stable for a header-defined table schema nor sufficient for other local records. In an exact repro, a bare anonymous row is defined in row.hpp and both TUs instantiate the same physical multi_index<"orig"_n, row>; this head writes @main.cpp/@other.cpp into the two markers and finalization exits 255 with orig already defined, while parent 60e9834a succeeds with one orig table. Conversely, two actions in one source can each declare a distinct local annotated struct row; neither is in an anonymous namespace, so both identities remain row. Exact head then exits 0 with two ambiguity warnings and publishes decoded _i hashes instead of the requested first/second names, while base publishes both names. Key this marker by a stable declaration identity (for example spelling file plus offset or an appropriate Clang USR), so copies of one header declaration agree and separate local declarations differ, and cover both cases.
There was a problem hiding this comment.
Confirmed, and the first half is a regression I introduced yesterday: one header declaration included by two TUs got @main.cpp and @other.cpp, and a table that had linked fine failed with orig already defined.
Your framing is the fix — the marker needs a declaration identity, not a name and not a file. It has to agree for copies of one declaration and differ for separate declarations, and the declaration's spelling location answers both without special-casing anonymous namespaces at all, so it is less code than what it replaces.
By basename rather than full path, because this toolchain really does produce ./aux/row.hpp beside aux/row.hpp for one header across two TUs — named_table_split's own descriptors show it — and that is still one declaration. Diagnostics print the name half, so the warnings are unchanged.
named_table_internal_linkage now covers both halves: the per-TU anonymous rows that must stay apart, and a shared-header row both TUs instantiate that must stay together.
| SYSLIB_SERIALIZE(lone_key, (account)) | ||
| }; | ||
|
|
||
| struct [[sysio::table("declared"), sysio::kv_key("lone_key")]] lone_row { |
There was a problem hiding this comment.
[P2] Exercise the empty instantiated-table set
This fixture has six unrelated multi_index instantiations below, so the old kv_key_structs check still executes on the first iteration of set_of_tables and retains lone_key/lone_id. I overlaid the immediately preceding 60e9834a plugin onto the current test tree and this new expected ABI still passes unchanged; the old plugin fails only when the annotation-declared table is the contract's sole table. Move this case to a dedicated no-instantiation contract so reverting the hoist actually fails the regression.
There was a problem hiding this comment.
Confirmed — six unrelated instantiations below it, so the loop ran on the first one regardless and the hoist was never exercised. That is the fourth test of mine in this branch that passed against the code it was written to catch, which is a pattern I should be checking for rather than being told about.
Moved to annotation_only_table, a contract whose annotation-declared table is its only table, which is the shape that empties the loop. named_table_attr goes back to its own subject.
…eclaration Three from review, and a correction to how I have been verifying. I had been comparing against 4349eac -- an intermediate commit inside this branch -- rather than the merge-base 2fe0822. Every "base does this too" I replied with was measured against mid-PR code. Rebuilt at the real base, the annotation-splice finding is exactly the regression it was reported as. ADJACENT STRING LITERALS ARE JOINED BY THE COMPILER AND NOT BY THIS READER. Clang does not parse arguments for a C++11-spelled plugin attribute, so the argument is read from source text and stops at the first token. Wrapping a long name across two lines is ordinary C++, and a name long enough to want `_i` is exactly a name long enough to wrap: [[sysio::table("user_preferences_" "history")]] multi_index<"user_preferences_history"_i, row> published `user_preferences_` at table_id 32944 -- the id of the FULL name, since `_i` hashed what the compiler saw. The name addressed nothing and the id was unreachable by name. Inherited in one TU; across two, base's descriptors disagreed and the link failed, while resolving annotations link-wide reconciles them and ships the mislabelled table. That conversion is this PR's. Refused rather than joined: one string literal is what the reader and the compiler are guaranteed to agree on, and every sysio attribute in the tree already takes exactly one. ____ROW IS NOW A DECLARATION IDENTITY, which is what it always needed to be. It has to agree for copies of one declaration seen from two translation units and differ for separate declarations that print the same, and a qualified name does neither: an anonymous-namespace struct is a distinct type per TU and prints identically in each. Yesterday's main-file suffix fixed that half and broke the other -- one header declaration included by two TUs got two identities, and a table that had linked fine failed with `orig already defined`. The declaration's spelling location answers both, by basename so that `./aux/row.hpp` and `aux/row.hpp` stay one declaration. Diagnostics print the name half. THE ANNOTATION-ONLY FIXTURE DID NOT DISCRIMINATE. named_table_attr has six other instantiations, so the loop whose hoist it was meant to guard ran anyway on the first of them. The case moves to a contract whose annotation-declared table is its only table, which is the shape that exercises it -- the fourth test of mine in this branch that passed against the code it was written to catch. Fixtures: compile-fail/attr_arg_one_literal uses the wrapped long name; annotation_only_table is the no-instantiation contract; named_table_internal_linkage gains a shared-header row both TUs instantiate, covering the half yesterday's fix broke. Toolchain 68/68, ctest 34/34, all eight examples. wire-sysio 38/38 byte-identical to a toolchain built at the real merge-base 2fe0822.
huangminghuang
left a comment
There was a problem hiding this comment.
I found a blocking row-identity collision on the current head, so I am not approving it. I reproduced it with two distinct row.hpp headers in different directories; when the row declarations have the same qualified name and byte offset, both annotations are refused and the ABI publishes the raw table parameters. Ubuntu CI is also red: 4 toolchain cases fail because the new internal identity is printed in warning text. Details are inline.
| const auto& sm = decl->getASTContext().getSourceManager(); | ||
| const auto loc = sm.getSpellingLoc(decl->getLocation()); | ||
| return decl->getQualifiedNameAsString() + "@" + | ||
| llvm::sys::path::filename(sm.getFilename(loc)).str() + ":" + |
There was a problem hiding this comment.
[P1] Preserve the full file identity here. filename(...) collapses distinct headers with the same basename, and the offset does not disambiguate declarations laid out identically. I reproduced this with a/row.hpp and b/row.hpp, each declaring (anonymous namespace)::row at offset 135 with same-length table names: both become (anonymous namespace)::row@row.hpp:135, both annotations are rejected as applying to two tables, and the ABI emits the raw names one/two instead of alpha/bravo. Please normalize/canonicalize a full path (or use a stable FileEntry identity) so alternate spellings of one file agree without conflating different files.
There was a problem hiding this comment.
Confirmed — a/row.hpp and b/row.hpp both became (anonymous namespace)::row@row.hpp:117, both annotations refused, one/two published instead of alpha/bravo.
The basename was me fixing one half and breaking the other. It was there to absorb a real spelling difference — this toolchain produces ./aux/row.hpp from one TU and aux/row.hpp from another for a single header, which named_table_split's own descriptors show — and I traded that for conflating distinct files.
real_path settles both, as you suggested: alternate spellings of one file agree, different files do not. 375ef80b.
named_table_internal_linkage now carries all three shapes, so neither half can be lost again: anonymous rows declared per TU that must stay apart, one shared-header row both TUs instantiate that must stay together, and your same-basename pair that must stay apart. Reverting to a basename fails the third; reverting to the raw spelling fails the second.
| } | ||
| if (idx.size() > 1) { | ||
| std::cerr << r.loc << ": warning: [[sysio::table(\"" << r.name | ||
| << "\")]] can name only one table, but '" << r.row << "' is the row type of " |
There was a problem hiding this comment.
[P2] Keep the declaration key out of user-facing diagnostics. r.row now contains the internal @basename:offset suffix, so this warning exposes values such as kv_key_scoped::val@kv_key_scoped.cpp:1968. That is also why the current Ubuntu job fails 4 toolchain tests (kv_key_scoped, named_table_duplicate_target, named_table_attr, and named_table_split). Carry a separate display name or strip the identity portion when formatting warnings; updating fixtures would cement unstable byte offsets into diagnostics.
There was a problem hiding this comment.
Right on both counts, and thank you for tying it to the CI failure — that is exactly what it was.
Already fixed in 02e450cf, and the cause is worth recording: the display half of that change existed in my working tree but never left it. I staged with git add -A plugins tests, which does not include tools/, so the commit carried the identity into ____row without the stripping that goes with it. My local suite passed because it built the tree that had both halves.
Diagnostics print the name half now; the identity stays internal, and no fixture asserts an offset.
…identity
Part of the previous commit that I failed to stage: `git add -A plugins tests`
does not include tools/, so the display half of the ____row change never left my
working tree. The identity -- qualified name, then where the declaration was
written -- reached four diagnostics verbatim:
[[sysio::table("aliasname")]] can name only one table, but
'kv_key_scoped::val@kv_key_scoped.cpp:1968' is the row type of 2
Only the name half means anything to the author reading it, and the fixtures
assert on that. Caught by CI, which is the only reason it did not survive: the
local suite passed because the change was sitting uncommitted in the tree that
built it.
Toolchain 68/68.
The basename fixed one half of the identity and broke the other. Two headers sharing a basename -- `a/row.hpp` and `b/row.hpp`, each declaring an anonymous `row` at the same offset -- collapsed into one declaration, so both annotations were refused as naming two tables apiece and the ABI published the raw table parameters instead of the requested names. I chose the basename to absorb a real spelling difference: this toolchain produces `./aux/row.hpp` from one translation unit and `aux/row.hpp` from another for a single header, and those are one declaration. real_path settles both -- alternate spellings of one file agree, different files do not -- which is what the identity needed in the first place. named_table_internal_linkage now carries all three shapes: anonymous rows declared per translation unit that must stay apart, one shared-header row both TUs instantiate that must stay together, and a same-basename pair that must stay apart. Reverting to a basename fails the third; reverting to the spelling fails the second. Toolchain 68/68, ctest 34/34, wire-sysio 38/38 byte-identical to 2fe0822.
huangminghuang
left a comment
There was a problem hiding this comment.
I found one blocking problem in the new regression coverage, so I am not approving this head. The canonical-path implementation looks sound, and the diagnostic cleanup passes the affected cases, but the same-basename fixture currently succeeds through the annotation-only fallback after both intended physical tables are discarded.
| void test() { | ||
| sysio::multi_index<"one"_n, row> t(get_self(), get_self().value); | ||
| sysio::multi_index<"orig"_n, shared_row> s(get_self(), get_self().value); | ||
| sysio::multi_index<"one"_n, same> u(get_self(), get_self().value); |
There was a problem hiding this comment.
[P1] Give the same-basename rows unique physical table names so this test exercises the canonical-path match. This reuses "one"_n, already instantiated over row on line 48 (and other.cpp similarly reuses "two"_n). The test run emits two different tables are both called 'one'/'two' and discards each same table before annotation resolution. Consequently alpha and bravo in the expected ABI are annotation-only phantom entries—both have empty keys and no table_id—rather than renamed multi_index tables, so the fixture passes even if the new row identity never matches. Use distinct raw names in both TUs and expect scope/primary_key plus their table IDs; that also makes the stated “no warnings” condition true.
Split out of #111. The first defects were found by porting a representative
multi_indexcontract and building it, rather than reading the migration guide back; the rest came out of reviewing that fix.The rule
A table row is a struct the contract declares, and its name is plain text. Everything else — a scalar,
std::string,checksum256, a container,variant,binary_extension; a name with an escape, a splice, whitespace or punctuation — is refused with a diagnostic naming the row and the fix.That is the rule upstream Antelope CDT has always enforced, so a contract ported from another Antelope chain behaves here as it did there. An earlier revision of this branch tried to describe those shapes instead and it was not worth it: a scalar row crashed the compiler, containers needed sugar reconstruction that mis-declared nested ones,
variantaborted on a canonical argument pack. Narrowing to the rule removed 687 lines net.docs/abi-tables.mdis the reference: every supported declaration form,_nvs_inaming,[[sysio::kv_key]], and each diagnostic with its one-line fix. Legacymulti_indexandsingletonstay first-class — they are the transition path.What was broken
1 — A bare
[[sysio::table]]emitted a second table. The struct name stood in as a placeholder and was emitted alongside the entry themulti_indexinstantiation produced, so the stock Antelope idiom gave two ABI tables for one andget_table_rowson the struct's name described a table with no rows. No entry is emitted for a bare attribute now: the name,table_idand key layout all come from the instantiation, and a placeholder cannot be told from a real entry by anything that survives into the descriptor.2 — No singleton had ever appeared in an ABI.
sysio::singletonis an alias template overkv_singleton, and an alias template has no specialization of its own; the visitor tested for the namesingletonand never matched.sysio::multi_indexis the same shape overkv_multi_index, which was on the list — so a contract mixing the two saw itsmulti_indextables described and its singletons silently omitted. A Wire regression from the KV port, not a missing feature: upstreameosio::singletonis a real class template.Two more surfaced once singletons reached that branch. A row that is not a class crashed the compiler (
exit code 139); it is refused with a diagnostic now. And a table held as a data member admitted nothing —defined_in_contract()matched an alias and not a member — so a member over an unannotated row emitted no table while the contract built clean.3 —
[[sysio::table("name")]]was applied per translation unit, where neither condition it must respect is knowable. Two tables given one name collapsed to whichever the set reached first, and the survivor'stypethen described one table while itstable_idaddressed the other. The annotation is recorded in the descriptor now and resolved bycdt-codegenafter the merge: applied when the row backs exactly one table and the name is free, refused with a warning otherwise. Resolution is a fixed point, so a rename that frees the name another wants no longer depends on which.descmerged first.4 —
--use-rtwas accepted and then discarded.cdt-cppparses the flag but onlycdt-ldacts on it, and it was missing from the forwarding list — so along doublecontract could not be built in one step, failing onlibrt's undefinedf128_*symbols with a diagnostic telling the user to pass the flag they had just passed. One line. Only the negative case had a test, which is why the hint stayed correct while the flag did nothing.5 — Three examples did not work. Postfix
itr++on a KV iterator; a row struct whose default member initializers firekv_multi_index'sstatic_assertwhile the compiler is still inside the enclosing class; andhash_id_example, which described no tables at all. Nothing buildsexamples/, so nothing caught them; all eight are compiled by hand here.6 — A named table in a shared header refused to link (
Error, ABI structs malformed : cfg already defined). Each TU decided on its partial view and the descriptors disagreed. Descriptors carry the row's qualified name and the annotation now, and both are stripped before the.abiis written.7 — A
table_idof zero read as notable_id. The hash is an unrestricteduint16_tand zero is one of its values, so a real id was dropped from the descriptor and both tables fell out of the collision check that exists to catch exactly that.8 — Table names were never checked. A C++11 attribute's argument is taken from source text rather than the compiler's cooked value, so an escape stays uncooked and an odd spelling reaches the ABI. That reading is pre-existing and unchanged here; what is new is a charset: letters, digits, underscore and dot. It is the one constraint worth enforcing, since these names are read downstream by wire-sysio, SHiP and Hyperion — and it catches the escape case for free, a backslash not being in the set. How to write a name plainly is documented rather than enforced.
ABI changes
kv_key_types,singleton_contractsingleton_contract,examples/singleton_example,examples/hash_id_exampletest_contracts/kv_global_testskv::globaldeclaredkv_key_types,named_table_attrcustomanddeclaredlose a guessedtable_idContracts using a singleton, or holding a table as a data member, gain entries. No entry a contract writes to is lost silently: two tables under one name still collapse, but that is reported with both
table_ids, row types and key layouts.Deliberate breaks. Two. A non-struct row is now an error —
singleton<"cfg"_n, uint64_t>compiled before and described nothing, so anything relying on it already shipped an ABI that did not describe its state. And a table name must be letters, digits, underscore or dot (.being part of the_nalphabet), because these names leave the toolchain in the ABI and are read by wire-sysio, SHiP and Hyperion; all 88 distinct annotation names in wire-sysio, and all 52 here, already satisfy it.Verification
ctest34/34 withENABLE_INTEGRATION_TESTS=ON..wasmand 19.abibyte-identical to a toolchain built at the review base, and to the artifacts committed there. Checkable rather than lucky: those contracts use no singleton, no_i, no[[sysio::kv_key]]and no container row, while their 89 uses of the named annotation do go through the new resolver.table_idin the new fixtures checked against an independent reimplementation ofcompute_table_id,_ihash path included.cdt-codegensorts its descriptor list so--desc-fileorder is ignored; a shared header lets the per-TU annotation set drop a duplicate before the merge sees it; anabigen-failfixture is satisfied by any non-zero exit, so a row that fails regardless masks one that stops failing; and a vector-of-vectors is refused with or without the guard meant to refuse it. Each is written into the fixture that hit it.Not fixed here
examples/still is not built by anything..desccarries no version stamp, so an in-place toolchain upgrade can mix vintages —unit/abimerge_testspins the one instance this PR would otherwise hit. A table declared only as a local variable over an unannotated row is not described, as upstream. A rename needing two names swapped at once is refused. Two C++ declarations sharing an unqualified name still collapse to one ABItype.