Skip to content

Debugging

Roger Johansson edited this page Jan 14, 2026 · 1 revision

Debugging

Debugging aids and diagnostic tools in Asynkron.JsEngine.


AST-Free Execution Assertion

File: EvaluationContext.cs

Runtime detection of AST evaluation during IR-only execution paths.

Usage

#if DEBUG
var originalValue = EvaluationContext.AssertNoAstEvaluation;
try
{
    EvaluationContext.AssertNoAstEvaluation = true;

    // Execute code - will throw if AST evaluation is triggered
    await engine.Evaluate(program);
}
catch (InvalidOperationException ex)
{
    // AST evaluation was invoked during IR execution
    // Example: "AST evaluation invoked for BinaryExpression during IR execution"
    Console.WriteLine(ex.Message);
}
finally
{
    EvaluationContext.AssertNoAstEvaluation = originalValue;
}
#endif

How It Works

  • Flag is checked in ExpressionNode.EvaluateExpression() and StatementNode.EvaluateStatementJsValue()
  • When enabled, throws InvalidOperationException with node type information
  • Only available in DEBUG builds (compiled out in RELEASE)
  • Used to validate IR/bytecode coverage as it increases

Testing

See tests/Asynkron.JsEngine.Tests/AstFreeExecutionAssertionTests.cs for examples.


Pool Debug Invariants

The engine uses a two-tier system to catch pooling bugs (double-lease, use-after-return, async races).

Tier 1: PoolDebug (DEBUG-only)

File: PoolDebug.cs

Uses ConditionalWeakTable<object, LeaseState> to track ownership:

Method Purpose
MarkLeased(object) Called on rent - throws if already leased
MarkReturned(object) Called on return - throws if not leased
AssertOwned(object, string) Verifies object is currently owned

All methods use [Conditional("DEBUG")] - zero overhead in RELEASE builds.

[Conditional("DEBUG")]
public static void MarkLeased(object item)
{
    if (!LeaseTable.TryGetValue(item, out var state))
    {
        state = new LeaseState();
        LeaseTable.Add(item, state);
    }

    if (state.IsLeased)
        throw new InvalidOperationException("Double-lease detected!");

    state.IsLeased = true;
}

Tier 2: PoolGuard (Runtime, optional)

File: PoolGuard.cs

Uses lease IDs to detect cross-async mismatches:

  • Enable via JSENGINE_DEBUG_POOL_GUARDS=true environment variable
  • Each rent gets a unique atomic lease ID
  • Objects verify their lease ID during operations
public static bool Enabled { get; } =
    Environment.GetEnvironmentVariable("JSENGINE_DEBUG_POOL_GUARDS") == "true";

private static long _leaseIdCounter;

public static long NextLeaseId() => Interlocked.Increment(ref _leaseIdCounter);

Usage Pattern

Pooled objects expose both layers:

// Runtime guard (when JSENGINE_DEBUG_POOL_GUARDS=true)
state.MarkLeased(PoolGuard.NextLeaseId());
state.AssertLease(expectedLeaseId, "for-of iterator state");
state.MarkReturned();

// DEBUG-only guard
state.MarkLeasedDebug();
state.AssertOwnership("for-of iterator state");
state.MarkReturnedDebug();

In hot loops:

var stateLeaseId = PoolGuard.Enabled ? state.PoolLeaseId : 0;

while (!context.ShouldStopEvaluation)
{
    if (stateLeaseId != 0)
        state.AssertLease(stateLeaseId, "for-of iterator state");
    state.AssertOwnership("for-of iterator state");
    // ...
}

What These Catch

  1. Double-lease - object rented while still in use
  2. Use-after-return - object accessed after returned to pool
  3. Async races - iterator state used with wrong environment due to async interleaving

Realm Logger

Use FakeLogger with DebugMode = true to capture slot hits/misses.

Setup

using Microsoft.Extensions.Logging.Testing;

var fakeLogger = new FakeLogger();
var engine = new JsEngine(new JsEngineOptions
{
    DebugMode = true,
    Logger = fakeLogger
});

Inspection

var logs = fakeLogger.Collector.GetSnapshot();
foreach (var entry in logs)
{
    Console.WriteLine($"[{entry.Level}] {entry.Message}");
}

Example Assertions

// Ensure no slot read misses
Assert.Empty(logs.Where(l => l.Message.Contains("SlotMiss")));

// Confirm expected hits for identifiers
Assert.Contains(logs, l => l.Message.Contains("SlotHit: i"));

AST Slot Metadata Checks

Scope analysis stamps AST nodes with slot information.

Verification

var parsed = engine.ParseProgram(script);
var runDecl = (FunctionDeclaration)parsed.Body[0];
var slotMap = runDecl.Function.SlotMap;

// Verify expected slots exist
Assert.True(slotMap.ContainsKey(Symbol.Create("i")));
Assert.True(slotMap.ContainsKey(Symbol.Create("sum")));

Available Metadata

Property Type Description
ScopeId int Unique scope identifier
SlotCount int Number of slots in this scope
SlotMap ImmutableDictionary<Symbol, int> Symbol -> slot index
RootSlotMap ImmutableDictionary<Symbol, int>? Function-scope slots
FlatSlotMappings ImmutableDictionary<int, ...>? Flat slot assignments

Debug Mode Features

Enable with JsEngineOptions.DebugMode = true:

  1. IR Trace Logging - Each instruction logged with program counter
  2. Slot Hit/Miss Logging - Variable access patterns
  3. Pool State Logging - Rent/return operations
  4. Assertion Checks - Additional runtime validation

Example IR Trace

[IR:  0] PushEnvironment(scopeId=1, slots=2)
[IR:  1] SimpleVariableDeclaration(i, 0)
[IR:  2] Branch(condition, then=3, else=10)
[IR:  3] IncrementSlot(i, flatSlotId=0)
[IR:  4] Jump(2)

Test Bomb Methodology

For systematic debugging of complex failures. See agents/how-to-test-bombs.md.

Concept

  1. Create minimal failing test case
  2. Add assertions at each stage
  3. Binary search for failure point
  4. Add more assertions around failure
  5. Repeat until root cause found

Example

[Fact]
public void TestBomb_LoopCounter()
{
    var engine = new JsEngine();

    // Stage 1: Parse
    var program = engine.ParseProgram("for (let i = 0; i < 3; i++) { }");
    Assert.NotNull(program);

    // Stage 2: Check slot assignment
    var loop = (ForStatement)program.Body[0];
    Assert.True(loop.Init.SlotIndex >= 0, "Loop counter should have slot");

    // Stage 3: Execute
    var result = engine.Evaluate(program);
    Assert.Equal(JsValue.Undefined, result);
}

Layered Tests Methodology

Stage-by-stage verification. See agents/how-to-layered-tests.md.

Layers

  1. Parse Layer - AST structure correct
  2. Analysis Layer - Scope/slot analysis correct
  3. IR Layer - Instructions generated correctly
  4. Execution Layer - Runtime behavior correct

Example

[Fact]
public void Layered_ForLoop()
{
    var engine = new JsEngine();
    var script = "for (let i = 0; i < 3; i++) { sum += i; }";

    // Layer 1: Parse
    var program = engine.ParseProgram(script);
    var forStmt = Assert.IsType<ForStatement>(program.Body[0]);

    // Layer 2: Analysis
    Assert.NotNull(forStmt.Function?.SlotMap);
    Assert.Contains(Symbol.Create("i"), forStmt.Function.SlotMap.Keys);

    // Layer 3: IR
    var plan = ExecutionPlanBuilder.TryBuild(forStmt, out var executionPlan, out _);
    Assert.True(plan);
    Assert.Contains(executionPlan.Instructions,
        i => i.Kind == InstructionKind.IncrementSlot);

    // Layer 4: Execution
    var result = engine.Evaluate(program);
    // Assert expected behavior
}

Environment Variables

Variable Effect
JSENGINE_DEBUG_POOL_GUARDS=true Enable runtime pool guards
JSENGINE_TRACE_IR=true Enable IR execution tracing

See Also

Clone this wiki locally