Debugging Workflow

April 25, 2026 · View on GitHub

Step-by-step debugging for common failures in the Beamtalk compiler and runtime.

Compiler Crashes

# 1. Enable panic backtraces
RUST_BACKTRACE=1 beamtalk build failing.bt

# 2. Identify which phase failed
# Lexer error:     "unexpected character at line X, column Y"
# Parser error:    "expected X, found Y"
# Codegen error:   "failed to generate code for ..."

# 3. Create minimal repro case
echo "minimal failing code" > test.bt
beamtalk build test.bt

# 4. Add debug output in relevant layer
# For parser: add dbg!(&ast) in crates/beamtalk-core/src/source_analysis/parser/mod.rs
# For codegen: add dbg!(&expr) in crates/beamtalk-core/src/codegen/core_erlang/

Runtime Errors

# 1. Inspect generated Core Erlang
cat build/module_name.core | less

# Look for:
# - Function definitions ('functionName'/Arity)
# - Pattern matches (case ... of)
# - Error calls (call 'erlang':'error')

# 2. Test generated BEAM in Erlang shell
# Single-file build outputs to ./build with bt@ prefix
erl -pa build
1> 'bt@module_name':function_name(Args).

# 3. Enable Erlang debug traces
2> dbg:tracer().
3> dbg:p(all, c).
4> dbg:tpl('bt@module_name', '_', []).
5> 'bt@module_name':function_name(Args).

Test Failures

# 1. Run single test with output
cargo test test_name -- --nocapture

# 2. Check what the test expects
# - Snapshot test: see tests/snapshots/*.snap
# - REPL-protocol test: see tests/repl-protocol/cases/*.btscript
# - Unit test: read test source

# 3. Update snapshots if intentional
cargo test test_name
# Review changes in git diff
cargo insta accept

# 4. Run all tests in module
cargo test --test module_name

REPL-Protocol Test Failures

# 1. Check REPL daemon logs
just test-repl-protocol 2>&1 | tee repl-protocol.log
grep "ERROR\|Warning\|failed" repl-protocol.log

# 2. Test fixture manually
cd tests/repl-protocol/fixtures
../../target/debug/beamtalk build counter.bt
cat build/counter.core

# 3. Run REPL interactively
beamtalk repl
> :load tests/repl-protocol/fixtures/counter.bt
> Counter spawn
> c increment

# 4. Check expected output in test file
cat tests/repl-protocol/cases/actors.btscript
# Look for // => expected output comments

Codegen Debugging

# 1. Generate and inspect Core Erlang
beamtalk build failing.bt
cat build/failing.core

# 2. Look for suspicious patterns:
# - Missing State/Self parameters
# - Unbound variables (StateX, State1, etc.)
# - Wrong function arities
# - Call to undefined functions

# 3. Compare with working example
beamtalk build examples/counter.bt
diff build/counter.core build/failing.core

# 4. Add codegen debug output
# Edit crates/beamtalk-core/src/codegen/core_erlang/expressions.rs
dbg!(&expr);
// Rebuild and check output

Runtime/REPL Debugging

# 1. Check if modules loaded
beamtalk repl
> Beamtalk allClasses
> Beamtalk classNamed: #Counter

# 2. Enable CLI diagnostics for the REPL
RUST_LOG=beamtalk=debug beamtalk repl

# 3. Run the node in the foreground (no daemonization)
beamtalk repl --foreground

# 4. Check actor state
> c := Counter spawn
> c class
> c respondsTo: #increment

# 5. Inspect Erlang process state
# In separate terminal:
erl -name debug@127.0.0.1 -setcookie beamtalk
(debug@127.0.0.1)1> nodes().
(debug@127.0.0.1)2> observer:start().
# Find beamtalk_repl process, inspect state

Performance Debugging

# 1. Profile compilation
time beamtalk build large_file.bt

# 2. Profile runtime (connect Erlang shell to running node)
# Start REPL in one terminal, then in another:
erl -remsh beamtalk@localhost -name profiler@localhost
1> timer:tc(fun() -> 'bt@counter':spawn() end).
{TimeInMicroseconds, Result}

# 3. Check memory usage (Observer in Erlang shell)
1> observer:start().
% Use the Memory tab to see allocation by process

# 4. Flame graphs (advanced)
# Enable Erlang profiling
erl -pa build
1> fprof:apply(Module, Function, Args).
2> fprof:profile().
3> fprof:analyse().

Codegen Diagnostics (BT-1343)

The compiler can emit detailed diagnostics about code generation decisions. These are off by default (too noisy for normal use) and gated behind environment variables.

Environment Variables

VariableEffect
BEAMTALK_CODEGEN_DIAGNOSTICS=1Enable all codegen diagnostics (info-level hints)
BEAMTALK_WARN_STATEACC=1Promote StateAcc fallback diagnostics to warning level (requires BEAMTALK_CODEGEN_DIAGNOSTICS=1)
# See all codegen decisions
BEAMTALK_CODEGEN_DIAGNOSTICS=1 beamtalk build myfile.bt

# Highlight StateAcc fallbacks as warnings
BEAMTALK_CODEGEN_DIAGNOSTICS=1 BEAMTALK_WARN_STATEACC=1 beamtalk build myfile.bt

Diagnostic Categories

1. Block calling convention chosen

Reports which optimization mode was selected for each stateful loop:

  • direct-params — pure locals, no field mutations (BT-1275)
  • tuple-acc — local mutations in foldl list ops (BT-1276)
  • hybrid — locals + field reads/mutations as direct params (BT-1326)
  • StateAcc — fallback map-based threading

Example: Loop at line 42: using direct-params (3 locals, 0 field mutations)

2. StateAcc fallback reason

When falling back to StateAcc, includes the specific reason:

  • self-send in loop body
  • nested list op with cross-scope mutation
  • tier-2 value call on threaded local
  • inline conditional writing to threaded local
  • condition has state effects
  • control-flow sub-expression with mutations

Example: Loop at line 15: StateAcc fallback — self-send in loop body

3. Non-local return (^) in block

Emitted when ^ inside a block generates throw/catch, which can prevent BEAM JIT from optimizing the enclosing function.

Example: Non-local return at line 15: compiled via throw/catch, may inhibit JIT optimization

4. Synchronous self-send in loop

Flags deadlock risk when a loop body sends a message to self.

Example: Self-send 'self bar' inside loop at line 30: synchronous call to own mailbox, potential deadlock

5. Dynamic dispatch fallback

When a message send can't be statically resolved and uses runtime dispatch via beamtalk_message_dispatch:send/3.

Example: Send 'foo:' at line 23: dynamic dispatch (receiver type unknown)

6. Large extracted arity

Informational when a loop extracts >8 parameters as direct fun arguments.

Example: Loop at line 10: 14 extracted params

When All Else Fails

  1. Simplify — Remove code until it works, then add back
  2. Compare — Find similar working code, diff against it
  3. Ask — Share error + what you tried, get fresh eyes
  4. Rubber duck — Explain the problem out loud to yourself
  5. Sleep — Come back tomorrow with fresh perspective