Repository navigation
Tags: dfinity/candid
Tags
fix: Bound subtype checks on shared recursive types; release candid 0… ….10.39 (#776) ## What A subtype check could take exponential time on a recursive type reachable along many paths. This happened in both `subtype_check_all` (and so `service_compatibility_report` and `didc check`) and `subtype`/`subtype_with_config` (and so `service_compatible` and the decoder's check of `func`/`service` references): ```candid // new.did: A0 … A22, each level pointing at the next twice type A0 = record { A1; A1 }; … type A22 = nat; service : { go : () -> (A0) } // old.did: the same shape with B0 … B22, leaf `text` ``` On `master`, `didc check new.did old.did` runs for about 13 s and then runs out of memory. At depth 18 it reports 262,144 = 2^18 "incompatible changes", all for the same `nat` → `text`. The `opt` variant `A_i = record { opt A_{i+1}; opt A_{i+1}; nat }` is exponential in time through the opt rule's probes. ## Why A failed pair is withdrawn from `Gamma`, which is necessary because the opt-rule probes share it and must not assume a failed pair. But nothing remembered that the pair had failed, so every later path to it checked it again from scratch. The collector keeps going after errors, and a probe falls back after failing, so both reach the same pair along every path. ## Fix - **Refuted pairs are remembered.** `Assumptions` keeps the pairs the check has refuted, and `subtype_` fails at once on one of them. This is sound because assumptions only make a check more permissive: a pair that fails under some assumptions fails under any. - **The collector reports each failed pair once.** Its own `failed` set is separate from the refuted pairs. A later path to a reported pair adds one entry, `A1 is not a subtype of B1 (see above)`, so a type shared by two methods is still reported under both. A pair refuted only inside a probe is reported in full when the collector reaches it. - **Hitting the recursion limit fails the whole check.** That failure depends on the stack, not on the types, so it can't be cached. But re-exploring it would be exponential again, because an opt probe that hit the limit used to fall back and accept with a warning. - `RecursionDepth` now counts its overflows. After the first one, `subtype_` and the collector return at once, the opt fallback no longer accepts, and nothing the overflow touched is cached. - As a backstop, `subtype_with_config` and `subtype_check_all` check the count when they finish. If the limit was hit, they withdraw the check's assumptions and fail. `subtype_check_all` adds `recursion limit exceeded; the check stopped there, so this report may be incomplete`. The depth-64 versions of both examples now finish instantly. The first reports 65 entries and the `opt` one reports 1. **Behaviour change:** an opt probe that hits the recursion limit now fails the check instead of accepting with a warning. Public signatures are unchanged, and `equal` is unchanged. ## Tests In `rust/candid_parser/tests/compatibility.rs`: - `shared_recursive_types_are_checked_once` - `type_shared_by_two_methods_is_reported_under_both` - `shared_recursive_types_under_opt_are_probed_once` - `pair_refuted_by_a_probe_is_still_reported` `rust/candid/tests/subtype_recursion_limit.rs` runs on a thread with a fixed 1 MiB stack, after measuring the shortest chain that overflows there: - `probe_that_hits_the_limit_fails_the_check`: an opt diamond whose field only fails by hitting the limit. - `pair_that_hit_the_limit_is_not_reported_as_incompatible`: a pair that overflows when reached deep is not reported as "see above" when reached shallow. - `overflow_while_tracing_an_alias_fails_the_check`: tries every wrapper depth around the limit, so the overflow lands while tracing an alias for one of them. Each of these fails or hangs without the change it covers. `cargo test` passes under the three feature sets `rust.yml` uses, and `cargo clippy --features all --tests -D warnings` is clean. ## Release: candid 0.10.39 Patch release of `candid` / `candid_derive`, 0.10.38 → 0.10.39, cut in this PR together with the fix. It also ships #775. - Bump `candid` and `candid_derive` to 0.10.39, and the `=` pin between them - Date the changelog entries as `2026-10-21` / `Candid 0.10.39` (#775 and this PR) - Refresh all four lockfiles: `Cargo.lock`, `rust/bench/Cargo.lock`, `tools/ui/Cargo.lock`, `rust/candid/fuzz/Cargo.lock` (version bumps only, no dependency churn) `candid_parser` stays at 0.4.1; its `candid` dependency is a `0.10.16` floor. ### Test plan - [x] `cargo check -p candid -p candid_derive -p candid_parser` - [x] `cargo test -p candid --features all`: all suites pass - [x] `cd tools/ui && cargo build --target wasm32-unknown-unknown --profile canister --package didjs --locked`, the release workflow's exact command (built against placeholder `dist/` assets, since `npm` was not available locally) - [x] `cd rust/candid/fuzz && cargo check` - [ ] Reviewer to confirm release scope and changelog date ### After merge 1. Tag `2026-10-21` on `master`. `candid-ui-release.yml` triggers on it to publish the `candid_ui` canister wasm 2. Run the **Crates Publish** workflow (`workflow_dispatch`) with the `candid` input checked, which publishes `candid_derive` then `candid` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
chore: Release candid 0.10.37 (#772) Patch release of `candid` / `candid_derive`, 0.10.36 → 0.10.37. No source changes — it dates the two decoder fixes already on `master`. ## Summary - Bump `candid` and `candid_derive` 0.10.36 → 0.10.37, and the `=` pin between them - Date the `Unreleased` changelog entries as `2026-09-25` / `Candid 0.10.37`, covering: + Bounding the byte length of the type-table header, 64 KiB by default and configurable with `set_max_header_len` (#770) + Bounding the size of a type named in a decoder diagnostic, so its rendering no longer follows the type's own width and depth (#771) - Refresh `Cargo.lock`, `rust/bench/Cargo.lock`, `tools/ui/Cargo.lock` and `rust/candid/fuzz/Cargo.lock` `candid_parser` is unchanged at 0.4.1; its dependency on `candid` is a `0.10.16` floor, so it needs no bump. The lockfile diffs are version bumps only — no dependency churn. ## All four lockfiles this time The 0.10.36 release noted that `rust/candid/fuzz/Cargo.lock` was stale at 0.10.34 and that all four lockfiles should move together as a release-checklist step. This one does that, so `fuzz` goes 0.10.34 → 0.10.37. `tools/ui` patches `candid` to the workspace path, so its lockfile pins the path crate's version and has to move with the bump. `candid-ui-release.yml` builds `didjs` there with `--locked` and triggers on the date tag, so a stale lock would only fail at tag time, after merge. ## Test plan - [x] `cargo check -p candid -p candid_derive -p candid_parser` - [x] `cargo test -p candid --features all` — all suites pass - [x] `cd tools/ui && cargo build --target wasm32-unknown-unknown --profile canister --package didjs --locked` — the release workflow's exact command, succeeds - [x] `cd rust/candid/fuzz && cargo check` - [ ] Reviewer to confirm release scope and changelog date ## After merge 1. Tag `2026-09-25` on `master` — this is what `candid-ui-release.yml` triggers on to publish the `candid_ui` canister wasm 2. Run the **Crates Publish** workflow (`workflow_dispatch`) with the `candid` input checked, which publishes `candid_derive` then `candid` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: fix didc-release on musl with precise-builds (#746) Fix https://github.com/dfinity/candid/actions/runs/28178275960/job/83460624957
ci: release candid_ui (#692) GItHub releases will include prebuilt `candid_ui.wasm` so that tools like `icp-cli` can integrate. A test release can be found [here](https://github.com/dfinity/candid/releases/tag/test_release_candid_ui_2).
PreviousNext