qip comply

qip comply is for turning a QIP component contract into an executable conformance check.

A comply module is a small conformance suite. It is not the implementation's own unit test suite, and it is not a wrapper around qip run. Current Compliance components drive the implementation through the host-owned qip bridge; legacy checkers import the implementation as impl. Both report whether the implementation obeys a reusable contract.

Use comply modules for behavior that more than one component may need to satisfy: "preserve empty input", "reject invalid UTF-8", "normalize phone numbers", or "accept exactly the Luhn-valid account numbers".

We usually write small comply modules in WebAssembly text (.wat) because the host contract is tiny and the memory layout is explicit. Larger spec suites can be written in a higher-level language when the checker needs parsing, embedded fixtures, or a large corpus.

Why A Separate Checker #

Think of qip comply as double-entry accounting for component behavior. The implementation and the comply module are written separately, then you check that the two agree.

That separation is useful because it reduces implementation bias. When possible, derive the comply module from a trusted specification, published examples, or a small independent oracle. For new behavior, writing the comply module first can serve the same role as test-driven development: it forces the contract to exist before implementation details start shaping the tests.

Note if both the implementation and the checker are copied from the same mistaken code path, they can still agree on wrong behavior.

Command #

qip comply <impl.wasm> [--with <check.wasm> ...] [-v|--verbose] [--timeout-ms <ms>]

Examples In This Repo #

# Expects that modules/utf8/e164.wasm produces normalized phone numbers, and preserves empty input.
qip comply modules/utf8/e164.wasm --with compliance/e164.comply.wasm --with compliance/preserve-empty.wasm

# Expects that modules/utf8/utf8-must-be-valid.wasm traps when provided a range of invalid UTF-8, and also accepts whitespace or empty strings untouched.
qip comply modules/utf8/utf8-must-be-valid.wasm --with compliance/trap-invalid-utf8.wasm --with compliance/preserve-empty.wasm --with compliance/preserve-whitespace.wasm

# Expects that modules/utf8/luhn.wasm accepts normalized Luhn-valid input and traps on invalid input.
qip comply modules/utf8/luhn.wasm --with compliance/luhn.comply.wasm

What It Does #

  1. Base validation (always):
  1. Static qip contract checks (always, when qip exports are present):
  1. Optional behavior checks (--with):

Base Contract Rules #

render component requires:

tile component requires:

Memory Model #

Current bridge components

Current Content Compliance components own their memory and export comply() -> i32. They declare cases through imports from the qip module rather than importing implementation memory directly:

set_uniform_u32 lets a compliance component exercise configuration as part of its corpus. The name is a lowercase uniform key such as currency; the host calls uniform_set_currency(i32) -> i32 on the implementation and returns the applied value to the compliance component. The call must happen between cases, not while an examination is open. It does not consume an ordinal or count as a case.

The compliance component remains responsible for changing its own oracle state after selecting an implementation uniform. A formatter checker can therefore select currency 392, declare its JPY cases, select 840, and declare its USD cases during one run without exporting a currency uniform itself.

Legacy shared-memory components

A comply module shares the implementation's linear memory. It usually imports:

The usual render checker loop is:

  1. Call input_ptr() and a capacity export.
  2. Reserve a scratch area inside the implementation input buffer.
  3. Write test input bytes at input_ptr.
  4. Call render(input_size).
  5. Read actual output from output_ptr() using the returned size.
  6. Compare actual output with the expected bytes.
  7. On mismatch, export failure detail pointers so the CLI can print the case.

Keep scratch memory simple. For example, compliance/luhn.comply.wat writes input at input_ptr and expected output at input_ptr + 256, after first requiring at least 512 bytes of input capacity. We prefer this style because the memory contract is visible in three lines and failures are easy to reproduce.

Check Component ABI #

A check component passed with --with must:

Optional:

Status convention:

Returning the number of checks that passed is a useful convention. Returning a negative scenario code is useful on failure. The host only requires the sign, but humans reading a failure log benefit from stable codes.

Positive And Negative Phases #

positive():

negative():

positive() and negative() run against separate fresh implementation instances. Separate --with modules also run independently and in parallel. A checker should therefore carry its own fixtures and setup, and should not depend on state left behind by another checker.

Use positive() for inputs the implementation must accept. Use negative() for inputs the implementation must reject by trapping. If invalid input should return an empty output instead of trapping, keep that in positive() and compare the empty output explicitly.

Authoring Strategies #

Choose the smallest checker shape that teaches the contract.

calls render(0) and expects zero bytes.

e164.comply.wat embeds concrete inputs and expected normalized outputs.

generate. luhn.comply.wat builds many valid numbers, computes their check digit, and expects the implementation to normalize them.

Luhn generates a valid number, flips one digit, and requires a trap.

The CommonMark checker embeds the upstream spec examples and runs them as a conformance corpus.

The tradeoff is complexity. A table check is easier to review, but it can miss whole classes of bugs. A generated checker covers more space, but it must contain an independent oracle that is simpler than the implementation under test. If the checker re-implements the same complicated algorithm, you have two places to hide the same mistake.

Luhn As A Pattern #

The Luhn comply module shows the current pattern for an algorithmic checker:

implementation input buffer into input and expected-output regions.

failure_actual_output_*, so failures show the exact bytes that reproduced the mismatch.

check digit from generated prefixes.

cases without a large fixture table.

output, while negative() uses qip.render_must_trap for empty, too-short, malformed, and checksum-invalid input.

That pattern generalizes well. For a validator, generate known-good values, assert the normalized output, then perturb one property at a time and assert rejection. For a formatter, generate representative inputs and assert stable canonical output. For a parser, carry a corpus and include failure messages that identify the source case.

Failure Detail Exports (Optional) #

If a check fails, qip comply tries to print reproducible failure context from optional exports on the check component.

Input detail:

Message detail:

Expected vs actual output detail:

Legacy output detail fallback:

Aliases with a fail_ prefix are also accepted. Prefer the failure_ names for new modules.

Minimal WAT Example #

(module
  (import "impl" "memory" (memory 1))
  (import "impl" "input_ptr" (func $input_ptr (result i32)))
  (import "impl" "render" (func $render (param i32) (result i32)))
  (import "impl" "output_ptr" (func $output_ptr (result i32)))

  (func (export "positive") (result i32)
    ;; Write input at (call $input_ptr), call $render, compare output from
    ;; (call $output_ptr), and return >0 on pass.
    (drop (call $render (i32.const 0)))
    (i32.const 1))
)

Negative Trap Example #

(module
  (import "impl" "memory" (memory 1))
  (import "qip" "render_must_trap" (func $render_must_trap (param i32) (result i32)))

  (func (export "positive") (result i32)
    (i32.const 1))

  (func (export "negative") (result i32)
    (if (i32.ne (call $render_must_trap (i32.const 0)) (i32.const 1))
      (then (return (i32.const -1))))
    (i32.const 1))
)

Authoring Workflow #

  1. Start with the behavior sentence: "This component accepts X, normalizes to Y,

and rejects Z."

  1. Pick the strategy: fixture, table, generated oracle, mutation, or corpus.
  2. Reserve scratch memory and check capacity before writing.
  3. Export failure detail before returning a failing status.
  4. Build the implementation and compliance module.
  5. Run qip comply impl.wasm --with compliance.wasm.
  6. Add the command to make -j test coverage when the contract should protect

the repo permanently.

When Not To Use It #

Do not reach for qip comply when a plain snapshot or unit test would explain the behavior better. Compliance modules are best when the contract should be portable across implementations, when a component must reject invalid input in a specific way, or when the checker itself can be a useful executable spec.