-
Notifications
You must be signed in to change notification settings - Fork 1
Debugging
Debugging aids and diagnostic tools in Asynkron.JsEngine.
File: EvaluationContext.cs
Runtime detection of AST evaluation during IR-only execution paths.
#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- Flag is checked in
ExpressionNode.EvaluateExpression()andStatementNode.EvaluateStatementJsValue() - When enabled, throws
InvalidOperationExceptionwith node type information - Only available in DEBUG builds (compiled out in RELEASE)
- Used to validate IR/bytecode coverage as it increases
See tests/Asynkron.JsEngine.Tests/AstFreeExecutionAssertionTests.cs for examples.
The engine uses a two-tier system to catch pooling bugs (double-lease, use-after-return, async races).
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;
}File: PoolGuard.cs
Uses lease IDs to detect cross-async mismatches:
- Enable via
JSENGINE_DEBUG_POOL_GUARDS=trueenvironment 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);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");
// ...
}- Double-lease - object rented while still in use
- Use-after-return - object accessed after returned to pool
- Async races - iterator state used with wrong environment due to async interleaving
Use FakeLogger with DebugMode = true to capture slot hits/misses.
using Microsoft.Extensions.Logging.Testing;
var fakeLogger = new FakeLogger();
var engine = new JsEngine(new JsEngineOptions
{
DebugMode = true,
Logger = fakeLogger
});var logs = fakeLogger.Collector.GetSnapshot();
foreach (var entry in logs)
{
Console.WriteLine($"[{entry.Level}] {entry.Message}");
}// 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"));Scope analysis stamps AST nodes with slot information.
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")));| 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 |
Enable with JsEngineOptions.DebugMode = true:
- IR Trace Logging - Each instruction logged with program counter
- Slot Hit/Miss Logging - Variable access patterns
- Pool State Logging - Rent/return operations
- Assertion Checks - Additional runtime validation
[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)
For systematic debugging of complex failures. See agents/how-to-test-bombs.md.
- Create minimal failing test case
- Add assertions at each stage
- Binary search for failure point
- Add more assertions around failure
- Repeat until root cause found
[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);
}Stage-by-stage verification. See agents/how-to-layered-tests.md.
- Parse Layer - AST structure correct
- Analysis Layer - Scope/slot analysis correct
- IR Layer - Instructions generated correctly
- Execution Layer - Runtime behavior correct
[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
}| Variable | Effect |
|---|---|
JSENGINE_DEBUG_POOL_GUARDS=true |
Enable runtime pool guards |
JSENGINE_TRACE_IR=true |
Enable IR execution tracing |
- Performance Patterns - Pool implementation details
- agents/how-to-debugging.md - Full debugging guide
- agents/how-to-test-bombs.md - Test bomb methodology
- agents/how-to-layered-tests.md - Layered test approach