Skip to content

aprender-contracts-macros: #[contract] fails open in four places, and the doc comment claims the opposite #2699

Closed as not planned

Description

@noahgift

Filed from paiml/rmedia, where #[contract] sat on 16 call sites enforcing nothing for
months
while CLAUDE.md advertised "compile-time enforcement". Three of the four causes
were rmedia's own and are being fixed there (paiml/rmedia#401). These four are upstream, and
they are what made the rmedia-side bugs silent rather than loud.

All line numbers are aprender-contracts-macros-0.64.0 as published — verified identical to
crates/aprender-contracts-macros/src/lib.rs in this repo.

1. The doc comment states the opposite of the code

src/lib.rs:26:

//! 2. `#[contract("name", equation = "eq")]` expands to a `const` that reads
//!    the corresponding env var via `env!()`. Missing env var = compile error.

src/lib.rs:174 and :200:

const #const_name: Option<&str> = option_env!(#env_key);

option_env! yields None. A missing env var is a silent no-op, not a compile error.
This is the sentence a downstream reader relies on when deciding whether the attribute is
load-bearing — and it is the reason nobody in rmedia questioned it for months.

2. read_contract_assertions treats "no contract" and "no data" as the same thing

src/lib.rs:240-246:

let count: usize = std::env::var(&count_key).ok().and_then(|v| v.parse().ok()).unwrap_or(0);

Absent var → 0 → zero assertions → the function is exactly as unconstrained as one with no
attribute at all. Its own comment says "If no env vars are found (e.g., crates.io build),
returns empty vec (graceful)."
Graceful is right for a crates.io consumer who never ran
build.rs; it is wrong for a workspace whose build.rs ran and emitted the wrong names, which
is precisely what happened in rmedia. The two cases are indistinguishable to the caller.

3. An unparseable condition is skipped silently

src/lib.rs:~258:

// If parsing fails, skip silently (the expression may not be valid Rust)

A YAML precondition with a typo, or written in a dialect that is not Rust, produces no
assertion and no diagnostic. The contract still reads as enforced at every call site.

4. make_env_key is private, so every producer must re-derive it

src/lib.rs has:

fn make_env_key(contract: &str, equation: &str) -> String {
    let contract_part = contract.to_uppercase().replace(['-', '.'], "_");
    let equation_part = equation.to_uppercase().replace(['-', '.'], "_");
    format!("CONTRACT_{contract_part}_{equation_part}")
}

It is private, and aprender-contracts exports no equivalent (grepped 0.64.0). So every
build.rs that wants to feed this macro must reimplement the key format by hand — two
derivations of one truth, guaranteed to drift.

They did drift. rmedia's build.rs emitted PRE_{stem}_{eq}_{i}; the macro reads
{env_key}_{PRE|POST}_COUNT then {env_key}_{PRE|POST}_{i}. Measured with the registry
present and gaps == 0:

warning: [contract] AllImplemented: 1/1 implemented, 0 partial, 0 gaps
warning: [contract] PRE/POST: 210 preconditions, 335 postconditions emitted
NO PANIC: precondition t.is_finite() not enforced; got Color { r: 0, g: 0, b: 0 }

$ grep -c 'PRE_COUNT' target/debug/build/rmedia-core-*/output
0

545 conditions emitted under names nothing reads, and every layer reported success.

Suggested fixes, roughly in value order

  1. Export the key builder. pub fn env_key(contract, equation) -> String from
    aprender-contracts, so a producer imports it instead of copying it. This alone would
    have prevented the whole rmedia incident.
  2. Make the doc comment true — either switch to env!() (breaking for crates.io
    consumers), or correct the sentence. Right now it is worse than no documentation.
  3. Distinguish "not built with contracts" from "built and got nothing." An explicit
    opt-out (feature flag, or a CONTRACT_ENFORCEMENT=off var) would let the absent case be
    loud by default and quiet on request, instead of always quiet.
  4. Emit a compile_error! or at minimum a warning when a condition fails to parse,
    rather than dropping it.

Note on scope

Also worth stating: the macro emits debug_assert!, so enforcement is debug-only and
compiles out in release. That is a defensible design choice, but the crate description
("Proc macros for compile-time contract enforcement") and §2 of the module doc together
imply something considerably stronger than what ships.

Downstream tracking: paiml/rmedia#401.

Activity

  1. noahgift commented on Sep 5, 2026

    @noahgift
    MemberAuthor

    Blocks PP-066 P-1.1 (spec §5 Track P) — parented under #2870 per the epic body (Track P lives under #2870 → #2556).

    #2870's ticket map: phase 1.1 = PP-066 P-1.1 (#[contract] fails closed: generated registry, compile_error!, explicit optional, lib.rs:26 fixed, trybuild table) with P-1.2 repointing the 10 dangling sites — the card that lets R-0's discover() carry a #[contract] that means something. Comment names the card.

    PP-066 triage 2026-09-05 (issue #2873); ledger docs/audits/pp-066-triage-2026-09-05.md.

  2. added this to the 0.69.0 milestone on Sep 10, 2026
  3. added
    bugSomething isn't working
    P1High priority
    on Sep 11, 2026
  4. modified the milestones: 0.69.0, 0.73.0, 0.70.0 on Sep 17, 2026
  5. noahgift commented on Sep 23, 2026

    @noahgift
    MemberAuthor

    triage 2026-09-23 (aprender-dd, standing triage owner): milestoned to 0.70.0, theme Fast Train (CI, gates, pv, release tooling, hygiene); it moved out of backlog (the 2026-09-17 overflow bucket, not a release).

  6. added
    kind:codeWork is a code change (derived rule, #4159)
    on Sep 24, 2026
  7. modified the milestones: 0.70.0, backlog on Sep 24, 2026
  8. noahgift commented on Sep 25, 2026

    @noahgift
    MemberAuthor

    Parked by APR-EPIC-001: over S2 budget (rank 4/38). Reopen = pull. Applied by aprender-d1 under a 3-lane quorum receipt (handoff/balance-quorum-rev4.json).

  9. added
    parkedClosed over epic budget (APR-EPIC-001 rule 5). Reopen = pull.
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High prioritybugSomething isn't workingkind:codeWork is a code change (derived rule, #4159)parkedClosed over epic budget (APR-EPIC-001 rule 5). Reopen = pull.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions