Skip to content

docs: Candid spec reference#69

Merged
marc0olo merged 2 commits into
mainfrom
docs/reference-candid-spec
Apr 16, 2026
Merged

docs: Candid spec reference#69
marc0olo merged 2 commits into
mainfrom
docs/reference-candid-spec

Conversation

@marc0olo
Copy link
Copy Markdown
Member

Summary

  • Type system: all primitive types (bool, nat, int, nat8/16/32/64, int8/16/32/64, float32/64, text, blob, null, reserved, empty, principal) with Motoko/Rust/JavaScript mappings table
  • Constructed types: opt, vec, record, variant — field hash formula, tuple shorthand, idiomatic patterns
  • Reference types: func (query/composite_query/oneway annotations), service, type definitions, mutual recursion, imports
  • Service description grammar: full BNF with service constructors (InitArgs pattern)
  • Subtyping rules: covariant results, contravariant arguments, option rules, record/variant evolution, safe-upgrade patterns table
  • Binary encoding: DIDL magic bytes, type opcode table, LEB128/SLEB128 summary
  • Language mappings: consolidated table across Motoko/Rust/JavaScript with edge-case notes
  • didc tool: check, bind, encode, decode, subtype, hash commands with examples

Sync recommendation

informed by dfinity/candidspec/Candid.md; informed by dfinity/portaldocs/references/candid-ref.mdx, docs/building-apps/interact-with-canisters/candid/candid-concepts.mdx

@marc0olo marc0olo force-pushed the docs/reference-candid-spec branch from 025bf68 to 8605aa5 Compare April 16, 2026 13:37
@marc0olo
Copy link
Copy Markdown
Member Author

Review: Candid Specification

Must fix

  • Safe upgrade table — misleading note on variant tag removal: The row "Remove a variant tag from results | Yes | Must wrap variant in opt to handle unknown tags" has a backwards note. Removing a tag from a result variant is safe precisely because fewer tags = subtype (variant { ok } <: variant { ok; err } per spec §Variants). The opt wrapping technique is for the opposite scenario: adding new tags to a result variant so that old clients receive null for unrecognized tags. The note as written implies wrapping is required when removing a tag, which is wrong. Either change the note to something like "Callers depending on the removed tag will fail to match it" or add a separate row "Add a new variant tag to results | Only safe if wrapped in opt | Old clients receive null for unrecognized tags". Source: .sources/candid/spec/Candid.md §Variants ("For a specialised variant, the type of a tag can be specialised, or a tag can be removed") and §Note after variant rules.

  • Function subtyping — missing annotation equality constraint: The spec (.sources/candid/spec/Candid.md, line 885) states: "Viewed as sets, the annotations on the functions must be equal." The Function subtyping section on the page omits this entirely. This is a critical constraint: you cannot change a query function to a non-annotated (update) function or vice versa through subtyping. The section currently lists only the argument/result rules but makes no mention of this. Add a bullet: "Function annotations (query, composite_query, oneway) must be the same set on both sides — changing a method's annotation (e.g., query → update) is never a valid subtype."

  • didc subtype syntax — possible wrong argument format: The page shows didc subtype '(nat)' '(int)'. The didc subtype command (see .sources/candid/tools/didc/src/main.rs) takes two positional IDLType arguments — not IDLArgs. Parenthesized syntax (nat) is the argument list syntax used by didc encode and didc decode, not a type expression. The correct invocation for the subtype command should be didc subtype nat int (plain types without parentheses). This should be verified and corrected if wrong; a user running the parenthesized form may get a parse error.

Suggestions

  • Hash formula — undefined variable k: The formula hash(id) = (Sum_i utf8(id)[i] * 223^(k-i)) mod 2^32 uses k without defining it in the page. The spec defines k = |utf8(id)|-1. Consider adding this inline: where k = |utf8(id)| - 1 to match the spec and make the formula self-contained.

  • Primitive type table and language mapping table inconsistency for bool JavaScript: Line 22 (primitive types table) shows the JavaScript column for bool as `true`/`false` (literal values), but the language type mapping table at line 359 shows boolean (the type name). Both are defensible, but they are inconsistent within the same page. Pick one convention — either show the JS type name (boolean) or the value literals (true/false), and apply it consistently in both tables.

  • Grammar section — argtype named form presented as core production: The page shows <argtype> ::= <datatype> | <name> : <datatype> as if both are in the core grammar. Per the spec (§Shorthand: Named Parameters and Results), <name> : <datatype> is a shorthand that desugars to <datatype> — it is not part of the core grammar. Consider noting it is a shorthand, e.g.: <argtype> ::= <datatype> // or <name> : <datatype> (shorthand, name has no semantic effect).

  • didc bind target list is incomplete: The tool supports 7 targets: js, ts, did, mo, rs, rs-agent, rs-stub. The page lists only 4. The omission of rs-agent and rs-stub (which generate agent-style and stub bindings for Rust) may matter to Rust developers. Either add them or note that the list is partial.

  • composite_query note missing "from outside of IC" exception: The page says composite_query "cannot be called from query or update functions, and cannot cross subnets." The spec (.sources/candid/spec/Candid.md §Functions) adds "and from outside of IC" as an allowed caller context. Consider adding: "but can be called by external users (ingress messages)."

Verified

  • All type opcodes in the binary encoding table (0x7f through 0x68) match .sources/candid/spec/Candid.md §Types exactly.
  • Magic bytes DIDL (0x4449444c) confirmed against spec.
  • didc encode '(42, vec {1;2;-3})'4449444c016d7c027c002a0301027d matches the README example in .sources/candid/tools/didc/README.md verbatim.
  • didc decode '4449444c016d7c027c002a0301027d'(42, vec { 1; 2; -3; }) confirmed in same README.
  • didc encode '("hello")' -d service.did -m greet-d and -m flags are valid per .sources/candid/tools/didc/src/main.rs.
  • didc bind service.did -t js/ts/rs/mo — all four targets are valid per .sources/candid/tools/didc/src/main.rs.
  • didc hash "first_name" — command exists and the form is correct; hash value 1224700491 not independently verified (no test fixture in source).
  • nat <: int subtyping direction confirmed against spec §Primitive Types.
  • empty as bottom type and reserved as top type confirmed against spec §Primitive Types.
  • Option subtyping rules (covariance, null <: opt t, reserved <: opt t, non-optional T as subtype of opt t') match spec §Options.
  • Record subtyping (add fields, remove optional fields from inbound) matches spec §Records.
  • Service subtyping as record-of-functions confirmed against spec §Services.
  • bool encoding as 0x00/0x01 confirmed against spec §Memory.
  • text encoding as UTF-8 with length prefix confirmed against spec §Memory.
  • All internal links resolve: ../guides/canister-calls/candid.md resolves to candid.mdx (Astro resolves .md links to .mdx); ic-interface-spec.md exists.
  • No banned patterns: no dfx, no mo:base, no internetcomputer.org/docs/ links.
  • Frontmatter is complete: title, description, and sidebar.order present.
  • <!-- Upstream: --> comment present at end of page.
  • Type mapping tables for Motoko, Rust, JavaScript accurate per spec and portal reference (candid-ref.mdx).
  • float32 has no Motoko mapping — correctly noted in the page (confirmed via .sources/portal/docs/references/candid-ref.mdx).
  • Recursive type definitions and vacuous cycle rules match spec §Type Definitions.
  • Import semantics (import vs import service) match spec §Imports.
  • Service constructor syntax confirmed against spec §Services.

…mmar

- Fix misleading safe-upgrade table note: removing a variant tag is safe because fewer tags is a subtype; add separate row for adding tags (requires opt wrap)
- Add missing function annotation equality constraint to function subtyping section
- Fix didc subtype syntax: nat int (IDLType), not '(nat)' '(int)' (IDLArgs)
- Add k definition to hash formula: where k = |utf8(id)| - 1
- Fix bool JS type inconsistency: use 'boolean' in both tables
- Note that argtype named form is a shorthand in grammar section
- Add missing didc bind targets: rs-agent, rs-stub, did
- Add composite_query can be called from outside of IC (ingress messages)
@marc0olo
Copy link
Copy Markdown
Member Author

<!-- feedback-addressed -->

Feedback addressed:

Changes applied

Must fix

  • Safe upgrade table — variant tag removal note: Fixed. The original note "Must wrap variant in opt to handle unknown tags" was backwards — it described the technique for adding tags, not removing them. Updated the "Remove a variant tag from results" row to: "Fewer tags is a subtype; callers depending on the removed tag will fail to match it." Added a separate new row: "Add a new variant tag to results | Only safe if wrapped in opt | Old clients receive null for unrecognized tags when the variant is wrapped in opt." Verified against .sources/candid/spec/Candid.md §Variants.

  • Function subtyping — missing annotation equality constraint: Fixed. Added a new bullet to the Function subtyping section: "Function annotations (query, composite_query, oneway) must be the same set on both sides — changing a method's annotation (e.g., query → update) is never a valid subtype." Verified against spec line 885: "Viewed as sets, the annotations on the functions must be equal."

  • didc subtype syntax: Fixed. Changed didc subtype '(nat)' '(int)' to didc subtype nat int. Verified in .sources/candid/tools/didc/src/main.rs and .sources/candid/rust/candid_parser/src/grammar.lalrpop: the Subtype command takes two IDLType arguments parsed by TypParser, which does NOT accept parenthesized tuple syntax (nat). The parenthesized form is TupTyp/IDLArgs, used by encode/decode, not subtype.

Suggestions applied

  • Hash formula — undefined k: Fixed. Updated formula to: hash(id) = (Sum_i utf8(id)[i] * 223^(k-i)) mod 2^32 where k = |utf8(id)| - 1. Verified against spec line 372.

  • bool JavaScript inconsistency: Fixed. Changed the primitive types table (top of page) from literal values to boolean to match the language type mapping table. Both tables now consistently use the type name boolean.

  • Grammar section — argtype named form as shorthand: Fixed. Updated the grammar rule to note the named form is a shorthand per spec §Shorthand: Named Parameters and Results.

  • didc bind target list incomplete: Fixed. Added rs-agent, rs-stub, and did to the bind examples. All 7 targets verified in .sources/candid/tools/didc/src/main.rs.

  • composite_query missing "from outside of IC": Fixed. Updated the annotation description to note that composite_query can be called by external users (ingress messages). Verified against spec line 196.

Items skipped

None — all 3 must-fix items and all 5 suggestions were applied.

Build note

The build fails with a pre-existing error in docs/guides/backends/https-outcalls.mdx (missing .sources/examples file due to uninitialized submodule). This error was present before any changes in this PR and is unrelated to candid-spec.md. Confirmed by stashing changes and re-running the build — same error.

@marc0olo marc0olo merged commit 2af2ece into main Apr 16, 2026
1 check passed
@marc0olo marc0olo deleted the docs/reference-candid-spec branch April 16, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant