The EvaluationContext.AssertNoAstEvaluation flag enables runtime detection of AST evaluation during IR-only execution paths. This helps ensure that bytecode/IR implementations are complete and don't fall back to AST interpretation.
#if DEBUG
// Enable assertion before execution
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
Console.WriteLine(ex.Message);
// Example: "AST evaluation invoked for BinaryExpression during IR execution"
}
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 comprehensive examples:
- Verifying assertion triggers on expressions and statements
- Testing flag can be toggled during execution
- Validating error messages include node type information
Related to issues #398, #415, #364, #401 (IR-only execution epic).
- Use
FakeLogger(Microsoft.Extensions.Logging.Testing) withJsEngineOptions { DebugMode = true, Logger = fakeLogger }. - After running, inspect
fakeLogger.Collector.Snapshot(). - Example assertions: ensure no slot read misses; confirm expected hits for identifiers to prove fast paths.
- Scope analysis stamps
FunctionExpressionandBlockStatementwithScopeId,SlotCount, andSlotMap(symbol → slot index). - Example:
var parsed = engine.ParseProgram(script);
var runDecl = (FunctionDeclaration)parsed.Body[0];
var slotMap = runDecl.Function.SlotMap;
Assert.True(slotMap.ContainsKey(Symbol.Create("i")));- Verifies identifiers received slots in expected scopes before execution.
The engine uses a two-tier invariant system to catch pooling bugs (double-lease, use-after-return, async races).
Located in src/Asynkron.JsEngine/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.
Located in src/Asynkron.JsEngine/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
Key types implementing IRentable:
JsEnvironment- execution scopesIteratorDriverState- for-of loop stateForInDriverState- for-in loop state
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 (e.g., IteratorDriverPlanExtensions.cs):
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