chore: remove dead intrinsic surface and unify the ABI version default - #112
Conversation
Three related cleanups to the toolchain's intrinsic and ABI-version surface. 1. Remove the security-group API. CDT declared add_security_group_participants, remove_security_group_participants, in_active_security_group and get_active_security_group, but wire-sysio implements none of them -- `grep -rli security_group` over that tree matches zero files, so they are absent from genesis_intrinsics.cpp, the webassembly interface, the sys-vm registration and the OC intrinsic map. Because sysio_wasm_import turns a declaration straight into a WASM import, a contract calling one compiled and linked, then failed at deploy against an unknown import. Drops the two headers, the four native stubs and their intrinsics_def entries. 2. Prune 60 stale db_* names from cdt.imports.in. The legacy db_*_i64 and db_idx* intrinsics are gone from CDT (db.h is a stub forwarding to kv.h) and from the chain, so listing them in the --allow-undefined-file only delayed a diagnosis from link time to deploy time. The kv_* intrinsics that replaced them do not need an entry: the sysio_wasm_import attribute makes them imports rather than undefined symbols. 3. Unify the ABI version default. Three defaults disagreed -- abi.hpp had 1.1, compiler_options.hpp.in 1.2 and cdt-codegen 1.3 -- so a contract built through cdt-cpp got 1.2 while a standalone cdt-codegen run got 1.3. All four sites now take abi_version::default_major / default_minor from tools/include/sysio/abi.hpp, alongside protobuf_minor for the bump a protobuf_types section requires and a shared version_string() that replaces the "sysio::abi/" literal in three places. Fixes the version parser while there. Both drivers derived the minor by float round-trip, which truncated on any decimal without an exact binary expansion: -abi-version 1.3 emitted sysio::abi/1.2 and 1.4 emitted 1.3. The components are now parsed as integers by one shared abi_version::parse(), and malformed input gets a diagnostic and a non-zero exit instead of an uncaught std::stoi throw. Contract ABI output is unchanged: cdt-cpp already passed 1.2 explicitly, which is what the abigen-pass fixtures pin. abi_version_tests.sh gains coverage for the previously untested paths -- the toolchain baseline with no flag, the two versions the float arithmetic got wrong, and rejection of a malformed version. Also broadens the CLion build-dir ignore to cmake-build-*/ and ignores prequel's local review state. Full suite green: 29/29 ctest, including toolchain_tests and integration_tests.
huangminghuang
left a comment
There was a problem hiding this comment.
I found three correctness gaps that the clean CI run does not exercise. Details are inline; the intrinsic/header removals themselves otherwise match the current host surface.
| intrinsic_macro(remove_security_group_participants) \ | ||
| intrinsic_macro(in_active_security_group) \ | ||
| intrinsic_macro(get_active_security_group) \ | ||
| intrinsic_macro(blake2_f) \ |
There was a problem hiding this comment.
[P2] Refresh the staged headers when removing these entries. libraries/native/CMakeLists.txt:367-369 and libraries/sysiolib/CMakeLists.txt:51 copy headers into build/include only during the stamped CDTWasmLibraries configure step; the inner reconfigure dependencies do not include these headers. Reusing a pre-PR build tree therefore rebuilds libnative.a from this shorter tuple and refreshes cdt.imports, but leaves the old four-entry-longer intrinsic table and both deleted security-group headers staged. Native consumers then compile against a tuple/enum layout that disagrees with the rebuilt library, and install/CPack still ships the removed API. Please make staging/cleanup a build dependency and cover upgrading an existing build tree.
There was a problem hiding this comment.
Confirmed, and fixed here in 705b29f.
You were right about more than the mechanism: the two deleted headers were still staged in my own build tree after the rebuild that produced the "29/29 green" in the PR body. So the green run was against a partially stale staging area — thanks for catching it.
Staging moves out of the three configure-time file(COPY) calls into cmake/stage_headers.cmake, run from a stage_cdt_headers build target that prunes before it copies.
One thing worth flagging from doing it: the destinations overlap. sysiolib owns include/sysiolib, and native's second copy lands in include/sysiolib/native, inside it. Two independent prune+copy steps would race to delete each other's output, so it is one ordered script rather than a step per source dir. file(COPY) preserves source timestamps and skips files already current, so re-running it every build causes no downstream rebuild churn.
Verified the upgrade case specifically, since a green ctest does not demonstrate it — I planted both removed headers back into the existing tree and rebuilt without reconfiguring:
[3/17] Performing build step for 'CDTWasmLibraries'
[1/1] Staging CDT headers into .../cmake-build-debug-vcpkg/include
...
pruned: include/sysiolib/capi/sysio/security_group.h
pruned: include/sysiolib/contracts/sysio/security_group.hpp
CDTWasmLibraries-stamp/CDTWasmLibraries-configure was untouched throughout (dated 11 days earlier), so the configure step never re-ran — which is the case you described.
New tests/unit/staged_headers_tests.sh pins the invariant that every staged header has a source counterpart, so this catches any future stale staging, not just this deletion. It fails on the pre-fix tree and passes after.
|
|
||
| try { | ||
| major_out = std::stoi(major_text); | ||
| minor_out = std::stoi(minor_text); |
There was a problem hiding this comment.
[P2] Finish replacing float-based version handling for every spelling accepted here. This parser accepts 1.10, but sysio_abigen_frontend_action::ParseArgs still uses stof/modf (plugins/sysio/abigen.hpp:1462-1467) and ABIMerger still applies stod to the final three characters (tools/include/sysio/abimerge.hpp:51-67). I reproduced HEAD parsing 1.10 as (1,10) and then merging it into sysio::abi/1.1, with action_results omitted. Please either restrict the grammar to one-digit minors or use structured integer parsing/comparison at those sites too, with a 1.10 regression test.
There was a problem hiding this comment.
Confirmed and fixed in 705b29f — I reproduced your 1.10 trace exactly, and took the structured-parsing option rather than restricting the grammar.
Tracing it end to end, the mangling happened three times: the driver's parse() gave (1,10), the plugin's stof/modf turned the forwarded string back into (1,1) so the .desc said sysio::abi/1.1, and then ABIMerger read ".10" off the end as 0.10 — losing to 1.1 in merge_version and failing the >= 12 gate, which is where action_results disappeared.
The suffix trick also mis-read any major >= 10: "sysio::abi/10.2" -> "0.2". That is why I fixed the parsing rather than fencing the input.
plugins/sysio/abigen.hppusesabi_version::parse.ABIMergerstores the version it was constructed with and compares parsed components, via a newabi_version::parse_version_stringthat ignores the namespace prefix — so an inheritedeosio::abi/1.2descriptor parses like asysioone.- The gate is now
abi_version::action_results_minorrather than a bare12. - The single-argument
ABIMerger(ojson)ctor was dead (only the three-arg form is called,cdt-codegen.cpp:560), so it is removed — a merger can no longer exist without a known version.
Regression tests as requested: -abi-version 1.10 round-trips to sysio::abi/1.10, and a separate assertion that the emitted ABI still contains action_results — that second one is the case that fails on the old code.
No stof/modf/stod remains in the version path.
| } catch (const std::exception&) { | ||
| return false; // out of int range | ||
| } | ||
| return true; |
There was a problem hiding this comment.
[P2] Reject or correctly propagate major zero. parse() accepts 0.1, but cdt-cpp records and forwards a version only when opts.abi_version.first > 0 (tools/cc/cdt-cpp.cpp.in:69,153). Thus cdt-cpp -abi-version 0.1 silently falls back to 1.2 while standalone cdt-codegen --abi-version 0.1 uses 0.1, preserving the default divergence this change is intended to remove. Please reject major zero or track explicit option presence separately, and add a round-trip/rejection test.
There was a problem hiding this comment.
Confirmed and fixed in 705b29f. parse() now rejects a zero major.
Your diagnosis was exactly right: first > 0 was doing double duty as "was the option given?", so -abi-version 0.1 took the fallback in the driver while cdt-codegen --abi-version 0.1 honoured it — the same divergence this PR set out to remove, just through a different door.
There is no ABI 0.x, so rejecting it at the parser is the honest fix, and it retires the sentinel: with a non-zero major guaranteed and a non-zero default, the two > 0 guards in cdt-cpp.cpp.in were dead and are gone. The version is now always recorded and always forwarded.
That last part does change sidecar content — abi_version is now always written, and cdt-ld requires sidecars to agree() on it. Same driver invocation writes them all, so they agree, and multidir_contract_tests (which is what would catch it otherwise) passes.
Rejection tests added for 0.1, plus 1.2.3 and 1x while I was there.
| * cdt-cc, cdt-ld, cdt-codegen and `sysio_abigen` all take their default from here, | ||
| * so a standalone `cdt-codegen` run and an `add_contract()` build cannot stamp | ||
| * different versions into a contract's `.abi`. Bumping the format is a one-line | ||
| * change here plus a refresh of the `tests/toolchain/abigen-pass/*.abi` fixtures, |
There was a problem hiding this comment.
[P3] Avoid /* inside this block comment. The abigen-pass/*.abi text triggers Clang's default -Wcomment in every translation unit that includes this header; spelling it as abigen-pass/<fixture>.abi avoids the new warning.
There was a problem hiding this comment.
Fixed in 705b29f — respelled as tests/toolchain/abigen-pass/<fixture>.abi. Good catch; a warning in every TU that includes the header is not a fair trade for a glob in prose.
Four review findings on #112, all confirmed against the code. Prune the staged include tree (was: deleting a header left it staged forever). Header staging was three configure-time file(COPY) calls, which are additive and never remove a copy whose source is gone. Because the CDTWasmLibraries configure step is stamped, reusing a build tree across a deletion never recovered: the removed header stayed in <build>/include, shipped by install/CPack and visible to native consumers whose compiled view could disagree with the rebuilt library. Staging moves into cmake/stage_headers.cmake, run from a stage_cdt_headers build target, which prunes before it copies. One ordered script matters here: the destinations overlap -- sysiolib owns include/sysiolib and native's second copy lands in include/sysiolib/native, inside it -- so two independent steps would race to delete each other's output. file(COPY) preserves source timestamps and skips files already current, so re-running every build costs no downstream rebuilds. New staged_headers_tests.sh pins the invariant that every staged header still has a source counterpart, catching any future stale staging rather than just this one. Finish replacing float-based version handling. The previous commit fixed the two drivers but left two more parsers, so a two-digit minor was mangled three different ways: the abigen plugin's stof/modf read 1.10 as 1.1, and ABIMerger derived the version from the string's last three characters (".10" -> 0.10), which both picked the wrong version in merge_version and silently dropped action_results by failing its >= 12 gate. That suffix trick also mis-read any major >= 10 ("sysio::abi/10.2" -> "0.2"). The plugin now uses abi_version::parse. ABIMerger stores the version it was constructed with and compares parsed components, via a new abi_version::parse_version_string that ignores the namespace prefix -- so an inherited eosio::abi/1.2 descriptor parses like a sysio one. The gate constant becomes abi_version::action_results_minor. The single-argument ABIMerger constructor was dead and is removed, so a merger cannot exist without a version. Reject a zero major. parse() accepted 0.1, but cdt-cpp reads a zero major as "the option was never given", so cdt-cpp -abi-version 0.1 fell back to the default while cdt-codegen --abi-version 0.1 honoured it -- reopening the very divergence this PR closes. There is no ABI 0.x, so parse() rejects it, which retires the > 0 sentinel in cdt-cpp; the version is now always forwarded. Avoid `/*` inside the abi.hpp block comment, which tripped -Wcomment. abi_version_tests.sh gains the 1.10 round-trip, an assertion that 1.10 keeps action_results, and rejection of 0.1, 1.2.3 and 1x alongside the existing cases. Verified the upgrade path the review asked for: planted both removed headers back into an existing build tree, rebuilt without reconfiguring (CDTWasmLibraries' configure stamp untouched, 11 days old), and the build step pruned them. Full suite 30/30, including toolchain_tests, multidir_contract_tests -- which covers the sidecar agreement affected by always forwarding the version -- and integration_tests.
| DESTINATION "${STAGE_BINARY_DIR}/include" | ||
| ${header_patterns}) | ||
|
|
||
| if(STAGE_NATIVE) |
There was a problem hiding this comment.
[P2] Prune the native tree when native mode is disabled
Because include/sysio/native is removed only inside this truthy branch, reusing a build directory and switching the supported ENABLE_NATIVE_COMPILER option from ON to OFF leaves the old native headers behind. include/sysiolib/native happens to disappear when its parent is pruned, but InstallCDT.cmake installs the entire include tree, so an OFF build/package can still ship include/sysio/native. I reproduced the ON-to-OFF transition at this head, and the new staging test still passes because those files have source counterparts. Move the removal before the conditional and add transition/absence coverage.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5. Both trees are now pruned unconditionally, before either is repopulated — include/sysio/native no longer depends on STAGE_NATIVE being on to be cleaned.
You were also right that the staging test could not catch it: its invariant is "no staged file without a source counterpart", and these files have counterparts — they simply should not be staged in that configuration. Those are orthogonal properties, so it needed a separate assertion rather than a stronger version of the same one. staged_headers_tests.sh now takes ENABLE_NATIVE_COMPILER and asserts the native trees are absent when it is off (and present when on).
| # Stage the CDT-owned headers into ${BASE_BINARY_DIR}/include at BUILD time, pruning | ||
| # any whose source has been deleted. See cmake/stage_headers.cmake for why this cannot | ||
| # be a configure-time file(COPY). | ||
| add_custom_target(stage_cdt_headers ALL |
There was a problem hiding this comment.
[P2] Make the standalone tarball target run staging
package-tgz in cmake/package.cmake is a standalone custom target with no dependency on CDTWasmLibraries or all. Now that pruning occurs only during this nested libraries build, cmake --build . --target package-tgz on a reused tree after a header deletion can invoke CPack without running stage_cdt_headers and ship the prior staged copy. The generated package and install targets are ordered after all, but this documented convenience target is not. Add a dependency on CDTWasmLibraries or explicitly stage before CPack, and cover the packaging-only path.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5 — add_dependencies(package-tgz CDTWasmLibraries), with a comment recording why this target needs it when package and install do not.
This one only became reachable because of my own change: before, staging ran at configure time, so a standalone CPack run over a configured tree saw current headers. Moving it into the build step to fix the pruning is exactly what left this target behind. Good catch.
| // Compare parsed components: deriving them from the string's last three | ||
| // characters mis-read any two-digit minor ("sysio::abi/1.10" -> ".10") and | ||
| // any major >= 10 ("sysio::abi/10.2" -> "0.2"). | ||
| if (version_of(abi) >= std::pair<int, int>{abi_version::default_major, |
There was a problem hiding this comment.
[P2] Gate on the version that the merge actually emits
ret["version"] selects the maximum of the two documents, but this gate examines only the left-hand abi. A probe with ABIMerger(ojson{}, 1, 1) and a sysio::abi/1.10 descriptor emitted version 1.10 while dropping the descriptor action results; 1.1 plus 1.3 behaves the same way. Compute the merged/max version once and use it for both the emitted version and the capability gate, with a mixed-version regression.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5. merge() computes merged_version once as the max of both sides and uses it for the capability gate, so it can no longer disagree with the version ret["version"] actually emits.
Worth noting the reachability, since it changes how much the regression is worth: after the protobuf-promotion fix below, the driver constructs ABIMerger with the same version it hands the plugin, and the accumulator starts empty and is seeded from it — so the two sides agree on every path cdt-cpp and cdt-ld drive, including multi-TU finalize. The divergence needs a merger constructed at one version and fed a descriptor stamped at another, which is your direct ABIMerger(ojson{}, 1, 1) probe rather than anything the CLI produces. I fixed it as a correctness invariant rather than a live bug, and did not add a shell regression for it because the driver cannot express the case; if you want it covered, it needs a host-side unit target for tools/include, which this repo does not currently have. Happy to add one if you think it is worth the scaffolding.
| int abi_version_minor = (int)(std::modf(std::stof(str), &tmp) * 10); | ||
| int abi_version_major = abi_version::default_major; | ||
| int abi_version_minor = abi_version::default_minor; | ||
| if (!abi_version::parse(str, abi_version_major, abi_version_minor)) { |
There was a problem hiding this comment.
[P2] Align accepted major versions with action-result serialization
This parser accepts every nonzero major, for example 2.0 or 10.2, and the changed merger treats those versions as newer than 1.2, but to_json() still serializes action_results only when version_major == 1. A non-void action built with -abi-version 10.2 therefore loses its result metadata. Either reject unsupported major versions here or use one shared supports_action_results(major, minor) predicate in the producer and merger, and assert the actual result entry in a higher-major test.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5, taking the shared-predicate option and also bounding the major.
abi_version::supports_action_results(major, minor) is now the single rule, used by to_json, ABIMerger and cdt-abidiff. Alongside it, parse() rejects anything above max_supported_major (1): to_json only knows the 1.x shape, so accepting 2.0 or 10.2 meant stamping a version whose implied sections the toolchain would then omit. Rejecting is more honest than emitting a version we cannot honour, and it makes the predicate total rather than something each call site has to remember to pair with a major check.
-abi-version 2.0 and 10.2 are now in the rejection tests.
| abi["version"] = "sysio::abi/1.3"; | ||
| // The protobuf_types section is only understood from abi_version::protobuf_minor | ||
| // onwards, so a contract that emits one is bumped up to it. | ||
| if (abi_version_major == abi_version::default_major && |
There was a problem hiding this comment.
[P2] Promote the effective protobuf version before descriptor generation
The abigen plugin has already run by the time this promotion happens. With --abi-version 1.1 plus protobuf files, the plugin suppresses action_results under its 1.2 capability gate; this code then stamps the incomplete output as 1.3 and cannot recover the omitted entries. Compute the effective protobuf version before gen_actions passes the version to the plugin, and add a 1.1 plus protobuf regression that checks a non-void action result.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5, and the fix was one step further out than your comment describes — worth recording.
Computing the effective version before gen_actions is right, but that is not sufficient: gen_actions is only called when !finalize_mode, and cdt-ld runs the merge/stamp as a separate cdt-codegen --finalize invocation that forwards both --protobuf-files and --abi-version. Putting the promotion at the gen_actions call site left the finalize pass unpromoted, which the assertion I had added caught immediately on pb_tests. It now sits in main() right after argument parsing, so both passes settle on the same version.
Verified end to end rather than by reasoning:
$ cdt-cpp -abigen -abi-version 1.1 -contract pb_tests \
-protobuf-dir ... -protobuf-files test.proto ... pb_tests.cpp
version: sysio::abi/1.3
action_results: [{"name": "hiproto", "result_type": "protobuf::test.ActResult"}]
Before the fix the plugin suppressed the section under its 1.1 gate, so the key was absent from an ABI stamped 1.3. Both assertions are now in abi_version_tests.sh.
| * @param minor_out set to the minor component on success; untouched on failure | ||
| * @return true when @p text carries a well-formed version, false otherwise | ||
| */ | ||
| inline bool parse_version_string(const std::string& text, int& major_out, int& minor_out) { |
There was a problem hiding this comment.
[P2] Route cdt-abidiff through the structured parser too
cdt-abidiff still parses abi["version"] with stod(ver.substr(ver.size() - 3)) * 10. For the newly supported and emitted sysio::abi/1.10, that returns 1, so its >= 11 and >= 12 guards skip variant and action-result diffs. Use this helper and compare (major, minor) components there as well, with a 1.10 regression.
There was a problem hiding this comment.
Confirmed and fixed in 2377cb5. cdt-abidiff now uses abi_version::parse_version_string and the shared supports_variants / supports_action_results predicates instead of stod(ver.substr(ver.size() - 3)) * 10.
I had flagged this one myself while replying on #111 — the same suffix read was making cdt-abidiff report no difference between ABIs whose table metadata had changed — but scoped it out of this PR at the time. With 1.10 now a version the toolchain actually emits, it stopped being theoretical, so it is in.
New abidiff_tests.sh covers it: two ABIs differing by one action_results entry must report the difference at 1.2, 1.3 and 1.10 — the last fails on the old parser, which scored it 1 and skipped both gates — plus a version-only difference and a no-difference case.
huangminghuang
left a comment
There was a problem hiding this comment.
The four findings from my first pass are fixed, but this revision still has several version-capability and incremental-staging gaps; details are inline.
Please also refresh the PR description as required by this repository review-follow-up rules. It still describes the pre-follow-up state (19 parser checks and 29/29 overall) and omits the staged-header cleanup, the 1.10 and 0.1 behavior, the new staging test, and the current 30/30 validation.
…l prune Six further findings, all confirmed. Version capabilities were decided in three places that disagreed. abigen's to_json emitted action_results only for major == 1, the merger ranked any higher major above 1.2, and cdt-abidiff scored the version with the same stod-over-the-last-three-characters trick this PR removed elsewhere -- reading "sysio::abi/1.10" as 1 and so skipping both its variant and action-result diffs. There is now one predicate, abi_version::supports_action_results (and supports_variants), used by the producer, the merger and abidiff alike. parse() also bounds the major at abi_version::max_supported_major. to_json only knows the 1.x shape, so accepting 2.0 or 10.2 stamped a version whose implied sections the toolchain then omitted. The merger gated on the wrong document. ret["version"] is the newer of the two, but the gate consulted only the left-hand side, so merging a 1.10 descriptor into a 1.1 accumulator emitted 1.10 while dropping the action_results the newer side carried. The merged version is computed once and used for both. Protobuf promotion happened too late. The abigen plugin had already run by the time the version was raised to 1.3, so --abi-version 1.1 plus protobuf made the plugin suppress action_results under its own 1.2 gate and codegen then stamped the incomplete result as 1.3. The promotion moves into main(), before gen_actions hands the version to the plugin -- and into main() specifically because the finalize pass never calls gen_actions, which an assertion caught. Staging pruned the native tree only when native mode was on, so a reused tree whose ENABLE_NATIVE_COMPILER went ON -> OFF kept the previous build's native headers, and InstallCDT.cmake installs the whole include tree. Both trees are now pruned unconditionally, before either is repopulated. package-tgz had no dependency on the nested libraries build that now does the pruning, so packaging a reused tree could ship a header a prior build deleted. It depends on CDTWasmLibraries. Tests: new abidiff_tests.sh pins the 1.10 gates and a version-only difference; abi_version_tests gains 2.0/10.2 rejection and a 1.1-plus-protobuf case asserting both the 1.3 stamp and the surviving non-void action result; staged_headers_tests takes ENABLE_NATIVE_COMPILER and asserts the native tree is absent when off. Full suite 31/31.
The protobuf promotion regression added in the previous commit located
magic_enum by searching the vcpkg tree:
find "${BUILD_DIR}/vcpkg_installed" -maxdepth 3 -type d -name magic_enum | head -1
That matches both `<triplet>/include/magic_enum` and `<triplet>/share/magic_enum`,
and find(1) does not order its results. Locally the include copy came first;
on the ubuntu24 runner the share copy did, so the hand-rolled compile was given
an include path with no headers under it and failed with
test.pb.hpp:6:10: fatal error: 'magic_enum/magic_enum.hpp' file not found
The include directory now comes from CMake, which already resolves it, passed
as a third argument from tests/CMakeLists.txt. The skip guard checks for the
header itself rather than a directory name, so a build tree without it skips the
case instead of failing on a path that merely looks right.
Full suite 31/31; the skip path verified by passing a bogus and an empty dir.
Matches the repo's convention (tools/packaging/tests/verify-tgz.sh uses grep -E for alternation, plain grep -q for literals) and drops the reliance on `\|`, which is a GNU/BSD extension rather than POSIX BRE. Not a fix -- the macOS job ran the previous form successfully -- just not worth depending on.
| int major_v = abi_version::default_major; | ||
| int minor_v = abi_version::default_minor; | ||
| if (abi.has_key("version")) | ||
| abi_version::parse_version_string(abi["version"].as<std::string>(), major_v, minor_v); |
There was a problem hiding this comment.
[P2] Do not turn failed document-version parses into ABI 1.2
parse_version_string() leaves its outputs untouched on failure, but this return value is ignored after initializing them to the 1.2 defaults. Because the shared parser rejects majors above 1, cdt-abidiff now treats sysio::abi/2.0 (and malformed or missing versions) as 1.2; I confirmed that comparing otherwise-identical 2.0 and 1.2 documents produces no version difference. Please diagnose a failed parse instead of laundering it to the emission default, and apply the same fix to ABIMerger::version_of().
There was a problem hiding this comment.
Confirmed and fixed in 7a10f46. This was fallout from adding max_supported_major last round: parse_version_string leaves its outputs untouched on failure, and both call sites seeded them with the 1.2 defaults and then discarded the return value, so a version the parser now rejects read as the emission default.
cdt-abidiff diagnoses and exits non-zero — refusing to diff is right when every gate below keys off the value.
ABIMerger::version_of needed the two cases separated rather than the same treatment: a document with no version legitimately inherits the merger's own (that is how an empty accumulator is seeded), while a version that is present but unparsable is a malformed descriptor and now throws, consistent with add_object_to_array.
New abidiff_tests.sh case, and I checked it has teeth rather than assuming: reverting this fix plus the matcher below takes the suite from 7/7 to 5 passed, 2 failed.
| # Staged-header hygiene — every header under <build>/include must still exist in | ||
| # libraries/. Catches a stale copy left behind when a source header is deleted. | ||
| configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unit/staged_headers_tests.sh ${CMAKE_BINARY_DIR}/tests/unit/staged_headers_tests.sh COPYONLY) | ||
| add_test(NAME staged_headers_tests COMMAND ${CMAKE_BINARY_DIR}/tests/unit/staged_headers_tests.sh "${CMAKE_BINARY_DIR}" "${CMAKE_SOURCE_DIR}" "${ENABLE_NATIVE_COMPILER}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) |
There was a problem hiding this comment.
[P2] Normalize the CMake boolean passed to the staging test
The stage target canonicalizes this option with $<BOOL:...>, but this test passes the raw cache spelling and the shell recognizes only ON, on, and 1. With the valid setting -DENABLE_NATIVE_COMPILER=TRUE, staging correctly creates both native trees while this test takes its OFF branch; I reproduced the resulting 2-pass/1-fail run. Please pass $<BOOL:${ENABLE_NATIVE_COMPILER}> here (or implement the full CMake boolean vocabulary in the script).
There was a problem hiding this comment.
Confirmed and fixed in 7a10f46 — the test now takes $<BOOL:${ENABLE_NATIVE_COMPILER}>, the same canonicalization the stage target uses, and the script compares against 1.
Verified the expression is actually evaluated rather than passed through: the generated CTestTestfile.cmake now shows "1" where it previously showed the raw ON, so TRUE/YES/Y all reduce the same way by $<BOOL:> semantics.
Teaching the shell CMake's boolean vocabulary was the other option; matching the producer's canonicalization seemed better than maintaining a second copy of that vocabulary.
| // Compare parsed components: deriving them from the string's last three | ||
| // characters mis-read any two-digit minor ("sysio::abi/1.10" -> ".10"). | ||
| if (abi_version::supports_action_results(merged_version.first, merged_version.second)) { | ||
| ret["action_results"] = merge_action_results(other); |
There was a problem hiding this comment.
[P2] Treat missing versioned sections as empty during mixed-version merges
The new merged-version gate now calls merge_action_results() for a 1.1 accumulator plus a 1.10 descriptor, but a valid 1.1 document omits action_results and add_object_to_array() indexes both documents unconditionally. I reproduced ABIMerger(old_1_1_without_results, 1, 1).merge(new_1_10) throwing Key 'action_results' not found, so the exact newer-right-hand-side case this change intends to fix fails instead of retaining the result. Please normalize a missing capability section to an empty array in both merge directions and add a sequential 1.1→1.10 regression.
There was a problem hiding this comment.
Confirmed and fixed in 7a10f46 — and you are right that this failed the exact case the round-2 gate change was meant to enable.
Reproduced end to end before fixing, through cdt-codegen --finalize with a hand-written 1.1 descriptor (no action_results) and a 1.10 one:
terminate called after throwing an instance of 'jsoncons::key_not_found'
what(): Key 'action_results' not found
no ABI emitted. With the fix: version sysio::abi/1.10, and the newer side's result entry retained.
Fixed in add_object_to_array via a section() helper rather than at the call site, so it covers every versioned section in both merge directions rather than just action_results. The 1.1 → 1.10 regression is now in abi_version_tests.sh, driven through --finalize — the real ABIMerger entry point, since no cdt-cpp invocation can produce mixed-version descriptors.
| if ( get_version(abi_1) >= 12 && get_version(abi_2) >= 12 ) | ||
| if ( abi_version::supports_action_results(v1_major, v1_minor) && | ||
| abi_version::supports_action_results(v2_major, v2_minor) ) | ||
| diff_action_results(); |
There was a problem hiding this comment.
[P2] Fix result matching before routing 1.10 through it
This new gate sends 1.10 documents into find_action_results(), but that matcher compares the matched entry against abi2["action_results"].at(i) rather than .at(j). Compiling the current source and comparing two semantically identical 1.10 ABIs whose two result entries are merely reversed reports all four </> entries as changes; unequal lengths can also throw. Please fix the matching index and add a reordered-input regression so enabling the 1.10 path does not create false diffs.
There was a problem hiding this comment.
Confirmed and fixed in 7a10f46 — .at(i) → .at(j), with a comment recording why.
One clarification on scope: this is pre-existing, and it is not limited to the newly enabled path. The matcher has always compared against the wrong index, so reordered results have been mis-reported at 1.2 and 1.3 as well; what this PR changed is only that 1.10 now reaches it too. I fixed it here rather than splitting it out, since routing more traffic through a broken matcher without fixing it would be worse.
Regression added, and confirmed to fail first: two 1.10 ABIs whose two identical results are merely reversed report all four entries as changed on the pre-fix binary, and report nothing after. My existing cases used differing single entries and could never have caught it.
| WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" | ||
| COMMENT "Packaging ${CPACK_PACKAGE_FILE_NAME}.tar.gz (portable toolchain)" | ||
| VERBATIM) | ||
| add_dependencies(package-tgz CDTWasmLibraries) |
There was a problem hiding this comment.
[P2] Remove stale native archives before packaging an OFF rebuild
This dependency runs the nested build, but reconfiguring a reused tree from native ON to OFF only removes the native targets; it does not delete the archives they previously copied into ${CMAKE_BINARY_DIR}/lib. InstallCDT.cmake still picks up stale libnative*.a for dev/TGZ and stale libsf.a for base/TGZ. In an existing tree, libnative.a still contains all four security-group symbols removed by this PR, so the documented upgrade/package path can ship the old implementation even after this dependency runs. Please prune those outputs when native mode is off (or make installation configuration-aware) and cover the ON→OFF package payload.
There was a problem hiding this comment.
Confirmed and fixed in 7a10f46, and I agree this one mattered most — it meant the documented upgrade path could ship the very API this PR deletes.
Two layers:
stage_cdt_tree(renamed fromstage_headers, since it no longer stages only headers) pruneslib/libnative*andlib/libsf.awhen native mode is off. That target already runs at build time and already prunes the header trees for exactly this reason, so it is the same mechanism rather than a new one.- The
libnative*install inInstallCDT.cmakeis now guarded onENABLE_NATIVE_COMPILER, so a stale file cannot be packaged even if something reintroduces one.
Verified the prune against a scratch tree seeded the way an earlier ON build leaves one:
before: libc.a libnative.a libnative_sysio.a libsf.a
after: libc.a
native header trees: 0
libsf.a was the easy one to miss — it goes to the base component, not dev, so it would have shipped in the plain package too.
staged_headers_tests.sh now asserts those archives are absent when native is off, alongside the header trees.
| check "1.1 + protobuf is promoted to 1.3" \ | ||
| "${WORK}/pb11.abi" '"version": "sysio::abi/1.3"' | ||
| check "1.1 + protobuf keeps the non-void action's result" \ | ||
| "${WORK}/pb11.abi" '"name": "hiproto"' |
There was a problem hiding this comment.
[P2] Assert the action-result entry, not the action name
"name": "hiproto" is present in the top-level actions array even when the old late-promotion path suppresses action_results, so this assertion passes on the exact regression it is meant to catch. Please scope the check to .action_results[] and verify both name == "hiproto" and result_type == "protobuf::test.ActResult" (or at least match that unique result_type).
There was a problem hiding this comment.
You are right, and this is the worst of the six: the assertion passed on the exact regression it was written to catch, so my "verified end to end" claim last round was worth less than I said.
Confirmed against the built ABI — hiproto appears in both arrays:
actions: [{"name": "hiproto", "type": "protobuf::test.ActData", ...}]
action_results: [{"name": "hiproto", "result_type": "protobuf::test.ActResult"}]
Fixed in 7a10f46 to assert "result_type": "protobuf::test.ActResult", which occurs only under action_results.
I applied the same check to the new mixed-version merge test while writing it — its first draft asserted "name": "actb", which is in the actions array too, so it would have had the identical hole. That one now asserts result_type as well.
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed current head 55e77c0. The six issues from the prior round appear substantively fixed, and all required Ubuntu/macOS/package checks are green.
I am still requesting changes for the six line-level issues posted here: mixed-version ABI merging throws when the older side lacks action_results; failed document-version parses are silently coerced to 1.2; the protobuf promotion regression assertion is a false positive; ON→OFF builds can still package stale native archives (including the removed symbols); valid CMake true spellings make the staging test fail; and the newly enabled 1.10 action-result diff path produces false differences for reordered equivalent entries.
Local validation included git diff --check, shell syntax checks, a direct STAGE_NATIVE=TRUE reproduction, and compiled probes against the current ABIMerger and cdt-abidiff sources.
…FF packaging
Six findings. Four are regressions from the previous round, one of which was a
test that never tested anything; two are pre-existing defects adjacent to code
this PR touches.
A failed version parse is no longer laundered into 1.2. parse_version_string
leaves its outputs untouched on failure, and both call sites seeded them with the
1.2 defaults and ignored the result -- so once the previous round made parse()
reject majors above 1, cdt-abidiff read sysio::abi/2.0 as 1.2 and reported no
version difference against a 1.2 document. cdt-abidiff now diagnoses and exits;
ABIMerger::version_of distinguishes a document with no version (which legitimately
inherits the merger's, as an empty accumulator is seeded that way) from one whose
version is present but unparsable, which throws.
A missing versioned section reads as empty. Gating on the merged version was
right, but add_object_to_array then indexed both documents unconditionally, so a
valid 1.1 descriptor -- which omits action_results -- merged with a 1.10 one threw
`Key 'action_results' not found`, failing the exact mixed-version case the gate
was changed to support. Fixed in add_object_to_array so every versioned section is
covered in both directions.
find_action_results compared the matched entry against abi2[...].at(i) inside a
loop over j. Reordered but equivalent results reported as changed, and a shorter
right-hand side threw. Pre-existing -- it affects 1.2 and 1.3 -- but this PR
routes 1.10 into that path.
An ON->OFF rebuild no longer packages stale native archives. libnative*, and
libsf.a are copied into lib/ by POST_BUILD commands that exist only while native
mode is on; reconfiguring a reused tree to OFF drops the targets but not the
files, and InstallCDT installs lib/ wholesale. In an existing tree those archives
still hold the security-group symbols this PR removes, so the documented upgrade
path could ship the very API being deleted. The staging script prunes them when
native is off -- it already runs at build time and already prunes the header trees
for the same reason -- and the libnative* install is now guarded on the option.
stage_headers.cmake becomes stage_cdt_tree.cmake since it no longer stages only
headers.
The staging test takes $<BOOL:${ENABLE_NATIVE_COMPILER}>, matching the stage
target's own canonicalization. Comparing the raw cache spelling meant a valid
-DENABLE_NATIVE_COMPILER=TRUE staged both native trees while the test took its
OFF branch.
The protobuf regression assertion checked `"name": "hiproto"`, which is in the
top-level actions array regardless -- so it passed on the regression it was
written to catch. It now asserts result_type, which appears only under
action_results.
Each new assertion was confirmed to fail before the fix rather than only to pass
after it: reverting the matcher and the version guard fails 2 of 7 abidiff cases,
and reverting the section helper reproduces `Key 'action_results' not found` with
no ABI emitted. The mixed-version merge is now driven end to end through
`cdt-codegen --finalize`, the real ABIMerger entry point. 31/31 ctest.
huangminghuang
left a comment
There was a problem hiding this comment.
All six findings from the previous pass are addressed, and the current head is green on Ubuntu, macOS, and package verification. This pass found two reproducible ABI correctness regressions plus one unexercised state-transition fix, detailed inline.
Please also refresh the PR description before the next review. It still says the follow-up covers two review rounds and reports 28 abi_version_tests.sh assertions; the current branch includes a third review round and 30 assertions, plus the strict abidiff parse rejection, mixed-version merge handling, reordered action-result fix, ON-to-OFF archive pruning, canonical CMake boolean, and corrected protobuf assertion.
| /// a 1.1 accumulator merged with a 1.10 descriptor reaches this code and indexing the | ||
| /// older side unconditionally threw `Key 'action_results' not found` -- failing exactly | ||
| /// the mixed-version merge the gate was changed to support. | ||
| static const ojson& section(const ojson& doc, const std::string& type) { |
There was a problem hiding this comment.
[P2] Keep required descriptor sections mandatory
section() is used by the generic add_object_to_array(), so this now treats types, structs, actions, tables, and ricardian_clauses as optional too. Those baseline arrays are always emitted by abigen; I reproduced a 1.2 descriptor missing actions being accepted and emitting "actions": [], whereas the old indexing failed with Key 'actions' not found. A truncated descriptor can therefore silently lose contract interface content. Limit the empty fallback to explicitly optional/version-gated sections (such as action_results) and add a missing-required-section regression. Please exercise both merge orders as well: cdt-codegen sorts the descriptor paths, so the current new.desc/old.desc fixture actually processes the newer file first.
There was a problem hiding this comment.
Confirmed and fixed in dd9ccdf. You are right that section() over-reached: it backs the generic add_object_to_array, which handles eight sections, so making the fallback unconditional turned the baseline arrays optional along with the version-gated ones.
section() now takes a section_kind, defaulting to required; only action_results, variants and enums — the sections that entered the format at a version — pass version_gated. A descriptor missing a required section throws again, with a clearer message than the raw jsoncons one.
Good catch on the merge order too: cdt-codegen.cpp:553 does std::sort(desc_files...), so filenames rather than --desc-file order decide the accumulator, and my new.desc/old.desc fixture always processed the newer first. The test now copies the descriptors to explicit a_first/b_second names and runs both directions.
Both new assertions confirmed to fail first: reverting the change accepts the truncated descriptor and emits "actions": [].
| const auto [v1_major, v1_minor] = get_version(abi_1, "file1"); | ||
| const auto [v2_major, v2_minor] = get_version(abi_2, "file2"); | ||
| if ( abi_version::supports_variants(v1_major, v1_minor) && | ||
| abi_version::supports_variants(v2_major, v2_minor) ) |
There was a problem hiding this comment.
[P2] Fix variant matching before enabling this path
This gate now routes 1.10 documents through find_variants(), but that matcher unconditionally sets found = true after breaking on a type mismatch and does not compare the list lengths. At this head, two 1.10 ABIs differing only by types: ["uint64"] versus ["string"] produce no output and exit 0; ["uint64", "string"] versus ["uint64"] throws Invalid array subscript. Compare the complete arrays (including size) before marking a same-name variant as found, and add non-empty 1.10 variant regressions.
There was a problem hiding this comment.
Confirmed and fixed in dd9ccdf — and this one is on me beyond the finding itself. I fixed find_action_results last round and did not check its sibling, which had the same defect in a slightly different shape.
find_variants broke out of the element loop on a type mismatch and then set found = true unconditionally, so a same-named variant counted as unchanged however its types differed; and with no length comparison, at(k) threw on a shorter right-hand side. It now compares the whole list, length first.
Three regressions added, all confirmed to fail before the fix:
FAIL: 1.10 variants differing by type report a difference (reported nothing)
FAIL: 1.10 variants of differing length are compared without throwing
PASS: identical 1.10 variants report no difference
Pre-existing, as with the action-result matcher — it affects 1.1 upward — but this PR is what routes 1.10 into the path, so it belongs here.
| # Staged-header hygiene — every header under <build>/include must still exist in | ||
| # libraries/. Catches a stale copy left behind when a source header is deleted. | ||
| configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unit/staged_headers_tests.sh ${CMAKE_BINARY_DIR}/tests/unit/staged_headers_tests.sh COPYONLY) | ||
| add_test(NAME staged_headers_tests COMMAND ${CMAKE_BINARY_DIR}/tests/unit/staged_headers_tests.sh "${CMAKE_BINARY_DIR}" "${CMAKE_SOURCE_DIR}" "$<BOOL:${ENABLE_NATIVE_COMPILER}>" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) |
There was a problem hiding this comment.
[P2] Exercise the reused-tree OFF cleanup in CI
This registers the staging test only for the current configure mode. ENABLE_NATIVE_COMPILER defaults to ON and neither CI workflow overrides it, so the new OFF assertions for native headers and archives never execute. Even adding a clean OFF build would not prove the ON-to-OFF prune, because that tree contains no stale native outputs. Seed an isolated staging tree with native header/archive sentinels, invoke stage_cdt_tree.cmake with STAGE_NATIVE=0, and assert they are removed (plus ON population if useful); otherwise deleting the new OFF cleanup still leaves every current check green.
There was a problem hiding this comment.
Confirmed and fixed in dd9ccdf, and your point about a clean OFF build is the part I had missed: it would not prove the prune either, because it has no stale outputs to remove.
The test now does exactly what you describe, and what I had only done by hand last round — seed an isolated tree with native archive and header sentinels the way a previous ON build leaves one, invoke stage_cdt_tree.cmake directly with STAGE_NATIVE=0, and assert they are gone. It also asserts an unrelated libc.a survives, so the prune is targeted rather than a wipe.
Being mode-independent, it runs in every configuration rather than only when the build happens to be configured OFF:
-- ON -> OFF prune (isolated tree) --
PASS: STAGE_NATIVE=0 prunes stale native archives and header trees
PASS: the prune leaves unrelated archives alone
The mode-specific assertions stay as well, so an OFF build still checks its own staged tree.
…rune coverage Three findings. Two are fallout from the previous round, one is the sibling of a matcher fixed there that I did not check at the time. Required descriptor sections are mandatory again. The section() helper added last round backs the generic add_object_to_array, which handles eight sections, so it made structs, types, actions, tables and ricardian_clauses optional along with the version-gated ones. A truncated descriptor missing actions merged to "actions": [] where it had previously thrown, silently dropping contract interface content. section() now takes a section_kind, and only action_results, variants and enums -- the sections that entered the format at a version -- may be absent. The mixed-version merge test only exercised one order. cdt-codegen sorts the descriptor paths, so filenames rather than argument order decide which document becomes the accumulator, and new.desc/old.desc always processed the newer first. The fixture now names them explicitly and runs both directions. find_variants had the same defect as find_action_results: it broke out of the element loop on a type mismatch and then set found unconditionally, so a same-named variant counted as unchanged however its types differed, and with no length comparison at(k) threw on a shorter right-hand side. Pre-existing, but this PR routes 1.10 documents into that matcher -- and I should have checked the sibling when fixing the first one. It now compares the whole list, length first. The ON->OFF prune had no coverage that runs. The staging test only described the mode the build was configured in, ENABLE_NATIVE_COMPILER defaults ON and neither workflow overrides it, so the OFF assertions never executed; and a clean OFF build would not prove the prune either, having no stale outputs to remove. The test now seeds an isolated tree the way a previous ON build leaves one, invokes stage_cdt_tree.cmake with STAGE_NATIVE=0 directly, and asserts the archives and header trees are gone while an unrelated archive survives. That is mode-independent and always runs. Each new assertion was confirmed to fail before its fix: reverting the variant matcher fails both variant cases (no difference reported, then a throw on the shorter side), and reverting the section change accepts the truncated descriptor. 31/31 ctest.
huangminghuang
left a comment
There was a problem hiding this comment.
The three findings from the previous pass are fixed: required baseline sections are rejected again, the 1.10 variant matcher handles type and length differences, and the mode-independent ON-to-OFF staging probe exercises the real cleanup. The current Ubuntu, macOS, and package checks are green.
Four remaining blockers are detailed inline. Please also clean up the documentation before the next review: the PR introduction still says two review rounds, Review round 1 names the superseded stage_headers.cmake / stage_cdt_headers, and the footer refers to a nonexistent emplace guard. Separately, cmake/CDTMacros.cmake.in still uses the removed db_store_i64 surface as its example of a native-module intrinsic; use a current kv_* intrinsic or a generic description.
| /// but unparsable is malformed, and is rejected rather than quietly treated as the | ||
| /// default: every capability gate below keys off this value. | ||
| std::pair<int, int> version_of(const ojson& doc) const { | ||
| if (!doc.has_key("version")) |
There was a problem hiding this comment.
[P2] Reject versionless descriptors instead of defaulting them
The constructor already seeds an actually empty accumulator with version at lines 22–30, so a nonempty document reaching version_of() without this key is an external malformed descriptor, not the accumulator case described above. At this head, merging a descriptor containing every array but no version succeeds and emits sysio::abi/1.2; the base implementation rejected it when merge_version() indexed the missing key. Throw for a missing document version and add a merger regression, rather than silently stamping the command default.
There was a problem hiding this comment.
Confirmed and fixed in bdbb798. You are right that the accumulator justification does not hold: the constructor seeds an empty accumulator with a version (lines 22-30) and merge() always stamps ret["version"], so every document reaching version_of() legitimately carries one. The fallback was unreachable for valid input and fired only on a malformed external descriptor — where it stamped the emission default onto input the base implementation rejected.
It now throws Error, ABI is missing its version, alongside the existing rejection for a version that is present but unparsable.
Worth naming the shape of my mistake, since it repeats in the next thread: I was fixing a too-strict failure (the Key not found throw) and over-corrected into a too-permissive one, in both cases by defaulting instead of rejecting.
| static const ojson empty = ojson::array(); | ||
| if (doc.has_key(type)) | ||
| return doc[type]; | ||
| if (kind == section_kind::version_gated) |
There was a problem hiding this comment.
[P2] Require gated sections once the document version supports them
version_gated currently means “optional at every version.” A malformed 1.10 descriptor missing action_results or variants is therefore accepted and normalized to an empty array, even though supports_action_results(1, 10) / supports_variants(1, 10) say that version carries the section and abigen emits both keys. I reproduced both cases at this head. Permit absence only when that specific document predates the section (before 1.2 for action results, before 1.1 for variants); keep independently optional enums separate, and add supported-version missing-section rejection tests.
There was a problem hiding this comment.
Confirmed and fixed in bdbb798. version_gated meaning "optional at every version" was too weak, exactly as you say.
I checked which sections abigen actually emits before choosing the thresholds:
| Section | abigen | Rule now |
|---|---|---|
variants |
unconditional (abigen.hpp:1070) |
required at >= 1.1 |
action_results |
only when the version supports it (:1076) |
required at >= 1.2 |
enums |
only when non-empty (:1108) |
optional everywhere — no threshold |
So sections now carry the version they entered at, and absence is permitted only when the document's own version predates it. enums is kept separate as independently optional, per your note. A 1.10 descriptor missing action_results or variants is rejected with the version named in the message.
Because the two sides of a merge can declare different versions, the check is made per document rather than once for the pair — section() takes that document's parsed version.
| mkdir -p "$dir" | ||
| cp "${WORK}/${first}.desc" "${dir}/a_first.desc" | ||
| cp "${WORK}/${second}.desc" "${dir}/b_second.desc" | ||
| if "$CDT_CODEGEN" --finalize --contract mix --output-dir "$dir" \ |
There was a problem hiding this comment.
[P2] Seed the older-first merge at ABI 1.1
These two filename orders still do not exercise a missing section on the accumulator side because neither invocation passes --abi-version 1.1. The empty merger is seeded at the 1.2 default, so after processing old.desc first its intermediate ABI is already 1.2 with an empty action_results key. In both named cases the only missing key encountered is therefore the old descriptor on the right; a one-sided RHS fallback passes both while the original old-LHS failure remains. Run these finalize cases with a 1.1 seed (or add a direct host test) so older-first genuinely creates a 1.1 accumulator without the section.
There was a problem hiding this comment.
Confirmed and fixed in bdbb798 — and this is the one I would have missed, because the test looked like it covered both orders.
You are exactly right about the mechanism: without --abi-version the merger is seeded at the 1.2 default and the constructor gives it an empty action_results, so the only absent key was ever the descriptor on the right. Both named cases would have passed a right-side-only fallback.
Both cases now seed at 1.1, which makes the first merge emit an intermediate carrying no action_results at all, so the second merge meets the missing section on the left.
Verified rather than assumed — with the left-side fallback removed:
FAIL: older-first: descriptors merge
PASS: newer-first: emits the newer version
PASS: newer-first: retains the newer side's action_result
That is the asymmetry the old fixture could not see.
|
|
||
| mkvariant "${WORK}/vsame1.abi" '["uint64", "string"]' | ||
| mkvariant "${WORK}/vsame2.abi" '["uint64", "string"]' | ||
| out="$("$ABIDIFF" "${WORK}/vsame1.abi" "${WORK}/vsame2.abi" 2>&1 || true)" |
There was a problem hiding this comment.
[P2] Do not mask failures in valid-input abidiff cases
cdt-abidiff returns 0 after reporting ordinary differences; the comment at lines 52–54 claiming otherwise is incorrect. The repeated || true discards real process failures, so this identical-variant case—and the quiet/equivalent cases at lines 90 and 110—can count a crash or generic runtime error as PASS whenever its text lacks the searched keyword. Assert exit status 0 for every valid-input invocation before checking output, and reserve expected nonzero handling for the unsupported-version case.
There was a problem hiding this comment.
Confirmed and fixed in bdbb798, and the comment you flagged was simply wrong — I checked:
$ cdt-abidiff a.abi b.abi # ABIs differing in action_results
exit code with differences = 0
So || true was not redundant-but-harmless, it was hiding real failures: any crash or rejection whose output lacked the searched keyword counted as a pass.
Valid-input invocations now go through a helper that fails the case outright on a non-zero exit before any output matching, and only the unsupported-version case expects non-zero. That also simplified the differing-length variant case, which had been sniffing for Invalid array subscript in the text — a throw now surfaces as a non-zero exit and is reported as such.
huangminghuang
left a comment
There was a problem hiding this comment.
The foreign-namespace and abi_extensions regression cases are now pinned, and current-head CI is green. I still cannot approve this revision: the protobuf normalizer introduces a reproducible false-negative, and the native-disabled probe does not yet prove that the archive is built and staged. Details are inline.
Please also refresh the full PR description and contradictory source comments, as required by this repository's review-follow-up rules. The body still describes the removed abidiff capability gates, claims thresholds key off max_supported_major, repeats the corrected discarded-variant history, calls standard index_type Wire-only, and reports 36/53/5 assertions instead of 50/59/15; it also omits the latest softfloat and protobuf changes. Stale source claims remain in abimerge.hpp:100-104, abi.hpp:63-66, the abidiff test preamble, stage_cdt_tree.cmake:65-69, staged_headers_tests.sh:303-306, and the removed db_store_i64 example in CDTMacros.cmake.in:185.
| if (text.empty()) | ||
| return ojson::null(); // empty string == absent | ||
| try { | ||
| return ojson::parse(text); // string encoding of the same object |
There was a problem hiding this comment.
[P2] Canonicalize with FileDescriptorSet semantics
Generic JSON parsing accepts roots that Wire's protobuf parser rejects and does not normalize protobuf-JSON equivalents. With this head, cdt-abidiff is silent for an absent protobuf_types field versus the nonempty string "null"; the chain retains that string and JsonStringToMessage rejects it with "Root element must be a message", while absence is valid. A raw array versus a string containing that array is similarly collapsed despite different chain behavior. Conversely, numeric and symbolic protobuf-JSON spellings that deserialize to the same FileDescriptorSet still report a difference. Canonicalize through FileDescriptorSet using the chain's parse options and compare a deterministic protobuf representation; at minimum, only adopt a parsed string when its root is an object and add invalid-root controls.
There was a problem hiding this comment.
Reproduced both:
absent vs "null" : collapsed (wrong)
raw [1,2] vs string "[1,2]" : collapsed (wrong)
c3be9d7a adopts a parsed string only when its root is an object, which is what JsonStringToMessage requires. "null" and "[1,2]" stay the strings they are and therefore differ from absence and from a raw array respectively. Controls for both; reverting the restriction fails them.
On the rest: full canonicalisation through FileDescriptorSet would mean linking protobuf into cdt-abidiff, which only cdt-codegen does today. Rather than imply an equivalence the tool does not implement, the comment now states the limit — two spellings that decode to the same FileDescriptorSet but differ as JSON are still reported as different. It errs toward reporting a difference that is not one, never toward hiding one. Happy to do the protobuf-level version if you would rather take the dependency.
| sed 's/^/ /' "${OFFDIR}/cfg.log" | ||
| else | ||
| pass "the libraries project configures with ENABLE_NATIVE_COMPILER=OFF" | ||
| targets="$(ninja -C "${OFFDIR}" -t targets all 2>/dev/null || true)" |
There was a problem hiding this comment.
[P2] Execute the native-disabled target, not just list it
Ninja's target inventory proves only that libsf.a is declared. It still lists a target or subdirectory marked EXCLUDE_FROM_ALL, and it does not execute the POST_BUILD copy into out/lib. Either change would leave this probe green while a default native-disabled build/package still lacks the staged archive. There is no OFF matrix leg despite the comment above. Build sf or the default graph in this scratch configuration and assert the resulting out/lib/libsf.a exists (and is Wasm), alongside the absence of native-host archives.
There was a problem hiding this comment.
Fair — listing proves declaration, not reachability, and never runs the POST_BUILD copy.
c3be9d7a builds the default graph in the scratch OFF tree and asserts out/lib/libsf.a exists and holds WebAssembly objects. It costs about five seconds, since those objects are already in the compiler cache from the main build, so a full OFF matrix leg is not needed for this property. Re-gating add_subdirectory(native) fails it.
Two harness bugs surfaced while doing it, both mine: the probe did not create out/lib, so cmake -E copy wrote a file named lib; and find | head -1 took SIGPIPE, which under set -o pipefail exited the suite mid-run with 141 rather than failing a case. -print -quit now.
| echo " no libsf.a target in the generated graph" | ||
| fi | ||
| # ...while the native-host archives are correctly absent. | ||
| if grep -qE "libnative[a-z_]*\.a" <<< "$targets"; then |
There was a problem hiding this comment.
[P2] Match the native C++ archive too
This character class excludes plus signs, so it does not match the real libnative_c++.a target. Accidentally ungating only the native libc++ target therefore passes the claimed no-native assertion. Match the complete libnative* archive surface rather than only lowercase letters and underscores.
There was a problem hiding this comment.
Correct — [a-z_]* excludes +, so libnative_c++.a never matched. Confirmed against the real build output:
libnative.a libnative_c++.a libnative_c.a libnative_rt.a libnative_sysio.a
c3be9d7a matches the full archive surface.
| # BOTH destinations: staging owns include/sysio/native and include/sysiolib/native, and | ||
| # checking only the first left the second free to disappear with the suite still green. | ||
| missing_native=() | ||
| for d in "${INCLUDE_DIR}/sysio/native" "${INCLUDE_DIR}/sysiolib/native"; do |
There was a problem hiding this comment.
[P2] Require staged files in both native trees
The new loop verifies only that each directory exists. If a staging-pattern regression creates the destinations but copies no headers, the package has no native API while this assertion passes; removing every file from both directories still leaves the suite green. Require a nonzero file count in each destination, as the test already does for the other staged trees.
There was a problem hiding this comment.
Right. c3be9d7a requires a nonzero file count in each destination rather than the directory existing, matching what the other staged trees already assert.
…f roots Four findings. The native-disabled probe listed targets instead of building. `ninja -t targets` shows a declared target even when it is EXCLUDE_FROM_ALL, and listing never runs the POST_BUILD copy -- so either change left the probe green while a default OFF package still shipped no softfloat archive. It builds the default graph now and asserts out/lib/libsf.a exists and holds WebAssembly objects. Costs about five seconds: these objects are already in the compiler cache from the main build. Re-gating add_subdirectory(native) fails it. The no-native-archive pattern was `libnative[a-z_]*\.a`, which excludes plus signs and so never matched the real libnative_c++.a -- ungating only the native libc++ would have passed. Matches the full archive surface now. The both-trees check tested directory existence, so a staging-pattern regression that created the destinations and copied nothing would ship a package with no native API while the assertion passed. Requires a nonzero file count in each, as the other trees already did. protobuf_types canonicalisation adopted any parseable string. The chain's JsonStringToMessage requires a message root, so a string holding "null" or an array is content it REJECTS -- decoding it equated `"null"` with absence, and a string `"[1,2]"` with a raw array the chain reads differently. Only an object root is adopted; anything else stays the string it is. Controls for both, and reverting the restriction fails them. Stated rather than implied: this is JSON-level canonicalisation. Two spellings that decode to the same FileDescriptorSet but differ as JSON are still reported as different, since collapsing them means parsing through FileDescriptorSet with the chain's options and this tool does not link protobuf. It errs toward reporting a difference that is not one, never toward hiding one. Also fixed in the probe: `find | head -1` took SIGPIPE, which under `set -o pipefail` exited the suite mid-run with 141 rather than failing a case. -print -quit instead. abidiff_tests 52, staged_headers 17. ctest 31/31, abigen-pass 16/16.
huangminghuang
left a comment
There was a problem hiding this comment.
The four findings from the previous pass now reproduce as fixed: the abidiff suite passes 52/52, the staging suite passes 17/17, the clean native-OFF build stages a WebAssembly libsf.a without native archives, and current-head CI is green. Two remaining coverage/correctness issues and one diagnostic issue are inline.
Before the next review, please also complete the repository's required follow-up validation and documentation:
- Run and record all three suites at the current head with
ENABLE_INTEGRATION_TESTS=ON.CLAUDE.mdrequires that sweep for ABI/runtime changes; the workflows currently run 31 default tests without enabling integration tests, and the documented downstream run excludes four contracts. - Refresh the PR description and stale source comments to describe the current implementation. The body still documents the removed capability gates and
max_supported_majorbehavior and reports old test totals (36/53/5 rather than 52/59/17). The staging comments attests/unit/staged_headers_tests.sh:230-231,343-346andcmake/stage_cdt_tree.cmake:65-69likewise still say the OFF configuration does not rebuildlibsf.a, although this head now builds it.
| // that the chain reads quite differently. Anything else stays the string it is, | ||
| // and therefore differs from both. | ||
| if (parsed.is_object()) | ||
| return parsed; |
There was a problem hiding this comment.
[P2] Preserve protobuf differences hidden by duplicate JSON members
ojson discards duplicate object members while parsing both the outer ABI and this embedded object. As a result, current-head cdt-abidiff reports no difference between a raw protobuf_types value containing "file":[{"name":"a.proto"}],"file":[{"name":"b.proto"}] and a string value containing only "file":[{"name":"b.proto"}]. Wire's FC parser preserves both members, and protobuf 33.4 merges duplicate repeated fields, so the runtime sees valid descriptor sets [a.proto,b.proto] and [b.proto]. This contradicts the guarantee above that this normalization can only create false positives. Please either reject duplicate members explicitly or canonicalize with the runtime's FileDescriptorSet semantics, and add this pair as a regression test.
There was a problem hiding this comment.
Reproduced, and you are right that it falsifies the guarantee I wrote — this is an under-report, not an over-report.
The vendored jsoncons has no option to reject duplicates at parse time, and canonicalising through FileDescriptorSet means linking protobuf into cdt-abidiff, which only cdt-codegen carries. So fc675ede takes the other option you offered: a document with duplicate object members is refused at load, via a SAX pass tracking the keys of each open object.
$ cdt-abidiff dup.abi onlyb.abi
Error: file1 has a duplicate object member 'file'. jsoncons keeps only the last, while the
chain preserves both and protobuf merges duplicate repeated fields -- so any
comparison here would describe a value the runtime never sees.
exit=1
It applies to the whole document, not just protobuf_types, since the blindness was never specific to that section. All 44 real ABIs in the tree are still accepted. The comment now states this exception instead of claiming a guarantee the tool cannot make.
It also immediately found two malformed fixtures of mine — mk_legacy already emits "types", so passing it again duplicated that member.
| # ...while no native-HOST archive is produced. [^[:space:]]* rather than [a-z_]*: | ||
| # the real targets include libnative_c++.a, whose plus signs a | ||
| # letters-and-underscores class silently excludes. | ||
| stray_native="$(ls "${OFFDIR}/out/lib" 2>/dev/null | grep -E "^libnative[^[:space:]]*\.a$" || true)" |
There was a problem hiding this comment.
[P2] Assert native headers are absent in this clean OFF build
This probe checks only ${OFFDIR}/out/lib, so it does not pin the CMake-to-staging wiring for native headers. I forced the stage command in libraries/CMakeLists.txt to pass STAGE_NATIVE=1; all 17 assertions still passed while the clean OFF output contained four files under each of out/include/sysio/native and out/include/sysiolib/native. Those trees would be packaged because InstallCDT.cmake installs the entire include tree. Please assert both native include directories are absent after this nested OFF build.
There was a problem hiding this comment.
Confirmed by forcing STAGE_NATIVE=1 exactly as you describe — 17/17 green with four files under each native tree.
fc675ede asserts both out/include/sysio/native and out/include/sysiolib/native are absent after the nested OFF build. With the flag forced it now fails, naming both:
FAIL: an OFF build stages no native header tree
staged: .../out/include/sysio/native (4 files)
staged: .../out/include/sysiolib/native (4 files)
| # while a directory-existence check stayed green. | ||
| missing_native=() | ||
| for d in "${INCLUDE_DIR}/sysio/native" "${INCLUDE_DIR}/sysiolib/native"; do | ||
| n="$(find "$d" -type f 2>/dev/null | wc -l)" |
There was a problem hiding this comment.
[P3] Do not let a missing directory abort the test harness
With set -euo pipefail, find returns nonzero when $d is absent, so this assignment terminates the script before missing_native is populated, the named failure is printed, later probes run, or the Results line appears. Removing include/sysiolib/native reproduces that silent early exit. Check -d first or otherwise make the pipeline safe; the same pattern at line 105 should be fixed as well.
There was a problem hiding this comment.
Correct, and the irony is that the abort happened precisely in the case the check exists to report — a missing directory produced no failure line and no Results line at all.
fc675ede adds a count_files helper returning 0 for an absent path, used at both sites. Removing include/sysiolib/native now yields:
FAIL: both native header trees are staged and non-empty (native enabled)
empty or absent: .../include/sysiolib/native (0 files)
Results: 17 passed, 1 failed
exit 1 rather than a silent 141. That is the third set -e pipeline hazard in this test file; I have stopped using find | head anywhere in it.
jsoncons keeps only the last of a repeated object member, so a duplicate is gone before any comparison sees it -- for every section, not only protobuf_types. The runtime disagrees: fc preserves both members and protobuf merges duplicate repeated fields, so a descriptor set written with two `file` members is [a,b] on chain and [b] here, and cdt-abidiff reported no difference against a document carrying only [b]. That also falsified this PR's own claim that the protobuf normalisation can only over-report. The vendored jsoncons has no option to reject duplicates at parse time, and linking protobuf to canonicalise through FileDescriptorSet is a dependency this tool does not carry -- so such a document is refused at load instead, via a SAX pass that tracks the keys of each open object. Refusing beats answering about a value the runtime never sees. The comment now states that exception rather than claiming a guarantee the tool cannot make. Verified: the reported pair exits 1 with a diagnostic naming the key, a clean pair still exits 0, and all 44 real ABIs in the tree are accepted. The detector immediately found two malformed fixtures of my own -- mk_legacy already emits "types", so passing '"types": []' duplicated it -- which is the kind of thing it exists to catch. Also on the staging probe: - it checked only out/lib, so forcing STAGE_NATIVE=1 left native headers in a clean OFF tree with every assertion green. Since InstallCDT installs the whole include tree, those would have shipped. Both native include destinations are asserted absent; forcing the flag now fails. - `find` on a missing directory exits non-zero, and under `set -euo pipefail` that aborted the suite before the named failure or the Results line appeared -- so the one case the check exists to report was the one it could not print. A count_files helper returns 0 for an absent path; removing a native tree now reports it and the suite completes. abidiff_tests 53, staged_headers 18. ctest 31/31, abigen-pass 16/16.
huangminghuang
left a comment
There was a problem hiding this comment.
fc675ede fixes the raw-object duplicate case and both staging findings from the previous review. I reproduced 53/53 abidiff assertions and 18/18 staging assertions, and current-head CI is green. One supported protobuf spelling still bypasses the duplicate check, and two packaging regressions remain unpinned; details are inline.
The two previously requested follow-ups also remain outstanding:
- Please run and record the current-head sweep with
ENABLE_INTEGRATION_TESTS=ON.CLAUDE.mdrequires all three suites for ABI/runtime changes; CI still runs only the 31 default tests, and the recorded integration runs predate the later ABI, codegen, runtime, and staging changes. - Please refresh the PR description and contradictory source comments after this follow-up commit. The body still describes removed capability gates and
max_supported_majorbehavior and reports 36/53/5 tests rather than the current 53/59/18. Staging comments still say the OFF configuration does not rebuildlibsf.a, although it now does.
| if (text.empty()) | ||
| return ojson::null(); // empty string == absent, per to_variant | ||
| try { | ||
| ojson parsed = ojson::parse(text); |
There was a problem hiding this comment.
[P1] Scan duplicate members inside the supported string spelling
load_checked() scans only the outer ABI JSON, so duplicate members inside a string-valued protobuf_types are opaque to its SAX handler. This parse then drops the earlier member: on the current head, a string containing {"file":[{"name":"a.proto"}],"file":[{"name":"b.proto"}]} compares equal to a string containing only b.proto (no output, exit 0). JsonStringToMessage accepts the first as two descriptors, a.proto and b.proto, so this is the same runtime-visible false negative the new detector is meant to prevent. Run duplicate detection on text before parsing/returning it (or canonicalize through FileDescriptorSet) and add the string-form regression.
There was a problem hiding this comment.
Confirmed and fixed in 5578e17. Reproduced against the built binary before touching anything:
$ cdt-abidiff pb_dup_str.abi pb_onlyb.abi # duplicate inside the STRING
exit=0 # no output
$ cdt-abidiff pb_dup_obj.abi pb_onlyb.abi # the same duplicate as an object
Error: file1 has a duplicate object member 'file'. ...
exit=1
Exactly the false negative the detector exists to prevent, in the one place its walk cannot see: to the SAX reader that spelling is a single opaque string value.
Fixed at load, not at the comparison, so a document is refused on the same terms whichever spelling it uses — load_checked parses, and when protobuf_types is a string, runs first_duplicate_member on the string's own text as well:
if (parsed.is_object() && parsed.count(protobuf_types_key)) {
const ojson& pb = parsed.at(protobuf_types_key);
if (pb.is_string()) {
const std::string nested = first_duplicate_member(pb.as<std::string>());
if (!nested.empty())
refuse_duplicate(which, nested, " inside protobuf_types");
}
}Not canonicalising through FileDescriptorSet: the tool does not link protobuf, which is the same reason canonical_protobuf stops at JSON-level canonicalisation. The refusal message and the protobuf_types key moved into file-local helpers so both paths share one wording, and the note in canonical_protobuf now says the refusal covers either spelling.
Three regressions in abidiff_tests.sh, via a shared expect_refused (an inline copy per spelling is how one of them stops being checked): the object form; your string form; and a duplicate-free protobuf_types string still comparing equal to its object counterpart — so the fix cannot be "refuse every string". Deleting the nested check fails only the new row (54/55). 55/55 with it.
There was a problem hiding this comment.
Correcting one line of the reply above: "refuses the document on the same terms as the object spelling" was not true of what I pushed, and 482e2ed fixes it.
A pre-push audit caught that the nested check refused on any duplicate in the string, which is stricter than the comparison it protects. canonical_protobuf adopts the parsed value only for an object root — anything else is returned as the string node and compared verbatim, which is what the pre-existing "a raw array differs from a string containing that array" assertion pins. So for a non-object root jsoncons drops nothing from the compared value, and the refusal's own reason does not hold:
# before 482e2edbb
$ cdt-abidiff arrdup.abi arrother.abi # protobuf_types: "[{\"a\":1,\"a\":2}]"
Error: file1 has a duplicate object member 'a' inside protobuf_types. ... rc=1
# after -- compared verbatim, as it was before this PR
< protobuf_types
"[{\"a\":1,\"a\":2}]"
> protobuf_types
"[{\"a\":9}]"
rc=0
The gate asks canonical_protobuf whether it would adopt rather than restating the rule, so the refusal rule and the adoption rule cannot drift apart:
if (pb.is_string() && canonical_protobuf(parsed).is_object()) {The object-root string is still refused, unchanged. The regression I had added alongside it — a duplicate-free string against its object counterpart — turned out to duplicate an assertion four lines up, so it is replaced by the case that earns its place: a duplicate inside a non-object-root string still compares, and reports the difference. Removing the gate fails it (54/55); 55/55 with it.
| # leaves these populated in an OFF build, and InstallCDT installs the whole | ||
| # include tree, so they would ship. | ||
| stray_hdrs=() | ||
| for d in "${OFFDIR}/out/include/sysio/native" "${OFFDIR}/out/include/sysiolib/native"; do |
There was a problem hiding this comment.
[P2] Assert the required non-native headers exist in the OFF output
This clean OFF build now proves native headers are absent, but it never proves the required header trees were staged. Fault-injecting removal of the OFF sysiolib, libc, libcxx, boost/preprocessor, and bluegrass outputs still lets the default build and all 18 assertions pass, yielding a package without its public headers. Please require each non-native destination under ${OFFDIR}/out/include to be nonempty here, so the OFF CMake-to-staging path is covered positively as well as negatively.
There was a problem hiding this comment.
Agreed — that probe was all negatives. Fixed in 5578e17.
The per-destination check is now a helper, require_non_native_dests <include root> <label>, run against both the live build tree and ${OFFDIR}/out/include, over one named list:
readonly NON_NATIVE_DESTS=(sysiolib libc libcxx boost/preprocessor bluegrass)which the sentinel-planting loop also walks, so the hardcoded expected 5 is gone with it — a tree added to stage_cdt_tree.cmake and missed in one of the three places is now the kind of gap the others can report.
It asserts a non-zero file count rather than directory existence, since a staging regression that creates the destinations and copies nothing would otherwise stay green.
Fault-injected exactly as you describe: wrapping the sysiolib/libc/libcxx/boost/preprocessor/bluegrass copies in if(STAGE_NATIVE) — a regression gated on the OFF branch, invisible to the ON-configured live tree — gives
FAIL: sysiolib is staged (native disabled)
FAIL: libc is staged (native disabled)
FAIL: libcxx is staged (native disabled)
FAIL: boost/preprocessor is staged (native disabled)
FAIL: bluegrass is staged (native disabled)
Results: 18 passed, 5 failed
with the original 18 all still green — so it is the OFF staging path those rows pin and nothing else. 23/23 unmutated.
| SCRATCH="$(mktemp -d)" | ||
| trap 'rm -rf "$SCRATCH"' EXIT | ||
| mkdir -p "${SCRATCH}/lib" "${SCRATCH}/include/sysio/native" "${SCRATCH}/include/sysiolib/native" | ||
| for f in libnative.a libnative_sysio.a libsf.a libc.a; do echo stale > "${SCRATCH}/lib/${f}"; done |
There was a problem hiding this comment.
[P2] Seed every archive the ON-to-OFF prune must remove
A real native-enabled build stages five host archives: libnative.a, libnative_sysio.a, libnative_c.a, libnative_c++.a, and libnative_rt.a. This probe seeds and checks only the first two. Restricting stage_cdt_tree.cmake to remove exactly those two still leaves all 18 assertions green while the other three stale archives would survive and be packaged after an ON-to-OFF reconfigure. Seed and assert removal of the complete libnative* surface, especially the +-bearing name.
There was a problem hiding this comment.
Correct — five archives, and the probe seeded two. Fixed in 5578e17.
One list now drives both the seeding and the assertion:
readonly NATIVE_ARCHIVES=(libnative.a libnative_sysio.a libnative_c.a libnative_c++.a libnative_rt.a)matching the POST_BUILD copies in libraries/{native,sysiolib,libc,libc++,rt}/CMakeLists.txt. Seeding and asserting from the same list is what stops them drifting apart again.
Narrowing stage_cdt_tree.cmake to remove exactly the two old names now fails, and names the three you predicted:
FAIL: STAGE_NATIVE=0 prunes stale native archives and header trees
survived: /tmp/tmp.eDGP6D86fv/lib/libnative_c.a
survived: /tmp/tmp.eDGP6D86fv/lib/libnative_c++.a
survived: /tmp/tmp.eDGP6D86fv/lib/libnative_rt.a
Results: 22 passed, 1 failed
libnative_c++.a is seeded and asserted literally, so the plus signs are exercised end to end rather than only by the OFF build's ^libnative[^[:space:]]*\.a$ scan.
…FF must stage Three findings from review, each verified by mutation. The duplicate-member detector walked only the outer ABI document, so duplicates inside a string-valued protobuf_types were invisible to it: that spelling is one opaque string value to the SAX reader. canonical_protobuf then parsed the string with jsoncons, which keeps only the last member, and a descriptor set carrying `file: a.proto` and `file: b.proto` compared EQUAL to one carrying only b.proto -- no output, exit 0 -- while JsonStringToMessage reads the original as both. The same runtime-visible false negative the detector exists to prevent, one level down. load_checked now runs the detector on the string's own text and refuses the document on the same terms as the object spelling; the message and the protobuf_types key move into file-local helpers so both paths share them. The isolated OFF build asserted only negatives -- no native headers, no libnative* archive -- and never that the required trees were staged. Gating the sysiolib, libc, libcxx, boost/preprocessor and bluegrass copies on STAGE_NATIVE left all 18 assertions green while an OFF package shipped no public headers at all; the live tree cannot catch that, being configured ON. The per-destination check is now a helper run against both roots. The ON -> OFF prune probe seeded two of the five native archives a real build stages. Narrowing the GLOB to exactly those two kept every assertion green while libnative_c.a, libnative_c++.a and libnative_rt.a survived a reconfigure and were packaged. Both the seeding and the assertion now walk one list of all five, libnative_c++.a included -- the name whose plus signs a hand-written character class drops.
…pted
The nested check added in the previous commit refused on any duplicate in a
protobuf_types string, which is stricter than the comparison it protects.
canonical_protobuf adopts the parsed value only for an OBJECT root; any other
root is returned as the string node and compared verbatim, so jsoncons drops
nothing from the compared value and the refusal's own reason does not hold. A
document spelling protobuf_types as "[{\"a\":1,\"a\":2}]" diffed faithfully
before and was refused after, with a diagnostic that misstated why.
The gate asks canonical_protobuf whether it would adopt, rather than restating
the rule, so the two cannot drift apart again.
The regression that shipped with it compared a duplicate-free string against its
object counterpart, which the pre-existing assertion four lines up already
covered. Replaced with the case that earns its place: a duplicate inside a
non-object-root string still compares, and reports the difference. Removing the
gate fails it.
5decec9 to
482e2ed
Compare
huangminghuang
left a comment
There was a problem hiding this comment.
The three inline findings from the previous pass are fixed. I reran the focused suites (55/55 abidiff and 23/23 staging assertions); current-head CI is green and the merge tree is clean. Two remaining correctness/coverage findings are inline.
The required current-head integration sweep and follow-up documentation refresh also remain outstanding: CLAUDE.md requires all three suites with ENABLE_INTEGRATION_TESTS=ON for ABI/runtime changes, while the workflows still run the default 31 tests and the recorded integration run predates the later codegen, ABI, and staging changes. The PR body also still describes removed gate/max-major behavior and reports 36/53/5 rather than the current 55/59/23 assertion counts; please refresh it and the corresponding stale source comments to describe the final implementation.
| if (!dup.empty()) | ||
| refuse_duplicate(which, dup, ""); | ||
|
|
||
| ojson parsed = ojson::parse(text); |
There was a problem hiding this comment.
[P1] Use strict JSON parsing before deciding equality. ojson::parse uses jsoncons's default handler, which silently accepts and removes C/C++ comments. On this head, cdt-abidiff reports no difference between a commented outer ABI and its uncommented form even though wire-sysio's FC parser rejects the former. The same false equality occurs for string-valued protobuf_types containing {"file":[]/*comment*/} versus the equivalent object value, although protobuf 6.33.4 rejects the string and accepts the object. Please use jsoncons::strict_parse_error_handler for this parse and the nested parse in canonical_protobuf, and add regressions, so a chain-rejected ABI cannot compare equal to an accepted one.
There was a problem hiding this comment.
Confirmed and fixed in 511b1ad. default_parse_error_handler swallows exactly one error and it is this one:
if (code == illegal_comment) { return false; } // jsoncons/parse_error_handler.hpp:118So a commented document — which fc will not load — compared equal to the uncommented one it does load. Same class of false equality as a dropped duplicate member, and invisible for the same reason: the difference is gone before anything compares it.
Both parses now go through one helper:
inline ojson parse_strict(const std::string& text) {
jsoncons::strict_parse_error_handler strict;
return ojson::parse(text, strict);
}- the outer parse in
load_checkedrefuses the document, naming the file:Error: file1 is not strict JSON: .... That also improves the diagnostic for ordinary malformed input, which previously surfaced as a baree.what()frommainwith no indication of which file. - the nested parse in
canonical_protobufsimply does not adopt, so a commented string stays the string it is and therefore differs from the object — which matches the chain, protobuf rejecting the string and accepting the object.
Three regressions: a commented document is refused; a commented protobuf_types string differs from the equivalent object; and — the direction that keeps the fix from being "reject every string" — an uncommented string still equals its object. Reverting both parses to ojson::parse fails the first two and nothing else (56/58). 58/58 with the fix.
expect_refused now takes the needle, since there are two distinct refusal reasons to tell apart.
I left the duplicate-member SAX walk on the default handler deliberately: its job is duplicates, not strictness, and it must not throw on input the parse is about to diagnose. A document with both a comment and a duplicate is reported as the duplicate — refused either way.
| require_non_native_dests() { # $1=include root $2=label suffix | ||
| local root="$1" label="$2" dest n | ||
| for dest in "${NON_NATIVE_DESTS[@]}"; do | ||
| n="$(count_files "${root}/${dest}")" |
There was a problem hiding this comment.
[P2] Verify the non-native sysiolib tree itself. This recursive count includes ${root}/sysiolib/native, so native headers can satisfy the purported non-native assertion while every regular sysiolib header is missing. I gated the main sysiolib copy behind NOT STAGE_NATIVE; the native-ON output retained only four sysiolib/native headers and lost all 63 regular headers, yet this suite still passed 23/23. Please exclude the native subtree from this count or verify the expected non-native headers/source set explicitly.
There was a problem hiding this comment.
Confirmed and fixed in 511b1ad — and your fault injection found a second gap behind the first, so it took two changes rather than one.
The count. count_non_native_files now prunes the nested native subtree:
find "$1" -type f -not -path '*/sysiolib/native/*' 2>/dev/null | wc -lso the four sysiolib/native headers can no longer stand in for the 63 regular ones.
That alone did not catch it. Running your injection against the fixed count still passed 23/23, because nothing re-ran the staging script in ON mode: the live ${INCLUDE_DIR} this suite inspects is a snapshot of whatever the last real build left, so a staging regression is invisible there until someone rebuilds — and the OFF probe is unaffected by a NOT STAGE_NATIVE gate by construction.
The isolated fresh staging already runs the script with STAGE_NATIVE=${NATIVE_ENABLED}, so it is now checked per destination too, before any sentinel is planted:
require_non_native_dests "${PRUNE_SCRATCH}/include" " (fresh staging)"Both halves are load-bearing, confirmed separately:
| variant | result |
|---|---|
| both fixes | 28/28 |
+ main sysiolib copy gated on NOT STAGE_NATIVE |
FAIL: sysiolib is staged (fresh staging) |
| same injection, but count reverted to plain recursive | 28/28 — invisible again |
Baseline is now 28 assertions, and sysiolib reports 63 files in all three roots rather than 67 in the ON one.
…aders Two findings from review. jsoncons's default parse handler silently accepts and DISCARDS C/C++ comments -- default_parse_error_handler swallows illegal_comment alone -- while fc rejects them. A commented document is therefore one the chain will not load, and parsing it leniently made it compare EQUAL to the uncommented document the chain does load: the difference gone before anything compared it, exactly like a dropped duplicate member. The same false equality reached the string spelling of protobuf_types, where the chain's verdict actually flips between the two -- protobuf rejects a commented string and accepts the equivalent object. Both parses now use strict_parse_error_handler. The outer one refuses the document with a diagnostic naming the file; the nested one in canonical_protobuf simply does not adopt, so a commented string stays the string it is and differs from the object. Three regressions cover it, including that an uncommented string still equals its object. The non-native destination count was recursive, and include/sysiolib/native nests inside include/sysiolib -- so four native headers satisfied an assertion about the 63 regular ones. Gating the main sysiolib copy on NOT STAGE_NATIVE dropped every regular header from a native-ON build with the suite still green. The count now prunes that subtree. Excluding it is necessary but not sufficient: nothing re-ran the staging script in ON mode, so the live include tree stayed a snapshot of the last real build and the regression was invisible until someone rebuilt. The isolated fresh staging, which already runs the script in the configured mode, is now checked per destination too. Each half is needed -- with either alone the fault injection still passes.
huangminghuang
left a comment
There was a problem hiding this comment.
The two inline defects from the previous pass are fixed. I compiled this head and reproduced 58/58 abidiff assertions; a freshly staged native-ON tree plus the isolated native-OFF build passes 28/28 staging assertions. Ubuntu's 30/30 default suite and package checks are green, the merge with current master is clean, and macOS was still building during this pass. One new parser-policy mismatch is inline.
Two previously noted merge requirements also remain unresolved:
- There is still no passing current-head
ENABLE_INTEGRATION_TESTS=ONsweep. This PR changes ABI generation and runtime-library staging, so theCLAUDE.mdpre-PR requirement applies; both workflows omit the option and the current Linux job runs only the 30 non-integration tests. The integration claims in early commit messages predate the later ABI/codegen/staging changes. - The PR description and source comments still describe superseded implementations. The body says cdt-abidiff uses capability predicates/gates, derives thresholds from
max_supported_major, reports 36/53/5 assertions and 31 ctests, and callsindex_typeWire-only; current behavior is unconditional diffing, explicit introduction-version pairs, 58/59/28 focused assertions, and 30 default ctests. Related stale comments remain inabi.hpp,abimerge.hpp,cdt-abidiff.cpp.in,tests/CMakeLists.txt,abidiff_tests.sh,stage_cdt_tree.cmake, andstaged_headers_tests.sh. Please refresh the final PR description and those comments after the implementation settles.
| parsed = parse_strict(text); | ||
| } catch (const std::exception& e) { | ||
| std::cerr << "Error: " << which << " is not strict JSON: " << e.what() << "\n" | ||
| << "The chain's parser rejects it, so any comparison here would describe\n" |
There was a problem hiding this comment.
[P2] Do not claim strict JSON matches the chain parser's accepted syntax. This catch handles every strict-jsoncons error and says FC rejects the document, but FC's legacy parser accepts trailing commas. I tested otherwise-valid ABIs with a trailing comma at the top level, inside an object, and inside an array: clio convert pack_hex ... --type abi_def packs all three with exit 0, while this head's cdt-abidiff file file exits 1 with Extra comma followed by this incorrect claim. The outer ABI and string-valued protobuf_types also pass through different runtime parsers (FC versus protobuf), so one shared strict policy cannot be described as matching both. Either make the outer parse mirror FC's accepted recoveries (jsoncons can recover extra_comma while still rejecting illegal_comment, with other FC leniencies audited) or explicitly document strictness as a cdt-abidiff policy and correct this diagnostic/comment; add a trailing-comma regression for the chosen contract.
There was a problem hiding this comment.
You are right about the claim, and I checked fc rather than taking it on faith — its array loop consumes any comma it meets:
while( in.peek() != ']' ) {
if( in.peek() == ',' ) { in.get(); continue; } // libfc/src/io/json.cpp
...so a trailing comma (a leading or repeated one too) loads fine. The diagnostic blamed the chain for a rule the chain does not have. Corrected in 97756f9.
I went with the second option — strictness as a documented cdt-abidiff policy — rather than mirroring fc, for three reasons:
It is not a behaviour change. default_parse_error_handler returns false for illegal_comment and true for everything else, so extra_comma was already propagating before this PR. I rebuilt with the parse reverted to ojson::parse(text) to be sure, and it rejects your input identically:
Error: file1 is not strict JSON: Extra comma at line 1 and column 55
The only behaviour this PR changed is the comment case. Making trailing commas parse would be a new leniency, not a restoration.
"Recover extra_comma" would not actually mirror fc. fc swallows commas anywhere in the loop, including leading and repeated ones; jsoncons's recovery is a different shape. Matching it properly means auditing fc's leniencies as a whole, which is its own change with its own risk — a lenient parse rewrites the document it then compares, which is the failure this parse exists to prevent.
Your parser point cuts against mirroring anything. The outer document is fc's to read; a string-valued protobuf_types is JsonStringToMessage's. One policy cannot be described as matching both, so it is now described as matching neither, deliberately.
The diagnostic now reads:
Error: file1 is not strict JSON: Extra comma at line 1 and column 55
cdt-abidiff refuses what it cannot read strictly, rather than let a
lenient parse normalise a difference away before anything compares
it. The runtime's parsers are laxer in places -- fc tolerates a stray
comma -- so this is this tool's policy, not a verdict on whether the
chain would load the file.
and the comment on parse_strict separates the two halves explicitly: the comment case is the bug (jsoncons accepts, fc rejects, false equality follows); everything else is policy, conservative in the direction that never answers "equal" about text it had to rewrite.
Two regressions, per your ask: the trailing comma is refused, and the diagnostic names whose rule it is. Restoring the old wording fails the second (59/60). 60/60 as it stands; full unit label 27/27.
…he chain
The diagnostic added with strict parsing said "The chain's parser rejects it"
for every error strict jsoncons raises. That is false for at least one of them:
fc's parser consumes any comma it meets -- libfc/src/io/json.cpp, whose array
loop is `if (in.peek() == ',') { in.get(); continue; }` -- so it loads a
trailing comma happily while cdt-abidiff refused the file and blamed the chain.
Refusing is still right, but as this tool's own policy: a lenient parse
normalises the document it then compares, and answering "equal" about text it
had to rewrite to read is the failure mode the strict parse exists to prevent.
Refusing is the conservative direction, and it costs nothing that was working --
jsoncons's default handler already propagated every error but the comment, so a
build from before this PR rejects a trailing comma identically. Only the comment
case changed behaviour.
There is also no single parser to mirror even if that were wanted: the outer
document is fc's to read, a string-valued protobuf_types is
JsonStringToMessage's, and their leniencies differ. One uniform policy is
deliberate, and the comment on parse_strict now says so.
Two regressions on the trailing comma: that it is refused, and that the
diagnostic names whose rule it is. Restoring the old wording fails the second.
huangminghuang
left a comment
There was a problem hiding this comment.
97756f94 resolves the previous inline finding: strictness is now correctly described as cdt-abidiff's policy rather than a claim about FC, and the new trailing-comma regression pins that contract. I compiled this head and the focused abidiff suite passes 60/60; the latest delta also passes git diff --check, the merge with current master is clean, and Ubuntu/package checks are green. I found no new code defect in this follow-up.
I still cannot approve because the two PR-wide requirements from the previous review remain unresolved:
- No passing current-head
ENABLE_INTEGRATION_TESTS=ONsweep is recorded. This PR changes ABI generation and runtime-library staging, so the repository's pre-PR requirement applies. Both workflows omit the option and the Linux job runs only the 30 non-integration tests; the integration claims in early commits predate the later ABI/codegen/staging changes. - The PR description and related source comments still describe superseded implementations. The body still says cdt-abidiff uses capability gates, derives thresholds from
max_supported_major, reports 36/53/5 focused assertions and 31 ctests, and callsindex_typeWire-only. Current totals are 60/59/28 and 30 default ctests, and cdt-abidiff now diffs the sections unconditionally. The stale comments previously identified inabi.hpp,abimerge.hpp,cdt-abidiff.cpp.in,tests/CMakeLists.txt,abidiff_tests.sh,stage_cdt_tree.cmake, andstaged_headers_tests.shalso remain. Please refresh the full PR description and those comments before the next review.
Documentation only; no behaviour change. Each claim was checked against the code rather than reworded. abi.hpp: supports_action_results said the abigen plugin, ABIMerger and cdt-abidiff all decide with it. Only abigen calls it -- ABIMerger reads the introduction constants directly, because it needs the (major, minor) pair itself to promote a merged version, and cdt-abidiff does not gate at all. supports_variants has no caller and now says so rather than implying three. cdt-abidiff.cpp.in: the get_version comment said "every gate below keys off this value" and the version comment said the prefix-insensitive parse is right for "deciding capability". There are no gates below any more -- every section is diffed unconditionally -- and what the parse is right for is ORDERING two versions. Both corrected, with the reason the gates went away stated where the parse is defined. tests/CMakeLists.txt and abidiff_tests.sh repeated the capability-gate framing in their preambles; the test file now records that both halves of the original bug are fixed, the parse and the gates. abimerge.hpp: the comment beside the version fixup said that when nothing forces a promotion "the newer document's own version STRING stands", so an inherited "eosio::abi/1.2" survives. merge_version canonicalises at the top of merge(), so what stands is already "sysio::abi/" -- ordering ignores the prefix so a foreign descriptor can be ingested, but the output never carries one. stage_cdt_tree.cmake and staged_headers_tests.sh both said an OFF configure never rebuilds libsf.a. It has been built in every configuration since the sf target moved outside the ENABLE_NATIVE_COMPILER guard in libraries/native/CMakeLists.txt, which is what the clean OFF probe asserts. CDTMacros.cmake.in cited db_store_i64 as the example unresolved intrinsic; this PR removes that surface, so it names kv_set/kv_get instead. Validated at this head with all three suites and ENABLE_INTEGRATION_TESTS=ON: ctest 31/31, the per-suite integration run 16/16, and 60/61/28 on the three focused shell suites.
|
Both PR-wide requirements are done — Integration sweep at the current headRun here rather than inferred from CI, which enables neither the option nor the
clang-18, Debug, One number differs from yours: I measure CommentsEach was checked against the code rather than reworded, and two turned out to be wrong in a way the summary had not captured:
PR descriptionRewritten for the four claims you named, each verified first:
Two more the body still had wrong, which you had flagged in earlier rounds:
|
huangminghuang
left a comment
There was a problem hiding this comment.
5f7d0c62 fixes the earlier parser-policy wording, refreshes the PR description, and records the required current-head integration sweep. I independently confirmed the ABI-version suite at 61/61; the prior abidiff 60/60 and staging 28/28 results remain applicable because this delta changes comments only. The merge tree is clean, Ubuntu/package checks are green, and macOS is still running. One stale schema statement remains inline; please also correct its mirrored wording in tests/unit/abidiff_tests.sh:326.
| } | ||
| return type; | ||
| // An absent key read through the CONST operator[] throws, and ABIs legitimately omit | ||
| // keys: table_id, index_type and secondary_indexes are Wire extensions, so a stock |
There was a problem hiding this comment.
[P2] Keep the schema comment aligned with the actual ABI fields
index_type is a standard Antelope table_def field; only table_id and secondary_indexes are Wire additions. The refreshed PR description now distinguishes these correctly, but this comment—and the mirrored test preamble at tests/unit/abidiff_tests.sh:326—still says a stock Antelope ABI carries no index_type. Please correct both so the implementation and regression test no longer document the superseded schema model.
There was a problem hiding this comment.
Right — I corrected that claim in the description and left it standing in the two places that document the code. Fixed in 3cc7752, both sites.
The test comment was contradicted by its own fixture ten lines below it:
"tables": [ { "name": "t", "type": "row", "index_type": "i64",
"key_names": ["id"], "key_types": ["uint64"] } ]t_antelope.abi carries index_type, key_names and key_types and omits only table_id and secondary_indexes — which is exactly what makes it a stock ABI rather than an empty one. Both comments now say which two are Wire's additions to table_def and point at that fixture as the evidence, so the next reader can check the claim without leaving the file.
One thing fell out while correcting it. The block above section_or_empty was really two comments: an optional-field paragraph ending "Every optional field is read through this", sitting on the function that reads whole sections. field_or_null is the one that sentence is true of, so the paragraph moved there. That is why the diff touches a little more than the one sentence you flagged.
abidiff_tests.sh 60/60, unit_tests 27/27 after the change; comments only, no behaviour touched.
Thanks for confirming 61/61 independently on the ABI-version suite — that matches what I measured, so the 59 in the earlier round was almost certainly the third-argument path.
The previous commit fixed this claim in the PR description but left it in the two places that document the code: cdt-abidiff.cpp.in and the abidiff test preamble both said table_id, index_type and secondary_indexes are Wire extensions that a stock Antelope ABI carries none of. index_type, key_names and key_types are standard table_def fields; only table_id and secondary_indexes are Wire's additions. The t_antelope fixture ten lines below the test comment carries index_type, key_names and key_types and omits only the other two, so the comment contradicted the fixture it introduces. Both now say which two are the additions and point at that fixture as the evidence. While correcting it, the comment above section_or_empty turned out to be two comments: an optional-FIELD paragraph ending "every optional field is read through this", sitting on the function that reads whole SECTIONS. The field paragraph moves to field_or_null, where that sentence is true.
huangminghuang
left a comment
There was a problem hiding this comment.
3cc7752c correctly fixes the index_type classification and moves the optional-field explanation beside field_or_null. The latest delta changes comments only, its shell test remains syntactically valid, the merge tree is clean, and all current GitHub checks are green. A cumulative reread found one other current-tense comment that still describes the zero-major sentinel removed by this PR; details inline.
| * expansion falls short in binary -- "1.3" parsed as minor 2 -- which silently | ||
| * desynced the version handed to the plugin from the one handed to ABIMerger. | ||
| * | ||
| * A zero major is rejected: there is no ABI 0.x, and cdt-cpp reads a zero major |
There was a problem hiding this comment.
[P2] Describe the retired zero-major sentinel in past tense
This paragraph says cdt-cpp currently interprets a zero major as "the option was never given", but this PR removes both > 0 guards: cdt-cpp.cpp.in now always records the parsed version in the manifest and always forwards it to codegen. Because parse() itself rejects zero, the described fallback is neither current nor reachable, and it directly contradicts the new driver comment that says there is no unset state. Please preserve the useful historical rationale in past tense (or explicitly say the sentinel was retired) so the shared parser documentation matches the implementation.
There was a problem hiding this comment.
Confirmed and fixed in 0729b54. The contradiction was two files apart:
// tools/cc/cdt-cpp.cpp.in:69
// Always recorded: abi_version::parse rejects a zero major and the default is
// non-zero, so there is no "unset" state left for a >0 test to stand in for.and there are no surviving > 0 guards on abi_version anywhere in tools/ or plugins/, so the fallback the paragraph described is unreachable as well as retired.
I kept the rationale rather than deleting it, in past tense, because it is the reason the driver can be as simple as it now is — the sentinel is retired precisely because zero never survives the parse, so the rejection is what stops it being needed again:
* A zero major is rejected: there is no ABI 0.x.
*
* That rule once carried a second job... cdt-cpp USED to read a zero major as "the option
* was never given"... That sentinel is retired: the driver now records and forwards the
* parsed version unconditionally, which it can do precisely BECAUSE zero never survives
* this parse. Keeping the rejection is what stops the sentinel being needed again.
A third copy you did not flag carried the same claim — tests/unit/abi_version_tests.sh:150, on its own 0.1 case: "cdt-cpp reads a zero major as option absent -- accepting one would reopen the driver/codegen divergence." Corrected with it. The case still guards that divergence, now by keeping the sentinel unnecessary rather than by feeding it. I grepped never given|option was never|zero major|unset state across tools/, plugins/ and tests/ to be sure those three were all of them.
Rebuilt and reran: ctest 31/31 with integration on, abi_version_tests 61/61.
parse_version_string's doc said cdt-cpp reads a zero major as "the option was never given", which stopped being true when the driver's `> 0` guards went: it now records the parsed version in the manifest and forwards it to codegen unconditionally, and says so in its own comment two files away. The paragraph contradicted that, and described a fallback parse() itself makes unreachable. The rationale is kept, in past tense, because it is why the driver can be as simple as it is: the sentinel is retired precisely BECAUSE zero never survives the parse, so keeping the rejection is what stops it being needed again. tests/unit/abi_version_tests.sh:150 carried the same claim about its own 0.1 case and is corrected with it -- the case still guards the driver/codegen divergence, now by keeping the sentinel unnecessary rather than by feeding it. ctest 31/31 with integration on; abi_version_tests 61/61.
huangminghuang
left a comment
There was a problem hiding this comment.
0729b548 resolves the outstanding zero-major documentation issue in both the shared parser and its regression-test preamble. The latest delta is comments only; its diff and shell syntax are clean. I rechecked the cumulative PR, commit list, description, merge tree, and current checks. The merge is clean and all required GitHub checks pass. No further blocking issues found.
Both PRs the docs referenced as pending are merged, and one of them made a sentence here false. Every claim below was re-verified against a CDT built from merged master, not just reworded. The "unchanged host functions" list promised "every privileged.h setter". #112 removed set_kv_parameters_packed, which was one. The list now names the setters that remain, and the two removals get rows in the "Removed on Wire" table with the diagnostic each produces -- they differ, and the difference is useful when porting: <sysio/security_group.h> is gone outright ("file not found"), while privileged.h is still there and set_kv_parameters_packed is an undeclared identifier in it. Both confirmed by compiling; set_privileged from the same header still builds. The ABI section told the reader not to reach for cdt-abidiff, on the strength of limitations #112 fixed. Inverted: it now lists the sections the tool compares -- version as the full string, structs, types, actions, tables with the full metadata, clauses, enums, protobuf_types, variants, action_results and error_messages -- and keeps the old-toolchain caveat and the jq fallback for anyone on an older CDT. The legacy-database and multi_index sections described #112 and #113 as forthcoming. They describe the current toolchain now, with the pre-merge behaviour as the caveat. Verified against merged master: `it++` is still rejected with "overload resolution selected deleted operator '++'", a hand declared db_store_i64 now fails with "wasm-ld: undefined symbol: db_store_i64", and the documented static_cast escape hatch plus lower_bound on name, uint64_t and {42} all compile. Both docs also now say the #113 receiver guard reaches sysio::singleton: get_or_create, set and remove mutate through the kv_multi_index it holds, so a singleton handle on another account's code is read-only like a table handle. The kv-multi-index singleton section also said singleton "is backed by" kv_multi_index, which reads as inheritance; it holds one.
Summary
Three related cleanups that surfaced while writing the migration guide (#111), plus the matcher defects found while auditing them.
db_*declarations.1 — dead intrinsic surface
CDT declared intrinsics the chain does not export. A contract calling one compiles, links, and fails at deploy against the import allowlist.
Removed: the four security-group intrinsics, 60 stale
db_*entries from the import allowlist, and — found in the last review round —set_kv_parameters_packed. (Thedb_*declarations were already gone from<sysio/db.h>on master; what survived was the allowlist.) The last one was found by comparing everysysio_wasm_importdeclaration underlibraries/sysiolib/capi/sysio/against the chain's 116 intrinsics inintrinsic_mapping.hpp: master carries 108 declarations, of which 5 have no chain counterpart; after this PR there are 103 and none is dead. wire-sysio mentionsset_kv_parameters_packedonly in a CHANGELOG.imports/cdt.imports.ingoes from 152 entries to 87 — exactly the 60db_*, the four security-group names, andset_kv_parameters_packed. No removed name is exported anywhere in wire-sysio, so there are no false positives. Verified dead: zerosecurity_grouphits anywhere in wire-sysio; zerodb_*names in the chain'sgenesis_intrinsics.cpp.2 — ABI version handling
Four places derived the ABI version independently, and one of them read the version by taking the string's last three characters — so
"sysio::abi/1.10"parsed as.10→ 1, and the version-gated diffs incdt-abidiffsilently skipped the variant and action-result sections for any two-digit minor.abi_version::parse_version_stringis now the single reader, comparing(major, minor)components. Incdt-abidiffthose gates are gone entirely rather than fixed: every section is diffed unconditionally, because a section one document carries and the other does not is exactly the difference the tool exists to report, whatever version either side declares. The parse survives there for two other jobs — the version string is itself compared, and an unreadable one stops the run.Section gating is consistent:
variantsfrom 1.1 andaction_resultsfrom 1.2, both consulting the merged version.variantswas previously emitted unconditionally, so a 1.0 document merged at 1.0 produced a 1.0 ABI carrying avariantsarray — contradicting the rule declared a few lines below it.Each threshold is an explicit introduction pair of its own (
variants_major/minor,action_results_major/minor). An earlier revision derived them frommax_supported_major, which ties a fixed point in the format's history to the highest major this toolchain happens to accept: raising that maximum would have moved every introduction with it, sosupports_variants(1, 10)would have turned false whileparse()still accepted major 1 — letting valid 1.x documents omit sections they require.abigenreads the rule throughsupports_action_results;ABIMergerreads the same constants directly, because it needs the pair itself to promote a merged version.cdt-codegen's protobuf branch stamped the CLI version unconditionally, downgrading a merged document whose descriptors declared something newer. It takes the newer of the two now. Anassertbeside it — added by an earlier commit on this branch, not inherited from master — was tautological and compiled away under the default ReleaseTOOLS_BUILD_TYPE; it is gone.No emitted ABI changes for existing contracts
All 16
tests/toolchain/abigen-pass/*.abifixtures pinsysio::abi/1.2and none is modified. Noabigen-pass/*.jsonsets-abi-version, andadd_contractnever passes one, so wire-sysio's contracts stay on the default. The "four disagreeing defaults" all collapsed to 1.2 on master anyway — the float round-trip turnedcdt-codegen's nominal 1.3 into 1.2 before it reached the plugin — so unifying on 1.2 is a no-op.Two behaviour changes, both for versions that were previously emitted wrong: explicit
-abi-version 1.3/1.4/1.10, and a populated version-gated section now promotes the emitted version.Correcting an earlier version of this description, which said master discarded the variant definition at 1.0. It did not — master emitted
variantsunconditionally, so a contract with astd::variantparameter built at-abi-version 1.0produced a 1.0 ABI carrying a 1.1 section: a version stamp contradicting its own content, not a dangling type reference. The section now promotes the document instead:3 — matcher correctness
Auditing the version gates turned up six defects in the code they gate, tabulated below. All six are pre-existing, and several repeat the shape of one already fixed in the same file — which is why they were missed more than once.
cdt-abidifffind_structsbreak-ed out without clearing it, so only a first-field change was ever reported. Seeded false and set only inside the loop, so two identical zero-field structs compared as different — and every parameterless action generates one, so the tool false-positived on essentially every real contract.cdt-abidifffind_tablesnameandtype.index_type,key_names,key_typesandtable_idcould all change with no difference reported — the metadata a contract upgrade turns on.ABIMergervariant_is_same["uint64"]and["uint64","string"]compared equal. Merging them dropped thestringalternative silently, or failed the build, depending on sorted.descfilename order.ABIMergerstruct_is_samecdt-abidiffprint_clauseabi["clauses"]whilefind_clausesiteratesabi["ricardian_clauses"], so the tool threw the moment it had a clause difference to report — it has never been able to report one.cdt-abidifffind_enums/protobuf_typestable_is_samealso never comparedkey_types,table_idorsecondary_indexes; it now does, with the same empty-array tolerance already documented forkey_names.tables_matchlikewise gainedsecondary_indexes— each secondary index carries its owntable_id.Optional keys.
table_idandsecondary_indexesare the Wire additions totable_def—index_type,key_namesandkey_typesare standard Antelope fields, and an earlier version of this description wrongly groupedindex_typewith the extensions. A stock Antelope ABI carries neither addition; reading an absent key through jsoncons' constoperator[]throws. Every optional field is read through afield_or_nullaccessor, so comparing against an upstream ABI works instead of aborting.This corrects a diagnosis I gave in an earlier thread. I said the version parse was why
cdt-abidiffreported no difference between ABIs whose table metadata had changed. It was not —find_tablesnever compared those fields.4 — staging
Configure-time
file(COPY)is additive and the ExternalProject's configure step is stamped, so a header deleted from a source tree stayed staged in<build>/includeforever — shipped by install/CPack, and visible to compiles whose view then disagreed with the rebuilt library.cmake/stage_cdt_tree.cmakeruns as a build step and prunes first. Two corrections from review:libc,libcxx,boost/preprocessorandbluegrasswere still configure-time copies, keeping the exact bug the rework exists to fix. All six are pruned and recopied together now — verified by planting a file in each staged tree and confirming the staging step removes all five vendored ones plussysiolib.REMOVE_RECURSEs a directory, missingsysio_malloc,sysio_dsm,sysio_cmem,c,c++,rt,sfand thenative_*variants. It enumerates the directory tree instead.Packaging:
InstallCDT.cmakeandpackage.cmakedo change, for the native-component gating described below — but no archive moves between components. An earlier revision of this PR excludedlibsf.afrom the base install on the belief that it was native-only. It is not — it is the WebAssembly softfloat archive thatcdt-ldlinks via-lsffor--use-rtand--fquery*(compiler_options.hpp.in:578), and it matches neither install component once excluded, so that would have shipped a sysroot unable to link a--use-rtcontract. Reverted.Tests
tests/unit/abidiff_tests.sh(60 assertions),tests/unit/abi_version_tests.sh(61) andtests/unit/staged_headers_tests.sh(28), all registered intests/CMakeLists.txt;CLAUDE.md's list of shell-script tests is updated to match. Those counts grew across review: the three suites started at 36/53/5.Every assertion pinning a fix was confirmed to fail against the pre-fix code. Some of the others are deliberate controls that pass on both sides — an earlier version of this description claimed every added assertion had been confirmed to fail first, which was not true, and the distinction is now stated rather than glossed:
find_structsandfind_tablesfails 6 of the new cases — the non-first-field change, the identical zero-field struct, and all four table-metadata fields.variant_is_sameandstruct_is_samefails 3, and reproduces the order-dependence exactly: the variant case merged in one descriptor order and refused in the other.Full validation at the current head
CLAUDE.mdrequires all three suites, with-DENABLE_INTEGRATION_TESTS=ONwhen a change can affect generated code, the ABI, or the runtime libraries. This one does, so that sweep was run and is recorded here rather than inferred from CI — which enables neither the integration option nor thesysio_DIRit needs, and so runs only the 30 default tests.ctest— all labels, integration ONunit_tests, 3toolchain_tests, 1integration_tests)ctest --test-dir tests/integration— per Boost suiteabidiff_tests.shabi_version_tests.shstaged_headers_tests.shBuilt with
clang-18,CMAKE_BUILD_TYPE=Debug,x64-linux-releasetriplets, againstwire-sysio fix/opp-outpost-registry-followups @ 55e20cd4a7. WithoutENABLE_INTEGRATION_TESTSthe same tree is 30/30, which is the number CI reports; master is 28, and the delta is exactly the new shell tests.The 16 integration suites are
action_results,bls_primitives,capi,codegen,crypto_primitives,get_code_hash,hash_id,instant_finality,kv_cached,kv_global,kv_indexed_table,kv_scoped_table,kv_singleton,memory,multi_indexandname_pk— i.e. the ABI, codegen and KV-runtime paths this PR touches, exercised against the real chain runtime.Other behaviour changes worth knowing
-abi-versionis now validated. Master accepted0.1(→1.3),1x(→1.0) and1.2.3(→1.2), and crashed on2.0with an uncaughtjsoncons::key_not_found. All four are now refused with a diagnostic.extern "C" db_store_i64links on master and now fails withwasm-ld: undefined symbol: db_store_i64. A declaration carryingsysio_wasm_importemits an explicit import and still links either way.variantsarray is omitted below 1.1, where master emitted"variants": []. No contract in this repo or wire-sysio is affected — all 199 emitted artifacts are byte-identical to master's.Downstream verification
CLAUDE.md requires a change to the CMake package templates or the install layout to be verified against
wire-sysiobefore it is called done. This PR changesInstallCDT.cmake,package.cmake,stage_cdt_tree.cmakeandlibraries/CMakeLists.txt, so that was done through the real consumer path rather than from the build tree:cmake --installthis branch to a clean prefix. The installed sysroot carries everything a contract links against —libsysio.a,libc.a,libc++.a,librt.aandlibsf.a(the archive an earlier revision of this PR wrongly excluded), plus the stagedinclude/sysiolib,include/libcxx(extensionless headers included) andinclude/libctrees.wire-sysio origin/master'scontracts/withfind_package(cdt)+-DCMAKE_TOOLCHAIN_FILE=<prefix>/lib/cmake/cdt/CDTWasmToolchain.cmakeand-DBUILD_SYSTEM_CONTRACTS=ON, exactly ascmake/contract-tools.cmake:19does..wasm/.abicommitted in wire-sysio, which were produced by a master-era CDT.Result: 19/19 contracts byte-identical, wasm and abi. (Four contracts —
sysio.authex,sysio.dclaim,sysio.system,sysio.tokens— could not be built in this harness because they include wire-sysio's ownfc-lite/, generatedtypes.pb.hppandsysio/protocol/headers, which live outsidecontracts/; that is a limitation of building that directory standalone, not a toolchain failure.)CI's Verify packages job also passes, which exercises the deb/rpm layout this PR alters.
Not in this PR
The
.gitignorehunk was byte-identical to #113's and conflicted with it; it lands once, on #113.