Skip to content

Debugging

C3 provides several powerful features and compiler flags to help identify memory corruption, logic errors, and performance bottlenecks.

The temporary allocator (tmem) is extremely fast but can lead to “use-after-scope” bugs if pointers to temporary data are stored in globals or long-lived structs.

To debug these issues, you can enable the Virtual Memory tracking mode by passing the -D VMEM_TEMP flag to the compiler (or adding "VMEM_TEMP" to your project.json features).

When VMEM_TEMP is enabled:

  1. Hardware Protection: The allocator uses the OS virtual memory system (MMU) to manage pages.
  2. Instant Crash: When a @pool or test scope ends, the memory pages are removed or marked as protected. Any attempt to access “dead” temporary data will cause an immediate Segfault.
  3. Large Address Space: It reserves a wide virtual address range (typically 4GB) to ensure allocations don’t overlap, making corruption much easier to catch.

In Safe Mode (default), C3 automatically generates detailed backtraces when a panic or crash occurs.

You can capture a backtrace at any time as a string:

import std::os::backtrace;
fn void log_stack() {
String bt = backtrace::get(tmem)!;
io::eprint(bt);
}

C3 supports integration with LLVM’s Address Sanitizer (ASAN) and Thread Sanitizer (TSAN).

To enable ASAN, compile with:

Terminal window
c3c compile --sanitize=address my_project.c3

ASAN will detect:

  • Out-of-bounds access to heap, stack, and globals.
  • Use-after-free bugs.
  • Memory leaks.

For multi-threaded applications, TSAN helps find data races:

Terminal window
c3c compile --sanitize=thread my_project.c3

The TrackingAllocator is a wrapper that can be placed around any other allocator to detect memory leaks and capture backtraces for every allocation.

fn void main() {
TrackingAllocator tracker;
tracker.init(mem); // Wrap the default 'mem' allocator
defer tracker.free();
Allocator a = &tracker;
// Use 'allocator::new' to pass a specific allocator:
int* p = allocator::new(a, int);
// If not freed, tracker.print_report() will show any leaks.
tracker.print_report();
}

For convenience, C3 provides macros to automatically wrap a block of code with a tracking allocator.

This macro runs the enclosed code and automatically prints a full memory report at the end of the scope.

fn void main() {
@report_heap_allocs_in_scope()
{
void* p = mem::malloc(100);
// ...
};
}

Similar to the report macro, but instead of just printing, it will assert that no memory has leaked. If leaks are found, it triggers a panic with a report.

fn void main() {
@assert_leak()
{
// code that should not leak
void* p = mem::malloc(64);
mem::free(p);
};
}

C3 includes a built-in testing framework in std::core::test. These macros provide descriptive failure messages, stringifying the expressions being tested.

fn void test_math() @test {
int x = 10;
int y = 20;
test::eq(x + y, 40);
// Test failed ^^^ ( example.c3:4 ) `30` != `40`
}

Used for runtime checks that should always be true. In Safe Mode, a failed assertion triggers a panic with a backtrace. In Fast Mode, assertions are removed.

assert(divisor != 0, "Cannot divide by zero!");

Marks a code path that logically should never be hit.

  • Safe Mode: Triggers a panic with the provided message and a backtrace.
  • Fast Mode: Generates an LLVM unreachable instruction. This is an optimization hint telling the compiler this path is impossible. If the path is actually reached, the program will have undefined behavior (which often manifests as a crash or very strange execution state).
switch (state) {
case START: // ...
case END: // ...
default: unreachable("Invalid state encountered");
}

C3 supports Contracts using the @require and @ensure attributes. These are checked in Safe Mode.

  • @require: Pre-conditions that must be true when the function is called.
  • @ensure: Post-conditions that must be true when the function returns.
<*
@require b != 0 : "Divisor must not be zero"
@ensure return == a / b
*>
fn float divide(float a, float b)
{
return a / b;
}

If a contract is violated in safe mode, the program panics with a descriptive message and a backtrace.

Understanding the difference between modes is crucial for debugging:

FeatureSafe Mode (-O0, -O1)Fast Mode (-O2+)
Bounds CheckingEnabledDisabled
Null ChecksEnabledDisabled
ContractsEvaluatedIgnored
BacktracesGeneratedOptional/None
Zero-InitGuaranteedGuaranteed

Always perform your primary development and testing in Safe Mode. Switch to Fast Mode only for final releases or performance profiling once the logic is verified.