Debugging
C3 provides several powerful features and compiler flags to help identify memory corruption, logic errors, and performance bottlenecks.
Virtual Memory Temp Allocator (VMEM_TEMP)
Section titled “Virtual Memory Temp Allocator (VMEM_TEMP)”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).
How it works:
Section titled “How it works:”When VMEM_TEMP is enabled:
- Hardware Protection: The allocator uses the OS virtual memory system (MMU) to manage pages.
- Instant Crash: When a
@poolor test scope ends, the memory pages are removed or marked as protected. Any attempt to access “dead” temporary data will cause an immediate Segfault. - Large Address Space: It reserves a wide virtual address range (typically 4GB) to ensure allocations don’t overlap, making corruption much easier to catch.
Backtraces
Section titled “Backtraces”In Safe Mode (default), C3 automatically generates detailed backtraces when a panic or crash occurs.
Manual Backtraces:
Section titled “Manual Backtraces:”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);}Sanitizers
Section titled “Sanitizers”C3 supports integration with LLVM’s Address Sanitizer (ASAN) and Thread Sanitizer (TSAN).
Address Sanitizer (ASAN)
Section titled “Address Sanitizer (ASAN)”To enable ASAN, compile with:
c3c compile --sanitize=address my_project.c3ASAN will detect:
- Out-of-bounds access to heap, stack, and globals.
- Use-after-free bugs.
- Memory leaks.
Thread Sanitizer (TSAN)
Section titled “Thread Sanitizer (TSAN)”For multi-threaded applications, TSAN helps find data races:
c3c compile --sanitize=thread my_project.c3Tracking Allocator
Section titled “Tracking Allocator”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();}Allocation Tracking Macros
Section titled “Allocation Tracking Macros”For convenience, C3 provides macros to automatically wrap a block of code with a tracking allocator.
@report_heap_allocs_in_scope
Section titled “@report_heap_allocs_in_scope”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); // ... };}@assert_leak
Section titled “@assert_leak”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); };}Testing Macros
Section titled “Testing Macros”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`}Assertions and Unreachable
Section titled “Assertions and Unreachable”assert
Section titled “assert”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!");unreachable
Section titled “unreachable”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
unreachableinstruction. 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");}Contracts
Section titled “Contracts”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.
Safe vs. Fast Mode
Section titled “Safe vs. Fast Mode”Understanding the difference between modes is crucial for debugging:
| Feature | Safe Mode (-O0, -O1) | Fast Mode (-O2+) |
|---|---|---|
| Bounds Checking | Enabled | Disabled |
| Null Checks | Enabled | Disabled |
| Contracts | Evaluated | Ignored |
| Backtraces | Generated | Optional/None |
| Zero-Init | Guaranteed | Guaranteed |
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.