Repository navigation
Fix expression compiler compatibility - #558
jogibear9988 wants to merge 189 commits into
Conversation
|
Maaaaaan, a lot of Visitors and seems a lot of allocations too. |
Allocation/performance measurements and independent reviewFollowing @dadhi's concern about visitors and allocations, I compared the current patch against both its parent and current upstream, and reviewed the production code independently of the earlier test-fixing work. The allocation concern is justified. I recommend keeping this draft while the common-path regressions and the newly reproduced correctness issue below are addressed. Passing the imported test suite alone is not sufficient evidence for merging this change. Revisions: upstream Main observations
Independent review findings
The added visitor families are feature-gated. ArgumentSpillScope scans runtime Expression invocation/Dynamic arguments; QuoteVariableScope examines quotes, and QuoteBinder rebuilds captured quotes at execution. They are not all run on every ordinary CompileFast call. There is still duplicated feature-specific traversal, linear bound/free-variable searches, and quote rebuilding allocation. Unconditional scope bookkeeping and broad capture representation have measurable costs even where those visitors do not run. Exception-region objects are also allocated for handler trees without jumps. A blanket visitor rewrite is not justified by this review; remove duplicate work and isolate common-path costs first. Source locations at the reviewed head:
Measurement scope and methodWindows ARM64 virtual machine (Apple Silicon host, six exposed logical processors), SDK 10.0.301; .NET 10.0.9 and 8.0.28, Release, native ARM64. Production source snapshots were compiled unchanged in isolated dependency-free library projects with the appropriate target-framework and LIGHT_EXPRESSION symbols. The main checkout and PR head were not changed. 31 scenario shapes: 28 work on both baselines/head, and 3 newly supported shapes (closed/captured quotes and RuntimeVariables) are reported without a before/after ratio. Each backend's observable outputs are checked against System compilation for inputs -2, 0 and 3 before timing; returned captures/quotes are executed during verification and RuntimeVariables get/set is checked. This is a targeted control, not exhaustive semantic equivalence. Strict FEC is used with no outer fallback. Measured separately: tree construction (there is no text parser here), compilation of a reused tree, compilation plus first outer execution, and warm invocation. For escaping captures, first-use/warm measurements also create and call the returned delegate; for quotes/RuntimeVariables, they create/return the object, while returned quote compilation is only part of correctness verification. System uses its IL compiler, not the interpreter. Light trees are constructed natively, and equivalent System trees are prepared before System compilation timing; Light -> System conversion is excluded from that comparison. The RuntimeVariables Light factory is unavailable publicly, so that one construction case uses System construction + ToLightExpression and is explicitly not a native-Light construction comparison. Two fresh-process rounds per version, forward then reverse version order, seven warmed samples per phase, adaptive batches targeting ~8ms (caps 2,000 compile/construction or 1,000,000 invocation operations). TieredCompilation and ReadyToRun are disabled consistently. GC is forced outside each timed sample. Allocations use This measures managed allocation volume, not retained heap size or native/JIT memory; static initialization and application cold startup are warmed away. First execution means the newly created method's first call in a warmed process. It does not claim all possible expression graphs, Framework/x64, or production workload performance were measured. No whole imported correctness rerun was needed for this read-only review; the independent reproduction above adds a known issue beyond that historical baseline. Complete measured phase resultsIn the tables, B -> A is parent -> head, .NET 10.0.9, standard: all 31 scenarios, all four phases
.NET 10.0.9, light: all 31 scenarios, all four phases
.NET 8.0.28, standard: all 31 scenarios, all four phases
.NET 8.0.28, light: all 31 scenarios, all four phases
ReproductionsFresh Dynamic reduction reproduction (.NET 8/10, Standard FEC)using System;
using System.Dynamic;
using System.Linq.Expressions;
using FastExpressionCompiler;
class FreshDynamic : Expression {
public int Reductions;
readonly Expression argument;
readonly bool cache; Expression cached;
public FreshDynamic(Expression a,bool cache=false){argument=a;this.cache=cache;}
public override Type Type=>typeof(int);
public override ExpressionType NodeType=>ExpressionType.Extension;
public override bool CanReduce=>true;
public override Expression Reduce(){Reductions++;if(cache&&cached!=null)return cached;return cached=Convert(Dynamic(new AddBinder(),typeof(object),argument,Constant(2)),typeof(int));}
}
class AddBinder : BinaryOperationBinder {
public AddBinder():base(ExpressionType.Add){}
public override DynamicMetaObject FallbackBinaryOperation(DynamicMetaObject target,DynamicMetaObject arg,DynamicMetaObject errorSuggestion)=>new DynamicMetaObject(Expression.Convert(Expression.Add(Expression.Convert(target.Expression,typeof(int)),Expression.Convert(arg.Expression,typeof(int))),typeof(object)),BindingRestrictions.GetTypeRestriction(target.Expression,typeof(int)).Merge(BindingRestrictions.GetTypeRestriction(arg.Expression,typeof(int))));
}
class Program {
static void Main(){
foreach(var flags in new[]{CompilerFlags.Default,CompilerFlags.NoInvocationLambdaInlining}) {
var x=Expression.Parameter(typeof(int));var extension=new FreshDynamic(x);var tree=Expression.Lambda<Func<int,int>>(extension,x);
try {var f=tree.CompileFast(true,flags);Console.WriteLine($"FEC {flags}: {(f==null?"null":f(5).ToString())}; reductions={extension.Reductions}");}catch(Exception e){Console.WriteLine($"FEC {flags}: reductions={extension.Reductions}; "+e);}
Console.WriteLine("System: "+tree.Compile()(5));
}
var p=Expression.Parameter(typeof(int));var direct=Expression.Lambda<Func<int,int>>(Expression.Convert(Expression.Dynamic(new AddBinder(),typeof(object),p,Expression.Constant(2)),typeof(int)),p);
var result=direct.CompileFast(true);Console.WriteLine("Direct Dynamic FEC: "+(result==null?"null":result(5).ToString()));
var stable=Expression.Lambda<Func<int,int>>(new FreshDynamic(p,true),p).CompileFast(true);
Console.WriteLine("Cached reduction FEC: "+(stable==null?"null":stable(5).ToString()));
}
}Head: NRE at Dynamic metadata lookup, reductions=2, under both flags. System/direct/cached controls return 7. Parent/upstream: null, not an exception. Inherited #555 integration control (Light)using System;
using System.Linq.Expressions;
using FastExpressionCompiler.LightExpression;
using E=System.Linq.Expressions.Expression;
class Program {
static void Main(){
var p=E.Parameter(typeof(decimal));
var tree=E.Lambda<Func<decimal,decimal>>(E.AddAssign(p,E.Constant(1m),typeof(decimal).GetMethod("op_Addition",new[]{typeof(decimal),typeof(decimal)}),null),p);
Console.WriteLine("System: "+tree.Compile()(2m));
var light=tree.ToLightExpression();Console.WriteLine("FEC direct: "+light.CompileFast(true)(2m));
try {var roundtrip=(System.Linq.Expressions.Expression<Func<decimal,decimal>>)light.ToExpression();Console.WriteLine("Roundtrip: "+roundtrip.Compile()(2m));}catch(Exception e){Console.WriteLine("Roundtrip: "+e);}
}
}Head/parent: System=3, direct FEC=3, round-trip=NRE. Upstream: all three return 3. Reproducing the measurementsThe source harness below is shared across versions. For Standard, compile it against the four production Set Benchmark harness and snapshot build scriptsusing System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json;
#if LIGHT
using FastExpressionCompiler.LightExpression;
using E = FastExpressionCompiler.LightExpression.Expression;
using X = FastExpressionCompiler.LightExpression.Expression;
using L = FastExpressionCompiler.LightExpression.LambdaExpression;
#else
using FastExpressionCompiler;
using E = System.Linq.Expressions.Expression;
using X = System.Linq.Expressions.Expression;
using L = System.Linq.Expressions.LambdaExpression;
#endif
public class Box { public int Value; public Box(int x) { Value=x; } public int Add(int x)=>Value+x; }
public static class Program {
static object sink; static long checksum;
#if LIGHT
static L systemSource;
static System.Linq.Expressions.LambdaExpression systemPrepared;
#endif
record Scenario(string Name, Func<L> Build);
static X Balanced(X x,int depth) => depth==0?x:E.Add(Balanced(x,depth-1),Balanced(x,depth-1));
static L Build(string name) {
var x=E.Parameter(typeof(int),"x"); X body=x;
switch(name) {
case "constant": body=E.Constant(42);break;
case "add": body=E.Add(x,E.Constant(1));break;
case "arithmetic64": for(int i=0;i<64;i++)body=E.Add(body,E.Constant(i));break;
case "balanced256":body=Balanced(x,8);break;
case "conditional":body=E.Condition(E.LessThan(x,E.Constant(0)),E.Negate(x),x);break;
case "nullable":body=E.Coalesce(E.Convert(x,typeof(int?)),E.Constant(7));break;
case "new_member":body=E.Field(E.New(typeof(Box).GetConstructor(new[]{typeof(int)}),x),"Value");break;
case "constant_object":body=E.Call(E.Constant(new Box(7)),typeof(Box).GetMethod("Add"),x);break;
case "array":body=E.ArrayIndex(E.Constant(new[]{2,3,5,7}),E.And(x,E.Constant(3)));break;
case "new_array":body=E.ArrayLength(E.NewArrayInit(typeof(int),x,x,x));break;
case "interned_string":body=E.Property(E.Constant("fixed literal"),"Length");break;
case "noninterned_string":body=E.Property(E.Constant(new string('z',37)),"Length");break;
case "try_catch":body=E.TryCatch(E.Divide(E.Constant(42),x),E.Catch(typeof(DivideByZeroException),E.Constant(-1)));break;
case "try_finally":body=E.TryFinally(E.Add(x,E.Constant(1)),E.Empty());break;
case "switch":body=E.Switch(x,E.Constant(8),E.SwitchCase(E.Constant(3),E.Constant(0)),E.SwitchCase(E.Constant(4),E.Constant(1)));break;
case "goto":{var t=E.Label(typeof(int));body=E.Block(E.Return(t,x),E.Label(t,E.Constant(0)));break;}
case "loop":{var v=E.Variable(typeof(int));var t=E.Label(typeof(int));body=E.Block(new[]{v},E.Assign(v,E.Constant(0)),E.Loop(E.IfThenElse(E.LessThan(v,E.Constant(8)),E.Block(E.PostIncrementAssign(v),E.Empty()),E.Break(t,v)),t));break;}
case "blocks32":case "blocks128":{int n=name=="blocks32"?32:128;var arr=new List<X>();for(int i=0;i<n;i++){var v=E.Variable(typeof(int),"v"+i);arr.Add(E.Block(new[]{v},E.Assign(v,x),E.Add(v,E.Constant(i))));}body=E.Block(arr);break;}
case "invoke_literal":{var p=E.Parameter(typeof(int));body=E.Invoke(E.Lambda<Func<int,int>>(E.Add(p,E.Constant(2)),p),x);break;}
case "invoke_delegate":body=E.Invoke(E.Constant((Func<int,int>)(v=>v+2)),x);break;
case "invoke_try_argument":{var p=E.Parameter(typeof(int));body=E.Invoke(E.Lambda<Func<int,int>>(E.Add(p,E.Constant(2)),p),E.TryFinally(x,E.Empty()));break;}
case "capture1": return E.Lambda<Func<int,Func<int>>>(E.Lambda<Func<int>>(E.Add(x,E.Constant(1))),x);
case "capture_reference":{var v=E.Variable(typeof(Box),"box");return E.Lambda<Func<int,Func<int>>>(E.Block(new[]{v},E.Assign(v,E.Constant(new Box(7))),E.Lambda<Func<int>>(E.Field(v,"Value"))),x);}
case "nested_blocks8":case "nested_blocks32":case "nested_blocks128":{int count=int.Parse(name.Substring("nested_blocks".Length));for(int i=0;i<count;i++)body=E.Block(body);break;}
case "capture8":{var vs=Enumerable.Range(0,8).Select(i=>E.Variable(typeof(int),"v"+i)).ToArray();X sum=x;foreach(var v in vs)sum=E.Add(sum,v);return E.Lambda<Func<int,Func<int>>>(E.Block(vs,vs.Select(v=>(X)E.Assign(v,x)).Concat(new X[]{E.Lambda<Func<int>>(sum)})),x);}
case "quote_closed":return E.Lambda<Func<int,System.Linq.Expressions.Expression<Func<int>>>>(E.Quote(E.Lambda<Func<int>>(E.Constant(9))),x);
case "quote_capture":return E.Lambda<Func<int,System.Linq.Expressions.Expression<Func<int>>>>(E.Quote(E.Lambda<Func<int>>(x)),x);
case "runtime_variables":
#if LIGHT
var sp=System.Linq.Expressions.Expression.Parameter(typeof(int));
return System.Linq.Expressions.Expression.Lambda<Func<int,IRuntimeVariables>>(System.Linq.Expressions.Expression.RuntimeVariables(sp),sp).ToLightExpression();
#else
return E.Lambda<Func<int,IRuntimeVariables>>(E.RuntimeVariables(x),x);
#endif
default:throw new Exception(name);
}
return E.Lambda<Func<int,int>>(body,x);
}
static Delegate Compile(L tree,bool system) {
if(system) {
#if LIGHT
if(!ReferenceEquals(systemSource,tree)){systemSource=tree;systemPrepared=(System.Linq.Expressions.LambdaExpression)tree.ToExpression();}
return systemPrepared.Compile();
#else
return tree.Compile();
#endif
}
return tree.CompileFast(true)??throw new NotSupportedException("strict FEC returned null");
}
static string Verify(Delegate d) {
var parts=new List<string>();
foreach(int v in new[]{-2,0,3}) {
var result=d.DynamicInvoke(v);
if(result is Func<int> nested) result=nested();
if(result is System.Linq.Expressions.Expression<Func<int>> quote) result=quote.Compile()();
if(result is IRuntimeVariables vars) {var initial=vars[0];vars[0]=17;result=initial+":"+vars[0];}
parts.Add(result?.ToString()??"null");
}
return string.Join("|",parts);
}
static Action<int> Driver(Delegate d) {
if(d is Func<int,int> f)return n=>{long s=0;for(int i=0;i<n;i++)s+=f(3);checksum=s;};
if(d is Func<int,Func<int>> f2)return n=>{long s=0;for(int i=0;i<n;i++){var v=f2(3);sink=v;s+=v();}checksum=s;};
if(d is Func<int,System.Linq.Expressions.Expression<Func<int>>> fq)return n=>{for(int i=0;i<n;i++)sink=fq(3);};
if(d is Func<int,IRuntimeVariables> fr)return n=>{for(int i=0;i<n;i++)sink=fr(3);};
throw new Exception("driver "+d.GetType());
}
record Sample(string Version,string Runtime,string Flavor,string Scenario,string Phase,int Round,int SampleIndex,int Operations,double NsPerOp,double BytesPerOp,int Gen0,int Gen1,int Gen2);
static void Measure(string version,string flavor,string scenario,string phase,int round,Action<int> op) {
op(30);
int n=phase=="invoke"?10000:20;
var sw=Stopwatch.StartNew();op(n);sw.Stop();
n=(int)Math.Clamp(n*8.0/Math.Max(sw.Elapsed.TotalMilliseconds,0.01),phase=="invoke"?1000:10,phase=="invoke"?1000000:2000);
for(int k=0;k<7;k++) {
GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();
long allocated=GC.GetAllocatedBytesForCurrentThread();int g0=GC.CollectionCount(0),g1=GC.CollectionCount(1),g2=GC.CollectionCount(2);
long start=Stopwatch.GetTimestamp();op(n);long end=Stopwatch.GetTimestamp();
long bytes=GC.GetAllocatedBytesForCurrentThread()-allocated;
Console.WriteLine(JsonSerializer.Serialize(new Sample(version,Environment.Version.ToString(),flavor,scenario,phase,round,k,n,(end-start)*1e9/Stopwatch.Frequency/n,bytes/(double)n,GC.CollectionCount(0)-g0,GC.CollectionCount(1)-g1,GC.CollectionCount(2)-g2)));
}
}
public static void Main(string[] args) {
string version=args[0],flavor=args[1];int round=int.Parse(args[2]);bool system=version=="system";
string[] names={"constant","add","arithmetic64","balanced256","conditional","nullable","new_member","constant_object","array","new_array","interned_string","noninterned_string","try_catch","try_finally","switch","goto","loop","blocks32","blocks128","invoke_literal","invoke_delegate","invoke_try_argument","capture1","capture8","quote_closed","quote_capture","runtime_variables"};
names=names.Concat(new[]{"capture_reference","nested_blocks8","nested_blocks32","nested_blocks128"}).ToArray();
if(args.Length>3)names=names.Where(n=>args.Skip(3).Contains(n)).ToArray();
foreach(string name in names) {
try {
var tree=Build(name);var oracle=Compile(tree,true);var d=Compile(tree,system);var expected=Verify(oracle);var actual=Verify(d);
if(expected!=actual)throw new Exception("behavior mismatch "+actual+" expected "+expected);
Console.Error.WriteLine("PASS "+version+" "+flavor+" "+name+" "+actual);
Action<int> build=n=>{for(int i=0;i<n;i++)sink=Build(name);};
Action<int> compile=n=>{for(int i=0;i<n;i++)sink=Compile(tree,system);};
Action<int> first=n=>{for(int i=0;i<n;i++){var c=Compile(tree,system);if(c is Func<int,int> f)checksum=f(3);else if(c is Func<int,Func<int>> ff){var nested=ff(3);sink=nested;checksum=nested();}else if(c is Func<int,IRuntimeVariables> fr)sink=fr(3);else sink=((Func<int,System.Linq.Expressions.Expression<Func<int>>>)c)(3);}};
Measure(version,flavor,name,"construct",round,build);
Measure(version,flavor,name,"compile",round,compile);
Measure(version,flavor,name,"compile_first",round,first);
Measure(version,flavor,name,"invoke",round,Driver(d));
} catch(Exception e) {Console.Error.WriteLine("FAIL "+version+" "+flavor+" "+name+" "+e);}
}
GC.KeepAlive(sink);GC.KeepAlive(checksum);
}
}
from pathlib import Path
import subprocess,json
root=Path(__file__).resolve().parent
repo=Path(r'C:\Data\Git\Github\FastExpressionCompiler')
commits={'upstream':'ff907e1986700a30f05a56e5cee28e952faee363','parent':'e4c232da108d98790f0ca73f23ec90756da0cad7','head':'b8f8b38bc44367f129b49a55b2b2043e81b04ccb'}
for name,commit in commits.items():
for kind in ('standard','light'):
dest=root/name/kind;dest.mkdir(parents=True,exist_ok=True)
paths=['src/FastExpressionCompiler/'+f for f in ['FastExpressionCompiler.cs','ImTools.cs','ILReader.cs','TestTools.cs']]
if kind=='light': paths+=['src/FastExpressionCompiler.LightExpression/'+f for f in ['Expression.cs','ExpressionVisitor.cs','FlatExpression.cs']]
for p in paths:
(dest/Path(p).name).write_bytes(subprocess.check_output(['git','show',commit+':'+p],cwd=repo))
defines='<DefineConstants>LIGHT_EXPRESSION</DefineConstants>' if kind=='light' else ''
(dest/'Library.csproj').write_text('<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFrameworks>net8.0;net10.0</TargetFrameworks><LangVersion>latest</LangVersion><AssemblyName>FecSnapshot.'+kind+'</AssemblyName><Optimize>true</Optimize>'+defines+'</PropertyGroup></Project>')
(root/'commits.json').write_text(json.dumps(commits,indent=2))
from pathlib import Path
import subprocess
root=Path(__file__).resolve().parent
for version in ('upstream','parent','head'):
for flavor in ('standard','light'):
dest=root/version/flavor
(dest/'Harness.csproj').write_text('<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFrameworks>net8.0;net10.0</TargetFrameworks><LangVersion>latest</LangVersion><EnableDefaultCompileItems>false</EnableDefaultCompileItems><Optimize>true</Optimize>'+('<DefineConstants>LIGHT</DefineConstants>' if flavor=='light' else '')+'</PropertyGroup><ItemGroup><Compile Include="../../Program.cs"/><ProjectReference Include="Library.csproj"/></ItemGroup></Project>')
# Exclude harness from library default items (harness source is outside project).
with (dest/'build.log').open('w') as log:
p=subprocess.run(['dotnet','build',str(dest/'Harness.csproj'),'-c','Release','--nologo','-v:q'],stdout=log,stderr=subprocess.STDOUT)
print(version,flavor,p.returncode,flush=True)
if p.returncode: print((dest/'build.log').read_text());raise SystemExit(1)No production fixes, new commits, pushes, or changes to #557/#559 were made as part of this review. The PR remains a draft. Suggested next steps: address the Dynamic reduction failure, remove duplicated Invoke reflection, reduce capture metadata/reflection allocations, and eliminate label-free scope-stack heap growth; then rerun the same paired measurements and correctness controls. Shared-cell semantics should be preserved throughout. |
Follow-up: regression repairs and measured resultsPushed 46 logical source/test repair commits plus the evidence commit, through @dadhi, the allocation concern was valid. The worst common-path allocation regressions are repaired, but before-PR performance parity is still not achieved. What improvedRepresentative Standard/.NET 10 managed B/op from the latest broad run (rounded):
The mechanisms are per-compilation metadata/signature reuse, conservative capture storage selection, lazy quote/runtime-variable metadata, and bounded/compressed scope buffers. There is no cross-compilation user-type cache or outer System compiler fallback. The direct emitter and object[] closure layout are preserved. Quote/runtime-variable exposure and shared mutation retain the required live storage. The final compact scope pool saves 8,104 B in the deep-scope cold causal control; measured warm/reentrant diagnostics show no new scope-buffer misses. Deferring unrelated quote metadata removes the 880 B (.NET 8) / 888 B (.NET 10) common cold overhead in the causal control. Light private array capture is 32 B below pre-PR on both runtimes. The independently reproduced fresh-extension-reduction + Dynamic failure is fixed by preserving collected reductions; regression coverage also includes Quote composition. Framework readonly reference loads now carry the required types in verified anonymous methods. The previous What is still worse
Ratios above one mean slower. Compile time is still about 11.4% / 9.8% worse by this descriptive median. These medians do not promise parity in each scenario; the report lists every timing residual, including runtime phases.
No total native/JIT or retained-memory parity claim is made. Matching finalized managed IL in a capture control also does not prove identical native code or invalidate an observed timing residual. The known deep-tree crash remains deferred as requested. Validation and reproduction
Evidence directory · Decisions and limitations · Exact remaining costs · Validation · Repair commits. The directory contains the interactive report, complete phase CSV/JSON (including System), raw latest/historical archives, reproduction scripts, source/assembly hashes, and causal controls. Historical failures and rejected experiments remain preserved separately. The PR description has been updated with the same results and limitations. This remains a draft with unresolved performance regressions. |
Further allocation/compilation repairs@dadhi, pushed 7 additional logical fixes and their full measurement/validation evidence. Measured source:
The five Framework stages separately reduce constructor reflection (-192 B in the mutable control), reuse typed rented signatures (-64/-120 B), restore owned boxed cells (-320 B), and omit proven redundant delegate/closure casts (-32 B and -120 to -264 B). Runtime allocations do not increase in these causal controls. Compared with pre-PR, mutable nested invocation improves 24 B -> 0 B (factory plus invocation 208 B -> 184 B), while compilation still costs more. The other repairs avoid unused validator JIT and bypass the large collector for eligible binary expressions without a second pass. Full per-commit observations and cold controls are included; no uniformly faster CPU claim is made.
Measured compilation medians remain above pre-PR. These samples do not establish CPU parity; every scenario and phase remains in the full results. Allocation lower/same/higher counts for 41 common Standard shapes: .NET 8 14/27/0; .NET 10 14/27/0. The full table includes every runtime/activation phase, both libraries, Framework and System controls. Small fractional allocation differences in the adaptive runs did not reproduce with equal operation counts: none of 100 targeted current-versus-published phase comparisons allocate more (52 modern, 48 Framework). Empty observer windows allocate 0 bytes; no counter overhead is subtracted. The original results and modern/Framework confirmations are both retained. The allocation table above uses these equal-count controls. Still open: deep mixed128 cold scope allocation cost, small-EH cold initialization costs, protected/typed Framework costs and the timing residuals listed in the report. Shared-VM timings have substantial variation; total native/JIT/retained memory was not measured. Universal pre-PR parity is not established. Rejected experiments are retained, including wrapper cleanup with a consistently higher sampled cold allocation mode. Validation: 35,064 imported passes, 0 failures/crashes among executed cases, existing 69 exclusions unchanged; full native .NET 8/.NET 10 and Framework suites plus all configured production builds pass. Fresh 47-shape matrices cover before/published/current/System on Standard/Light modern runtimes and Framework. Each phase has two opposite-order process rounds and 7 samples; causal microexperiments retain separate raw/reversed/cold observations. Report and complete evidence | Per-commit measurements | Exact remaining costs | Validation | Commits | Decisions. |
| // Framework prohibits using the anonymous module with the module overload. | ||
| return new DynamicMethod(string.Empty, returnType, parameterTypes, restrictedSkipVisibility: true); | ||
| #else | ||
| // Preserve the anonymous module's non-Exception catch policy, without the |
There was a problem hiding this comment.
@jogibear9988 The hell is that hack? :)
Anyway, I am already feeling the boost in my knowledge of .NET internals.
There was a problem hiding this comment.
From where should I know?
I now wanted to let AI finish this so we get nearly the same perf. and know it can work. Then we can use this as a base when you want to do it error by error.
There was a problem hiding this comment.
Yes — this is a runtime-specific workaround, and your objection is fair. Looking more closely at the runtime source, I should remove this version.
The reason for changing the host is observable exception behavior: the host assembly's WrapNonExceptionThrows policy affects matching of raw CLR throws (e.g. a string) and RuntimeWrappedException. FEC's ordinary owner-hosted method and System's anonymous method do not have the same policy. The regression tests exercise raw/wrapped payload identity, catch matching, filters/rethrow, and private-member access; simply returning to the ordinary host for these methods would undo that behavior fix.
The workaround tries to get both properties: reuse the anonymous module for its exception policy, but use the module constructor to avoid the restricted anonymous constructor's eager JIT in CreateDelegate. However, .NET's initialization code explicitly asserts that this anonymous module must not be passed by callers. It works in the tested .NET 8/10 release runtimes, but that is not a supported contract. Framework already rejects this combination, hence the conditional fallback.
I removed the module-reuse workaround in 70f297cb and now use the supported anonymous DynamicMethod(..., restrictedSkipVisibility: true) constructor for exception-aware methods, retaining ordinary owner hosting elsewhere. .NET explicitly compiles that restricted method in CreateDelegate, so the measurements report compile-only, compile + first execution, steady-state calls, and allocations separately. Moving JIT cost into compilation must not be described as an equivalent increase in total first-use cost.
The correction, validation and measurements are pushed, and the description is updated. Complete results and remaining regressions. The PR remains a draft: universal pre-PR performance/allocation parity is not established.
| @@ -645,6 +779,184 @@ public struct CompilerContext | |||
| internal SmallMap4<Expression, NoValue, RefEq<Expression>> ArgsContainingComplexExpression; | |||
|
|
|||
| internal bool HasComplexExpression; | |||
| internal bool HasExceptionBlock; | |||
There was a problem hiding this comment.
combine those and follow up bools into the bit flags
There was a problem hiding this comment.
I tried packing the nine internal booleans into a ushort flag field, keeping the public LastEmitIsAddress field and existing ClosureStatus unchanged. It saves 8 bytes in CompilerContext on every tested target. I also tested a compact single-load setter form: each setter's IL shrinks by 5 bytes, with the same representation and semantics.
The correctness checks pass, but I have held both versions under the requirement not to introduce performance regressions. In the bounded matched tests, the compact version's Framework Light capture8 compilation increased from 6896.625 to 7088.625 B, and compile+first from 7304 to 7496 B: +192 B in each phase. The exact allocation mechanism is unresolved. Some compilation controls also slowed, despite a small improvement in the median across scenarios. The smaller context alone is therefore not enough to claim this change is an improvement. The original sources, both candidates, raw samples, IL evidence and limitations are preserved in the report; the production flags remain unchanged.
| // Frames follow the existing collector traversal; persistent ancestry is allocated | ||
| // only for expressions that actually contain a label or a jump. | ||
| internal SmallList<LabelScopeFrame, Stack4<LabelScopeFrame>, ScopeArrayPool<LabelScopeFrame>> LabelScopes; | ||
| internal SmallList<(LabelTarget Target, int Scope), Stack4<(LabelTarget Target, int Scope)>, NoArrayPool<(LabelTarget Target, int Scope)>> ScopedLabels; |
There was a problem hiding this comment.
Do we need whole int for the Scope?
Maybe for the compactness and usability we can put Target and Scope into struct with strict layout?
(same for the Target Region pair above)
There was a problem hiding this comment.
I checked the actual layout: the existing target+int tuples occupy 16 bytes on all six tested Standard/Light runtime configurations. Named sequential structs keep that reference-alignment footprint, so replacing the tuples alone does not produce the proposed saving.
Scope and Region are ancestry indices with no established ushort bound; the 1024 pool-retention limit does not limit valid input size. I kept full-width IDs and the existing named tuple fields rather than adding an input limit or an unaligned-reference layout. The separate one-word region-frame experiment is reported below, including its costs.
| return; | ||
| } | ||
| } | ||
| PushNewLabelScopeFrame(kind); |
There was a problem hiding this comment.
wut? so here you specifically prevent inlining?
There was a problem hiding this comment.
Yes, deliberately. PushLabelScope is aggressively inlined: repeated adjacent scope kinds update the last frame and return. The separate helper keeps the append/list-growth path outlined, instead of pulling that path into the common wrapper. This is a performance choice, not required for correctness or added just for measuring.
I tested removing only NoInlining against 8f, leaving the automatic JIT policy to decide. All 252 flag/version/shape checks passed, but the bounded matched benchmark did not establish a consistent win: 42 compilation comparisons had a median paired ratio 1.015, with controls ranging 0.921–1.149. For example Light .NET 8 label/goto compilation was 14.9% slower and Light .NET 10 balanced compilation 9.5% slower in that run. These are observations, not proof that this attribute is universally optimal, and unchanged generated-delegate runtime differences should not be attributed to the compiler helper. I retained the outline and added a source comment explaining it in ed431c7. The raw samples and limitations are in the report.
| internal int Region; | ||
| } | ||
|
|
||
| // These scalar scope lists grow their heap part as 4, 9, 19, 39, ... . |
There was a problem hiding this comment.
The logarithm reduces the pool's bookkeeping array, not the size of the rented buffers. These lists grow their heap storage as 4, 9, 19, 39, ... (2*n+1), so each normal growth length has a different floor(log2(length)) bucket. The 1024-element retention limit therefore needs 11 array-reference slots, instead of a table indexed by every possible length (1024 or 1025 slots). This avoids allocating about 8 KB of bookkeeping at the first overflow on 64-bit runtimes.
Different arbitrary lengths can still share a bucket. That can evict a reusable buffer, but cannot give the caller the wrong size: exact rents check equality, minimum rents check the actual length and search larger buckets, and a miss allocates the requested length. Interlocked.Exchange transfers ownership; the 1024 retention bound is unchanged. I clarified this in the source comment in ed431c7; the pool policy is unchanged.
| internal SmallList<LightAndSysExpr> Converted; | ||
| } | ||
| #endif | ||
| private class QuoteVariableScope : System.Linq.Expressions.ExpressionVisitor |
There was a problem hiding this comment.
Those are visitors, each for the specific case, but there is already manual visiting traversal in CollectAndValidate. Can we move this code there?
There was a problem hiding this comment.
Some shared traversal is possible, but these visitors have different stages and semantics. QuoteBinder runs at execution time with the current activation's live capture cells, which CollectAndValidate does not have. Quoted syntax is returned data: executable collection would prune branches, lower Dynamic expressions, collect nested lambdas, or reduce opaque extensions that must be preserved/rejected without callbacks. The existing lexical scope handling is already shared between quote analysis and binding.
ArgumentSpillScope also deliberately sees Try/Goto in constant-pruned branches. That controls whether runtime-expression compilation and Dynamic target reads happen before or after argument effects/jumps; the ordinary collector prunes those branches. Re-running collection would also risk duplicate Dynamic reductions and extension callbacks. I added boundary tests for opaque quoted extensions and pruned Try/Goto argument ordering in 35622f3, comparing the latter with System.
I measured three bounded cleanups rather than merging the traversals blindly: a lazy free-variable list (32/40 B less per captured-quote activation), a separate compile-only quote collector (40/48 B less), and a shared lazy argument scanner (24 B less terminal-argument compilation). They pass the correctness controls, but the quote revisions slowed compile+first-use, and argument timings were mixed. I held these changes to respect the no-regression requirement. The report retains their sources, benefits, costs and limitations; no visitor change is included in this update.
|
@dadhi, the anonymous-module workaround is removed in Four logical commits:
Validation: 35,064 imported passes, zero failures/crashes among executed cases; all production/native Standard/Light builds and suites pass. The existing 69 exclusions, including deferred deep-tree modes, are unchanged. The final matrix compares the last pushed
The decimal-chain first-use cost improves 14–35% versus the last pushed revision across the six configurations. Warm compilation removes 5,832 B on modern .NET / 14,048 B on Framework. However, modern EH compile-only now includes eager JIT: the .NET 10 chain's compile-only time is 57.3x its previously deferred-JIT baseline, while compile + first is 0.703x that baseline. The phase boundary matters. Identical emitted IL alone does not establish identical call performance. The try/catch and real-finally controls have matching canonical instructions/locals/EH but different host flags; .NET 10 Standard try/catch call time changes approximately 2.29 -> 1.87 ns. This does not prove a particular native-entry mechanism. Separate same-compiler A/A and native-code diagnostics are included; they are not used to erase observed costs. Remaining costs, retained in full:
The restore-guard experiment was rejected and empty-finally remains held. The unsupported supplied-ref-operator and preexisting pending-operand/outward-goto families remain documented; no new imported exclusions were added. Earlier pre-PR measurements retain their original source identities rather than being relabeled as final results. |
|
Reviewed all seven comments in review 5577873439 and replied individually with the decisions and measurements. Pushed three commits: The proposed packing/visitor changes remain held. They have real memory benefits, but the measured costs prevent claiming an improvement under the requested performance constraint:
Full validation: 35,064 imported cases pass, zero executed failures or crashes, 69 existing exclusions unchanged. Both production builds and .NET 8/10 native suites pass; Framework passes 1,041 TestX and 1,595 legacy tests. No emitter or production behavior change is included in this follow-up. Report, exact sources, all 13,596 warm observations, 60 cold allocation observations, hashes and validation logs. Compilation, first invocation, activation and nested invocation are separate; these are bounded managed-allocation/time measurements, not a claim of universal CPU parity. Review threads are left for the maintainer to resolve. This PR stays draft; #557 and #559 were not changed. |
187b9b8 to
ae4d9a5
Compare
|
Rebased #558 onto current dadhi/master The conflict resolutions preserve upstream's map-to-set cleanup and the existing compatibility fixes. Both expression membership trackers now use Fresh validation at
Rebase mapping, integration diff, hashes and full validation logs. Earlier performance data retains its original source hashes; no new timing or performance-parity claim is made for this base. Draft status is retained, and #559 was not changed. |
|
Merged current dadhi/master
Validation at Narrow before/after measurements: merged source Timing medians are mixed: converted-shape compilation ranges from 8.9% faster to 2.8% slower. Slower cases are Light net8 addition (+2.8%) and Light net10 selected method (+2.7%). Runtime medians range from about 0.2% faster to 2.5% slower, with the largest absolute increase about 0.036 ns. These small synthetic measurements do not establish universal speedup or parity; cold/first-use, tree construction, Framework/x64 timing and application workloads were not remeasured. Historical reports remain tied to their original revisions. Exact results, all raw samples, hashes, emitted IL, scripts and full validation logs. This remains draft. #559 is unchanged. |
|
Follow-up merge is pushed: Fresh checks pass: 35,064 imported cases, zero executed failures/crashes, 69 unchanged exclusions; both production builds and full .NET 8/10 ARM64 / Framework native suites pass. Exact counters, hashes and full logs. The runtime-value source clarification and review reply are included in |
…sTrue, Quote; opti for Invocation(Lambda(..))
Use unsigned-source checked conversion opcodes, zero-extend unsigned values widened to Int64, and sign-extend signed integers converted unchecked to UInt64. Apply ConvertChecked semantics and underlying enum conversions before constructing nullable targets. Keep the conversion switch and shared System/Light emitter layout. Add boundary matrices for primitive numeric types and signed/unsigned enums, covering nullable inputs/outputs, null, overflow, NaN, and infinity. Update the existing signed-source IL assertion and propagate ITestX failures to the native runner exit code. Validation: all 1,712 existing native tests plus the new regression methods pass on .NET 10; focused System/Light regressions pass on .NET 8. The 3,407-case imported conversion/coalesce slice improves from 353 failures to 39 failures, with no compiler fallback.
Implement planned nullable bool Not, AndAlso and OrElse support (dadhi#496) in the existing emitter. Share three-valued AND/OR result selection with the non-short-circuit operators while evaluating each operand once and respecting ignored results. Keep the nonnullable paths unchanged and reuse existing locals, nullable accessors and IL emission helpers. Preserve nullable result types in the LightExpression And/AndAlso/OrElse factories using the existing left-typed node. Add shared System/Light truth-table, side-effect, exception, member-access, ignored-result and long-branch regressions, including direct Light factory construction. Require strict compilation in issue dadhi#480 and run its LightExpression variant. Validation: all ten focused imported nullable Boolean cases now pass (previously ten failures). The adjacent logical slice retains its 79 passes and 40 existing custom-operator failures. The native .NET 10 runner passes all 1,712 existing tests and new ITestX regressions; 24 focused regression methods pass across System/Light on .NET 8.
Guard the existing collection and emission reduction loops against null or self reductions. Return the normal unsupported result instead of looping forever; leave system fallback and its validation exceptions unchanged. No extra graph traversal, reduction cache or rewrite phase is introduced. Add native regressions for irreducible and null-reducing nodes, valid reduction chains with parameters, a reduction that stops progressing during emission, strict/throw/fallback contracts, and CompileFastToIL. Validation: native .NET 10 runner passes all existing tests and the new regression groups. All 29 focused System/Light regression methods pass on .NET 8. The imported irreducible-extension cases now terminate immediately; their strict-system exception assertions remain failures because FEC uses its existing unsupported-expression contract.
Use the existing boxing helper before isinst, preserve void operand effects, and emit false for void/pointer targets with nonvoid operands. Keep TypeEqual unsupported. Add shared Standard/LightExpression regressions for nullable, enum, struct, reference, constant and local operands, plus ignored results and side effects. All 28 focused imported type-test cases pass; all 1712 native .NET 10 tests and 37 focused regression methods pass. Broader imported classes still encounter the separate boxed-constant conversion defect.
Register all four regression groups for Standard and LightExpression where supported, including the strict original issue 480 reproduction. Validation: .NET Framework passes 138 TestX methods and 1595 legacy tests with en-US UI culture supplied externally. Both libraries build all configured targets. All 37 new regression methods pass on .NET 8 and .NET 10. The full .NET 8 native runner retains the issue 346 crash reproduced on the parent branch; German UI culture retains two existing issue 261 error-text assertions.
Avoid a second virtual NodeType read on recursive collector entry; tail iterations still read their next node and preserve null/error handling. Native Standard and Light net10 tests pass. Collection-only paired samples improve balanced trees by about seven percent on net10 and three percent on net8; integrated compilation and runtime measurements follow in the repair report.
Quote and RuntimeVariables hoists now mark the root as requiring typed cells before any existing-hoist early return, then skip the ordinary mutation scan. Ordinary capture requests still analyze the entire root before choosing boxed storage, so any earlier nested body has already detected quotes, runtime variables or unknown storage uses and retained typed cells. Later ordinary analysis preserves the explicit typed flag. Native net10 Std/Light1712 legacy tests and regression fixtures pass, including independent Quote-only/RV-only/mixed earlier-child cases. Source-embedded net472/net10 probes pass same-assembly custom intrinsic and reducible Extension hidden-quote cases plus receiver/EH behavior. Both builds have zero warnings/errors. No timing or cold-start measurements performed.
Initialize CaptureAnalysisRoot at the public collection boundary before terminal intrinsic callbacks, rather than after the private terminal fast return. An opaque root intrinsic can collect a mutable nested lambda and then a quote through separate public calls; the old path analyzed only the first child, compiled boxed access, then changed the owner's storage to StrongBox and threw InvalidCastException. All validated roots and public custom collection calls funnel through the boundary, while recursive private helpers inherit the initialized root. Remove the repeated nonnull root probe from recursive collection. Add a root Constant intrinsic regression collecting both children separately and checking shared typed storage under default, no-inline and debug flags. Isolated publishedb8 control passes8/10/472; pre-fix09050 reproduces InvalidCastException8/10 and passes472; fixed passes all3 runtimes. Native net10 Std/Light1712 legacy tests and registered regressions pass1135ms, zero warnings/errors. No timing performed.
Use the existing argument-provider helpers when examining NewArray operands, matching other capture-analysis traversals. Light small-array Expressions getters allocate a new array on every access, so the previous Count/index/Count loop allocated three operand arrays per visit for a one-element array. This only changes access to the same operands; the private-reference safety proof and closure layout are unchanged. Prepared source-only while the frozen20d benchmark runs continue. Native correctness and targeted before/after allocation/performance validation remain pending; no measured parity claim is made.
Framework anonymous EH methods verify object[] slot loads as object. Cast readonly nonpassed reference values to their declared type before field/method access or typed returns, both in the normal captured-value emitter and the public reference-slot helper. Include UserProvided contexts because collection-skipping APIs also use anonymous hosting without a collected HasExceptionBlock flag. Owned ordinary methods and modern emitters retain their existing IL. Regular closure constants already cast on Framework; also type the declaring object before a Light public refField constant load on verified contexts. No closure representation, hosting, factory or security policy changes. Cover readonly local/parameter field and method captures across EH, typed/null/interface/array captures, ordinary/precreated constants, the public UserProvided slot helper, and Light field-backed constants under default/no-inline/debug flags. Focused CapturedValueRef fixture passes Std29/Light32 tests on net472/net8/net10, all6 builds clean. Isolated beforee4 returns7, unfixed20d reproduces VerificationException, and fixed returns7 with castclass Box in the same anonymous host. No benchmark timing performed.
Keep the three helper MethodInfo caches in one precisely initialized nested holder. Plain expressions avoid their reflection initialization, while quote and runtime-variable compilation resolves all three together. This preserves the old feature initialization point and does not move merge metadata work to the first quoted-runtime-variable invocation. Existing emitted calls, closure layout, and object.GetType metadata remain unchanged. Allocation-only validation retained all 216 samples: nine fresh processes for each scenario/runtime/version comparison on .NET 8 and .NET 10. Add and balanced compilation medians save 880/888 bytes relative to 0fa003b and match e4c232d. Quote/runtime-variable cold allocation envelopes are unchanged. First-invocation and warm compile/invoke min, median, and max allocations never exceed the current baseline; normal values match. All cold bimodality and occasional baseline noise remain recorded. No timing claim is made.
The nested holder does not inherit the surrounding compiler's RequiresUnreferencedCode context. Apply the same contract to its reflection initializers instead of disabling analysis. Normal native and analyzer-enabled Standard/Light production builds pass.
Replace the 1024-entry exact-length tables used only by lexical and exception scope metadata with a lazy internal 11-bucket pool. Preserve SmallList growth, requested allocation lengths, global sharing, atomic exclusive leases, and the 1024-element retention limit. The used heap sizes 4,9,19,39,79,159,319,639 occupy separate buckets, retaining reentrant size classes without one-largest-buffer eviction. No traversal or validation changes. Allocation-only causal controls versus 0ec7a13 on .NET 8 and 10: cold nested_mixed128 saves 8104 bytes in all three fresh-process repetitions; cold add and balanced256 remain identical. Twelve warmed scenarios retain the same allocation floors. Instrumented scratch-only repeats (5x1000 operations on locals, mixed scopes, capture8 and reentrant nested compilation per runtime) report zero new scope-buffer misses; small 160/168-byte runtime allocation events occur without pool misses. Full Standard and Light native .NET 10 runner: all 1712 tests pass with normal analyzers, including concurrent scope-overflow compilation. No timing claim.
Guard the post-collection validators with their existing goto-list counts. ValidateLexicalJumps only traverses ScopedGotos and ValidateExceptionJumps only traverses ExceptionGotos; when both lists are empty neither has work or side effects. Preserve the existing scope-info cleanup and all validation for trees with jumps. This adds no prescan, cache, visitor or closure layout change. EventPipe JIT controls confirm the two unused validators are no longer compiled on the first ordinary addition. Nine alternating fresh-process controls for five shapes per runtime keep all managed allocation medians identical. Cold compile-plus-first ratios versus 6487c9b are 0.907-0.974 on .NET 8 and 0.964-1.013 on .NET 10; these include host/process variability and are not a universal speedup claim. Warm compilation controls are mostly within a few percent with unchanged allocation medians. Runtime controls and their variability are retained alongside the cold samples, not treated as an emitter change. Full native Standard/Light suites pass: .NET 10 1712, .NET 8 1711, including lexical/exception jump rejection and valid control-flow regressions.
CLR 4 allocates 144 bytes for each closed generic constructor resolved with GetMethodFromHandle. Its ordinary empty constructor lookup allocates nothing, and value constructor lookup needs 96 bytes with the existing rented Type array. Use those APIs for Framework only, and avoid initializing unused open handles. Keep modern constructor handles and all closure representations unchanged. Allocation-only causal probe: per-type constructor lookups 288 -> 96 bytes. Separate-process warmed mutable-capture compilation: 5272 -> 5080 bytes (-192). Activation stays 184 bytes, nested invocation 0, factory/invoke 184. Readonly capture8/reference bytes stay unchanged. Forward/reverse fresh-process timing medians observe mutable compilation ratio 0.927; unchanged-path timing variation is retained and is not attributed to this metadata-only change. Validation: normal Standard and Light production builds pass every configured target with zero warnings/errors. CapturedValueRefRegressionTests passes 29 Standard and 32 Light cases on net472, net8.0 and net10.0. Parsing/compilation, activation, nested runtime and factory/runtime measurements captured separately for Framework baseline/current/candidate and modern current/candidate controls. Scratch reproduction and raw data: work/pr558-repair/frame-capture-round2/.
For Framework nested lambdas with user parameters, declare the hidden argument as ArrayClosureWithNonPassedParams. Existing Curry<C,...> already supports this type, so capture accesses no longer need repeated closure casts. Record the private method's exact signature in a status bit; public manual emitter contexts keep their original cast behavior. Keep zero-user-argument signatures unchanged: a separate cached array caused a measured cold allocation regression and was rejected. No closure fields or runtime allocation layout changes. Add regression coverage for private reference captures under exception handling, zero-argument parent signature reuse, Action<T> currying and shared mutation, debug flags, ordinary parent signature rentals and concurrent compilation. Framework warmed and first-use allocation: Func<T> and EH Func<T> compile -120 B; Action<T> -64 B; constant-folded dead capture unchanged. Runtime activation, nested invocation and factory/invoke allocation unchanged. Modern control bytes unchanged. Parsing/runtime timings are captured independently; mixed compiler ratios and VM variability do not establish a CPU guarantee. No extra first-use signature array is allocated by this conservative version. Validation: normal Standard and Light production builds pass all configured targets without warnings/errors. CapturedValueRefRegressionTests: Standard30, Light33 on net472/net8/net10. The broader typed prototype also passed full Framework1003 TestX and1595 legacy tests before zero-argument paths were narrowed back to the existing implementation. Scratch source/raw measurements and the rejected no-argument cold tradeoff remain under work/pr558-repair/frame-capture-*.
Collect eligible root binary operands directly through the existing terminal collector. This avoids JIT-compiling the large scoped visitor for a simple root, without adding a prepass, changing closure storage, or bypassing jump checks on complex trees. Preserve left-to-right failures, constant identity and public cleanup. Trust ordinary Light nodes as requested; explicit intrinsic roots or operands retain their existing collection callbacks and scopes. Measured Standard add warm compile ratios: 0.961 (.NET 8), 0.927 (.NET 10). Nine fresh-process add compile+first medians: -21% / -34%, identical bytes. Framework add cold compile+first: 156.40 -> 118.90 ms; 27080 -> 27040 B. Trusted Light paired add warm compile ratios: 0.961 / 0.920, 872 B unchanged. All measured warmed allocation medians are unchanged, including invocation. Cold and fallback timing controls varied substantially with host load; retain all raw results rather than claim universal CPU parity or speedups. The rejected Light generic-type-provenance prototype added 160/168 B cold and is not included. Add public collector error-order and bound-constant identity regressions, plus Light intrinsic callback/scope regressions. Full native Standard/Light suites: 1712 passing on .NET 10, 1711 on .NET 8. Separate Framework harness checks agree with System results. Evidence: workspace work/pr558-round2/trivial-binary*, to be published with the integrated round-2 report after final validation.
Enable the existing native boxed value-cell representation for nonnullable Framework captures only in the normal fully collecting root entry. Carry that root eligibility through nested contexts and require completed capture analysis. Any exception region anywhere in the root, quote, runtime variables, or unknown node retains the StrongBox representation throughout the root. Manual and collection-skipping APIs never establish owned-root eligibility, including their privately compiled children. Hosting/security policy and the existing object[] closure layout are unchanged. Add controls for sibling order, outer exception regions, flags/debug mode, shared identity, and manual default/UserProvided collection. Reuse existing struct, byref, enum, nullable, quote, runtime-variable and unknown-node tests. Framework measurements against 7a871c4d, separate one-compiler processes: - Mutable capture compile: 5080 -> 4760 B (-320 B). Pre-PR e4c232d is 4376 B, so the remaining compile allocation residual is explicitly +384 B. - Activation/invoke stay 184 B, nested invocation stays 0 B. - Fresh first compile+invoke: 40272 -> 37888 B (-2384 B). - Capture8/reference/protected capture compile and runtime bytes unchanged. - All 32 modern runtime/scenario/phase allocation controls unchanged. - Descriptive two-order warm medians: mutable compile 43.92 -> 41.92 us, activation 31.80 -> 30.23 ns, nested invocation 6.064 -> 5.993 ns. Three fresh cold medians: compile+first 246.84 -> 221.64 ms. Unchanged controls show substantial host timing variation; these timing observations do not establish universal CPU parity or causal speedups. Validation: normal Standard/Light production builds all configured targets, zero warnings/errors; focused Standard/Light fixture 32/35 on Framework, 31/34 on net8 and net10, all passing; full Framework en-US runner passes 1007 TestX and 1595 legacy tests. Initial German-culture run had only existing English-message assertion failures. Independent source review found no actionable correctness defect. Raw evidence: work/pr558-repair/frame-owned-boxed.
Restore FEC's original nested-delegate slot load only when the normal root has completed capture analysis and proves owned hosting throughout without Try, Quote, runtime variables or unknown nodes. Manual/UserProvided and anonymous exception-hosted methods retain the exact verifier cast. Hosting, closure layout and delegate/Curry construction are unchanged. Against 25955bb5, four-shape one-compiler Framework probes show -32 B compile for mutable, capture8 and reference captures, warm and cold; protected capture_try and all runtime allocations are unchanged. Mutable compile is 4728 B versus pre-PR 4376 B, leaving +352 B explicitly unresolved. Two opposite process-order warm rounds and three fresh cold reps measure mutable compile 40.81 -> 36.21 us, activation 26.02 -> 24.88 ns, nested invocation 5.467 -> 5.483 ns, compile+first 185.55 -> 183.44 ms. Host timing variation in unchanged controls prevents universal CPU claims. Normal Standard/Light builds all targets: zero warnings/errors. Focused fixtures pass on Framework/net8/net10 (32/31/31 Standard, 35/34/34 Light). Full Framework en-US runner: 1007 TestX +1595 legacy passing. Independent source audit confirms the conservative owned-host guard. Evidence: work/pr558-repair/frame-owned-delegate/measurements.json and raw logs.
Omit the hidden closure cast only under the same completed ordinary owned-root proof as the nested delegate cast. Typed signatures keep their existing fast path; all anonymous/manual/UserProvided/EH/Quote/RV/unknown cases keep exact typing. No signature arrays, metadata caches, hosting or layout changes. Against 3320b13e, Framework warm/cold compilation saves 208 B for mutable, 120 B for readonly reference and 264 B for capture8; protected capture_try and runtime allocations are unchanged. Mutable compile is now 4520 B versus pre-PR 4376 (+144 B); reference is 4256 versus 4264 (-8 B). Two opposite process-order warm rounds and three fresh cold repetitions: mutable compile 42.83 -> 38.82 us, nested invocation 5.730 -> 4.963 ns, compile+first 239.07 -> 204.11 ms. Activation 25.67 -> 28.55 ns despite its unchanged emitted code illustrates host noise; no universal CPU claim. Normal Standard/Light builds all targets pass with zero warnings/errors. Focused fixtures pass on Framework/net8/net10 (32/31/31 Standard, 35/34/34 Light), including protected siblings, manual contexts and shared mutation. Combined final full suite is scheduled by the parent task. Evidence: work/pr558-repair/frame-owned-closure/measurements.json and raw logs.
Reuse the existing atomic-operand predicate for non-lifted operands. Keep complex right operands, nullable lifting, custom Light intrinsics and operator evaluation order on their existing paths. No new metadata query, cache, analysis pass or closure layout is introduced. For 64 decimal additions under real finally blocks, isolated compilation allocations fall from 11496 to 5664 B on modern .NET and 21136 to 7088 B on Framework. Warm compile improves 16-30%, compile plus first execution 12-44%, and invocation 7-19% across Standard/Light on .NET 8/10/Framework. Native fixture checks pass in all six configurations. Cold controls and material timing outliers remain in the evidence; universal parity is not established. Full combined validation follows this logical commit.
Emit non-byref try body and catch results with the existing value-parent mask before storing the typed result temporary. Receiver, lvalue and duplication flags must not cause a managed address to be stored as a struct value. Preserve enclosing EH/inlining context and genuine byref Try results; mark a loaded ordinary result as a value. Fixes the introduced Light new-struct receiver regression and existing variable/field/ref-parameter receiver forms, preserving mutation-copy semantics. Register seven native regression methods for Standard/Light on modern .NET and Framework. All six focused configurations pass40cases; modern byref-return aliases remain correct. No hosting, metadata cache, closure layout or empty-finally elimination is changed. Performance controls retain unchanged runtime/activation allocations. A genuine Framework Light compilation-history allocation residual of 192B in two capture controls remains documented; isolated controls match. This correctness repair does not claim universal performance parity. Full combined native/imported validation follows this logical commit.
Retain collision and stable-reference tests against the master implementation.
…ware methods Address dadhi review discussion_r4318926508: passing the runtime anonymous module into the module overload violates an explicit CoreCLR assertion even though tested release runtimes accept it. Remove that module-reuse workaround and its lazy holder. Use the public restricted anonymous constructor on every target; ordinary owner-hosted methods are unchanged. Keep raw/wrapped non-Exception catch matching and private-member access semantics. The supported constructor eagerly JITs at CreateDelegate, so compile-only timings must be reported separately from compile plus first execution. NonExceptionThrowPolicy, ExceptionRegionValidation and TryResultValue fixtures pass Standard/Light on net8.0, net10.0 and net472.
aaa79fe to
dd98968
Compare
Freeze master f49acef, locally merged dadhi#558 at 593c72e2, and optimized dadhi#559 source ef577910. Record 44 workloads, Standard/Light, .NET 8/10, construction/compilation/warm invocation plus a separate compilation-and-first-invocation pass. Retain 34496 main/reference samples, per-commit allocation/timing evidence, actual root and nested IL strings, source/binary hashes, rejected candidates and validation logs. Compile-only medians improve 0.7-2.2% versus dadhi#558 but remain 8.2-9.9% above master. Compilation plus first invocation remains 0.1-1.5% above master on median workload ratios. The apparent nested-exception compile-only outlier shifts JIT work into CreateDelegate for the correctness-required anonymous host; the end-to-end pass accounts for it. Keep slower System comparison cases and unchanged-code allocation controls explicit. Correct the historical IL claim: its serializer compared StringBuilder metadata. The new actual-instruction check matches all 176 root/nested bundles between dadhi#558 and dadhi#559. Production and native suites, Framework 1051 TestX plus 1601 legacy tests, and all 35064 executed imported tests pass. Existing exclusions unchanged; production source unchanged by this evidence commit.
Rebased onto current master (25 October 2026)
This PR is now based directly on dadhi/master
0f37356fc9ca2c05b085f94592cfaf492f78eacf. The imported-suite dependency is already in master. The history is linear: 189 commits, zero merge commits, replacing the previous published 210-commit history.Removed upstream/superseded implementation commits and the reverted quote-block experiment. Folded the old merge corrections into their owning changes, including direct-lambda/runtime-expression invocation handling. Retained extra SmallMap regression coverage against master's implementation and folded in its helper rename. Historical measurement and checkpoint reports are consolidated into one documentation commit.
Master's constructor signature-pool return is preserved. Its smaller nullable-Boolean emitters remain the paths for supported exact Boolean shapes; the broader compatibility emitters handle additional operand/method forms. Existing upstream tests are retained.
Framework validation sets current and default-thread UI culture to English: existing Issue261 assertions expect English exception text and fail under this machine's German defaults. Those assertions were not changed. The imported total includes additional behavior proofs; original fixed-import case totals below remain historical. Deferred deep-tree exclusions remain in place.
Rebase details, old/new commit map, source hashes and complete validation logs.
The earlier timings below describe their recorded source snapshots; no new timing/allocation claim is made for this rebase. PR #559 is unchanged and will need its own rebase onto this rewritten history. This PR remains draft.
Historical master merge and runtime-expression clarification
f089ff5bdocuments that a LightExpression parameter/constant can carry a SystemExpression<TDelegate>runtime value without mixing AST node types. The review reply links the existing parameter/constant regression test.Merged current dadhi/master
233ed5adin7949ee5d, retaining upstream's iterative conversion stripping and node-type predicate while preserving #558's shared comparison/arithmetic emitter and reference-comparison semantics. The conflict did not restore a duplicate standalone comparison emitter.Fresh validation at
7949ee5d: 35,064 imported passes, zero executed failures/crashes, 69 existing exclusions unchanged. Both production builds, full .NET 8/10 ARM64 native suites (1,713 / 1,714 legacy cases plus registered regressions), and Framework (1,041 TestX + 1,597 legacy cases) pass. Hashes, exact counters and full logs. No fresh timing claim for this merge; earlier measurements below retain their original source IDs. Remains draft.Earlier upstream integration (24 October 2026)
Merged dadhi/master
5473b427, including #562 and upstream's SmallMap repair, in700053a1. Applied #562 review feedback to #558's shared arithmetic emitter in884ed766: recognize ordinary converted simple operands, while explicitly preserving intrinsic emitters throughout conversion chains. #562 regression coverage is included and extended; duplicate comparison emission is avoided.Fresh validation at
884ed766: 35,064 imported passes, zero executed failures/crashes, the same 69 exclusions. Full .NET 8/10 ARM64 native and Framework suites pass; targeted Standard/Light checks also pass on .NET 8/10 ARM64/x64.Converted-addition compilation allocates 8 B less in the narrow ARM64 measurements; selected-method/scalar allocations are unchanged, delegate execution allocations remain zero. Compilation timings for converted shapes range from 8.9% faster to 2.8% slower, runtime medians from about 0.2% faster to 2.5% slower. No universal timing parity/speedup claim. Exact measurements and validation evidence. Earlier reports below remain historical and source-specific. PR stays draft; #559 is unchanged.
Rebased onto current upstream master (20 October 2026)
Rebased onto dadhi/master
3aefb5e5, which includes merged #557. Conflict resolutions retain upstream'sSmallSet4tracking and the compatibility fixes. The Issue476 test now uses the renamed map helper. One already-upstream Light conversion patch was dropped as redundant. #559 was not rewritten.Fresh full validation at
5ce56174: 35,064 imported cases pass; zero executed failures/crashes; the same 69 exclusions. Production builds and .NET 8/10 native suites pass (1,714/1,713 legacy cases). Framework passes 1,041 TestX and 1,597 legacy cases.Rebase mapping, exact integration delta, source/assembly hashes and validation logs. Earlier performance measurements remain labeled with their original source revisions; they were not remeasured on this base. The PR remains draft.
Maintainer review follow-up (20 October 2026)
Reviewed and answered all seven comments in review 5577873439. Added source explanations for logarithmic pool bookkeeping and the outlined frame-growth helper (
ed431c7f), plus boundary coverage for opaque quotes and constant-pruned Try/Goto argument ordering (35622f32).The packing/visitor/inlining experiments are not merged: they showed useful size/allocation savings but did not meet the requested performance constraint. Packed flags V2 observed +192 B per Framework Light capture compilation/first-use; quote revisions slowed first use; region packing and argument-scan timings were mixed. Full sources, distributions, hashes and held patches are preserved in the review report.
Validation at
35622f32: 35,064 imported cases pass; zero executed failures/crashes; 69 existing exclusions unchanged. Both production builds and .NET 8/10 native suites pass. Framework passes 1,041 TestX and 1,595 legacy tests. The compiler's behavior and emitter remain unchanged from8f12292f; this is not a new claim of universal pre-PR/System parity.The earlier compatibility/performance work and its source-specific measurements remain below. This PR remains draft; #557 and #559 are unchanged.
Base
Based on upstream master, now merged through
233ed5ad(#557 and #562 are merged). This PR contains the remaining compatibility work. Keep this PR draft; the separate performance PR #559 is unchanged.Problem and resulting behavior
The imported dotnet/runtime expression tests exposed incorrect results, invalid IL, missing supported operations, and validation gaps. These commits repair them within FEC's existing direct IL emitter and LightExpression architecture. The existing object[] closure layout remains in use. There is no outer System compiler fallback, whole-tree lowering pipeline, global capture cache, stack-size workaround, or test-specific production behavior. The separate performance PR #559 remains separate; the regression repairs and measurements for this layer are summarized below.
Preserve numeric, nullable and operator semantics, exact TypeEqual behavior, reference identity, control flow and exception cleanup.
Correct rank-one/non-zero-bound array accesses, typed element addresses, ref argument storage and property/indexer copyback across methods, constructors and delegate invocation. Retain readonly defensive copies, writable static/Unbox/array receiver addresses, exact typed aliases and runtime covariance checks.
Emit nested member/list bindings with correct struct mutation, once-only getters, side effects and nonvoid Add cleanup.
Support invoking runtime expression-tree values. Compiling that runtime value is an intrinsic operation, matching System's behavior; FEC still compiles the surrounding lambda directly. Preserve argument-local returns and jumps through enclosing calls, constructors and invocation wrappers.
Compile Dynamic expressions through per-occurrence DLR call sites, retaining binding rules and reading each site's current Target. Stateful nested occurrences keep separate sites.
Support captured quotes and nonempty RuntimeVariables with shared typed StrongBox cells in the existing closure slots; ordinary captures now use the conservative storage selection described in the repair update. Outer code, sibling delegates and returned trees see the same live mutable storage. Independent activations and block entries retain separate lifetimes; shadowing, duplicate aliases, mixed local/captured variables and more than 64 captures are covered. Capture-free quotes retain identity. By-ref/pointer/byref-like captures that cannot live in heap cells remain rejected.
Bind reused labels within lexical scopes, validate illegal expression/block entry and exception-region jumps, preserve physical method-tail returns, and enforce rethrow/filter boundaries. Scope validation reuses collection metadata; branch pruning and the public collection signature remain intact.
Restore declaring-variable scopes correctly, avoid duplicate intermediate struct-field addresses, and load arithmetic/unary/conversion/type-operation operands by value while preserving actual writable and explicit by-reference storage.
Preserve ordered runtime switch-test evaluation and void results, TypeDelegator/TypeBuilder identity, custom-node reduction contracts, character width through managed references, and valid constructor fast paths. Reject invalid abstract construction and excessive method arity before returning invalid delegates.
Earlier feature/fix commits include
cdb0b13b(live quoted captures/RuntimeVariables),0c07d7b2(lexical jump validation), and3cf56a5c(unary/conversion value contexts). Fixes and associated regression coverage are committed in logical units.Latest repair and measurement update
The anonymous-module workaround is removed in
70f297cb, following your review. Exception-aware methods now use the supported restricted anonymous constructor; ordinary methods keep FEC's existing owner host. This remains a draft: universal pre-PR performance/allocation parity is not established.Four logical commits:
44ee17d7: remove unnecessary arithmetic spills under EH using the existing atomic-right check. No additional traversal/cache; nullable lifting, ordering and Light callbacks remain covered.6d397bc6: materialize Try results correctly before taking struct receiver addresses; preserve genuine byref results.3be541d3: fix the preexisting SmallMap16 promotion hang, with collision/growth/reference-stability tests. The interrupted earlier validation was this unchanged helper bug.70f297cb: remove anonymous-module reuse and use the supported constructor.Validation: 35,064 imported passes, zero failures/crashes among executed cases; all production/native Standard/Light builds and suites pass. The existing 69 exclusions, including deferred deep-tree modes, are unchanged.
The final matrix compares the last pushed
42f98478with production source70f297cband System: 9 shapes × 6 configurations, 12,474 samples / 198 phase groups. There are 21 rotated samples per compiler/phase. Compile-only uses independently calibrated counts; other phases share FEC counts, with System calibrated separately. All samples are retained, including 109 below 20 ms. These are machine-specific observations, not application-wide speedups.capture8contains eight block locals plus the outer parameter. Ratios above 1 mean slower. FEC compiles faster than System in 52/54 selected shape/configurations; compile + first call is faster in 47/54. Delegate-call performance is mixed.The decimal-chain first-use cost improves 14–35% versus the last pushed revision across the six configurations. Warm compilation removes 5,832 B on modern .NET / 14,048 B on Framework. However, modern EH compile-only now includes eager JIT: the .NET 10 chain's compile-only time is 57.3x its previously deferred-JIT baseline, while compile + first is 0.703x that baseline. The phase boundary matters.
Identical emitted IL alone does not establish identical call performance. The try/catch and real-finally controls have matching canonical instructions/locals/EH but different host flags; .NET 10 Standard try/catch call time changes approximately 2.29 -> 1.87 ns. This does not prove a particular native-entry mechanism. Separate same-compiler A/A and native-code diagnostics are included; they are not used to erase observed costs.
Remaining costs, retained in full:
All 90 runtime/activation allocation medians are unchanged versus
42f. One-capture creation still allocates 184 B versus System's 152 B; this is distinct from a zero-allocation child call.Primary compilation/first-use medians: 28 higher, 139 equal, 31 lower. Separate equal-count confirmation (5 samples × 200 operations, exact frozen libraries; 2,970 rows) gives 8 higher / 164 equal / 26 lower. Six Framework Light capture phases retain approximately +192 B/compile after prior EH history; two Framework Standard phases show +0.8 B medians. None of the original 22 tiny positive median differences remain positive in that separate check. Both datasets remain; the exact allocator is unresolved.
Timing costs remain visible, e.g. Standard .NET 10
capture8child calls are 1.082x the last pushed median. The map check has equal allocation medians but a worst SmallMap8 timing ratio of 1.112x; the map repair is not universally cost-free.Historical pre-PR regressions and cold allocation modes remain separately documented. There is no final-70f fresh-process cold matrix, and total native/JIT/retained memory was not measured.
The restore-guard experiment was rejected and empty-finally remains held. The unsupported supplied-ref-operator and preexisting pending-operand/outward-goto families remain documented; no new imported exclusions were added. Earlier pre-PR measurements retain their original source identities rather than being relabeled as final results.
Report, every phase comparison, allocation confirmation, reproduction sources/hashes, and raw validation.
Historical previously published repair/performance record
The following preserved record describes db640cb/42f and its separately frozen predecessors, not final70f timings. The latest source/validation and residual costs are above.
Further regression repairs and measurements
Keep this PR as a draft: universal pre-PR performance parity is not established.
The additional 7 logical repair commits preserve FEC's direct emitter and object[] closure layout. They skip empty jump validation, reduce Framework reflection metadata, type capture parameters in existing rented signatures, collect eligible binary roots with two parameter/constant operands without the large visitor, and restore cheaper Framework boxed cells/casts only where the existing owned host is proven. Ordinary Light node shapes are trusted; intrinsic callbacks retain their original scopes. No extra whole-tree prepass, global user-type cache, outer System fallback, or hosting/security-policy change is added.
Mutable Framework capture still has a compilation-allocation cost above pre-PR. Its nested mutation invocation improves 24 B -> 0 B, and factory plus one invocation 208 B -> 184 B; activation alone remains 184 B. Individual stages and timing observations are recorded per commit rather than attributing the entire difference to one patch.
These broad matrix ratios are descriptive medians across 41 working-on-both shapes; ratios above 1 are slower. They are not promises for every scenario. Compilation allocation counts (lower/same/higher): .NET 8 14/27/0, .NET 10 14/27/0, using 1 B tolerance for fractional amortized counters; complete values are retained.
Evidence and remaining limits
Combined modern matrix: 47 shapes, Standard/Light, .NET 8.0.28/10.0.9, pre-PR/published/current/System, two opposite-order process rounds and 7 samples per phase. Framework has the same 47-shape fresh before/published/current/System matrix, both flavors and both orders.
Construction, compilation, compilation plus first execution, warmed invocation, capture activation and nested invocation are separated. Microexperiments retain alternating pairs, fresh-process cold samples and reversed controls. Compile-only EH work can move to delegate creation; assess compile+first too.
Shared-VM timing variation is substantial, including unchanged-emitter controls. Full timing/allocation residuals remain listed in the report; matching IL is not used to erase an observed timing result.
Small fractional allocation residuals from adaptive loop counts were checked separately with identical operation counts. No allocations increased in the 100 targeted current-versus-published phase comparisons: 52 modern and 48 Framework. Original adaptive results are retained without subtraction or replacement. Empty-observer samples allocate 0 bytes; fractional sample contributions are not described as counter overhead. The Framework allocation table above uses the equal-count control; broad matrix timing ratios use the original adaptive runs.
Deep mixed128 cold scope growth remains a known cost: 10,328 B above pre-PR in the causal control (seven growth arrays plus compact pool table). Small EH examples' cold excess is not attributed to overflow arrays; they fit inline buffers. No larger shallow allocation is hidden to improve one deep shape.
Protected/typed Framework paths keep the casts/storage required for correct execution. Cold allocations and runtime-specific costs are separate from warm counts. No total native/JIT or retained-memory parity claim is made: counters measure managed allocations.
An independent source review of the seven repairs found no actionable correctness defect. Its scope and limits are recorded in the review; runtime conclusions use the separate frozen-source validation.
Rejected/held experiments are preserved: generic Light provenance checks, empty-pool atomic-read guard, terminal helper split, outlining trials, and wrapper-load cleanup. The latter repeatedly hit a higher .NET 10 cold allocation mode; its cause is unknown and parity was not claimed.
The broader binary-region shortcut is also held: balanced warm compilation improved, but repeated Standard .NET 10 cold scalar controls did not establish parity. Its NoInlining attribution was not corroborated by traces; neither variant is included.
Interactive report source and full evidence | Per-commit Framework results | Remaining costs | Validation | Logical commits | Decisions and limits.
Measured and validated source:
db640cba760028d1c49ad5e4abf67f38fe683bbf. Evidence/documentation head:42f9847843cec8edaaf11c00cb97a5dd576b82cf. Controls: pre-PR parente4c232da, last published #5586487c9b2, and System.Linq.Expressions. The earlier reviewed headb8f8b38band its original repair evidence remain available in the previous report. #557 and #559 are unchanged.Compatibility boundaries and behavior proofs
The original compatibility cases retain explicit behavior proofs:
runtime#19094)The CLR uses the generated method's host assembly policy when matching non-Exception throws. Collected methods with exception regions now use anonymous DynamicMethods, matching System. Ordinary methods and the no-argument constructor fast path retain FEC's owned hosting. The two APIs that skip collection also use anonymous hosting because their exception regions are unknown before emission. Hosting can shift work between delegate creation and first invocation, so measurements include both phases;
CompileFastToILinherits the caller's host policy. The direct emitter and object[] closure layout remain in place.Framework anonymous methods retain the type casts needed for verified constant/delegate loads and derived closure-field reads. Proven owned roots avoid redundant casts, and ordinary nonnullable mutable value captures can use existing boxed cells. Quotes, runtime variables, exception regions, opaque roots and manual APIs retain the required typed storage. These adaptations are guarded by NETFRAMEWORK. Framework Unbox receivers retain their original boxed object through argument spilling; the address is recreated after arguments, while an early validation preserves null/type exception timing and the operand is evaluated once. New Standard/Light regressions verify payload identity, both throwing-assembly wrapping policies, filter ordering/mutation, original rethrow identity, explicit throws, private access, nested/quoted delegates, no-collection APIs, caller-provided IL, and alternating owned/anonymous compilation with pooled IL and private struct signatures.
Additional deterministic string regressions preserve original references across string/object/interface declarations, explicit block result types, repeated constants and mixed-reference comparisons. Non-interned strings use the existing closure; already interned literals retain ldstr. Read-only intern-table classification occurs during compilation, with no runtime lookup or new global cache. Literal string fields retain literal semantics. No-closure compilation rejects strings requiring bound identity. System's IL compiler can lose non-interned string identity through ldstr; semantic controls use the modern interpreter and an explicit object-bound System compiled form (Framework uses the latter because Compile(true) still compiles). Original upstream reference-comparison assertions remain active; this adds no skips.
The new
compatibility-exclusions.csvis separate from exact-IL exclusions. The five excluded methods retain their original upstream bodies, data and System assertions; only their FEC-specific theory metadata changes. In System mode the two original compiled assignment rows still fail their interpreter-oriented assertion, while the two interpreted rows and all three interpreter filter rows pass. Five additional proof cases execute direct backend controls across Default, NoInlining and debug-strict FEC flags. Four invalid-node rows also check all three flags.Verified results
Frozen compiler/test source:
db640cba760028d1c49ad5e4abf67f38fe683bbf, Windows ARM64 .NET 10 Release, strict FEC with outer fallback disabled. Original denominator: 35,133; 35,064 passed, 0 failed, 69 existing exclusions. No additional exclusions are introduced by these optimizations; known deep-tree modes remain deferred. All 21 imported batches completed against the frozen assembly pair.All configured Standard/Light production targets build. Full native .NET 8/.NET 10 and Framework suites pass; exact counts, logs and frozen source/assembly hashes are in validation and its archive. The fresh performance matrices validate against System for each measured working shape. Existing System full-baseline evidence is retained rather than presented as a new full System-suite run.
Exclusions and open limitations
4 existing upstream skips.
56 representation-only cases, excluded only after System/FEC behavior checks. Different IL is accepted only when observable behavior is correct; System's original IL assertions remain available in System mode.
7 compiled/interpreter differences described above. These are documented compatibility boundaries, not repaired interpreter-only support.
2 original modes of one known deep-tree crash, temporarily deferred as requested. No broad traversal refactor or stack-size workaround was added; the crash remains open.
One deterministic reused-tail-label difference remains outside the imported baseline: direct System compilation returns true, interpretation returns false, and literal inlining returns false; FEC Default matches each compiled shape, while NoInlining creates a physical delegate method returning true. This is a mode difference, not run-to-run instability.
Reused method-tail targets reached with pending operands or from protected exception regions retain strict rejection (null /
NotSupported_MethodReturn_Context, result 1011) where supporting the return requires broader stack spilling. Zero active baseline failures does not mean these execution limits were implemented.