Skip to main content

Overview

Debugging AOT-compiled code differs from debugging interpreted JavaScript. Porffor provides several tools and techniques to help diagnose issues in compiled WebAssembly and native binaries.

Debug Mode

The -d flag enables debug mode, which includes additional information in compiled output:
porf -d input.js
Debug mode enables:
  • Function and variable names in Wasm
  • Debug logging during compilation
  • Source location information
  • Preserved symbol names
  • Better error messages

Using Debug Mode with Different Targets

porf -d input.js
Enables debug logging and better error messages during execution.

Disassembling WebAssembly

View Generated Instructions

Use the -f flag to print disassembled Wasm for your functions:
porf -f input.js
This shows the WebAssembly instructions generated for each function with debug annotations.

View Specific Function

To view a specific function’s Wasm:
porf -f=functionName input.js

Example Output

example.js
function add(a, b) {
  return a + b;
}

console.log(add(5, 3));
Disassemble:
porf -f=add -d example.js
Example output:
funcs:
  add (index: 0)
    local.get 0  # a
    local.get 1  # b
    f64.add      # a + b
    return

Using —disassemble Flag

For detailed disassembly:
porf --disassemble input.js
This provides:
  • Full instruction listing
  • Local variable information
  • Type information
  • Call graph details

The Debug Command

The debug command is very experimental. Expect rough edges and bugs.
Porffor includes an experimental source-level debugger:
porf debug input.js

Debug Command Features

Stepping:
  • Step Over: Execute current line, skip function calls
  • Step In: Enter function calls
  • Step Out: Execute until current function returns
  • Resume: Continue execution
Breakpoints:
  • Set breakpoints on specific lines
  • Execution pauses when breakpoint is hit
Inspection:
  • View current line
  • See call stack
  • Console output in dedicated panel

Debug Command Limitations

  • No variable inspection: Cannot view variable values
  • Limited expression evaluation: Cannot evaluate expressions at breakpoints
  • Function-level granularity: Stepping is per-line, not per-expression
  • No watch expressions: Cannot track specific variables
The debugger modifies your source code by injecting profiling calls, so debugging output may differ from normal execution.

Debugging Strategies

Adding Console Logging

The most reliable debugging technique:
function compute(values) {
  console.log('compute called with', values.length, 'values');
  
  let sum = 0;
  for (let i = 0; i < values.length; i++) {
    sum += values[i];
    console.log('i:', i, 'value:', values[i], 'sum:', sum);
  }
  
  console.log('final sum:', sum);
  return sum;
}
Compile and run:
porf compute.js

Using performance.now() for Timing

Profile sections of code:
function processData(data) {
  const t0 = performance.now();
  
  const step1Result = step1(data);
  console.log('Step 1:', performance.now() - t0, 'ms');
  
  const t1 = performance.now();
  const step2Result = step2(step1Result);
  console.log('Step 2:', performance.now() - t1, 'ms');
  
  const t2 = performance.now();
  const final = step3(step2Result);
  console.log('Step 3:', performance.now() - t2, 'ms');
  
  console.log('Total:', performance.now() - t0, 'ms');
  return final;
}

Bisecting Problems

Isolate issues by progressively commenting out code:
1

Verify basic structure

Start with minimal working version:
function process(data) {
  console.log('Starting');
  // Comment out everything else first
  return data;
}
2

Add code incrementally

Uncomment sections one at a time:
function process(data) {
  console.log('Starting');
  
  // Uncomment first step
  const step1 = transform(data);
  console.log('Step 1 done');
  
  // Uncomment second step
  // const step2 = validate(step1);
  // console.log('Step 2 done');
  
  return step1;
}
3

Find the failing section

When code breaks, the last uncommented section is the problem area.

Comparing with Node.js

Test behavior in Node.js to verify correctness:
# Run with Node.js
node input.js

# Run with Porffor
porf input.js
Differences indicate:
  • Porffor bugs
  • Unsupported features
  • Different runtime behavior

Debugging Native Binaries

Using GDB (Linux/macOS)

Compile with debug symbols:
porf native -d input.js output
Run with GDB:
gdb ./output
Basic GDB commands:
(gdb) run              # Start program
(gdb) break main       # Set breakpoint
(gdb) continue         # Continue after breakpoint
(gdb) step             # Step into
(gdb) next             # Step over
(gdb) print variable   # Print variable value
(gdb) backtrace        # Show call stack
Basic GDB commands:
(gdb) run              # Start program
(gdb) break main       # Set breakpoint
(gdb) continue         # Continue after breakpoint
(gdb) step             # Step into
(gdb) next             # Step over
(gdb) print variable   # Print variable value
(gdb) backtrace        # Show call stack

Using LLDB (macOS)

lldb ./output
Basic LLDB commands:
(lldb) run
(lldb) breakpoint set --name main
(lldb) continue
(lldb) step
(lldb) next
(lldb) frame variable
(lldb) bt
Basic LLDB commands:
(lldb) run
(lldb) breakpoint set --name main
(lldb) continue
(lldb) step
(lldb) next
(lldb) frame variable
(lldb) bt

Inspecting Generated C Code

Generate C code to understand what’s happening:
porf c -d input.js output.c
Open output.c to see:
  • How functions are compiled
  • Memory management
  • Type conversions
  • Control flow
This helps understand unexpected behavior.

Debugging WebAssembly

Using wasm2wat

Convert Wasm to text format:
porf wasm input.js output.wasm
wasm2wat output.wasm -o output.wat
Inspect output.wat to see:
  • Function structure
  • Local variables
  • Instruction sequence
  • Imports and exports

Browser DevTools

Some browsers support WebAssembly debugging:
  1. Load Wasm module in browser
  2. Open DevTools
  3. Find Wasm in Sources panel
  4. Set breakpoints
  5. Inspect stack and memory
Porffor Wasm doesn’t use WASI, so running in browser requires custom import setup.

Common Issues and Solutions

Check variable initialization:
// Problem
let data;
console.log(data.length);  // Error: undefined

// Solution
let data = [];
console.log(data.length);  // Works
Or add guards:
if (data && data.length > 0) {
  console.log(data[0]);
}
Porffor has limited scope support:
// Problem: nested scope
function outer() {
  let x = 1;
  function inner() {
    return x + 1;  // May fail
  }
  return inner();
}

// Solution: use parameters
function outer() {
  let x = 1;
  return inner(x);
}
function inner(x) {
  return x + 1;
}
Profile with the profile command:
porf profile input.js
Look for:
  • Functions called most frequently
  • Functions taking the most time
  • Unexpected memory allocations
Add logging for memory usage:
console.log('Memory:', Porffor.memorySize());
Enable debug mode to see detailed error:
porf -d input.js
Common causes:
  • Unsupported syntax (eval, Function constructor)
  • Complex async/await patterns
  • Scope issues
  • Parser errors
Try simplifying code:
# Try different parser
porf --parser=@babel/parser input.js

# Disable optimization
porf -O0 input.js

# Check TypeScript syntax
porf -t input.ts

The Profile Command

The profile command provides detailed performance information:
porf profile input.js

Profile Output

Shows for each function:
  • Call count: How many times called
  • Total time: Cumulative execution time
  • Average time: Mean time per call
  • Percentage: Portion of total execution time

Example Profile Session

example.js
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

function main() {
  const results = [];
  for (let i = 0; i < 30; i++) {
    results.push(fibonacci(i));
  }
  return results;
}

main();
Profile:
porf profile example.js
Output shows:
  • main called once, moderate time
  • fibonacci called thousands of times (recursive), most total time
  • Identifies performance bottleneck

Using Profile Data

Use profiling to:
  1. Find hot functions: Which functions consume most time
  2. Identify call patterns: How many times functions are called
  3. Guide optimization: Focus on expensive functions
  4. Measure improvements: Before/after optimization comparison

Debugging Checklist

1

Start with debug mode

Enable debug output:
porf -d input.js
2

Add console logging

Log at key points:
console.log('Checkpoint:', variable);
3

Isolate the problem

Comment out code sections to find the issue.
4

Compare with Node.js

Verify expected behavior:
node input.js  # Expected output
porf input.js  # Actual output
5

Inspect generated code

For Wasm:
porf -f input.js
For native:
porf c -d input.js output.c
cat output.c
6

Use the debugger if needed

For step-by-step execution:
porf debug input.js

Getting Help

If you’re stuck:
  1. Check documentation: Review guides and API reference
  2. Simplify code: Create minimal reproduction
  3. Try different flags: Test with -O0, different parsers, etc.
  4. Ask for help: Join the Porffor Discord

Next Steps

Profiling

Detailed performance analysis

Optimization

Make your code faster

TypeScript

Better type safety and debugging

Contributing

Report bugs and contribute fixes

Build docs developers (and LLMs) love