diff --git a/Compilation/CompilationContext.cs b/Compilation/CompilationContext.cs
index db747eb..948215f 100644
--- a/Compilation/CompilationContext.cs
+++ b/Compilation/CompilationContext.cs
@@ -46,6 +46,12 @@ public class CompilationContext
public TypeMapper TypeMapper { get; }
public LocalsManager Locals { get; }
+ ///
+ /// Validated IL builder that wraps the ILGenerator with compile-time checks.
+ /// Use this for new code to catch label, stack, and exception block errors early.
+ ///
+ public ValidatedILBuilder ILBuilder { get; private set; }
+
///
/// Type provider for resolving .NET types (runtime or reference assembly mode).
/// Use this instead of typeof() for type resolution to support --ref-asm compilation.
@@ -416,6 +422,16 @@ public CompilationContext(
Classes = classes;
Types = types ?? TypeProvider.Runtime;
Locals = new LocalsManager(il);
+ ILBuilder = new ValidatedILBuilder(il, Types);
+ }
+
+ ///
+ /// Updates the validated IL builder for a new ILGenerator (when switching methods).
+ /// Call this when the IL property changes to a new method's generator.
+ ///
+ public void UpdateILBuilder(ILGenerator newIL)
+ {
+ ILBuilder = new ValidatedILBuilder(newIL, Types);
}
public void DefineParameter(string name, int argIndex, Type? paramType = null)
diff --git a/Compilation/ILCompiler.Classes.Methods.cs b/Compilation/ILCompiler.Classes.Methods.cs
index c4e4e85..1211bb4 100644
--- a/Compilation/ILCompiler.Classes.Methods.cs
+++ b/Compilation/ILCompiler.Classes.Methods.cs
@@ -683,8 +683,9 @@ private void EmitMethod(TypeBuilder typeBuilder, Stmt.Function method, FieldInfo
lockTakenLocal = il.DeclareLocal(typeof(bool)); // bool __lockTaken
// Set up deferred return handling for the lock's exception block
+ // Use the builder to define the label so it's tracked for validation
ctx.ReturnValueLocal = il.DeclareLocal(typeof(object));
- ctx.ReturnLabel = il.DefineLabel();
+ ctx.ReturnLabel = ctx.ILBuilder.DefineLabel("lock_deferred_return");
ctx.ExceptionBlockDepth++;
// int __prevReentrancy = _lockReentrancy.Value;
@@ -719,8 +720,8 @@ private void EmitMethod(TypeBuilder typeBuilder, Stmt.Function method, FieldInfo
il.MarkLabel(skipEnterLabel);
- // Begin try block
- il.BeginExceptionBlock();
+ // Begin try block - use builder to keep exception depth in sync
+ ctx.ILBuilder.BeginExceptionBlock();
}
// Abstract methods have no body to emit
@@ -740,10 +741,10 @@ private void EmitMethod(TypeBuilder typeBuilder, Stmt.Function method, FieldInfo
// ReturnValueLocal is guaranteed non-null here (set up earlier in hasLock block)
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Stloc, ctx.ReturnValueLocal!);
- il.Emit(OpCodes.Leave, ctx.ReturnLabel);
+ ctx.ILBuilder.Emit_Leave(ctx.ReturnLabel);
- // Begin finally block
- il.BeginFinallyBlock();
+ // Begin finally block - use builder for exception block tracking
+ ctx.ILBuilder.BeginFinallyBlock();
// _lockReentrancy.Value = __prevReentrancy;
il.Emit(OpCodes.Ldarg_0); // this
@@ -763,13 +764,13 @@ private void EmitMethod(TypeBuilder typeBuilder, Stmt.Function method, FieldInfo
il.MarkLabel(skipExitLabel);
- // End try/finally block
- il.EndExceptionBlock();
+ // End try/finally block - use builder for exception block tracking
+ ctx.ILBuilder.EndExceptionBlock();
ctx.ExceptionBlockDepth--;
- // Mark return label and emit actual return
- il.MarkLabel(ctx.ReturnLabel);
+ // Mark return label and emit actual return - use builder since label was defined with builder
+ ctx.ILBuilder.MarkLabel(ctx.ReturnLabel);
il.Emit(OpCodes.Ldloc, ctx.ReturnValueLocal!); // Non-null in hasLock path
il.Emit(OpCodes.Ret);
}
diff --git a/Compilation/ILCompiler.Classes.Static.cs b/Compilation/ILCompiler.Classes.Static.cs
index c9e2f5c..7699b79 100644
--- a/Compilation/ILCompiler.Classes.Static.cs
+++ b/Compilation/ILCompiler.Classes.Static.cs
@@ -221,8 +221,9 @@ private void EmitStaticMethodBody(string className, Stmt.Function method)
lockTakenLocal = il.DeclareLocal(typeof(bool)); // bool __lockTaken
// Set up deferred return handling for the lock's exception block
+ // Use the builder to define the label so it's tracked for validation
ctx.ReturnValueLocal = il.DeclareLocal(typeof(object));
- ctx.ReturnLabel = il.DefineLabel();
+ ctx.ReturnLabel = ctx.ILBuilder.DefineLabel("static_lock_deferred_return");
ctx.ExceptionBlockDepth++;
// int __prevReentrancy = _staticLockReentrancy.Value;
@@ -254,8 +255,8 @@ private void EmitStaticMethodBody(string className, Stmt.Function method)
il.MarkLabel(skipEnterLabel);
- // Begin try block
- il.BeginExceptionBlock();
+ // Begin try block - use builder to keep exception depth in sync
+ ctx.ILBuilder.BeginExceptionBlock();
}
// Abstract methods have no body to emit
@@ -275,10 +276,10 @@ private void EmitStaticMethodBody(string className, Stmt.Function method)
// ReturnValueLocal is guaranteed non-null here (set up earlier in hasLock block)
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Stloc, ctx.ReturnValueLocal!);
- il.Emit(OpCodes.Leave, ctx.ReturnLabel);
+ ctx.ILBuilder.Emit_Leave(ctx.ReturnLabel);
- // Begin finally block
- il.BeginFinallyBlock();
+ // Begin finally block - use builder for exception block tracking
+ ctx.ILBuilder.BeginFinallyBlock();
// _staticLockReentrancy.Value = __prevReentrancy;
il.Emit(OpCodes.Ldsfld, staticReentrancyField); // _staticLockReentrancy
@@ -296,13 +297,13 @@ private void EmitStaticMethodBody(string className, Stmt.Function method)
il.MarkLabel(skipExitLabel);
- // End try/finally block
- il.EndExceptionBlock();
+ // End try/finally block - use builder for exception block tracking
+ ctx.ILBuilder.EndExceptionBlock();
ctx.ExceptionBlockDepth--;
- // Mark return label and emit actual return
- il.MarkLabel(ctx.ReturnLabel);
+ // Mark return label and emit actual return - use builder since label was defined with builder
+ ctx.ILBuilder.MarkLabel(ctx.ReturnLabel);
il.Emit(OpCodes.Ldloc, ctx.ReturnValueLocal!); // Non-null in hasLock path
il.Emit(OpCodes.Ret);
}
diff --git a/Compilation/ILEmitter.Calls.cs b/Compilation/ILEmitter.Calls.cs
index e0f69fa..1b3432d 100644
--- a/Compilation/ILEmitter.Calls.cs
+++ b/Compilation/ILEmitter.Calls.cs
@@ -731,19 +731,20 @@ private void EmitAmbiguousMethodCall(Expr obj, string methodName, List arg
IL.Emit(OpCodes.Stloc, objLocal);
// Check if it's a string
- var isStringLabel = IL.DefineLabel();
- var isListLabel = IL.DefineLabel();
- var doneLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var isStringLabel = builder.DefineLabel("ambiguous_string");
+ var isListLabel = builder.DefineLabel("ambiguous_list");
+ var doneLabel = builder.DefineLabel("ambiguous_done");
IL.Emit(OpCodes.Ldloc, objLocal);
IL.Emit(OpCodes.Isinst, _ctx.Types.String);
- IL.Emit(OpCodes.Brtrue, isStringLabel);
+ builder.Emit_Brtrue(isStringLabel);
// Assume it's a list if not a string
- IL.Emit(OpCodes.Br, isListLabel);
+ builder.Emit_Br(isListLabel);
// String path
- IL.MarkLabel(isStringLabel);
+ builder.MarkLabel(isStringLabel);
IL.Emit(OpCodes.Ldloc, objLocal);
IL.Emit(OpCodes.Castclass, _ctx.Types.String);
@@ -811,10 +812,10 @@ private void EmitAmbiguousMethodCall(Expr obj, string methodName, List arg
IL.Emit(OpCodes.Call, _ctx.Runtime!.StringConcat);
break;
}
- IL.Emit(OpCodes.Br, doneLabel);
+ builder.Emit_Br(doneLabel);
// List path
- IL.MarkLabel(isListLabel);
+ builder.MarkLabel(isListLabel);
IL.Emit(OpCodes.Ldloc, objLocal);
IL.Emit(OpCodes.Castclass, _ctx.Types.ListOfObject);
@@ -877,7 +878,7 @@ private void EmitAmbiguousMethodCall(Expr obj, string methodName, List arg
break;
}
- IL.MarkLabel(doneLabel);
+ builder.MarkLabel(doneLabel);
}
///
diff --git a/Compilation/ILEmitter.Expressions.cs b/Compilation/ILEmitter.Expressions.cs
index 99045af..a985bea 100644
--- a/Compilation/ILEmitter.Expressions.cs
+++ b/Compilation/ILEmitter.Expressions.cs
@@ -163,8 +163,9 @@ protected override void EmitSuper(Expr.Super s)
protected override void EmitTernary(Expr.Ternary t)
{
- var elseLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var elseLabel = builder.DefineLabel("ternary_else");
+ var endLabel = builder.DefineLabel("ternary_end");
EmitExpression(t.Condition);
// Handle condition based on what's actually on the stack
@@ -187,35 +188,36 @@ protected override void EmitTernary(Expr.Ternary t)
EnsureBoxed(); // Ensure it's boxed first
EmitTruthyCheck();
}
- IL.Emit(OpCodes.Brfalse, elseLabel);
+ builder.Emit_Brfalse(elseLabel);
EmitExpression(t.ThenBranch);
EmitBoxIfNeeded(t.ThenBranch);
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
- IL.MarkLabel(elseLabel);
+ builder.MarkLabel(elseLabel);
EmitExpression(t.ElseBranch);
EmitBoxIfNeeded(t.ElseBranch);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
// Both branches box, so result is Unknown (boxed object)
SetStackUnknown();
}
protected override void EmitNullishCoalescing(Expr.NullishCoalescing nc)
{
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var endLabel = builder.DefineLabel("nullish_end");
EmitExpression(nc.Left);
EmitBoxIfNeeded(nc.Left);
IL.Emit(OpCodes.Dup);
- IL.Emit(OpCodes.Brtrue, endLabel);
+ builder.Emit_Brtrue(endLabel);
IL.Emit(OpCodes.Pop);
EmitExpression(nc.Right);
EmitBoxIfNeeded(nc.Right);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
// Both branches box, so result is Unknown (boxed object)
SetStackUnknown();
}
diff --git a/Compilation/ILEmitter.Helpers.cs b/Compilation/ILEmitter.Helpers.cs
index 1e7f938..2fef7e9 100644
--- a/Compilation/ILEmitter.Helpers.cs
+++ b/Compilation/ILEmitter.Helpers.cs
@@ -15,7 +15,8 @@ public void FinalizeReturns()
if (_ctx.ReturnValueLocal != null)
{
// Mark the return label and emit the actual return
- IL.MarkLabel(_ctx.ReturnLabel);
+ // Use builder's MarkLabel since ReturnLabel was defined with builder
+ _ctx.ILBuilder.MarkLabel(_ctx.ReturnLabel);
IL.Emit(OpCodes.Ldloc, _ctx.ReturnValueLocal);
IL.Emit(OpCodes.Ret);
}
@@ -33,6 +34,7 @@ public void FinalizeReturns()
public void EmitDefaultParameters(List parameters, bool isInstanceMethod)
{
int argOffset = isInstanceMethod ? 1 : 0;
+ var builder = _ctx.ILBuilder;
for (int i = 0; i < parameters.Count; i++)
{
@@ -42,18 +44,18 @@ public void EmitDefaultParameters(List parameters, bool isInstan
int argIndex = i + argOffset;
// if (arg == null) { arg = ; }
- var skipDefault = IL.DefineLabel();
+ var skipDefault = builder.DefineLabel($"skip_default_{i}");
// Load argument and check if null
IL.Emit(OpCodes.Ldarg, argIndex);
- IL.Emit(OpCodes.Brtrue, skipDefault);
+ builder.Emit_Brtrue(skipDefault);
// Argument is null, emit default value and store
EmitExpression(param.DefaultValue);
EmitBoxIfNeeded(param.DefaultValue);
IL.Emit(OpCodes.Starg, argIndex);
- IL.MarkLabel(skipDefault);
+ builder.MarkLabel(skipDefault);
}
}
@@ -296,35 +298,36 @@ protected override void EmitTruthyCheck()
// - null => false
// - boxed false => false
// - everything else => true
- var checkBoolLabel = IL.DefineLabel();
- var falseLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var checkBoolLabel = builder.DefineLabel("truthy_checkbool");
+ var falseLabel = builder.DefineLabel("truthy_false");
+ var endLabel = builder.DefineLabel("truthy_end");
// Check for null
IL.Emit(OpCodes.Dup);
- IL.Emit(OpCodes.Brfalse, falseLabel);
+ builder.Emit_Brfalse(falseLabel);
// Check if it's a boolean
IL.Emit(OpCodes.Dup);
IL.Emit(OpCodes.Isinst, _ctx.Types.Boolean);
- IL.Emit(OpCodes.Brfalse, checkBoolLabel);
+ builder.Emit_Brfalse(checkBoolLabel);
// It's a boxed bool - unbox and use the value
IL.Emit(OpCodes.Unbox_Any, _ctx.Types.Boolean);
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
- IL.MarkLabel(checkBoolLabel);
+ builder.MarkLabel(checkBoolLabel);
// Not null and not bool - always truthy
IL.Emit(OpCodes.Pop);
IL.Emit(OpCodes.Ldc_I4_1);
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
- IL.MarkLabel(falseLabel);
+ builder.MarkLabel(falseLabel);
// Null - false
IL.Emit(OpCodes.Pop);
IL.Emit(OpCodes.Ldc_I4_0);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
}
private static bool IsComparisonOp(TokenType op) =>
diff --git a/Compilation/ILEmitter.Operators.cs b/Compilation/ILEmitter.Operators.cs
index f0331f4..5567da5 100644
--- a/Compilation/ILEmitter.Operators.cs
+++ b/Compilation/ILEmitter.Operators.cs
@@ -156,7 +156,8 @@ private bool IsComparisonExpr(Expr expr)
protected override void EmitLogical(Expr.Logical l)
{
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var endLabel = builder.DefineLabel("logical_end");
EmitExpression(l.Left);
EmitBoxIfNeeded(l.Left);
@@ -166,19 +167,19 @@ protected override void EmitLogical(Expr.Logical l)
if (l.Operator.Type == TokenType.AND_AND)
{
// Short-circuit: if left is falsy, return left
- IL.Emit(OpCodes.Brfalse, endLabel);
+ builder.Emit_Brfalse(endLabel);
}
else // OR
{
// Short-circuit: if left is truthy, return left
- IL.Emit(OpCodes.Brtrue, endLabel);
+ builder.Emit_Brtrue(endLabel);
}
IL.Emit(OpCodes.Pop); // Pop the duplicate left value
EmitExpression(l.Right);
EmitBoxIfNeeded(l.Right);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
SetStackUnknown(); // Logical operators return boxed object
}
diff --git a/Compilation/ILEmitter.Properties.cs b/Compilation/ILEmitter.Properties.cs
index 7978c60..2c43d98 100644
--- a/Compilation/ILEmitter.Properties.cs
+++ b/Compilation/ILEmitter.Properties.cs
@@ -162,21 +162,22 @@ protected override void EmitGet(Expr.Get g)
if (g.Optional)
{
- var nullLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var nullLabel = builder.DefineLabel("optional_null");
+ var endLabel = builder.DefineLabel("optional_end");
IL.Emit(OpCodes.Dup);
- IL.Emit(OpCodes.Brfalse, nullLabel);
+ builder.Emit_Brfalse(nullLabel);
IL.Emit(OpCodes.Ldstr, g.Name.Lexeme);
IL.Emit(OpCodes.Call, _ctx.Runtime!.GetProperty);
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
- IL.MarkLabel(nullLabel);
+ builder.MarkLabel(nullLabel);
IL.Emit(OpCodes.Pop);
IL.Emit(OpCodes.Ldnull);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
}
else
{
diff --git a/Compilation/ILEmitter.Statements.cs b/Compilation/ILEmitter.Statements.cs
index 3d5a179..628bc36 100644
--- a/Compilation/ILEmitter.Statements.cs
+++ b/Compilation/ILEmitter.Statements.cs
@@ -106,8 +106,9 @@ protected override void EmitIf(Stmt.If i)
}
// BothReachable: emit both branches with condition check
- var elseLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var elseLabel = builder.DefineLabel("if_else");
+ var endLabel = builder.DefineLabel("if_end");
EmitExpression(i.Condition);
// Handle condition based on what's actually on the stack
@@ -130,28 +131,29 @@ protected override void EmitIf(Stmt.If i)
EnsureBoxed();
EmitTruthyCheck();
}
- IL.Emit(OpCodes.Brfalse, elseLabel);
+ builder.Emit_Brfalse(elseLabel);
EmitStatement(i.ThenBranch);
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
- IL.MarkLabel(elseLabel);
+ builder.MarkLabel(elseLabel);
if (i.ElseBranch != null)
{
EmitStatement(i.ElseBranch);
}
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
}
protected override void EmitWhile(Stmt.While w)
{
- var startLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var startLabel = builder.DefineLabel("while_start");
+ var endLabel = builder.DefineLabel("while_end");
_ctx.EnterLoop(endLabel, startLabel);
- IL.MarkLabel(startLabel);
+ builder.MarkLabel(startLabel);
EmitExpression(w.Condition);
// Handle condition based on what's actually on the stack
if (_stackType == StackType.Boolean)
@@ -173,29 +175,30 @@ protected override void EmitWhile(Stmt.While w)
EnsureBoxed();
EmitTruthyCheck();
}
- IL.Emit(OpCodes.Brfalse, endLabel);
+ builder.Emit_Brfalse(endLabel);
EmitStatement(w.Body);
- IL.Emit(OpCodes.Br, startLabel);
+ builder.Emit_Br(startLabel);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
_ctx.ExitLoop();
}
protected override void EmitDoWhile(Stmt.DoWhile dw)
{
- var startLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
- var continueLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var startLabel = builder.DefineLabel("dowhile_start");
+ var endLabel = builder.DefineLabel("dowhile_end");
+ var continueLabel = builder.DefineLabel("dowhile_continue");
_ctx.EnterLoop(endLabel, continueLabel);
// Body executes at least once
- IL.MarkLabel(startLabel);
+ builder.MarkLabel(startLabel);
EmitStatement(dw.Body);
// Continue target is after the body, before condition check
- IL.MarkLabel(continueLabel);
+ builder.MarkLabel(continueLabel);
// Evaluate condition
EmitExpression(dw.Condition);
@@ -219,15 +222,16 @@ protected override void EmitDoWhile(Stmt.DoWhile dw)
EnsureBoxed();
EmitTruthyCheck();
}
- IL.Emit(OpCodes.Brtrue, startLabel);
+ builder.Emit_Brtrue(startLabel);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
_ctx.ExitLoop();
}
protected override void EmitForOf(Stmt.ForOf f)
{
_ctx.Locals.EnterScope();
+ var builder = _ctx.ILBuilder;
// Evaluate iterable
TypeInfo? iterableType = _ctx.TypeMap?.Get(f.Iterable);
@@ -248,9 +252,9 @@ protected override void EmitForOf(Stmt.ForOf f)
// For generators, use enumerator-based iteration (with its own labels)
if (iterableType is TypeInfo.Generator)
{
- var genStartLabel = IL.DefineLabel();
- var genEndLabel = IL.DefineLabel();
- var genContinueLabel = IL.DefineLabel();
+ var genStartLabel = builder.DefineLabel("forof_gen_start");
+ var genEndLabel = builder.DefineLabel("forof_gen_end");
+ var genContinueLabel = builder.DefineLabel("forof_gen_continue");
_ctx.EnterLoop(genEndLabel, genContinueLabel);
EmitForOfEnumerator(f, genStartLabel, genEndLabel, genContinueLabel);
return;
@@ -262,8 +266,8 @@ protected override void EmitForOf(Stmt.ForOf f)
// Try iterator protocol first: GetIteratorFunction(iterable, Symbol.iterator)
var iteratorFnLocal = IL.DeclareLocal(_ctx.Types.Object);
- var indexBasedLabel = IL.DefineLabel();
- var afterLoopLabel = IL.DefineLabel();
+ var indexBasedLabel = builder.DefineLabel("forof_index_based");
+ var afterLoopLabel = builder.DefineLabel("forof_after");
IL.Emit(OpCodes.Ldloc, iterableLocal);
IL.Emit(OpCodes.Ldsfld, _ctx.Runtime!.SymbolIterator);
@@ -272,13 +276,13 @@ protected override void EmitForOf(Stmt.ForOf f)
// If iterator function is null, fall back to index-based iteration
IL.Emit(OpCodes.Ldloc, iteratorFnLocal);
- IL.Emit(OpCodes.Brfalse, indexBasedLabel);
+ builder.Emit_Brfalse(indexBasedLabel);
// ===== Iterator protocol path =====
{
- var iterStartLabel = IL.DefineLabel();
- var iterEndLabel = IL.DefineLabel();
- var iterContinueLabel = IL.DefineLabel();
+ var iterStartLabel = builder.DefineLabel("forof_iter_start");
+ var iterEndLabel = builder.DefineLabel("forof_iter_end");
+ var iterContinueLabel = builder.DefineLabel("forof_iter_continue");
_ctx.EnterLoop(iterEndLabel, iterContinueLabel);
// Call the iterator function to get the iterator object
@@ -311,12 +315,12 @@ protected override void EmitForOf(Stmt.ForOf f)
var moveNext = _ctx.Types.GetMethod(_ctx.Types.IEnumerator, "MoveNext");
var current = _ctx.Types.IEnumerator.GetProperty("Current")!.GetGetMethod()!;
- IL.MarkLabel(iterStartLabel);
+ builder.MarkLabel(iterStartLabel);
// Call MoveNext
IL.Emit(OpCodes.Ldloc, enumLocal);
IL.Emit(OpCodes.Callvirt, moveNext);
- IL.Emit(OpCodes.Brfalse, iterEndLabel);
+ builder.Emit_Brfalse(iterEndLabel);
// Get Current
IL.Emit(OpCodes.Ldloc, enumLocal);
@@ -326,20 +330,20 @@ protected override void EmitForOf(Stmt.ForOf f)
// Emit body
EmitStatement(f.Body);
- IL.MarkLabel(iterContinueLabel);
- IL.Emit(OpCodes.Br, iterStartLabel);
+ builder.MarkLabel(iterContinueLabel);
+ builder.Emit_Br(iterStartLabel);
- IL.MarkLabel(iterEndLabel);
+ builder.MarkLabel(iterEndLabel);
_ctx.ExitLoop();
- IL.Emit(OpCodes.Br, afterLoopLabel); // Skip the index-based path
+ builder.Emit_Br(afterLoopLabel); // Skip the index-based path
}
// ===== Index-based fallback (for arrays, strings, etc.) =====
- IL.MarkLabel(indexBasedLabel);
+ builder.MarkLabel(indexBasedLabel);
{
- var startLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
- var continueLabel = IL.DefineLabel();
+ var startLabel = builder.DefineLabel("forof_idx_start");
+ var endLabel = builder.DefineLabel("forof_idx_end");
+ var continueLabel = builder.DefineLabel("forof_idx_continue");
_ctx.EnterLoop(endLabel, continueLabel);
// Create index variable
@@ -350,14 +354,14 @@ protected override void EmitForOf(Stmt.ForOf f)
// Loop variable
var indexLoopVar = _ctx.Locals.DeclareLocal(f.Variable.Lexeme, _ctx.Types.Object);
- IL.MarkLabel(startLabel);
+ builder.MarkLabel(startLabel);
// Check if index < length
IL.Emit(OpCodes.Ldloc, indexLocal);
IL.Emit(OpCodes.Ldloc, iterableLocal);
IL.Emit(OpCodes.Call, _ctx.Runtime!.GetLength);
IL.Emit(OpCodes.Clt);
- IL.Emit(OpCodes.Brfalse, endLabel);
+ builder.Emit_Brfalse(endLabel);
// Get current element
IL.Emit(OpCodes.Ldloc, iterableLocal);
@@ -368,7 +372,7 @@ protected override void EmitForOf(Stmt.ForOf f)
// Emit body
EmitStatement(f.Body);
- IL.MarkLabel(continueLabel);
+ builder.MarkLabel(continueLabel);
// Increment index
IL.Emit(OpCodes.Ldloc, indexLocal);
@@ -376,19 +380,21 @@ protected override void EmitForOf(Stmt.ForOf f)
IL.Emit(OpCodes.Add);
IL.Emit(OpCodes.Stloc, indexLocal);
- IL.Emit(OpCodes.Br, startLabel);
+ builder.Emit_Br(startLabel);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
_ctx.ExitLoop();
}
// Common exit point for both paths
- IL.MarkLabel(afterLoopLabel);
+ builder.MarkLabel(afterLoopLabel);
_ctx.Locals.ExitScope();
}
private void EmitForOfEnumerator(Stmt.ForOf f, Label startLabel, Label endLabel, Label continueLabel)
{
+ var builder = _ctx.ILBuilder;
+
// Use IEnumerable.GetEnumerator()/MoveNext()/Current pattern for generators
var getEnumerator = _ctx.Types.GetMethod(_ctx.Types.IEnumerable, "GetEnumerator");
var moveNext = _ctx.Types.GetMethod(_ctx.Types.IEnumerator, "MoveNext");
@@ -404,12 +410,12 @@ private void EmitForOfEnumerator(Stmt.ForOf f, Label startLabel, Label endLabel,
// Loop variable
var loopVar = _ctx.Locals.DeclareLocal(f.Variable.Lexeme, _ctx.Types.Object);
- IL.MarkLabel(startLabel);
+ builder.MarkLabel(startLabel);
// Call MoveNext
IL.Emit(OpCodes.Ldloc, enumLocal);
IL.Emit(OpCodes.Callvirt, moveNext);
- IL.Emit(OpCodes.Brfalse, endLabel);
+ builder.Emit_Brfalse(endLabel);
// Get Current
IL.Emit(OpCodes.Ldloc, enumLocal);
@@ -419,19 +425,20 @@ private void EmitForOfEnumerator(Stmt.ForOf f, Label startLabel, Label endLabel,
// Emit body
EmitStatement(f.Body);
- IL.MarkLabel(continueLabel);
- IL.Emit(OpCodes.Br, startLabel);
+ builder.MarkLabel(continueLabel);
+ builder.Emit_Br(startLabel);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
_ctx.Locals.ExitScope();
_ctx.ExitLoop();
}
protected override void EmitForIn(Stmt.ForIn f)
{
- var startLabel = IL.DefineLabel();
- var endLabel = IL.DefineLabel();
- var continueLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var startLabel = builder.DefineLabel("forin_start");
+ var endLabel = builder.DefineLabel("forin_end");
+ var continueLabel = builder.DefineLabel("forin_continue");
_ctx.EnterLoop(endLabel, continueLabel);
_ctx.Locals.EnterScope();
@@ -450,14 +457,14 @@ protected override void EmitForIn(Stmt.ForIn f)
// Loop variable (holds current key)
var loopVar = _ctx.Locals.DeclareLocal(f.Variable.Lexeme, _ctx.Types.Object);
- IL.MarkLabel(startLabel);
+ builder.MarkLabel(startLabel);
// Check if index < keys.Count
IL.Emit(OpCodes.Ldloc, indexLocal);
IL.Emit(OpCodes.Ldloc, keysLocal);
IL.Emit(OpCodes.Call, _ctx.Runtime!.GetLength);
IL.Emit(OpCodes.Clt);
- IL.Emit(OpCodes.Brfalse, endLabel);
+ builder.Emit_Brfalse(endLabel);
// Get current key: keys[index]
IL.Emit(OpCodes.Ldloc, keysLocal);
@@ -468,7 +475,7 @@ protected override void EmitForIn(Stmt.ForIn f)
// Emit body
EmitStatement(f.Body);
- IL.MarkLabel(continueLabel);
+ builder.MarkLabel(continueLabel);
// Increment index
IL.Emit(OpCodes.Ldloc, indexLocal);
@@ -476,9 +483,9 @@ protected override void EmitForIn(Stmt.ForIn f)
IL.Emit(OpCodes.Add);
IL.Emit(OpCodes.Stloc, indexLocal);
- IL.Emit(OpCodes.Br, startLabel);
+ builder.Emit_Br(startLabel);
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
_ctx.Locals.ExitScope();
_ctx.ExitLoop();
}
@@ -508,13 +515,15 @@ protected override void EmitReturn(Stmt.Return r)
if (_ctx.ExceptionBlockDepth > 0)
{
// Inside exception block: store value and leave
+ // Use builder for Leave validation (ensures we're inside exception block)
+ var builder = _ctx.ILBuilder;
if (_ctx.ReturnValueLocal == null)
{
_ctx.ReturnValueLocal = IL.DeclareLocal(_ctx.Types.Object);
- _ctx.ReturnLabel = IL.DefineLabel();
+ _ctx.ReturnLabel = builder.DefineLabel("deferred_return");
}
IL.Emit(OpCodes.Stloc, _ctx.ReturnValueLocal);
- IL.Emit(OpCodes.Leave, _ctx.ReturnLabel);
+ builder.Emit_Leave(_ctx.ReturnLabel);
}
else
{
@@ -545,15 +554,16 @@ protected override void EmitContinue(Stmt.Continue c)
protected override void EmitLabeledStatement(Stmt.LabeledStatement labeledStmt)
{
string labelName = labeledStmt.Label.Lexeme;
- var breakLabel = IL.DefineLabel();
- var continueLabel = IL.DefineLabel();
+ var builder = _ctx.ILBuilder;
+ var breakLabel = builder.DefineLabel($"labeled_{labelName}_break");
+ var continueLabel = builder.DefineLabel($"labeled_{labelName}_continue");
// For labeled statements, we need to handle both loops and non-loop statements.
// For loops, the inner loop will use its own labels for unlabeled break/continue,
// but labeled break/continue should use the labels registered here.
// Mark continue label at the start (for labeled continue, restart from here)
- IL.MarkLabel(continueLabel);
+ builder.MarkLabel(continueLabel);
_ctx.EnterLoop(breakLabel, continueLabel, labelName);
try
@@ -568,7 +578,7 @@ protected override void EmitLabeledStatement(Stmt.LabeledStatement labeledStmt)
}
// Mark the break label (after the statement, for labeled break)
- IL.MarkLabel(breakLabel);
+ builder.MarkLabel(breakLabel);
}
protected override void EmitSwitch(Stmt.Switch s)
@@ -577,9 +587,10 @@ protected override void EmitSwitch(Stmt.Switch s)
var switchAnalysis = _ctx.DeadCode?.GetSwitchResult(s);
bool skipDefault = switchAnalysis?.DefaultIsUnreachable == true;
- var endLabel = IL.DefineLabel();
- var defaultLabel = IL.DefineLabel();
- var caseLabels = s.Cases.Select(_ => IL.DefineLabel()).ToList();
+ var builder = _ctx.ILBuilder;
+ var endLabel = builder.DefineLabel("switch_end");
+ var defaultLabel = builder.DefineLabel("switch_default");
+ var caseLabels = s.Cases.Select((_, i) => builder.DefineLabel($"switch_case_{i}")).ToList();
// Evaluate subject once
EmitExpression(s.Subject);
@@ -594,23 +605,23 @@ protected override void EmitSwitch(Stmt.Switch s)
EmitExpression(s.Cases[i].Value);
EmitBoxIfNeeded(s.Cases[i].Value);
IL.Emit(OpCodes.Call, _ctx.Runtime!.Equals);
- IL.Emit(OpCodes.Brtrue, caseLabels[i]);
+ builder.Emit_Brtrue(caseLabels[i]);
}
// Jump to default or end (skip default if unreachable)
if (skipDefault || s.DefaultBody == null)
{
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
}
else
{
- IL.Emit(OpCodes.Br, defaultLabel);
+ builder.Emit_Br(defaultLabel);
}
// Emit case bodies
for (int i = 0; i < s.Cases.Count; i++)
{
- IL.MarkLabel(caseLabels[i]);
+ builder.MarkLabel(caseLabels[i]);
foreach (var stmt in s.Cases[i].Body)
{
if (stmt is Stmt.Break breakStmt)
@@ -623,7 +634,7 @@ protected override void EmitSwitch(Stmt.Switch s)
else
{
// Unlabeled break - exits switch only
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
}
}
else
@@ -637,7 +648,7 @@ protected override void EmitSwitch(Stmt.Switch s)
// Default case (skip if unreachable)
if (s.DefaultBody != null && !skipDefault)
{
- IL.MarkLabel(defaultLabel);
+ builder.MarkLabel(defaultLabel);
foreach (var stmt in s.DefaultBody)
{
if (stmt is Stmt.Break breakStmt)
@@ -650,7 +661,7 @@ protected override void EmitSwitch(Stmt.Switch s)
else
{
// Unlabeled break - exits switch only
- IL.Emit(OpCodes.Br, endLabel);
+ builder.Emit_Br(endLabel);
}
}
else
@@ -660,14 +671,17 @@ protected override void EmitSwitch(Stmt.Switch s)
}
}
- IL.MarkLabel(endLabel);
+ builder.MarkLabel(endLabel);
}
protected override void EmitTryCatch(Stmt.TryCatch t)
{
- _ctx.ExceptionBlockDepth++;
+ // Use ValidatedILBuilder for exception block operations - it tracks depth automatically
+ // and validates proper Begin/End pairing
+ var builder = _ctx.ILBuilder;
- IL.BeginExceptionBlock();
+ _ctx.ExceptionBlockDepth++;
+ builder.BeginExceptionBlock();
foreach (var stmt in t.TryBlock)
{
@@ -676,7 +690,7 @@ protected override void EmitTryCatch(Stmt.TryCatch t)
if (t.CatchBlock != null)
{
- IL.BeginCatchBlock(_ctx.Types.Exception);
+ builder.BeginCatchBlock(_ctx.Types.Exception);
if (t.CatchParam != null)
{
@@ -698,15 +712,14 @@ protected override void EmitTryCatch(Stmt.TryCatch t)
if (t.FinallyBlock != null)
{
- IL.BeginFinallyBlock();
+ builder.BeginFinallyBlock();
foreach (var stmt in t.FinallyBlock)
{
EmitStatement(stmt);
}
}
- IL.EndExceptionBlock();
-
+ builder.EndExceptionBlock();
_ctx.ExceptionBlockDepth--;
}
diff --git a/Compilation/ILEmitter.cs b/Compilation/ILEmitter.cs
index 5b8e1df..7e46358 100644
--- a/Compilation/ILEmitter.cs
+++ b/Compilation/ILEmitter.cs
@@ -57,7 +57,7 @@ private StackType _stackType
}
public ILEmitter(CompilationContext ctx)
- : base(new StateMachineEmitHelpers(ctx.IL, ctx.Types))
+ : base(new StateMachineEmitHelpers(ctx.IL, ctx.Types, ctx.ILBuilder))
{
_ctx = ctx;
_resolver = new LocalVariableResolver(ctx.IL, ctx, ctx.Types);
@@ -86,11 +86,12 @@ protected override bool IsDead(Stmt stmt)
protected override void EmitBranchToLabel(Label target)
{
- // Use Leave instead of Br when inside exception blocks
+ // Use builder for branch validation - it enforces Leave vs Br rules
+ var builder = _ctx.ILBuilder;
if (_ctx.ExceptionBlockDepth > 0)
- IL.Emit(OpCodes.Leave, target);
+ builder.Emit_Leave(target);
else
- IL.Emit(OpCodes.Br, target);
+ builder.Emit_Br(target);
}
protected override LocalBuilder? DeclareLoopVariable(string name)
diff --git a/Compilation/ILValidationException.cs b/Compilation/ILValidationException.cs
new file mode 100644
index 0000000..3e2fa1f
--- /dev/null
+++ b/Compilation/ILValidationException.cs
@@ -0,0 +1,42 @@
+namespace SharpTS.Compilation;
+
+///
+/// Exception thrown when IL validation fails during emit.
+///
+///
+/// This exception is thrown by when it detects
+/// an invalid IL emission pattern at compile time. The error is caught early,
+/// before the generated assembly would fail PEVerify or crash at runtime.
+///
+/// Common causes:
+///
+/// - A label was defined but never marked (forgotten MarkLabel call)
+/// - Stack depth mismatch at a branch target (inconsistent code paths)
+/// - Using Br inside an exception block (should use Leave)
+/// - Using Leave outside an exception block
+/// - Attempting to Box a reference type
+/// - Attempting to Unbox a value type
+/// - Insufficient values on the stack for an operation
+///
+///
+public class ILValidationException : Exception
+{
+ ///
+ /// Creates a new IL validation exception.
+ ///
+ /// Description of the validation failure.
+ public ILValidationException(string message)
+ : base($"IL Validation Error: {message}")
+ {
+ }
+
+ ///
+ /// Creates a new IL validation exception with an inner exception.
+ ///
+ /// Description of the validation failure.
+ /// The underlying exception.
+ public ILValidationException(string message, Exception innerException)
+ : base($"IL Validation Error: {message}", innerException)
+ {
+ }
+}
diff --git a/Compilation/StateMachineEmitHelpers.cs b/Compilation/StateMachineEmitHelpers.cs
index a30c8eb..e8be171 100644
--- a/Compilation/StateMachineEmitHelpers.cs
+++ b/Compilation/StateMachineEmitHelpers.cs
@@ -11,6 +11,7 @@ public class StateMachineEmitHelpers
{
private readonly ILGenerator _il;
private readonly TypeProvider _types;
+ private readonly ValidatedILBuilder? _builder;
private StackType _stackType = StackType.Unknown;
///
@@ -32,12 +33,52 @@ public StackType StackType
///
public TypeProvider Types => _types;
+ ///
+ /// The validated IL builder, if available. Use this for label and branch operations
+ /// to get compile-time validation of IL correctness.
+ ///
+ public ValidatedILBuilder? Builder => _builder;
+
public StateMachineEmitHelpers(ILGenerator il, TypeProvider types)
{
_il = il;
_types = types;
+ _builder = null;
+ }
+
+ public StateMachineEmitHelpers(ILGenerator il, TypeProvider types, ValidatedILBuilder builder)
+ {
+ _il = il;
+ _types = types;
+ _builder = builder;
+ }
+
+ #region Label Operations
+
+ ///
+ /// Defines a new label. Uses validated builder if available.
+ ///
+ /// Optional debug name for error messages.
+ public Label DefineLabel(string? debugName = null)
+ {
+ return _builder != null
+ ? _builder.DefineLabel(debugName)
+ : _il.DefineLabel();
}
+ ///
+ /// Marks a label at the current position. Uses validated builder if available.
+ ///
+ public void MarkLabel(Label label)
+ {
+ if (_builder != null)
+ _builder.MarkLabel(label);
+ else
+ _il.MarkLabel(label);
+ }
+
+ #endregion
+
#region Boxing and Type Conversion
///
@@ -438,7 +479,7 @@ public void EmitTruthyCheck(MethodInfo isTruthyMethod)
/// Runtime.IsTruthy method
public void EmitLogical(bool isAnd, Action emitLeft, Action emitRight, MethodInfo isTruthyMethod)
{
- var endLabel = _il.DefineLabel();
+ var endLabel = DefineLabel("logical_end");
emitLeft();
EnsureBoxed();
@@ -462,7 +503,7 @@ public void EmitLogical(bool isAnd, Action emitLeft, Action emitRight, MethodInf
EnsureBoxed();
}
- _il.MarkLabel(endLabel);
+ MarkLabel(endLabel);
SetStackUnknown();
}
@@ -471,8 +512,8 @@ public void EmitLogical(bool isAnd, Action emitLeft, Action emitRight, MethodInf
///
public void EmitTernary(Action emitCondition, Action emitThen, Action emitElse, MethodInfo isTruthyMethod)
{
- var elseLabel = _il.DefineLabel();
- var endLabel = _il.DefineLabel();
+ var elseLabel = DefineLabel("ternary_else");
+ var endLabel = DefineLabel("ternary_end");
emitCondition();
EnsureBoxed();
@@ -483,11 +524,11 @@ public void EmitTernary(Action emitCondition, Action emitThen, Action emitElse,
EnsureBoxed();
_il.Emit(OpCodes.Br, endLabel);
- _il.MarkLabel(elseLabel);
+ MarkLabel(elseLabel);
emitElse();
EnsureBoxed();
- _il.MarkLabel(endLabel);
+ MarkLabel(endLabel);
SetStackUnknown();
}
@@ -496,8 +537,8 @@ public void EmitTernary(Action emitCondition, Action emitThen, Action emitElse,
///
public void EmitNullishCoalescing(Action emitLeft, Action emitRight)
{
- var rightLabel = _il.DefineLabel();
- var endLabel = _il.DefineLabel();
+ var rightLabel = DefineLabel("nullish_right");
+ var endLabel = DefineLabel("nullish_end");
emitLeft();
EnsureBoxed();
@@ -505,12 +546,12 @@ public void EmitNullishCoalescing(Action emitLeft, Action emitRight)
_il.Emit(OpCodes.Brfalse, rightLabel);
_il.Emit(OpCodes.Br, endLabel);
- _il.MarkLabel(rightLabel);
+ MarkLabel(rightLabel);
_il.Emit(OpCodes.Pop);
emitRight();
EnsureBoxed();
- _il.MarkLabel(endLabel);
+ MarkLabel(endLabel);
SetStackUnknown();
}
diff --git a/Compilation/ValidatedILBuilder.cs b/Compilation/ValidatedILBuilder.cs
new file mode 100644
index 0000000..dd52427
--- /dev/null
+++ b/Compilation/ValidatedILBuilder.cs
@@ -0,0 +1,1345 @@
+using System.Reflection;
+using System.Reflection.Emit;
+
+namespace SharpTS.Compilation;
+
+///
+/// Type of value on the IL evaluation stack.
+///
+public enum StackEntryType
+{
+ /// Unknown or untracked type.
+ Unknown,
+ /// 32-bit integer (int32).
+ Int32,
+ /// 64-bit integer (int64).
+ Int64,
+ /// 64-bit floating point (float64).
+ Double,
+ /// 32-bit floating point (float32).
+ Float,
+ /// Boolean (represented as int32 0/1).
+ Boolean,
+ /// String reference.
+ String,
+ /// Object reference (any reference type).
+ Reference,
+ /// Null reference.
+ Null,
+ /// Value type (struct).
+ ValueType,
+ /// Native integer (IntPtr).
+ NativeInt
+}
+
+///
+/// Represents a single entry on the IL evaluation stack.
+///
+/// The category of the stack entry.
+/// Optional CLR type for more precise tracking.
+public readonly record struct StackEntry(StackEntryType Type, Type? ClrType = null)
+{
+ ///
+ /// Returns true if this entry is a value type that would need boxing.
+ ///
+ public bool IsValueType => Type is StackEntryType.Int32 or StackEntryType.Int64
+ or StackEntryType.Double or StackEntryType.Float or StackEntryType.Boolean
+ or StackEntryType.ValueType or StackEntryType.NativeInt;
+}
+
+///
+/// Snapshot of the stack state at a particular point, used for branch validation.
+///
+/// Stack depth at this point.
+/// Types on the stack (top of stack is last element).
+public readonly record struct StackSnapshot(int Depth, StackEntry[] Types);
+
+///
+/// Information about a defined label.
+///
+/// Optional debug name for error messages.
+/// Exception block depth when label was defined.
+public record LabelInfo(string? DebugName, int DefinedInExceptionDepth);
+
+///
+/// Information about an active exception block.
+///
+public class ExceptionBlockInfo
+{
+ /// Current phase of the exception block.
+ public ExceptionBlockPhase Phase { get; set; }
+
+ /// Stack depth when the block was entered.
+ public int EntryStackDepth { get; init; }
+
+ /// The label returned by BeginExceptionBlock.
+ public Label EndLabel { get; init; }
+
+ public ExceptionBlockInfo(ExceptionBlockPhase phase, int entryStackDepth, Label endLabel)
+ {
+ Phase = phase;
+ EntryStackDepth = entryStackDepth;
+ EndLabel = endLabel;
+ }
+}
+
+///
+/// Phase of an exception block.
+///
+public enum ExceptionBlockPhase { Try, Catch, Finally }
+
+///
+/// Validation mode for the IL builder.
+///
+public enum ValidationMode
+{
+ /// Throw immediately on validation error.
+ FailFast,
+ /// Collect all errors and report at method end.
+ CollectAll
+}
+
+///
+/// IL emission wrapper that validates label usage, stack balance, and exception blocks
+/// at emit time to catch errors before runtime PEVerify failures.
+///
+///
+/// This class wraps and intercepts critical operations to add
+/// compile-time validation. It does not modify the emitted IL; it only checks that
+/// the emission sequence is valid.
+///
+/// Validated operations:
+///
+/// - Labels: All defined labels must be marked before method ends
+/// - Stack: Branch targets must have consistent stack depth
+/// - Exception blocks: Br not allowed inside try/catch; use Leave instead
+/// - Boxing: Box requires value type on stack
+///
+///
+public sealed class ValidatedILBuilder
+{
+ private readonly ILGenerator _il;
+ private readonly TypeProvider _types;
+ private readonly ValidationMode _mode;
+ private readonly List _collectedErrors = [];
+
+ // Label tracking
+ private readonly Dictionary