Skip to main content

Printing and Debugging

Sector9 provides Debug.print for development-time output and debug_show for converting values to text. In verification, debug output itself is erased, but argument expressions are still checked and evaluated for their normal proof obligations.

The Debug Module​

Import the Debug module to access printing functions:

import Debug "mo:core/Debug";

Debug.print​

Prints text to the output stream:

Debug.print("Hello, world!");
Debug.print("Counter value: " # debug_show(42));

On the Internet Computer, output goes to the canister log. In the interpreter or standalone Wasm, output goes to standard out.

trap​

Terminates execution if a condition is true:

trap (amount > balance)("Insufficient funds");
trap (user == null)("User not found");

When the condition evaluates to true, execution halts and the caller's await receives an Error with code #canister_error. For verification, trap also marks that code path as unreachable, so the verifier must prove execution cannot continue when the condition holds.

The debug_show Prefix​

Convert any value to its text representation using debug_show:

let n : Nat = 42;
let pair = (1, "hello");
let record = { x = 10; y = 20 };

Debug.print(debug_show(n)); // "42"
Debug.print(debug_show(pair)); // "(1, \"hello\")"
Debug.print(debug_show(record)); // "{x = 10; y = 20}"

The debug_show prefix is purely computational and produces no side effects. It works with any type.

Debug Blocks​

Wrap debug code in debug { } blocks to enable conditional compilation:

debug {
Debug.print("This only runs in debug mode");
};

Debug blocks must return (). This is invalid:

debug { 42 };  // ERROR: expected (), got Nat

Release Mode​

Compile with --release to strip all debug blocks:

sector9 -c --release file.sr9

In release mode:

  • debug { } blocks are completely erased
  • Debug.print calls outside debug blocks still execute

Use debug blocks for development output that should not appear in production.

Debug vs Verification Assertions​

Debug output and verification assertions serve different purposes:

FeaturePurposeRuntime EffectVerification Effect
Debug.printDevelopment outputPrints textPrint erased; arguments still checked
debug_showValue to textComputes textModeled as a deterministic value for its argument type
debug { }Conditional debug codeRuns in debug modeBody expressions are still checked and modeled where evaluated
assertProof checkpointNone (erased)Must prove true
runtime:assertSafety checkTraps if falseMust prove true

Debug output is erased during Viper translation. The expressions you pass to it still matter: bounds checks, calls, debug_show arguments, and other evaluated subexpressions keep their normal proof obligations. A debug { ... } block is not a way to hide unsafe code from the verifier.

Primitive Print Functions​

The core Debug module wraps primitive print operations. Direct primitive imports are mainly for the core library and low-level tests:

import Prim "mo:⛔";

Prim.debugPrint("text"); // Print text
Prim.debugPrintNat(42); // Print Nat
Prim.debugPrintInt(-5); // Print Int
Prim.debugPrintChar('x'); // Print Char

Prefer Debug.print with debug_show for most uses.

Common Patterns​

Tracing Execution​

public func transfer(from : Principal, to : Principal, amount : Nat) : async Bool
modifies balances
{
debug {
Debug.print("transfer: " # debug_show({ from; to; amount }));
};

// ... implementation
}

State Inspection​

public func debugState() : async () {
Debug.print("balance = " # debug_show(balance));
Debug.print("counter = " # debug_show(counter));
Debug.print("config = " # debug_show(config));
}

Upgrade Debugging​

system func preupgrade() {
Debug.print("preupgrade: saving state");
Debug.print("items = " # debug_show(Array.size(items)));
}

system func postupgrade() {
Debug.print("postupgrade: state restored");
}

Memory Metrics​

import Prim "mo:⛔";

Debug.print(debug_show({
memory = Prim.rts_memory_size() / (1024 * 1024);
heap = Prim.rts_heap_size() / (1024 * 1024);
}));

Restrictions​

No Debug in Pure Functions​

Pure functions cannot use Debug.print because it performs I/O:

pure func square(n : Nat) : Nat {
Debug.print("computing"); // ERROR: side effect in pure function
n * n
}

No Debug in Lemmas​

Lemmas are proof-only and cannot contain runtime code:

lemma func addCommutes(a : Nat, b : Nat) : ()
ensures a + b == b + a
{
Debug.print("proving"); // ERROR: not allowed in lemma
}

Debug Blocks Must Return Unit​

Debug blocks must have type ():

debug { Debug.print("ok") };  // OK: returns ()
debug { 42 }; // ERROR: returns Nat
debug { if (x > 0) { } }; // OK: if without else returns ()

When to Use Debugging vs Verification​

Use Debug.print When​

  • Exploring code behavior during development
  • Tracing execution flow to understand logic
  • Inspecting values at specific program points
  • Debugging upgrade migrations

Use Verification Assertions When​

  • Documenting invariants that must always hold
  • Proving specified properties for symbolic inputs
  • Catching bugs before deployment
  • Expressing safety-critical properties

Debug output helps you understand what your code does. Verification checks that the code satisfies its written specifications.

Best Practices​

Wrap Development Output in Debug Blocks​

// GOOD: stripped in release
debug { Debug.print("debug info"); };

// AVOID: remains in production
Debug.print("debug info");

Use Structured Output​

// GOOD: structured and readable
Debug.print(debug_show({ operation = "transfer"; from; to; amount }));

// AVOID: hard to parse
Debug.print("transfer from " # debug_show(from) # " to " # debug_show(to));

Remove Debug Code Before Verification​

Debug output adds noise without proof benefit. Wrap it before compiling production builds; the verifier still checks the wrapped expressions when they remain in the verified source:

public func compute(n : Nat) : async Nat
ensures result == n * 2
{
// Debug output doesn't affect verification,
// but wrapping keeps code clean for production
debug { Debug.print("computing " # debug_show(n)); };
n * 2
}

Summary​

  • Debug.print outputs text for development; the print is erased in Viper, but its arguments are still checked
  • debug_show converts values to text representation and is modeled deterministically in proofs
  • debug { } blocks compile conditionally; stripped with --release, but still checked by the verifier while present in source
  • Debug utilities are erased during Viper translation
  • Pure functions and lemmas cannot use debug output
  • Use verification assertions for correctness proofs, debug output for development exploration
  • Wrap development output in debug blocks for clean production builds