diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md new file mode 100644 index 00000000..f8e89786 --- /dev/null +++ b/.scratch/flat-ideas.md @@ -0,0 +1,92 @@ +# Flat expression: compile-path ideas and their status + +Status: `todo` / `tried: ` / `dropped: `. +Baseline (Release net10.0, 200 exprs, ~35 nodes, noisy machine - compare rows within one run only): +flat collect 1.6-2.6us, Light collect 6.4-7.5us, flat native compile 12-13us, Light FEC 26-37us, +flat->sys->light->FEC 64-108us, System.Compile 169-198us. + +## Steps +1. `tried: done` Direct Flat -> Light converter (`FlatToLightExpression.cs`, mirrors `SysExprBuilder`), Flat -> System kept. + convert only 43.6us -> 15.0us; flat->light->FEC 45us vs 108us via System (same run). Verified: all tests, the generated-tree + property test, and the optional-children fuzzer (direct == via-system structurally, compiles and agrees with System). + Cost: ~330 duplicated lines (the two builders must be kept in sync). `GetChildren`/`ReadInlineValue` are shared statics on `ExprTree`. +2. `tried: done` Light -> Flat directly (`FlatFromLightExpression.cs`, mirrors `AddSysExpression`); Sys -> Flat already existed. + convert only 35.8us (via System) -> 14.6us (direct). `FromLightExprViaSystem` and `ToLightExpressionViaSystem` are kept as oracles. + Found and fixed a real bug on the way: Light `LoopExpression.CreateSysExpression` dropped the continue label when there was no + break label (Expression.cs); the optional-children fuzzer exposed it as a flat->light->flat mismatch in 80 of 300 trees. + The fuzzer now asserts flat->light->flat structural equality and direct == via-system. +3. `tried: measured` README expressions (`new X(closure a, b)`, `n => () => n + 1`) built flat (`Flat_compiles_the_readme_expressions_and_compares_the_time`). + Both are outside the native subset (closure constant, nested lambda, New), so `CompileFast` = probe (0.1-0.3us) + direct flat->light (~2-4us) + Light FEC (~8us / ~47us). + The conversion is a 25-40% overhead on top of Light for these; single noisy runs, the order of measurements moves numbers by 2x. + Conclusion: the native subset must grow to closures/New/nested lambdas before flat beats Light on the README cases. +4. `tried: done` Widened the native subset: reference-typed params/locals/results, `New`, static+instance `Call`, Field/Property getter, + closure constants (the `ArrayClosure` array is filled from `ClosureConstants`), string and null constants. The collect checks by construction what IL needs + (`FlatFits` only for the lambda return/params vs the delegate; per-node arg/instance type checks were dropped to match Light: the factories validate, and + `GetParameters()` allocated per Call/New. A tree built wrong now emits bad IL instead of falling back.) + README 1 (`new X(closure a, b)`): light FEC 5.2us -> flat native 3.2us (Release, one noisy run); README 2 (nested lambda) still falls back. + Cost: ~60 lines in FlatExpressionCompiler.cs. +5. `todo` Nested lambdas (closure with nested-lambda slots), Convert, value types (struct params/Nullable), Invoke, arrays, loops/labels/try. Each with a fuzz or deterministic test. + +## Construction-time ideas +- `todo` Profile node access: the scan is ~45ns/node, far from memory bound. Suspect the Stack32 indexer / Type compares. +- `todo` Parent index per node (2 bytes/node, one store at attach): ancestor walk gives scope check and closure-capture detection without an explicit stack. +- `todo` Aggregate flags OR-ed from the children at construction (has lambda / try / goto / closure const / non-int): root flags let simple trees skip collect. +- `todo` Seal pass: renumber to post-order once; subtree = contiguous range, O(1) ancestor test, sizes, usage counts in one pass. +- `todo` SOA split: hot fields (NodeType, ChildIdx, NextIdx, ChildCount) apart from Type/Obj; intern Type to a ushort id. +- `todo` Contiguous child lists instead of first-child + sibling chain (only with Seal). +- `todo` The builders take `ExprTree` by value (big Stack32 copy); pass by `in`/ref. +- `dropped (for now): closure-constant usage counts` - trivial in the collect scan, ClosureConstants.Count==0 already covers the no-closure case. +- `dropped (for now): parameter declaring owner alone` - weak without a parent link; parent index replaces it. +- `deferred: label id table` - conflicts with the "drop info lists" direction. + +## Step 6: closures / nested lambdas (tried) +- Design: captured vars live in StrongBox (shared mutation), nested lambda = separate DynamicMethod, open delegate stored in the shared constants array, curried at run-time with ArrayClosureWithNonPassedParams (same as FEC). +- Cost: recursive capture pre-pass + per-compile lists, only when lambdas > 1. +- Measured (noisy, Release): README 2 flat native ~30us vs Light FEC ~29us, so no win yet; the DynamicMethod per lambda dominates. README 1 stays ~3x faster natively. +- Todo: Convert, value types, Invoke, arrays, loops/labels/try, native-vs-System fuzzer for new nodes, linear capture scan via construction-time parent links. + +## Step 7: construction helps for the nested lambdas (tried, measured) +- 1 LambdaCount at construction: DONE. Collect no longer counts, the constants array is sized upfront. +- 2 linear capture scan by index range: REJECTED, unsound. The nodes of a subtree are not contiguous (shared nodes are built earlier, clones are appended later). Instead: the same top-down pass but with no allocations (SmallList on the stack, one FlatCtx object). +- 3 AssignedParams recorded by Assign: DONE. The never-assigned captured variable goes by value (boxed in NonPassedParams, no StrongBox, plain local in the declaring lambda). Separate list, not a node flag, because the comparer/hash read the flags. +- 4 counts per tree: DONE with 1 (closure constants count was already known). +- 5 SOA of the nodes: NOT TRIED. The collect scan of README 2 is 0.23 us of ~7 us, the floor of 2 bare DynamicMethods is 3 us; the layout is not the bottleneck. +- Biggest win was not structural: the per-delegate-type cache (Invoke, param types, open Func type, Curry method). Collect 0.71 -> 0.23 us, README 2 native 9.5 -> ~6.6 us (Light FEC ~10-11, min of 9 rounds). +- Todo: the remaining ~3 us over the DynamicMethod floor (IL token resolution, ctx), Convert, value types, Invoke, arrays, loops/try, native-vs-System fuzzer. + +## Step 8: via-System dropped from prod, README + ComplexExpression native (done) +- ToLightExpressionViaSystem / FromLightExprViaSystem removed from src; kept as test-only helpers for the round-trip fuzz. +- Native now: MemberInit (field/property bindings), NewArrayInit, ArrayIndex (rank 1), reference Convert (castclass, no user-defined/numeric/boxing). Hoisted `() => new X(a, b)`, manual lambda, `n => () => n + 1` and Complex all compile without fallback. +- Release (noisy), us / bytes per op: README 1 flat 4.4 / 1544 vs Light 7.7 / 1080 (prebuilt); Complex flat native 9.7 / 2016 vs Light FEC 13.8 / 1656; hoisted flat 5.1 / 1832 vs Light 7.6 / 1232. Two bare DynamicMethods floor: 2304 B. +- Allocation: flat allocates more than Light on one-lambda trees (FlatCtx, constants copy, delegate closure), but nothing here is above the DynamicMethod floor. TRIED nothing more; caching StrongBox Value/ctor lookups NOT DONE (only on assigned-captured path, not in README cases). +- Todo: alloc of FlatCtx (struct on stack), skip constants copy when the tree constants array can be reused, value-type members, Invoke, loops/try, native-vs-System fuzzer. + +## Step 9: allocations (done, measured) +- FlatCtx is one [ThreadStatic] instance (it embeds the 8-slot capture buffer, was allocated per compile); Constants is cleared after the compile. DONE. +- StrongBox type/ctor/Value field cached per variable type (FlatBox). DONE. +- Release B/op before -> after: README 1 flat 1544 -> 1368, README 2 3024 -> 2848, Complex 2016 -> 1840, hoisted 1832 -> 1656. Light FEC: 1080 / 3000 / 1656 / 1232. Floor of one DynamicMethod is ~1150 B. +- Left: object[] constants copy (could reuse the tree array if ImTools SmallList exposed its heap array), the ArrayClosure + delegate (needed), ILGenerator/LocalBuilder (BCL). Flat is now ~290 B over Light on one-lambda trees, below Light on README 2. +- Todo: ImTools SmallList hand-over of its backing array to skip the copy; check what else allocates in the 290 B with an allocation profiler. + +## Step 10: reuse the FEC pooling (done, measured) +- Learned from FEC TryCompileBoundToFirstClosureParam: it rents/frees the per-thread ILGenerator (DynamicMethodHacks.RentPooledOrNewILGenerator/FreePooledILGenerator), pools the closure+param types arrays, interprets Func constants, and sizes one object[] for constants+nested lambdas. Flat now uses the ILGenerator pool. DONE: -240 B per compile. +- Release B/op now: README 1 flat 1128 (Light 1080), README 2 2608 (3000), Complex 1448 (1656), hoisted 1416 (1232). Time: flat 3.1 / 6.6 / 6.6 / 3.3 us vs Light 4.1 / 10.9 / 9.2 / 4.8. +- Todo: Func interpreter shortcut (Interpreter is FEC's), the pooled local-var path (GetNextLocalVarLocation), create+compile rows (tree building cost is not measured yet), the nested ILGenerator is not pooled while the parent holds it (same as FEC). + +## Step 11: Func fold, FEC local indexes, create+compile rows (done, measured) +- Func over inline int/bool constants is folded (FlatFold) to the shared Interpreter.TrueFunc/FalseFunc, no DynamicMethod. DONE (FEC does the same with its Expression-bound Interpreter, which cannot read flat nodes, so the folder is a small flat one). +- Locals use FEC il.GetNextLocalVarIndex + EmitLoad/StoreLocalVariable: no LocalBuilder object, short opcodes. DONE (no change on the README cases, they have no locals; saves 1 LocalBuilder per block variable / box / assign result). +- FEC closure info: nothing more to take, FEC allocates the same ArrayClosure + object[] and the NestedLambdaInfo objects that flat does not need. +- Create+compile Release (us / B): README 1 flat 3.3 / 1128 vs Light 4.6 / 1210; README 2 flat 7.1 / 2608 vs Light 11.8 / 3144; Complex flat 7.1 / 1448 vs Light 9.9 / 2176. The flat tree build itself allocates 0 B. +- Todo: README 1 / hoisted compile is still 48-184 B above Light prebuilt; profile the rest (ArrayClosure + constants copy + delegate + DynamicMethod). + +## Step 12: 3 ponytails review rounds over all the Flat code +- R1 (compiler): Call/MemberAccess on a value-type instance (int.ToString) emitted callvirt on the value = invalid IL. FIXED: collect rejects it, falls back to Light (test added). Constants now use EmitLoadConstantInt (short IL forms). Rejected: the delegate-type check before the collect (RootIdx may be unset). +- R2 (converters): ToLightExpression copies the whole ExprTree into its builder; FromLight allocates ushort[] per call. NOT FIXED, @perf todos (needs ref fields on all targets / SmallList ids). Both are fallback/test paths only. +- R3 (invariants): by-value capture is sound only because by-ref params are rejected and Assign is the only emitted mutation; now written next to the check. Debug and Release: 1724 pass. + +## Step 13: native fuzzers +CsCheck fuzzers compare native flat vs System: closures (shared StrongBox, by-value capture, 2 instances) and objects (New/Call/Field/Property/MemberInit/ref Conditional). Both assert no fallback. + +## Step 14: FlatCtx is a stack struct +FlatCtx is a local struct passed by ref (as FEC does with its ClosureInfo); no ThreadStatic, no cleanup. Allocation is unchanged (1128/2608/1448 B). diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index b5ca2b2b..a4922df2 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -167,36 +167,10 @@ internal bool HasSameShape(ref ExprNode other) => _child == other._child; } -/// Maps a lambda node to a captured outer parameter or variable. -/// Uses the same 16-bit storage range as the flat node links and identities. -[StructLayout(LayoutKind.Explicit, Size = 6)] -public struct LambdaClosureParameterUsage -{ - /// The lambda node idx in the flat-expression node array. - [FieldOffset(0)] - public ushort LambdaIdx; - - /// The parameter-usage expression node idx in the flat-expression node array. - [FieldOffset(2)] - public ushort ParameterIdx; - - /// The shared parameter/variable identity stored in . - [FieldOffset(4)] - public ushort ParameterId; - - /// Creates the lambda capture mapping. - public LambdaClosureParameterUsage(ushort lambdaIdx, ushort parameterIdx, ushort parameterId) - { - LambdaIdx = lambdaIdx; - ParameterIdx = parameterIdx; - ParameterId = parameterId; - } -} - /// Stores an expression tree as flat nodes plus separate closure constants. -public struct ExprTree : IEquatable +public partial struct ExprTree : IEquatable { - private static readonly object ClosureConstantMarker = new(); + internal static readonly object ClosureConstantMarker = new(); private const byte ParameterByRefFlag = 1; private const byte BinaryLiftedToNullFlag = 1; private const byte LoopHasBreakFlag = 1; @@ -204,6 +178,8 @@ public struct ExprTree : IEquatable private const byte CatchHasVariableFlag = 1; private const byte CatchHasFilterFlag = 2; private const byte TryFaultFlag = 1; + private const byte TryHasFinallyFlag = 2; + private const byte SwitchHasDefaultFlag = 1; /// Gets or sets the root node idx. public int RootIdx; @@ -214,27 +190,22 @@ public struct ExprTree : IEquatable /// Gets or sets closure constants that are referenced from constant nodes. public SmallList, NoArrayPool> ClosureConstants; - /// Gets or sets all lambda node idxs added during construction. - /// The root lambda idx is stored in ; other entries are nested lambdas. - public SmallList, NoArrayPool> LambdaNodes; - - /// Gets or sets all block node idxs that carry explicit variable declarations. - /// A tracked block uses children.Count == 2: one child list for variables and one for expressions. - public SmallList, NoArrayPool> BlocksWithVariables; - - /// Gets or sets all node idxs, - /// including return, break, and continue. - public SmallList, NoArrayPool> GotoNodes; - - /// Gets or sets all expression node idxs. - public SmallList, NoArrayPool> LabelNodes; + /// The number of the lambdas built so far, so the compile does not count them and knows the nested lambdas slots upfront. + public ushort LambdaCount; - /// Gets or sets all node idxs for try/catch, try/finally, try/fault, and combined forms. - public SmallList, NoArrayPool> TryCatchNodes; + /// The ids of the parameters and variables that are assigned, recorded by . The compile passes + /// the never-assigned captured variable to the nested lambda by value, without the StrongBox. Any other node that mutates + /// the variable (increment, compound assign) must record it here too, once the compile supports it. + /// It is the separate list, not the node flag, so the structural equality and hash are untouched. + internal SmallList, NoArrayPool> AssignedParams; - /// Gets or sets captured outer parameter and variable usages for lambdas. - /// The stored idxs use the same 16-bit range as and . - public SmallList, NoArrayPool> LambdaClosureParameterUsages; + internal bool IsAssigned(ushort id) + { + for (var i = 0; i < AssignedParams.Count; ++i) + if (AssignedParams.GetSurePresentRef(i) == id) + return true; + return false; + } // Import-only identity maps (valid during FromSysExpr / FromLightExpr). private SmallMap16> _parameterIds; @@ -413,7 +384,10 @@ private ushort WithOneChild(ExpressionType nodeType, Type type, object obj, byte var ownerIdx = (ushort)idx; if (ch0 != 0) - Nodes.GetSurePresentRef(ownerIdx).SetChildrenInfo(1, MayBeCloneChildForOwner(ch0, ownerIdx)); + { + var child = MayBeCloneChildForOwner(ch0, ownerIdx); // may grow Nodes, so take the owner ref after it + Nodes.GetSurePresentRef(ownerIdx).SetChildrenInfo(1, child); + } return ownerIdx; } @@ -456,22 +430,19 @@ private ushort WithThreeChildren(ExpressionType nodeType, Type type, object obj, private ushort WithTwoOrMoreChildren(ExpressionType nodeType, Type type, object obj, byte flags, ExprNodeKind kind, ushort ch0, ushort ch1, ushort[] more) { + Debug.Assert(ch0 != 0 && ch1 != 0, "Both leading children must be provided."); var ownerIdx = AddNode(nodeType, type, obj, flags, kind); ushort first = 0, prev = 0, count = 0; - // todo: @wip if the children id is missing it is more of an error - if (ch0 != 0) - AppendPreparedChild(MayBeCloneChildForOwner(ch0, ownerIdx), ref first, ref prev, ref count); - if (ch1 != 0) - AppendPreparedChild(MayBeCloneChildForOwner(ch1, ownerIdx), ref first, ref prev, ref count); + AppendPreparedChild(MayBeCloneChildForOwner(ch0, ownerIdx), ref first, ref prev, ref count); + AppendPreparedChild(MayBeCloneChildForOwner(ch1, ownerIdx), ref first, ref prev, ref count); if (more != null) { for (var i = 0; i < more.Length; ++i) { - var ch = more[i]; - if (ch == 0) continue; - AppendPreparedChild(MayBeCloneChildForOwner(ch, ownerIdx), ref first, ref prev, ref count); + Debug.Assert(more[i] != 0, "Child index must not be zero."); + AppendPreparedChild(MayBeCloneChildForOwner(more[i], ownerIdx), ref first, ref prev, ref count); } } @@ -604,11 +575,21 @@ public ushort Property(ushort instance, PropertyInfo prop, params ushort[] args) /// Adds a binary node of the specified kind. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort MakeBinary(ExpressionType nodeType, ushort left, ushort right, bool isLiftedToNull = false, - MethodInfo method = null, ushort conversion = 0, Type type = null) => - WithThreeChildren( + MethodInfo method = null, ushort conversion = 0, Type type = null) + { + var idx = WithThreeChildren( nodeType, type ?? GetBinaryResultType(nodeType, Nodes[left].Type, method), method, isLiftedToNull ? BinaryLiftedToNullFlag : (byte)0, default, left, right, conversion); + if (nodeType == ExpressionType.Assign) + { + ref var target = ref Nodes.GetSurePresentRef(left); + if (target.NodeType == ExpressionType.Parameter && !IsAssigned(target.ChildIdx)) + AssignedParams.Add(target.ChildIdx); + } + return idx; + } + /// Adds a one-dimensional array index node. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort ArrayIndex(ushort array, ushort idx) => MakeBinary(ExpressionType.ArrayIndex, array, idx); @@ -662,7 +643,7 @@ public ushort Condition(ushort test, ushort ifTrue, ushort ifFalse, Type type = WithThreeChildren(ExpressionType.Conditional, type ?? Nodes[ifTrue].Type, null, default, default, test, ifTrue, ifFalse); /// Block layout: first child = BlockExprs sub-node (expression list); - /// optional following siblings = variable declarations. Tracked in when vars present. + /// optional following siblings = variable declarations. public ushort Block(Type type, ushort[] vars, params ushort[] exprs) { if (exprs == null || exprs.Length == 0) @@ -671,10 +652,7 @@ public ushort Block(Type type, ushort[] vars, params ushort[] exprs) var exprsSubNode = WithChildren(ExpressionType.Block, null, null, default, ExprNodeKind.BlockExprs, exprs); type ??= Nodes[exprs[exprs.Length - 1]].Type; - var blockIdx = WithOneOrMoreChildren(ExpressionType.Block, type, null, default, default, exprsSubNode, vars); - if (vars != null && vars.Length != 0) - BlocksWithVariables.Add(blockIdx); - return blockIdx; + return WithOneOrMoreChildren(ExpressionType.Block, type, null, default, default, exprsSubNode, vars); } /// Adds a block node without explicit variables. @@ -682,7 +660,7 @@ public ushort Block(Type type, ushort[] vars, params ushort[] exprs) public ushort Block(params ushort[] exprs) => Block(null, null, exprs); - /// Adds a lambda node. Layout: body then parameters. Tracks and captures. + /// Adds a lambda node. Layout: body then parameters. [MethodImpl(MethodImplOptions.AggressiveInlining)] #if NET10_0_OR_GREATER public ushort Lambda(Type delegateType, ushort bodyIdx, params ReadOnlySpan pars) @@ -693,6 +671,7 @@ public ushort Lambda(Type delegateType, ushort bodyIdx, params ushort[] pars) pars ??= Array.Empty(); #endif EnsureIndexZeroSentinel(); + ++LambdaCount; ref var lambdaRef = ref Nodes.AddDefaultAndGetRef(out var idx); ExprNode.Set(ref lambdaRef, ExpressionType.Lambda, delegateType); var lambdaIdx = (ushort)idx; @@ -733,8 +712,6 @@ public ushort Lambda(Type delegateType, ushort bodyIdx, params ushort[] pars) prevRef.NextIdx = lambdaIdx; // close the child chain with a link back to the lambda node - LambdaNodes.Add(lambdaIdx); - CollectLambdaClosureParameterUsages(lambdaIdx); return lambdaIdx; } @@ -791,23 +768,17 @@ private ushort LabelTargetWithId(Type type, string name, ushort id) => /// Adds a label-expression node. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort Label(ushort target, ushort defaultValue = 0) - { - var idx = defaultValue == 0 + public ushort Label(ushort target, ushort defaultValue = 0) => + defaultValue == 0 ? WithOneChild(ExpressionType.Label, Nodes[target].Type, null, default, default, target) : WithTwoChildren(ExpressionType.Label, Nodes[target].Type, null, default, default, target, defaultValue); - LabelNodes.Add(idx); - return idx; - } /// Adds a goto-family node. Kind is stored in flags. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort MakeGoto(GotoExpressionKind gotoKind, ushort target, ushort value = 0, Type type = null) { var resultType = type ?? (value != 0 ? Nodes[value].Type : typeof(void)); - var idx = WithTwoChildren(ExpressionType.Goto, resultType, null, (byte)gotoKind, default, target, value); - GotoNodes.Add(idx); - return idx; + return WithTwoChildren(ExpressionType.Goto, resultType, null, (byte)gotoKind, default, target, value); } /// Adds a goto node. @@ -832,8 +803,7 @@ public ushort Loop(ushort body, ushort @break = 0, ushort @continue = 0) /// Adds a switch-case node. Layout: test values then body. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort SwitchCase(ushort body, params ushort[] testValues) => - WithOneOrMoreChildren(default, null, null, default, ExprNodeKind.SwitchCase, 0, - AppendUShort(testValues, body)); + WithChildren(default, null, null, default, ExprNodeKind.SwitchCase, AppendUShort(testValues, body)); /// Adds a switch node. Layout: switchValue, optional defaultBody, optional SwitchCases group. public ushort Switch(Type type, ushort switchValue, ushort defaultBody, MethodInfo comparison, params ushort[] cases) @@ -841,7 +811,7 @@ public ushort Switch(Type type, ushort switchValue, ushort defaultBody, MethodIn var casesIdx = cases == null || cases.Length == 0 ? (ushort)0 : WithChildren(ExpressionType.Switch, type, null, default, ExprNodeKind.SwitchCases, cases); - return WithThreeChildren(ExpressionType.Switch, type, comparison, default, default, switchValue, defaultBody, casesIdx); + return WithThreeChildren(ExpressionType.Switch, type, comparison, defaultBody != 0 ? SwitchHasDefaultFlag : (byte)0, default, switchValue, defaultBody, casesIdx); } /// Adds a switch node without an explicit default case or comparer. @@ -880,36 +850,20 @@ public ushort MakeCatchBlock(Type test, ushort variable, ushort body, ushort fil } /// Adds a try/catch node. Layout: body, handlers… - public ushort TryCatch(ushort body, params ushort[] handlers) - { - var idx = WithOneOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, handlers); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryCatch(ushort body, params ushort[] handlers) => + WithOneOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, handlers); /// Adds a try/finally node. - public ushort TryFinally(ushort body, ushort @finally) - { - var idx = WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryFinally(ushort body, ushort @finally) => + WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryHasFinallyFlag, default, body, @finally); /// Adds a try/fault node. - public ushort TryFault(ushort body, ushort fault) - { - var idx = WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryFaultFlag, default, body, fault); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryFault(ushort body, ushort fault) => + WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryFaultFlag, default, body, fault); /// Adds a try node with optional finally block and catch handlers. Layout: body, finally, handlers… - public ushort TryCatchFinally(ushort body, ushort @finally, params ushort[] handlers) - { - var idx = WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally, handlers); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryCatchFinally(ushort body, ushort @finally, params ushort[] handlers) => + WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, TryHasFinallyFlag, default, body, @finally, handlers); /// Adds a type-test node. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -963,13 +917,15 @@ public ushort FromSysExpr(SysExpr expr) return (ushort)RootIdx; } - // @perf remove Light -> System -> Flat round trip => make it Light -> Flat. - /// Flattens a LightExpression into this tree and sets . + /// Flattens a LightExpression into this tree directly, without the System.Linq expressions in between, + /// and sets . Mirrors AddSysExpression: keep the two in sync. [RequiresUnreferencedCode(LightExpression.Trimming.Message)] public ushort FromLightExpr(LightExpression.Expression expr) { - // Prefer Sys round-trip for a single From core; Light→Sys preserves identity via ToExpression. - return FromSysExpr(expr.ToExpression()); + _parameterIds = default; + _labelIds = default; + RootIdx = AddLightExpression(expr); + return (ushort)RootIdx; } [RequiresUnreferencedCode(LightExpression.Trimming.Message)] @@ -1202,17 +1158,19 @@ private ushort AddSysExpression(SysExpr expr) } } - private ushort AddSysLabelTarget(SysLabelTarget target) + private ushort AddSysLabelTarget(SysLabelTarget target) => AddLabelTarget(target, target.Type, target.Name); + + private ushort AddLabelTarget(object target, Type type, string name) { var id = checked((ushort)GetId(ref _labelIds, target)); - // Reuse existing label-target node with the same id when the same SysLabelTarget is seen again. + // Reuse existing label-target node with the same id when the same label target is seen again. for (var i = 0; i < Nodes.Count; ++i) { ref var n = ref Nodes.GetSurePresentRef(i); if (n.Is(ExprNodeKind.LabelTarget) && n.ChildIdx == id) return checked((ushort)i); } - return LabelTargetWithId(target.Type, target.Name, id); + return LabelTargetWithId(type, name, id); } [RequiresUnreferencedCode(LightExpression.Trimming.Message)] @@ -1260,217 +1218,6 @@ private static int GetId(ref SmallMap16> ids, object return id; } - private void CollectLambdaClosureParameterUsages(ushort lambdaIdx) - { - var children = GetChildren(lambdaIdx); - if (children.Count == 0) - return; - - ChildIdxs lambdaParameterIds = default; - for (var i = 1; i < children.Count; ++i) - lambdaParameterIds.Add(ToStoredUShortIdx(Nodes[children[i]].ChildIdx)); - - SmallList, NoArrayPool> localParameterIds = default; - SmallList, NoArrayPool> captures = default; - CollectClosureParameterUsages(children[0], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - - for (var i = 0; i < captures.Count; ++i) - LambdaClosureParameterUsages.Add(captures[i]); - } - - private void CollectClosureParameterUsages( - ushort idx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - if (idx == 0) - return; - - ref var node = ref Nodes.GetSurePresentRef(idx); - - // Group / metadata kinds share some ExpressionType values with real expressions. - // Always walk their children as a plain sibling list — never apply Block/Lambda layouts. - if (node.Kind == ExprNodeKind.BlockExprs || - node.Kind == ExprNodeKind.SwitchCases || - node.Kind == ExprNodeKind.SwitchCase || - node.Kind == ExprNodeKind.CatchBlock || - node.Kind == ExprNodeKind.ObjectReference || - node.Kind == ExprNodeKind.UInt16Pair || - node.Kind == ExprNodeKind.LabelTarget || - node.Kind == ExprNodeKind.MemberAssignment || - node.Kind == ExprNodeKind.MemberMemberBinding || - node.Kind == ExprNodeKind.MemberListBinding || - node.Kind == ExprNodeKind.ElementInit) - { - if (node.Kind == ExprNodeKind.CatchBlock) - { - CollectCatchBlockClosureParameterUsages(idx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0) - { - if (node.NodeType == ExpressionType.Parameter) - { - var parameterId = ToStoredUShortIdx(node.ChildIdx); - if (!Contains(ref lambdaParameterIds, parameterId) && - !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIdx, idx, parameterId, ref captures); - } - return; - } - - switch (node.NodeType) - { - case ExpressionType.Parameter: - { - var parameterId = ToStoredUShortIdx(node.ChildIdx); - if (!Contains(ref lambdaParameterIds, parameterId) && - !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIdx, idx, parameterId, ref captures); - return; - } - case ExpressionType.Lambda: - PropagateNestedLambdaClosureParameterUsages(idx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - case ExpressionType.Block: - { - // Layout: first child = BlockExprs (expression list); remaining siblings = variables. - var localCount = localParameterIds.Count; - var exprListIdx = node.ChildIdx; - if (exprListIdx == 0) - return; - - ref var exprList = ref Nodes.GetSurePresentRef(exprListIdx); - - var varIdx = exprList.NextIdx; - for (var i = 1; i < node.ChildCount && varIdx != 0 && varIdx != idx; ++i) - { - ref var v = ref Nodes.GetSurePresentRef(varIdx); - localParameterIds.Add(ToStoredUShortIdx(v.ChildIdx)); - varIdx = v.NextIdx; - } - - if (exprList.Is(ExprNodeKind.BlockExprs)) - WalkClosureChildren(exprListIdx, ref exprList, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - else - CollectClosureParameterUsages(exprListIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - - localParameterIds.Count = localCount; - return; - } - case ExpressionType.Try: - { - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - } - - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - } - - private void WalkClosureChildren( - ushort idx, - ref ExprNode node, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - if (node.ChildCount == 0 || node.ChildIdx == 0) - return; - - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var cIdx = node.ChildIdx; - for (var i = 0; i < node.ChildCount && cIdx != 0 && cIdx != idx; ++i) - { - var next = Nodes.GetSurePresentRef(cIdx).NextIdx; - CollectClosureParameterUsages(cIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - cIdx = next; - } - } - - private void CollectCatchBlockClosureParameterUsages( - ushort idx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - ref var node = ref Nodes.GetSurePresentRef(idx); - Debug.Assert(node.Is(ExprNodeKind.CatchBlock)); - - var localCount = localParameterIds.Count; - var childIdx = node.ChildIdx; - if (node.HasFlag(CatchHasVariableFlag)) - { - localParameterIds.Add(ToStoredUShortIdx(Nodes.GetSurePresentRef(childIdx).ChildIdx)); - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - } - - var bodyIdx = childIdx; - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - if (node.HasFlag(CatchHasFilterFlag)) - CollectClosureParameterUsages(childIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - CollectClosureParameterUsages(bodyIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - localParameterIds.Count = localCount; - } - - private void PropagateNestedLambdaClosureParameterUsages( - ushort nestedLambdaIdx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - for (var i = 0; i < LambdaClosureParameterUsages.Count; ++i) - { - ref var usage = ref LambdaClosureParameterUsages[i]; - if (usage.LambdaIdx != nestedLambdaIdx) - continue; - if (Contains(ref lambdaParameterIds, usage.ParameterId) || - Contains(ref localParameterIds, usage.ParameterId)) - continue; - AddClosureParameterUsage(lambdaIdx, usage.ParameterIdx, usage.ParameterId, ref captures); - } - } - - private static void AddClosureParameterUsage( - ushort lambdaIdx, - ushort parameterIdx, - ushort parameterId, - ref SmallList, NoArrayPool> captures) - { - for (var i = 0; i < captures.Count; ++i) - if (captures[i].ParameterId == parameterId) - return; - captures.Add(new LambdaClosureParameterUsage(lambdaIdx, parameterIdx, parameterId)); - } - - private ChildIdxs GetChildren(int idx) - { - ref var node = ref Nodes.GetSurePresentRef(idx); - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) - return default; - var count = node.ChildCount; - ChildIdxs children = default; - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var childIdx = node.ChildIdx; - for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) - { - children.Add(childIdx); - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - } - return children; - } /// Reconstructs the flat tree as a System.Linq expression tree. [RequiresUnreferencedCode(LightExpression.Trimming.Message)] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", @@ -1480,11 +1227,6 @@ public SysExpr ToExpression() => ? new SysExprBuilder(this).ReadExpression(RootIdx) : throw new InvalidOperationException("Flat expression tree is empty."); - /// Reconstructs the flat tree as a LightExpression tree. - [RequiresUnreferencedCode(LightExpression.Trimming.Message)] - public LightExpression.Expression ToLightExpression() => - LightExpression.FromSysExpressionConverter.ToLightExpression(ToExpression()); - /// Structurally compares two flat expression trees. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ExprTree other) => @@ -1565,16 +1307,6 @@ private static Type GetArrayElementType(Type arrayType, int depth) return elementType ?? typeof(object); } - private static bool Contains(ref SmallList ids, ushort value) - where TStack : struct, IStack - where TPool : struct, ISmallArrayPool - { - for (var i = 0; i < ids.Count; ++i) - if (ids[i] == value) - return true; - return false; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort ToStoredUShortIdx(int idx) => checked((ushort)idx); @@ -1682,7 +1414,7 @@ public bool Eq(ref ExprTree xTree, ref ExprTree yTree) } break; - case ExpressionType.Block: + case ExpressionType.Block when x.Kind != ExprNodeKind.BlockExprs: // Layout: first child = BlockExprs; remaining siblings = variables. if (x.ChildCount == 0) return false; @@ -1844,7 +1576,7 @@ private int HashNode(ref ExprTree tree, int idx) case ExpressionType.Lambda: return HashLambda(ref tree, idx, h); - case ExpressionType.Block: + case ExpressionType.Block when node.Kind != ExprNodeKind.BlockExprs: return HashBlock(ref tree, idx, h); } @@ -2141,20 +1873,10 @@ public SysExpr ReadExpression(int idx) { // Layout: switchValue, optional defaultBody, optional SwitchCases group. var children = GetChildren(idx); - var defaultBody = default(SysExpr); - ChildIdxs caseIdxs = default; - if (children.Count > 1) - { - ref var lastChild = ref _tree.Nodes[children[children.Count - 1]]; - if (lastChild.Is(ExprNodeKind.SwitchCases)) - { - caseIdxs = GetChildren(children[children.Count - 1]); - if (children.Count == 3) - defaultBody = ReadExpression(children[1]); - } - else - defaultBody = ReadExpression(children[1]); - } + var hasDefault = node.HasFlag(SwitchHasDefaultFlag); + var defaultBody = hasDefault ? ReadExpression(children[1]) : null; + var casesIdx = hasDefault ? 2 : 1; + var caseIdxs = children.Count > casesIdx ? GetChildren(children[casesIdx]) : default; var cases = new SysSwitchCase[caseIdxs.Count]; for (var i = 0; i < cases.Length; ++i) cases[i] = ReadSwitchCase(caseIdxs[i]); @@ -2167,13 +1889,9 @@ public SysExpr ReadExpression(int idx) if (node.HasFlag(TryFaultFlag)) return SysExpr.TryFault(ReadExpression(children[0]), ReadExpression(children[1])); - var handlerStart = 1; - var @finally = default(SysExpr); - if (children.Count > 1 && !_tree.Nodes[children[1]].Is(ExprNodeKind.CatchBlock)) - { - @finally = ReadExpression(children[1]); - handlerStart = 2; - } + var hasFinally = node.HasFlag(TryHasFinallyFlag); + var @finally = hasFinally ? ReadExpression(children[1]) : null; + var handlerStart = hasFinally ? 2 : 1; var handlers = new SysCatchBlock[children.Count - handlerStart]; for (var i = 0; i < handlers.Length; ++i) @@ -2332,53 +2050,7 @@ private SysElementInit ReadElementInit(int idx) return SysExpr.ElementInit((MethodInfo)node.Obj, ReadExpressions(GetChildren(idx))); } - private ChildIdxs GetChildren(int idx) - { - ref var node = ref _tree.Nodes.GetSurePresentRef(idx); - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) - return default; - var count = node.ChildCount; - ChildIdxs children = default; - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var childIdx = node.ChildIdx; - for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) - { - children.Add(childIdx); - childIdx = _tree.Nodes.GetSurePresentRef(childIdx).NextIdx; - } - return children; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object ReadInlineValue(Type type, uint data) - { - if (type.IsEnum) - return Enum.ToObject(type, Type.GetTypeCode(Enum.GetUnderlyingType(type)) switch - { - TypeCode.Byte => (object)(byte)data, - TypeCode.SByte => (object)(sbyte)(byte)data, - TypeCode.Char => (object)(char)(ushort)data, - TypeCode.Int16 => (object)(short)(ushort)data, - TypeCode.UInt16 => (object)(ushort)data, - TypeCode.Int32 => (object)(int)data, - TypeCode.UInt32 => (object)data, - var tc => FlatExpressionThrow.UnsupportedInlineConstantType(type, tc) - }); - return Type.GetTypeCode(type) switch - { - TypeCode.Boolean => (object)(data != 0), - TypeCode.Byte => (object)(byte)data, - TypeCode.SByte => (object)(sbyte)(byte)data, - TypeCode.Char => (object)(char)(ushort)data, - TypeCode.Int16 => (object)(short)(ushort)data, - TypeCode.UInt16 => (object)(ushort)data, - TypeCode.Int32 => (object)(int)data, - TypeCode.UInt32 => (object)data, - TypeCode.Single => (object)FloatBits.ToFloat(data), - _ => FlatExpressionThrow.UnsupportedInlineConstantType(type) - }; - } + private ChildIdxs GetChildren(int idx) => ExprTree.GetChildren(_tree, idx); [RequiresUnreferencedCode(LightExpression.Trimming.Message)] private SysExpr[] ReadExpressions(in ChildIdxs childIdxs) @@ -2394,6 +2066,55 @@ private SysExpr[] ReadExpressions(in ChildIdxs childIdxs) Justification = "Flat expression round-trip stores the runtime type metadata explicitly for reconstruction.")] private static NewExpression CreateValueTypeNewExpression(Type type) => SysExpr.New(type); } + + // Shared by the System and the Light builders. + private static ChildIdxs GetChildren(in ExprTree tree, int idx) + { + ref var node = ref tree.Nodes.GetSurePresentRef(idx); + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) + return default; + var count = node.ChildCount; + ChildIdxs children = default; + // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this + // owner (last-child.NextIdx is the parent up-link). + var childIdx = node.ChildIdx; + for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) + { + children.Add(childIdx); + childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; + } + return children; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object ReadInlineValue(Type type, uint data) + { + if (type.IsEnum) + return Enum.ToObject(type, Type.GetTypeCode(Enum.GetUnderlyingType(type)) switch + { + TypeCode.Byte => (object)(byte)data, + TypeCode.SByte => (object)(sbyte)(byte)data, + TypeCode.Char => (object)(char)(ushort)data, + TypeCode.Int16 => (object)(short)(ushort)data, + TypeCode.UInt16 => (object)(ushort)data, + TypeCode.Int32 => (object)(int)data, + TypeCode.UInt32 => (object)data, + var tc => FlatExpressionThrow.UnsupportedInlineConstantType(type, tc) + }); + return Type.GetTypeCode(type) switch + { + TypeCode.Boolean => (object)(data != 0), + TypeCode.Byte => (object)(byte)data, + TypeCode.SByte => (object)(sbyte)(byte)data, + TypeCode.Char => (object)(char)(ushort)data, + TypeCode.Int16 => (object)(short)(ushort)data, + TypeCode.UInt16 => (object)(ushort)data, + TypeCode.Int32 => (object)(int)data, + TypeCode.UInt32 => (object)data, + TypeCode.Single => (object)FloatBits.ToFloat(data), + _ => FlatExpressionThrow.UnsupportedInlineConstantType(type) + }; + } } /// Union struct for reinterpreting float bits as uint without unsafe code. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs new file mode 100644 index 00000000..2e68c1ff --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -0,0 +1,807 @@ +// Experimental: the initial compilation path for the ExprTree, to learn how much of FEC could work on the flat nodes directly. +#nullable disable +namespace FastExpressionCompiler.LightExpression; + +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using FastExpressionCompiler.FlatExpression; +using FastExpressionCompiler.LightExpression.ImTools; +using ExprType = System.Linq.Expressions.ExpressionType; + +public static partial class ExpressionCompiler +{ + /// Compiles the flat tree. The int/bool subset goes straight to IL from the nodes, no Expression objects are made; + /// everything else is converted to the LightExpression and compiled the usual way. + public static TDelegate CompileFast(this ref ExprTree tree, bool ifFastFailedReturnNull = false, + CompilerFlags flags = CompilerFlags.Default) where TDelegate : class => + tree.TryCompileFlat() + ?? ((LambdaExpression)tree.ToLightExpression()).CompileFast(ifFastFailedReturnNull, flags); + + struct FlatVar + { + public ushort Id; + public Type Type; + public short Arg; // the argument index (the closure is 0) or -1 + public int Local; // the local index (no LocalBuilder allocated), or the local StrongBox if Boxed + public short Slot; // the item of the closure NonPassedParams if the variable is captured from the outer lambda, or -1 + public bool Boxed; // the variable is assigned and used by a nested lambda, so it lives in the StrongBox shared with that lambda + } + + /// The outer variable used by the nested lambda. The closure NonPassedParams of the lambda are its captures in the list order. + /// ByValue: the variable is never assigned, so the NonPassedParams item is the (boxed) value, otherwise it is the StrongBox. + private struct FlatCapture + { + public int LambdaIdx; + public ushort Id; + public Type Type; + public bool ByValue; + } + + /// The state of one compilation, a local of the TryCompileFlat passed by ref. + private struct FlatCtx + { + public object[] Constants; // the closure constants, then the open delegates of the nested lambdas + public int NextSlot; + public SmallList, NoArrayPool> Captures; + + public bool IsBoxed(ushort id) + { + for (var i = 0; i < Captures.Count; ++i) + { + ref var c = ref Captures.GetSurePresentRef(i); + if (c.Id == id && !c.ByValue) return true; + } + return false; + } + } + + private static readonly ConstructorInfo FlatArrayClosureCtor = typeof(ArrayClosure).GetConstructors()[0]; + + /// Finds which variables are used by the lambdas other than the declaring one. Recursive but only run for the nested lambdas. + /// The variable is declared by the lambda parameter or the block variable, the usage is the Parameter node with the same id. + private static void FlatFindCaptures(ref ExprTree tree, int idx, ref FlatCtx ctx, + ref SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>> declared, + ref SmallList, NoArrayPool> lambdas) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + switch (n.NodeType) + { + case ExprType.Constant: + return; + + case ExprType.Parameter: + { + var d = declared.Count - 1; + while (d >= 0 && declared.GetSurePresentRef(d).Id != n.ChildIdx) --d; + if (d < 0) + return; // not in scope, the emit throws + // the lambdas deeper than the declaring one are capturing it + for (var k = declared.GetSurePresentRef(d).Depth; k < lambdas.Count; ++k) + { + var lambdaIdx = lambdas.GetSurePresentRef(k); + var found = false; + for (var c = 0; c < ctx.Captures.Count && !found; ++c) + { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + found = cap.LambdaIdx == lambdaIdx && cap.Id == n.ChildIdx; + } + if (!found) + ctx.Captures.Add(new FlatCapture { LambdaIdx = lambdaIdx, Id = n.ChildIdx, Type = n.Type, ByValue = !tree.IsAssigned(n.ChildIdx) }); + } + return; + } + + case ExprType.Lambda: + { + lambdas.Add(idx); + var scope = declared.Count; + var bodyIdx = n.ChildIdx; + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + declared.Add((par.ChildIdx, lambdas.Count)); + parIdx = par.NextIdx; + } + FlatFindCaptures(ref tree, bodyIdx, ref ctx, ref declared, ref lambdas); + declared.Count = scope; + --lambdas.Count; + return; + } + + case ExprType.Block: + { + ref var exprs = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); + var scope = declared.Count; + var varIdx = exprs.NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); + declared.Add((v.ChildIdx, lambdas.Count)); + varIdx = v.NextIdx; + } + var exprIdx = exprs.ChildIdx; + for (var i = 0; i < exprs.ChildCount; ++i) + { + FlatFindCaptures(ref tree, exprIdx, ref ctx, ref declared, ref lambdas); + exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; + } + declared.Count = scope; + return; + } + + default: + { + var childIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + FlatFindCaptures(ref tree, childIdx, ref ctx, ref declared, ref lambdas); + childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; + } + return; + } + } + } + + /// The delegate type facts, found once per type: the reflection (Invoke, the Func types, the Curry method) is the biggest + /// part of the compile of the small lambda. The open delegate and the Curry are only for the nested lambda: the Func (the closure + /// is curried into it) with the supported number of parameters, otherwise they are null. + private sealed class FlatDelegateInfo + { + public Type ReturnType; + public Type[] ParamTypes; + public Type[] ClosureParamTypes; // the closure then the parameters, for the DynamicMethod + public Type OpenType; // Func + public MethodInfo CurryMethod; + } + + // the cache keeps the delegate types alive, same as the other FEC type caches + private static readonly ConcurrentDictionary FlatDelegates = new(); + + private static FlatDelegateInfo FlatGetDelegateInfo(Type delegateType) + { + if (FlatDelegates.TryGetValue(delegateType, out var info)) + return info; + + var invoke = delegateType.GetMethod("Invoke"); + if (invoke != null) + { + var pars = invoke.GetParameters(); + info = new FlatDelegateInfo { ReturnType = invoke.ReturnType, ParamTypes = new Type[pars.Length], ClosureParamTypes = new Type[pars.Length + 1] }; + info.ClosureParamTypes[0] = typeof(ArrayClosure); + for (var i = 0; i < pars.Length; ++i) + info.ParamTypes[i] = info.ClosureParamTypes[i + 1] = pars[i].ParameterType; + + if (pars.Length <= 5 && info.ReturnType != typeof(void) && Tools.GetFuncOrActionType(info.ParamTypes, info.ReturnType) == delegateType) // 5 is the CurryClosureFuncs max + { + info.OpenType = Tools.GetFuncOrActionType(info.ClosureParamTypes, info.ReturnType); + info.CurryMethod = CurryClosureFuncs.Methods[pars.Length].MakeGenericMethod(info.OpenType.GetGenericArguments()); + } + } + return FlatDelegates.GetOrAdd(delegateType, info); + } + + /// The lambda return and parameter types must be flat and be the same as the delegate has. + private static bool FlatLambdaFits(ref ExprTree tree, ref ExprNode lambda, bool nested) + { + var info = FlatGetDelegateInfo(lambda.Type); + if (info == null) + return false; + ref var body = ref tree.Nodes.GetSurePresentRef(lambda.ChildIdx); + if (!IsFlatType(info.ReturnType) || !FlatFits(info.ReturnType, body.Type) || info.ParamTypes.Length != lambda.ChildCount - 1) + return false; + + var parIdx = body.NextIdx; + for (var i = 0; i < info.ParamTypes.Length; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + if (par.Type != info.ParamTypes[i]) + return false; + parIdx = par.NextIdx; + } + return !nested || info.OpenType != null; + } + + private sealed class FlatBoxInfo { public Type Type; public ConstructorInfo Ctor; public FieldInfo Value; } + + private static readonly ConcurrentDictionary FlatBoxes = new(); + + private static FlatBoxInfo FlatBox(Type t) + { + if (FlatBoxes.TryGetValue(t, out var info)) + return info; + var boxType = typeof(StrongBox<>).MakeGenericType(t); + return FlatBoxes.GetOrAdd(t, new FlatBoxInfo { Type = boxType, Ctor = boxType.GetConstructor(Type.EmptyTypes), Value = boxType.GetField("Value") }); + } + + /// Creates the StrongBox and stores it into the new local, returns the local index. + private static int FlatNewBox(ILGenerator il, Type t) + { + var info = FlatBox(t); + il.Emit(OpCodes.Newobj, info.Ctor); + return EmittingVisitor.EmitStoreLocalVariable(il, info.Type); + } + + /// Loads the item of the closure NonPassedParams: the StrongBox or the boxed value of the captured variable. + private static void FlatLoadNonPassed(ILGenerator il, int slot) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + EmittingVisitor.EmitLoadConstantInt(il, slot); + il.Emit(OpCodes.Ldelem_Ref); + } + + private static void FlatLoadBox(ILGenerator il, in FlatVar v) + { + if (v.Slot < 0) + { + EmittingVisitor.EmitLoadLocalVariable(il, v.Local); + return; + } + FlatLoadNonPassed(il, v.Slot); + il.Emit(OpCodes.Castclass, FlatBox(v.Type).Type); + } + + private static void FlatLoadVar(ILGenerator il, in FlatVar v) + { + if (v.Boxed) + { + FlatLoadBox(il, in v); + il.Emit(OpCodes.Ldfld, FlatBox(v.Type).Value); + } + else if (v.Slot >= 0) + { + FlatLoadNonPassed(il, v.Slot); + il.Emit(OpCodes.Unbox_Any, v.Type); + } + else if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); + else EmittingVisitor.EmitLoadLocalVariable(il, v.Local); + } + + private static FlatVar FlatFindVar(ref SmallList, NoArrayPool> vars, ushort id) + { + for (var i = vars.Count - 1; i >= 0; --i) + { + ref var v = ref vars.GetSurePresentRef(i); + if (v.Id == id) + return v; + } + throw new InvalidOperationException("The parameter is not in scope"); // todo: @wip by construction check + } + + private static bool IsFlatType(Type t) => t == typeof(int) || t == typeof(bool) || !t.IsValueType && !t.IsByRef && !t.IsPointer; + + // the lambda and delegate return/parameter types are checked once per compile, not per node + private static bool FlatFits(Type to, Type from) => to == from || !to.IsValueType && !from.IsValueType && to.IsAssignableFrom(from); + + /// The collect phase on the flat nodes: one linear pass over the contiguous array, no recursion, no stack, no per-node casts. + /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter + /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). + /// Returns `OK` if the tree is the int/bool subset which can emit. + public static Result TryCollectFlatInfo(this ref ExprTree tree) => tree.CollectFlat(); + + private static Result CollectFlat(this ref ExprTree tree) + { + var count = tree.Nodes.Count; + if (count < 2) + return Result.NotSupported_UnknownExpression; + + for (var i = 1; i < count; ++i) + { + ref var n = ref tree.Nodes.GetSurePresentRef(i); + if (n.Kind == ExprNodeKind.BlockExprs) + continue; + if (n.Kind == ExprNodeKind.MemberAssignment) + { + // the instance field or the property with the setter, the binding is emitted by the MemberInit + if (!IsFlatType(n.Type) || !(n.Obj is FieldInfo bf && !bf.IsInitOnly && !bf.IsLiteral && !bf.IsStatic || + n.Obj is PropertyInfo bp && bp.GetSetMethod(true) is { IsStatic: false })) + return Result.NotSupported_UnknownExpression; + continue; + } + if (n.Kind != ExprNodeKind.Expression) + return Result.NotSupported_UnknownExpression; + + var t = n.Type; + switch (n.NodeType) + { + case ExprType.MemberInit: + if (!IsFlatType(t)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.NewArrayInit: + if (!t.IsArray || !IsFlatType(t.GetElementType())) return Result.NotSupported_UnknownExpression; + break; + case ExprType.ArrayIndex: + { + ref var arr = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); + if (!arr.Type.IsArray || arr.Type.GetArrayRank() != 1 || !IsFlatType(t) || + tree.Nodes.GetSurePresentRef(arr.NextIdx).Type != typeof(int)) + return Result.NotSupported_UnknownExpression; + break; + } + case ExprType.Convert: + // the reference cast only, no user-defined conversion method, no boxing and no numeric conversions + if (n.Obj != null || t.IsValueType || !IsFlatType(t) || tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) + return Result.NotSupported_UnknownExpression; + break; + case ExprType.Add: + case ExprType.Subtract: + case ExprType.Multiply: + if (t != typeof(int)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Conditional: + case ExprType.Block: + if (!IsFlatType(t)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Assign: + // the target must be a parameter, the member assignment is not emitted yet + if (!IsFlatType(t) || tree.Nodes.GetSurePresentRef(n.ChildIdx).NodeType != ExprType.Parameter) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Parameter: + // by-ref parameters are rejected here and Assign is the only emitted mutation, so `AssignedParams` is the complete list for the by-value captures + if (!IsFlatType(t) || n.Flags != 0) return Result.NotSupported_UnknownExpression; + break; + case ExprType.New: + // the arguments are not type-checked, same as the Light: the factories did that + if (!IsFlatType(t) || n.Obj is not ConstructorInfo) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Call: + // the instance of the value type needs the address and the constrained call, so the Light handles it + if (!IsFlatType(t) || n.Obj is not MethodInfo m || m.IsGenericMethodDefinition || + !m.IsStatic && tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) + return Result.NotSupported_UnknownExpression; + break; + case ExprType.MemberAccess: + if (!IsFlatType(t) || !(n.Obj is FieldInfo f && !f.IsLiteral || n.Obj is PropertyInfo p && p.CanRead) || + n.ChildCount != 0 && tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) + return Result.NotSupported_UnknownExpression; + break; + case ExprType.Constant: + { + var isInline = ReferenceEquals(n.Obj, ExprNode.InlineValueMarker); + if (isInline ? t != typeof(int) && t != typeof(bool) + : t.IsValueType || !(ReferenceEquals(n.Obj, ExprTree.ClosureConstantMarker) || n.Obj == null || n.Obj is string && t == typeof(string))) + return Result.NotSupported_UnknownExpression; + break; + } + case ExprType.Equal: + case ExprType.GreaterThan: + if (tree.Nodes.GetSurePresentRef(n.ChildIdx).Type != typeof(int)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.AndAlso: + case ExprType.OrElse: + case ExprType.Not: + if (t != typeof(bool)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Lambda: + if (!FlatLambdaFits(ref tree, ref n, i != tree.RootIdx)) return Result.NotSupported_UnknownExpression; + break; + default: + return Result.NotSupported_UnknownExpression; + } + } + + return tree.LambdaCount != 0 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; + } + + /// Folds the tree of the inline int/bool constants and the operations over them (the bool as 0/1). False if anything else is inside. + private static bool FlatFold(ref ExprTree tree, int idx, out int result) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + result = 0; + if (n.Kind != ExprNodeKind.Expression) + return false; + if (n.NodeType == ExprType.Constant) + { + if (!ReferenceEquals(n.Obj, ExprNode.InlineValueMarker)) + return false; + result = (int)n.InlineValue; + return true; + } + if (n.NodeType == ExprType.Not) + { + if (!FlatFold(ref tree, n.ChildIdx, out var v)) + return false; + result = v == 0 ? 1 : 0; + return true; + } + if (n.NodeType == ExprType.Conditional) + { + var ifTrueIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; + return FlatFold(ref tree, n.ChildIdx, out var test) && FlatFold(ref tree, test != 0 ? ifTrueIdx : ifFalseIdx, out result); + } + + var nodeType = n.NodeType; + if (nodeType != ExprType.AndAlso && nodeType != ExprType.OrElse && nodeType != ExprType.Add && nodeType != ExprType.Subtract && + nodeType != ExprType.Multiply && nodeType != ExprType.Equal && nodeType != ExprType.GreaterThan) + return false; + if (!FlatFold(ref tree, n.ChildIdx, out var l)) + return false; + var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + if (nodeType == ExprType.AndAlso && l == 0 || nodeType == ExprType.OrElse && l != 0) + { + result = l; // short-circuit, the right is not even looked at, same as the emit + return true; + } + if (!FlatFold(ref tree, rightIdx, out var r)) + return false; + result = nodeType switch + { + ExprType.Add => l + r, + ExprType.Subtract => l - r, + ExprType.Multiply => l * r, + ExprType.Equal => l == r ? 1 : 0, + ExprType.GreaterThan => l > r ? 1 : 0, + _ => r // AndAlso / OrElse with the left not short-circuiting + }; + return true; + } + + /// Returns null if the tree is not the supported subset or the delegate type is not the lambda type. + public static TDelegate TryCompileFlat(this ref ExprTree tree) where TDelegate : class + { + if (tree.CollectFlat() != Result.OK) + return null; + + if (tree.Nodes.GetSurePresentRef(tree.RootIdx).Type != typeof(TDelegate)) + return null; + + // the constant bool lambda needs no DynamicMethod at all, same as FEC does with its Interpreter + if (typeof(TDelegate) == typeof(Func) && FlatFold(ref tree, tree.Nodes.GetSurePresentRef(tree.RootIdx).ChildIdx, out var folded)) + return (TDelegate)(object)(folded != 0 ? ExpressionCompiler.Interpreter.TrueFunc : ExpressionCompiler.Interpreter.FalseFunc); + + var constantCount = tree.ClosureConstants.Count; + var ctx = new FlatCtx { NextSlot = constantCount }; + var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate + if (slotCount != 0) + { + ctx.Constants = new object[slotCount]; + for (var i = 0; i < constantCount; ++i) + ctx.Constants[i] = tree.ClosureConstants[i]; + } + + if (tree.LambdaCount > 1) + { + var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); + var lambdas = default(SmallList, NoArrayPool>); + FlatFindCaptures(ref tree, tree.RootIdx, ref ctx, ref declared, ref lambdas); + } + + var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); + return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ref ctx, typeof(TDelegate), closure); + } + + /// Emits the lambda into its own DynamicMethod. The root is closed over the closure; the nested one + /// (the delegate type is null) becomes the open delegate with the closure as the first parameter, to be curried at run-time. + private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, ref FlatCtx ctx, Type delegateType, object closure) + { + ref var lambda = ref tree.Nodes.GetSurePresentRef(lambdaIdx); + var bodyIdx = lambda.ChildIdx; + var info = FlatGetDelegateInfo(lambda.Type); + var paramCount = info.ParamTypes.Length; + var dynMethod = new DynamicMethod(string.Empty, info.ReturnType, info.ClosureParamTypes, typeof(ArrayClosure), true); + var il = DynamicMethodHacks.RentPooledOrNewILGenerator(dynMethod, info.ReturnType, info.ClosureParamTypes); + + var vars = default(SmallList, NoArrayPool>); + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + for (var i = 1; i <= paramCount; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + if (ctx.IsBoxed(par.ChildIdx)) + { + var box = FlatNewBox(il, par.Type); + EmittingVisitor.EmitLoadLocalVariable(il, box); + il.Emit(OpCodes.Ldarg, (short)i); + il.Emit(OpCodes.Stfld, FlatBox(par.Type).Value); + vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = -1, Local = box, Slot = -1, Boxed = true }); + } + else + vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = (short)i, Slot = -1 }); + parIdx = par.NextIdx; + } + + for (int c = 0, slot = 0; c < ctx.Captures.Count; ++c) + { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + if (cap.LambdaIdx == lambdaIdx) + vars.Add(new FlatVar { Id = cap.Id, Type = cap.Type, Arg = -1, Slot = (short)slot++, Boxed = !cap.ByValue }); + } + EmitFlat(ref tree, bodyIdx, il, ref vars, ref ctx, false); + il.Emit(OpCodes.Ret); + + var result = dynMethod.CreateDelegate(delegateType ?? info.OpenType, closure); + DynamicMethodHacks.FreePooledILGenerator(dynMethod, il); // only after the CreateDelegate, as FEC does + return result; + } + + private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, + ref SmallList, NoArrayPool> vars, ref FlatCtx ctx, bool ignoreResult) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + var nodeType = n.NodeType; + switch (nodeType) + { + case ExprType.Constant: + if (ignoreResult) + return; + if (ReferenceEquals(n.Obj, ExprNode.InlineValueMarker)) + EmittingVisitor.EmitLoadConstantInt(il, (int)n.InlineValue); + else if (ReferenceEquals(n.Obj, ExprTree.ClosureConstantMarker)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + EmittingVisitor.EmitLoadConstantInt(il, n.ChildIdx); + il.Emit(OpCodes.Ldelem_Ref); + if (n.Type != typeof(object)) + il.Emit(OpCodes.Castclass, n.Type); + } + else if (n.Obj is string s) + il.Emit(OpCodes.Ldstr, s); + else + il.Emit(OpCodes.Ldnull); + return; + + case ExprType.Parameter: + if (ignoreResult) + return; + FlatLoadVar(il, FlatFindVar(ref vars, n.ChildIdx)); + return; + + case ExprType.Assign: + { + var leftIdx = n.ChildIdx; + ref var left = ref tree.Nodes.GetSurePresentRef(leftIdx); + var rightIdx = left.NextIdx; + var v = FlatFindVar(ref vars, left.ChildIdx); + if (v.Boxed) + { + FlatLoadBox(il, in v); + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); + var result = -1; + if (!ignoreResult) + { + il.Emit(OpCodes.Dup); + result = EmittingVisitor.EmitStoreLocalVariable(il, v.Type); + } + il.Emit(OpCodes.Stfld, FlatBox(v.Type).Value); + if (result != -1) + EmittingVisitor.EmitLoadLocalVariable(il, result); + return; + } + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); + if (!ignoreResult) + il.Emit(OpCodes.Dup); + if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else EmittingVisitor.EmitStoreLocalVariable(il, v.Local); + return; + } + + case ExprType.Lambda: + { + if (ignoreResult) + return; + // the nested lambda is the separate method; at run-time the open delegate is curried with the new closure + // made of the shared constants and the captured variables: the StrongBox, or the boxed value if never assigned + var info = FlatGetDelegateInfo(n.Type); + var open = EmitFlatLambda(ref tree, idx, ref ctx, null, null); + var slot = ctx.NextSlot++; + ctx.Constants[slot] = open; + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + EmittingVisitor.EmitLoadConstantInt(il, slot); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, info.OpenType); + + var captureCount = 0; + for (var c = 0; c < ctx.Captures.Count; ++c) + if (ctx.Captures.GetSurePresentRef(c).LambdaIdx == idx) ++captureCount; + + if (captureCount != 0) + { + EmittingVisitor.EmitLoadConstantInt(il, captureCount); + il.Emit(OpCodes.Newarr, typeof(object)); + for (int c = 0, j = 0; c < ctx.Captures.Count; ++c) + { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + if (cap.LambdaIdx != idx) + continue; + il.Emit(OpCodes.Dup); + EmittingVisitor.EmitLoadConstantInt(il, j++); + var v = FlatFindVar(ref vars, cap.Id); + if (v.Slot >= 0) // captured by the outer lambda too, pass the item through + FlatLoadNonPassed(il, v.Slot); + else if (v.Boxed) + FlatLoadBox(il, in v); + else + { + FlatLoadVar(il, in v); + if (v.Type.IsValueType) + il.Emit(OpCodes.Box, v.Type); + } + il.Emit(OpCodes.Stelem_Ref); + } + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + il.Emit(OpCodes.Newobj, captureCount != 0 ? ArrayClosureWithNonPassedParamsAndConstantsCtor : FlatArrayClosureCtor); + + il.Emit(OpCodes.Call, info.CurryMethod); + return; + } + + case ExprType.New: + case ExprType.Call: + { + var argIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + EmitFlat(ref tree, argIdx, il, ref vars, ref ctx, false); + argIdx = tree.Nodes.GetSurePresentRef(argIdx).NextIdx; + } + if (nodeType == ExprType.New) + il.Emit(OpCodes.Newobj, (ConstructorInfo)n.Obj); + else + { + var method = (MethodInfo)n.Obj; + il.Emit(method.IsStatic ? OpCodes.Call : OpCodes.Callvirt, method); + } + break; + } + + case ExprType.MemberAccess: + { + if (n.ChildCount != 0) + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + if (n.Obj is FieldInfo field) + il.Emit(field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, field); + else + { + var getter = ((PropertyInfo)n.Obj).GetGetMethod(true); + il.Emit(getter.IsStatic ? OpCodes.Call : OpCodes.Callvirt, getter); + } + break; + } + + case ExprType.MemberInit: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + var bindingIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var binding = ref tree.Nodes.GetSurePresentRef(bindingIdx); + il.Emit(OpCodes.Dup); + EmitFlat(ref tree, binding.ChildIdx, il, ref vars, ref ctx, false); + if (binding.Obj is FieldInfo field) + il.Emit(OpCodes.Stfld, field); + else + il.Emit(OpCodes.Callvirt, ((PropertyInfo)binding.Obj).GetSetMethod(true)); + bindingIdx = binding.NextIdx; + } + break; + } + + case ExprType.NewArrayInit: + { + var elemType = n.Type.GetElementType(); + EmittingVisitor.EmitLoadConstantInt(il, n.ChildCount); + il.Emit(OpCodes.Newarr, elemType); + var elemIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + il.Emit(OpCodes.Dup); + EmittingVisitor.EmitLoadConstantInt(il, i); + EmitFlat(ref tree, elemIdx, il, ref vars, ref ctx, false); + il.Emit(elemType == typeof(int) ? OpCodes.Stelem_I4 : elemType == typeof(bool) ? OpCodes.Stelem_I1 : OpCodes.Stelem_Ref); + elemIdx = tree.Nodes.GetSurePresentRef(elemIdx).NextIdx; + } + break; + } + + case ExprType.ArrayIndex: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ref ctx, false); + il.Emit(n.Type == typeof(int) ? OpCodes.Ldelem_I4 : n.Type == typeof(bool) ? OpCodes.Ldelem_U1 : OpCodes.Ldelem_Ref); + break; + } + + case ExprType.Convert: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + if (!n.Type.IsAssignableFrom(tree.Nodes.GetSurePresentRef(n.ChildIdx).Type)) + il.Emit(OpCodes.Castclass, n.Type); + break; + } + + case ExprType.Not: + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + break; + + case ExprType.Conditional: + { + var testIdx = n.ChildIdx; + var ifTrueIdx = tree.Nodes.GetSurePresentRef(testIdx).NextIdx; + var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; + var elseLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + EmitFlat(ref tree, testIdx, il, ref vars, ref ctx, false); + il.Emit(OpCodes.Brfalse, elseLabel); + EmitFlat(ref tree, ifTrueIdx, il, ref vars, ref ctx, ignoreResult); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(elseLabel); + EmitFlat(ref tree, ifFalseIdx, il, ref vars, ref ctx, ignoreResult); + il.MarkLabel(endLabel); + return; + } + + case ExprType.AndAlso: + case ExprType.OrElse: + { + var isAnd = nodeType == ExprType.AndAlso; + var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + var shortCircuit = il.DefineLabel(); + var endLabel = il.DefineLabel(); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + il.Emit(isAnd ? OpCodes.Brfalse : OpCodes.Brtrue, shortCircuit); + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(shortCircuit); + il.Emit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); + il.MarkLabel(endLabel); + break; + } + + case ExprType.Block: + { + // layout: the first child is the BlockExprs list, the rest are the variables + var exprsIdx = n.ChildIdx; + ref var exprs = ref tree.Nodes.GetSurePresentRef(exprsIdx); + var varIdx = exprs.NextIdx; + var scopeStart = vars.Count; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); + if (ctx.IsBoxed(v.ChildIdx)) + vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = FlatNewBox(il, v.Type), Slot = -1, Boxed = true }); + else + vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = il.GetNextLocalVarIndex(v.Type), Slot = -1 }); + varIdx = v.NextIdx; + } + + var last = exprs.ChildCount - 1; + var exprIdx = exprs.ChildIdx; + for (var i = 0; i <= last; ++i) + { + EmitFlat(ref tree, exprIdx, il, ref vars, ref ctx, i != last || ignoreResult); + exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; + } + vars.Count = scopeStart; // the block variables go out of the scope + return; + } + + default: // the binary int/bool operations + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ref ctx, false); + switch (nodeType) + { + case ExprType.Add: il.Emit(OpCodes.Add); break; + case ExprType.Subtract: il.Emit(OpCodes.Sub); break; + case ExprType.Multiply: il.Emit(OpCodes.Mul); break; + case ExprType.Equal: il.Emit(OpCodes.Ceq); break; + default: il.Emit(OpCodes.Cgt); break; // GreaterThan, the rest is rejected by the collect + } + break; + } + } + + if (ignoreResult) + il.Emit(OpCodes.Pop); + } +} diff --git a/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs new file mode 100644 index 00000000..519aae9b --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs @@ -0,0 +1,280 @@ +#nullable disable +namespace FastExpressionCompiler.FlatExpression; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using FastExpressionCompiler.LightExpression.ImTools; +using LExpr = FastExpressionCompiler.LightExpression.Expression; +using LParameter = FastExpressionCompiler.LightExpression.ParameterExpression; +using LMemberBinding = FastExpressionCompiler.LightExpression.MemberBinding; +using LElementInit = FastExpressionCompiler.LightExpression.ElementInit; + +public partial struct ExprTree +{ + // todo: @perf the ushort[] per call, use the SmallList of the child ids as the ToLightExpression does + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort[] AddLightArguments(LightExpression.IArgumentProvider args) + { + var result = new ushort[args.ArgumentCount]; + for (var i = 0; i < result.Length; ++i) + result[i] = AddLightExpression(args.GetArgument(i)); + return result; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort[] AddLightExpressions(System.Collections.Generic.IReadOnlyList exprs) + { + var result = new ushort[exprs.Count]; + for (var i = 0; i < result.Length; ++i) + result[i] = AddLightExpression(exprs[i]); + return result; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightExpression(LExpr expr) + { + switch (expr.NodeType) + { + case ExpressionType.Constant: + return Constant(((LightExpression.ConstantExpression)expr).Value, expr.Type); + + case ExpressionType.Default: + return Default(expr.Type); + + case ExpressionType.Parameter: + { + var parameter = (LParameter)expr; + var id = checked((ushort)GetId(ref _parameterIds, parameter)); + return ParameterWithId(parameter.IsByRef ? parameter.Type : expr.Type, parameter.Name, id); + } + case ExpressionType.Lambda: + { + var lambda = (LightExpression.LambdaExpression)expr; + var pars = new ushort[lambda.ParameterCount]; + for (var i = 0; i < pars.Length; ++i) + pars[i] = AddLightExpression(lambda.GetParameter(i)); + return Lambda(expr.Type, AddLightExpression(lambda.Body), pars); + } + case ExpressionType.Block: + { + var block = (LightExpression.BlockExpression)expr; + ushort[] vars = null; + if (block.Variables.Count != 0) + { + vars = new ushort[block.Variables.Count]; + for (var i = 0; i < vars.Length; ++i) + vars[i] = AddLightExpression(block.Variables[i]); + } + var exprs = new ushort[block.Expressions.Count]; + for (var i = 0; i < exprs.Length; ++i) + exprs[i] = AddLightExpression(block.Expressions[i]); + return Block(expr.Type, vars, exprs); + } + case ExpressionType.MemberAccess: + { + var member = (LightExpression.MemberExpression)expr; + return member.Expression != null + ? MakeMemberAccess(AddLightExpression(member.Expression), member.Member) + : MakeMemberAccess(member.Member); + } + case ExpressionType.Call: + { + var call = (LightExpression.MethodCallExpression)expr; + var args = AddLightArguments(call); + return call.Object != null + ? Call(AddLightExpression(call.Object), call.Method, args) + : Call(call.Method, args); + } + case ExpressionType.New: + { + var @new = (LightExpression.NewExpression)expr; + if (@new.Constructor == null) + return New(expr.Type); + if (@new.ArgumentCount == 0) + return New(@new.Constructor); + return New(@new.Constructor, AddLightArguments(@new)); + } + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + { + var items = AddLightArguments((LightExpression.NewArrayExpression)expr); + return expr.NodeType == ExpressionType.NewArrayInit + ? NewArrayInit(expr.Type.GetElementType(), items) + : NewArrayBounds(expr.Type.GetElementType(), items); + } + case ExpressionType.Invoke: + { + var invoke = (LightExpression.InvocationExpression)expr; + var args = AddLightArguments(invoke); + return Invoke(AddLightExpression(invoke.Expression), args); + } + case ExpressionType.Index: + { + var indexExpr = (LightExpression.IndexExpression)expr; + var args = AddLightArguments(indexExpr); + var instance = indexExpr.Object != null ? AddLightExpression(indexExpr.Object) : (ushort)0; + return indexExpr.Indexer != null + ? Property(instance, indexExpr.Indexer, args) + : ArrayAccess(instance, args); + } + case ExpressionType.Conditional: + { + var conditional = (LightExpression.ConditionalExpression)expr; + return Condition( + AddLightExpression(conditional.Test), + AddLightExpression(conditional.IfTrue), + AddLightExpression(conditional.IfFalse), + expr.Type); + } + case ExpressionType.Loop: + { + var loop = (LightExpression.LoopExpression)expr; + return Loop( + AddLightExpression(loop.Body), + loop.BreakLabel != null ? AddLightLabelTarget(loop.BreakLabel) : (ushort)0, + loop.ContinueLabel != null ? AddLightLabelTarget(loop.ContinueLabel) : (ushort)0); + } + case ExpressionType.Goto: + { + var @goto = (LightExpression.GotoExpression)expr; + return MakeGoto(@goto.Kind, AddLightLabelTarget(@goto.Target), + @goto.Value != null ? AddLightExpression(@goto.Value) : (ushort)0, expr.Type); + } + case ExpressionType.Label: + { + var label = (LightExpression.LabelExpression)expr; + return Label(AddLightLabelTarget(label.Target), + label.DefaultValue != null ? AddLightExpression(label.DefaultValue) : (ushort)0); + } + case ExpressionType.Switch: + { + var @switch = (LightExpression.SwitchExpression)expr; + var cases = new ushort[@switch.Cases.Count]; + for (var i = 0; i < cases.Length; ++i) + { + var sc = @switch.Cases[i]; + var tests = AddLightExpressions(sc.TestValues); + cases[i] = SwitchCase(AddLightExpression(sc.Body), tests); + } + return Switch(expr.Type, AddLightExpression(@switch.SwitchValue), + @switch.DefaultBody != null ? AddLightExpression(@switch.DefaultBody) : (ushort)0, + @switch.Comparison, cases); + } + case ExpressionType.Try: + { + var @try = (LightExpression.TryExpression)expr; + if (@try.Fault != null) + return TryFault(AddLightExpression(@try.Body), AddLightExpression(@try.Fault)); + + var handlerCount = @try.Handlers?.Count ?? 0; + var handlers = new ushort[handlerCount]; + for (var i = 0; i < handlers.Length; ++i) + { + var h = @try.Handlers[i]; + var variable = h.Variable != null ? AddLightExpression(h.Variable) : (ushort)0; + var filter = h.Filter != null ? AddLightExpression(h.Filter) : (ushort)0; + handlers[i] = MakeCatchBlock(h.Test, variable, AddLightExpression(h.Body), filter); + } + + if (@try.Finally != null) + return handlers.Length != 0 + ? TryCatchFinally(AddLightExpression(@try.Body), AddLightExpression(@try.Finally), handlers) + : TryFinally(AddLightExpression(@try.Body), AddLightExpression(@try.Finally)); + + return TryCatch(AddLightExpression(@try.Body), handlers); + } + case ExpressionType.MemberInit: + { + var memberInit = (LightExpression.MemberInitExpression)expr; + var bindings = new ushort[memberInit.Bindings.Count]; + for (var i = 0; i < bindings.Length; ++i) + bindings[i] = AddLightMemberBinding(memberInit.Bindings[i]); + return MemberInit(AddLightExpression(memberInit.NewExpression), bindings); + } + case ExpressionType.ListInit: + { + var listInit = (LightExpression.ListInitExpression)expr; + var inits = new ushort[listInit.Initializers.Count]; + for (var i = 0; i < inits.Length; ++i) + inits[i] = AddLightElementInit(listInit.Initializers[i]); + return ListInit(AddLightExpression(listInit.NewExpression), inits); + } + case ExpressionType.TypeIs: + { + var typeBinary = (LightExpression.TypeBinaryExpression)expr; + return TypeIs(AddLightExpression(typeBinary.Expression), typeBinary.TypeOperand); + } + case ExpressionType.TypeEqual: + { + var typeBinary = (LightExpression.TypeBinaryExpression)expr; + return TypeEqual(AddLightExpression(typeBinary.Expression), typeBinary.TypeOperand); + } + case ExpressionType.Dynamic: + { + var dynamic = (LightExpression.DynamicExpression)expr; + return Dynamic(dynamic.DelegateType, dynamic.Binder, AddLightExpressions(dynamic.Arguments)); + } + case ExpressionType.RuntimeVariables: + { + var runtime = (LightExpression.RuntimeVariablesExpression)expr; + var vars = new ushort[runtime.Variables.Count]; + for (var i = 0; i < vars.Length; ++i) + vars[i] = AddLightExpression(runtime.Variables[i]); + return RuntimeVariables(vars); + } + case ExpressionType.DebugInfo: + { + var debug = (LightExpression.DebugInfoExpression)expr; + return DebugInfo(debug.Document.FileName, debug.StartLine, debug.StartColumn, debug.EndLine, debug.EndColumn); + } + default: + if (expr is LightExpression.UnaryExpression unary) + return MakeUnary(expr.NodeType, AddLightExpression(unary.Operand), expr.Type, unary.Method); + + if (expr is LightExpression.BinaryExpression binary) + return MakeBinary(expr.NodeType, + AddLightExpression(binary.Left), + AddLightExpression(binary.Right), + binary.IsLiftedToNull, + binary.Method, + binary.Conversion != null ? AddLightExpression(binary.Conversion) : (ushort)0, + expr.Type); + + throw new NotSupportedException($"Flattening of `ExpressionType.{expr.NodeType}` is not supported yet."); + } + } + + private ushort AddLightLabelTarget(LightExpression.LabelTarget target) => AddLabelTarget(target, target.Type, target.Name); + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightMemberBinding(LMemberBinding binding) + { + switch (binding.BindingType) + { + case MemberBindingType.Assignment: + return Bind(binding.Member, AddLightExpression(((LightExpression.MemberAssignment)binding).Expression)); + case MemberBindingType.MemberBinding: + { + var memberBinding = (LightExpression.MemberMemberBinding)binding; + var bindings = new ushort[memberBinding.Bindings.Count]; + for (var i = 0; i < bindings.Length; ++i) + bindings[i] = AddLightMemberBinding(memberBinding.Bindings[i]); + return MemberBind(binding.Member, bindings); + } + case MemberBindingType.ListBinding: + { + var listBinding = (LightExpression.MemberListBinding)binding; + var inits = new ushort[listBinding.Initializers.Count]; + for (var i = 0; i < inits.Length; ++i) + inits[i] = AddLightElementInit(listBinding.Initializers[i]); + return ListBind(binding.Member, inits); + } + default: + throw new NotSupportedException($"Flattening of member binding `{binding.BindingType}` is not supported yet."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightElementInit(LElementInit init) => ElementInit(init.AddMethod, AddLightArguments(init)); +} diff --git a/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs new file mode 100644 index 00000000..afcd41c2 --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs @@ -0,0 +1,346 @@ +#nullable disable +namespace FastExpressionCompiler.FlatExpression; + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using FastExpressionCompiler.LightExpression.ImTools; +using LExpr = FastExpressionCompiler.LightExpression.Expression; +using LParameter = FastExpressionCompiler.LightExpression.ParameterExpression; +using LLabelTarget = FastExpressionCompiler.LightExpression.LabelTarget; +using LSwitchCase = FastExpressionCompiler.LightExpression.SwitchCase; +using LCatchBlock = FastExpressionCompiler.LightExpression.CatchBlock; +using LMemberBinding = FastExpressionCompiler.LightExpression.MemberBinding; +using LElementInit = FastExpressionCompiler.LightExpression.ElementInit; +using ChildIdxs = LightExpression.ImTools.SmallList, LightExpression.ImTools.NoArrayPool>; + +public partial struct ExprTree +{ + /// Reconstructs the flat tree as a LightExpression tree directly, without the System.Linq expressions in between. + /// Mirrors SysExprBuilder: keep the two in sync. + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + // todo: @perf the builder copies the whole ExprTree (the big Stack32 inside); take it by ref when the ref fields are available on all the targets + public LightExpression.Expression ToLightExpression() => + Nodes.Count != 0 + ? new LightExprBuilder(this).ReadExpression(RootIdx) + : throw new InvalidOperationException("Flat expression tree is empty."); + + private struct LightExprBuilder + { + private readonly ExprTree _tree; + private SmallMap16 _parametersById; + private SmallMap16 _labelsById; + + public LightExprBuilder(ExprTree tree) + { + _tree = tree; + _parametersById = default; + _labelsById = default; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + public LExpr ReadExpression(int idx) + { + ref var node = ref _tree.Nodes[idx]; + if (node.Kind != ExprNodeKind.Expression) + throw new InvalidOperationException($"Node at idx {idx} is not an expression node."); + + switch (node.NodeType) + { + case ExpressionType.Constant: + if (ReferenceEquals(node.Obj, ClosureConstantMarker)) + return LExpr.Constant(_tree.ClosureConstants[node.ChildIdx], node.Type); + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker)) + return LExpr.Constant(ReadInlineValue(node.Type, node.InlineValue), node.Type); + return LExpr.Constant(node.Obj, node.Type); + case ExpressionType.Default: + return LExpr.Default(node.Type); + case ExpressionType.Parameter: + { + ref var parameter = ref _parametersById.Map.AddOrGetValueRef(node.ChildIdx, out var found); + if (found) + return parameter; + + var parameterType = node.HasFlag(ParameterByRefFlag) && !node.Type.IsByRef ? node.Type.MakeByRefType() : node.Type; + return parameter = LExpr.Parameter(parameterType, (string)node.Obj); + } + case ExpressionType.Lambda: + { + // Layout: children[0] = body, children[1..n] = parameter decl nodes. + var children = GetChildren(idx); + var body = ReadExpression(children[0]); + var parameters = new LParameter[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + parameters[i - 1] = (LParameter)ReadExpression(children[i]); + return LExpr.Lambda(node.Type, body, parameters); + } + case ExpressionType.Block: + { + // Layout: children[0] = BlockExprs; children[1..] = variable decls. + var children = GetChildren(idx); + var variables = new LParameter[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + variables[i - 1] = (LParameter)ReadExpression(children[i]); + var expressionIdxs = GetChildren(children[0]); + var expressions = new LExpr[expressionIdxs.Count]; + for (var i = 0; i < expressions.Length; ++i) + expressions[i] = ReadExpression(expressionIdxs[i]); + return LExpr.Block(node.Type, variables, expressions); + } + case ExpressionType.MemberAccess: + { + var children = GetChildren(idx); + return LExpr.MakeMemberAccess(children.Count != 0 ? ReadExpression(children[0]) : null, (MemberInfo)node.Obj); + } + case ExpressionType.Call: + { + var method = (MethodInfo)node.Obj; + var children = GetChildren(idx); + var hasInstance = !method.IsStatic; + var instance = hasInstance ? ReadExpression(children[0]) : null; + var arguments = new LExpr[children.Count - (hasInstance ? 1 : 0)]; + for (var i = hasInstance ? 1 : 0; i < children.Count; ++i) + arguments[i - (hasInstance ? 1 : 0)] = ReadExpression(children[i]); + return LExpr.Call(instance, method, arguments); + } + case ExpressionType.New: + { + var arguments = ReadExpressions(GetChildren(idx)); + return node.Obj is ConstructorInfo ctor ? LExpr.New(ctor, arguments) : LExpr.New(node.Type); + } + case ExpressionType.NewArrayInit: + return LExpr.NewArrayInit(node.Type.GetElementType(), ReadExpressions(GetChildren(idx))); + case ExpressionType.NewArrayBounds: + return LExpr.NewArrayBounds(node.Type.GetElementType(), ReadExpressions(GetChildren(idx))); + case ExpressionType.Invoke: + { + var children = GetChildren(idx); + var arguments = new LExpr[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + arguments[i - 1] = ReadExpression(children[i]); + return LExpr.Invoke(ReadExpression(children[0]), arguments); + } + case ExpressionType.Index: + { + var children = GetChildren(idx); + var property = (PropertyInfo)node.Obj; + var hasInstance = property != null || children.Count > 1; + var instance = hasInstance ? ReadExpression(children[0]) : null; + var arguments = new LExpr[children.Count - (hasInstance ? 1 : 0)]; + for (var i = hasInstance ? 1 : 0; i < children.Count; ++i) + arguments[i - (hasInstance ? 1 : 0)] = ReadExpression(children[i]); + return property != null + ? LExpr.Property(instance, property, arguments) + : LExpr.ArrayAccess(instance, arguments); + } + case ExpressionType.Conditional: + { + var children = GetChildren(idx); + return LExpr.Condition(ReadExpression(children[0]), ReadExpression(children[1]), ReadExpression(children[2]), node.Type); + } + case ExpressionType.Loop: + { + var children = GetChildren(idx); + var childIdx = 1; + var breakLabel = node.HasFlag(LoopHasBreakFlag) ? ReadLabelTarget(children[childIdx++]) : null; + var continueLabel = node.HasFlag(LoopHasContinueFlag) ? ReadLabelTarget(children[childIdx]) : null; + return LExpr.Loop(ReadExpression(children[0]), breakLabel, continueLabel); + } + case ExpressionType.Goto: + { + var children = GetChildren(idx); + var value = children.Count > 1 ? ReadExpression(children[1]) : null; + return LExpr.MakeGoto((GotoExpressionKind)node.Flags, ReadLabelTarget(children[0]), value, node.Type); + } + case ExpressionType.Label: + { + var children = GetChildren(idx); + var defaultValue = children.Count > 1 ? ReadExpression(children[1]) : null; + return LExpr.Label(ReadLabelTarget(children[0]), defaultValue); + } + case ExpressionType.Switch: + { + // Layout: switchValue, optional defaultBody, optional SwitchCases group. + var children = GetChildren(idx); + var hasDefault = node.HasFlag(SwitchHasDefaultFlag); + var defaultBody = hasDefault ? ReadExpression(children[1]) : null; + var casesIdx = hasDefault ? 2 : 1; + var caseIdxs = children.Count > casesIdx ? GetChildren(children[casesIdx]) : default; + var cases = new LSwitchCase[caseIdxs.Count]; + for (var i = 0; i < cases.Length; ++i) + cases[i] = ReadSwitchCase(caseIdxs[i]); + return LExpr.Switch(node.Type, ReadExpression(children[0]), defaultBody, (MethodInfo)node.Obj, cases); + } + case ExpressionType.Try: + { + // Layout: body, optional finally/fault, then CatchBlock handlers as direct children. + var children = GetChildren(idx); + if (node.HasFlag(TryFaultFlag)) + return LExpr.TryFault(ReadExpression(children[0]), ReadExpression(children[1])); + + var hasFinally = node.HasFlag(TryHasFinallyFlag); + var @finally = hasFinally ? ReadExpression(children[1]) : null; + var handlerStart = hasFinally ? 2 : 1; + + var handlers = new LCatchBlock[children.Count - handlerStart]; + for (var i = 0; i < handlers.Length; ++i) + handlers[i] = ReadCatchBlock(children[handlerStart + i]); + return LExpr.TryCatchFinally(ReadExpression(children[0]), @finally, handlers); + } + case ExpressionType.MemberInit: + { + var children = GetChildren(idx); + var bindings = new LMemberBinding[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + bindings[i - 1] = ReadMemberBinding(children[i]); + return LExpr.MemberInit((LightExpression.NewExpression)ReadExpression(children[0]), bindings); + } + case ExpressionType.ListInit: + { + var children = GetChildren(idx); + var initializers = new LElementInit[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + initializers[i - 1] = ReadElementInit(children[i]); + return LExpr.ListInit((LightExpression.NewExpression)ReadExpression(children[0]), initializers); + } + case ExpressionType.TypeIs: + return LExpr.TypeIs(ReadExpression(GetChildren(idx)[0]), (Type)node.Obj); + case ExpressionType.TypeEqual: + return LExpr.TypeEqual(ReadExpression(GetChildren(idx)[0]), (Type)node.Obj); + case ExpressionType.Dynamic: + { + var children = GetChildren(idx); + var delegateType = (Type)ReadObjectReference(children[0]); + var arguments = new LExpr[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + arguments[i - 1] = ReadExpression(children[i]); + return new LightExpression.DynamicExpression(delegateType, (System.Runtime.CompilerServices.CallSiteBinder)node.Obj, arguments); + } + case ExpressionType.RuntimeVariables: + { + var children = GetChildren(idx); + var variables = new LParameter[children.Count]; + for (var i = 0; i < children.Count; ++i) + variables[i] = (LParameter)ReadExpression(children[i]); + return new LightExpression.RuntimeVariablesExpression(variables); + } + case ExpressionType.DebugInfo: + { + var children = GetChildren(idx); + ReadUInt16Pair(children[0], out var startLine, out var startColumn); + ReadUInt16Pair(children[1], out var endLine, out var endColumn); + return LExpr.DebugInfo(LExpr.SymbolDocument((string)node.Obj), startLine, startColumn, endLine, endColumn); + } + default: + if (node.ChildCount == 1) + return LExpr.MakeUnary(node.NodeType, ReadExpression(GetChildren(idx)[0]), node.Type, node.Obj as MethodInfo); + + if (node.ChildCount >= 2) + { + var children = GetChildren(idx); + var conversion = children.Count > 2 ? (LightExpression.LambdaExpression)ReadExpression(children[2]) : null; + return LExpr.MakeBinary(node.NodeType, ReadExpression(children[0]), ReadExpression(children[1]), + node.HasFlag(BinaryLiftedToNullFlag), (MethodInfo)node.Obj, conversion); + } + + throw new NotSupportedException($"Reconstruction of `ExpressionType.{node.NodeType}` is not supported yet."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LSwitchCase ReadSwitchCase(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.SwitchCase)); + var children = GetChildren(idx); + var testValues = new LExpr[children.Count - 1]; + for (var i = 0; i < testValues.Length; ++i) + testValues[i] = ReadExpression(children[i]); + return LExpr.SwitchCase(ReadExpression(children[children.Count - 1]), testValues); + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LCatchBlock ReadCatchBlock(int idx) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.CatchBlock)); + var children = GetChildren(idx); + var childIdx = 0; + var variable = node.HasFlag(CatchHasVariableFlag) ? (LParameter)ReadExpression(children[childIdx++]) : null; + var body = ReadExpression(children[childIdx++]); + var filter = node.HasFlag(CatchHasFilterFlag) ? ReadExpression(children[childIdx]) : null; + return LExpr.MakeCatchBlock(node.Type, variable, body, filter); + } + + private LLabelTarget ReadLabelTarget(int idx) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.LabelTarget)); + ref var label = ref _labelsById.Map.AddOrGetValueRef(node.ChildIdx, out var found); + return found ? label : label = LExpr.Label(node.Type, (string)node.Obj); + } + + private object ReadObjectReference(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.ObjectReference)); + return _tree.Nodes[idx].Obj; + } + + private void ReadUInt16Pair(int idx, out int first, out int second) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.UInt16Pair)); + first = node.ChildIdx; + second = node.ChildCount; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LMemberBinding ReadMemberBinding(int idx) + { + ref var node = ref _tree.Nodes[idx]; + var member = (MemberInfo)node.Obj; + switch (node.Kind) + { + case ExprNodeKind.MemberAssignment: + return LExpr.Bind(member, ReadExpression(GetChildren(idx)[0])); + case ExprNodeKind.MemberMemberBinding: + { + var childIdxs = GetChildren(idx); + var bindings = new LMemberBinding[childIdxs.Count]; + for (var i = 0; i < childIdxs.Count; ++i) + bindings[i] = ReadMemberBinding(childIdxs[i]); + return LExpr.MemberBind(member, bindings); + } + case ExprNodeKind.MemberListBinding: + { + var childIdxs = GetChildren(idx); + var initializers = new LElementInit[childIdxs.Count]; + for (var i = 0; i < childIdxs.Count; ++i) + initializers[i] = ReadElementInit(childIdxs[i]); + return LExpr.ListBind(member, initializers); + } + default: + throw new InvalidOperationException($"Node at idx {idx} is not a member binding node."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LElementInit ReadElementInit(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.ElementInit)); + return LExpr.ElementInit((MethodInfo)_tree.Nodes[idx].Obj, ReadExpressions(GetChildren(idx))); + } + + private ChildIdxs GetChildren(int idx) => ExprTree.GetChildren(_tree, idx); + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LExpr[] ReadExpressions(in ChildIdxs childIdxs) + { + var expressions = new LExpr[childIdxs.Count]; + for (var i = 0; i < expressions.Length; ++i) + expressions[i] = ReadExpression(childIdxs[i]); + return expressions; + } + } +} diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 7fa152f4..1ba6d5e2 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq.Expressions; using FastExpressionCompiler.FlatExpression; +using SysExpr = System.Linq.Expressions.Expression; +using SysSwitchCase = System.Linq.Expressions.SwitchCase; +using SysCatchBlock = System.Linq.Expressions.CatchBlock; using static FastExpressionCompiler.LightExpression.Expression; #if NET8_0_OR_GREATER using CsCheck; @@ -26,6 +29,689 @@ public void Can_property_test_generated_flat_expression_roundtrip_structurally() print: testCase => $"{testCase.Seed}: {testCase.Spec}"); } + /// The flat-native collect+emit must agree with System.Linq on the generated int/bool trees and must not fall back. + public void Can_compile_generated_flat_expression_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var spec = GeneratedIntSpecFactory.Create(seed, maxDepth: 3, maxBreadth: 3); + var fe = CreateGeneratedFlatExpression(spec); + Ensure(fe.TryCollectFlatInfo() == ExpressionCompiler.Result.OK, "flat collect"); + var flat = fe.TryCompileFlat>(); + Ensure(flat != null, "flat native compile"); + var expected = CreateGeneratedLightExpression(spec).CompileSys(); + for (var arg = -3; arg <= 3; ++arg) + Ensure(flat(arg) == expected(arg), $"result for {arg}"); + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GeneratedIntSpecFactory.Create(seed, 3, 3)}"); + } + + /// The bool body compiles natively; the tree outside the subset (the string constant) falls back and still works. + public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() + { + var fe = default(ExprTree); + var p = fe.ParameterOf("p"); + fe.RootIdx = fe.Lambda>(fe.MakeBinary(ExpressionType.GreaterThan, p, fe.ConstantInt(1)), p); + var native = fe.TryCompileFlat>(); + Ensure(native != null && native(2) && !native(1), "bool lambda native"); + + var fe2 = default(ExprTree); + var p2 = fe2.ParameterOf("p"); + fe2.RootIdx = fe2.Lambda>(fe2.ConstantOf(typeof(string)), p2); + Ensure(fe2.TryCompileFlat>() == null, "the Type constant is outside the subset"); + Ensure(fe2.CompileFast>()(1) == typeof(string), "fallback result"); + } + + // the slower oracles of the direct conversions: through System.Linq expressions + static FastExpressionCompiler.LightExpression.Expression FlatToLightViaSystem(ref ExprTree tree) => + FromSysExpressionConverter.ToLightExpression(tree.ToExpression()); + + static void LightToFlatViaSystem(ref ExprTree tree, FastExpressionCompiler.LightExpression.Expression light) => + tree.FromSysExpr(light.ToExpression()); + + /// Prints how long the phases of the compilation take on the flat and on the light trees; asserts nothing but the agreement. + public void Experiment_compare_flat_and_light_compile_phases() + { + const int n = 200, reps = 20; + var flats = new ExprTree[n]; + var lights = new FastExpressionCompiler.LightExpression.Expression>[n]; + var nodes = 0; + for (var i = 0; i < n; ++i) + { + var spec = GeneratedIntSpecFactory.Create(i + 1, maxDepth: 4, maxBreadth: 3); + flats[i] = CreateGeneratedFlatExpression(spec); + lights[i] = CreateGeneratedLightExpression(spec); + nodes += flats[i].Nodes.Count; + } + + void Time(string name, int repeat, Action body) + { + for (var i = 0; i < n; ++i) body(i); // warm up + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (var r = 0; r < repeat; ++r) + for (var i = 0; i < n; ++i) body(i); + sw.Stop(); + Console.WriteLine($"//flat-vs-light {name,-34} {sw.Elapsed.TotalMilliseconds * 1000 / (n * repeat),9:F2} us/expr"); + } + + Console.WriteLine($"//flat-vs-light {n} exprs, {nodes / n} flat nodes avg"); + Time("collect: flat linear scan", reps * 10, i => flats[i].TryCollectFlatInfo()); + Time("collect: light TryCollectInfo", reps * 10, i => + { + var ctx = new ExpressionCompiler.CompilerContext(ExpressionCompiler.ClosureStatus.ToBeCollected, (FastExpressionCompiler.LightExpression.IParameterProvider)lights[i], CompilerFlags.Default); + ExpressionCompiler.TryCollectInfo(ref ctx, lights[i].Body, null, ref ctx.NestedLambdas, 0, true); + }); + Time("compile: flat native (collect+emit)", reps, i => flats[i].TryCompileFlat>()); + Time("compile: light FEC (collect+emit)", reps, i => lights[i].CompileFast(ifFastFailedReturnNull: true)); + Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)FlatToLightViaSystem(ref flats[i])).CompileFast>(true)); + Time("convert only: flat->sys->light", reps, i => FlatToLightViaSystem(ref flats[i])); + Time("convert only: flat->light direct", reps, i => flats[i].ToLightExpression()); + Time("convert only: light->sys->flat", reps, i => { var t = default(ExprTree); LightToFlatViaSystem(ref t, lights[i]); }); + Time("convert only: light->flat direct", reps, i => { var t = default(ExprTree); t.FromLightExpr(lights[i]); }); + Time("compile: flat->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); + Time("compile: System.Compile", 1, i => lights[i].CompileSys()); + } + + public class ReadmeA { } + public class ReadmeB { } + public class ReadmeX + { + public ReadmeA A; public ReadmeB B; + public ReadmeX(ReadmeA a, ReadmeB b) { A = a; B = b; } + } + + /// The README expressions (new X(closure a, b) and n => () => n + 1) built flat: the results must match Light and the phases are timed. + public void Flat_compiles_the_readme_expressions_and_compares_the_time() + { + var a = new ReadmeA(); + var ctor = typeof(ReadmeX).GetConstructors()[0]; + + var fe1 = default(ExprTree); + var b1 = fe1.ParameterOf("b"); + fe1.RootIdx = fe1.Lambda>(fe1.New(ctor, fe1.Constant(a, typeof(ReadmeA)), b1), b1); + + var fe2 = default(ExprTree); + var n2 = fe2.ParameterOf("n"); + fe2.RootIdx = fe2.Lambda>>( + fe2.Lambda>(fe2.MakeBinary(ExpressionType.Add, n2, fe2.ConstantInt(1))), n2); + + var bp = Parameter(typeof(ReadmeB), "b"); + var l1 = Lambda>(New(ctor, Constant(a, typeof(ReadmeA)), bp), bp); + var np = Parameter(typeof(int), "n"); + var l2 = Lambda>>(Lambda>(Add(np, Constant(1))), np); + + var bb = new ReadmeB(); + var x = fe1.CompileFast>()(bb); + Ensure(x.A == a && x.B == bb, "readme 1 result"); + Ensure(fe2.CompileFast>>()(41)() == 42, "readme 2 result"); + Ensure(((FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression()).CompileFast>(true)(bb).A == a, "readme 1 direct"); + + const int reps = 300; + void Time(string name, Action body) + { + for (var i = 0; i < 100; ++i) body(); + var best = double.MaxValue; // the min of the rounds, the DynamicMethod garbage makes the single run noisy + for (var round = 0; round < 9; ++round) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (var i = 0; i < reps; ++i) body(); + best = Math.Min(best, sw.Elapsed.TotalMilliseconds * 1000 / reps); + } + var allocated = GC.GetAllocatedBytesForCurrentThread(); + for (var i = 0; i < reps; ++i) body(); + allocated = (GC.GetAllocatedBytesForCurrentThread() - allocated) / reps; + Console.WriteLine($"//flat-readme {name,-36} {best,9:F2} us {allocated,7} B"); + } + + Time("1 light FEC", () => l1.CompileFast(true)); + Time("1 flat->light->FEC (CompileFast)", () => fe1.CompileFast>()); + Time("1 flat->light only", () => fe1.ToLightExpression()); + var l1f = (FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression(); + Time("1 prebuilt flat-made light FEC", () => l1f.CompileFast>(true)); + Time("1 prebuilt light FEC (generic)", () => l1.CompileFast>(true)); + Time("1 flat native (closure const + New)", () => fe1.TryCompileFlat>()); + Time("2 light FEC", () => l2.CompileFast(true)); + Time("2 flat->light->FEC (CompileFast)", () => fe2.CompileFast>>()); + Time("2 flat->light only", () => fe2.ToLightExpression()); + Time("2 flat native (nested lambda)", () => fe2.TryCompileFlat>>()); + Time("floor: collect only", () => fe2.TryCollectFlatInfo()); + + // the whole cost: create the tree and compile it + Time("1 create+compile light FEC", () => + { + var p = Parameter(typeof(ReadmeB), "b"); + Lambda>(New(ctor, Constant(a, typeof(ReadmeA)), p), p).CompileFast(true); + }); + Time("1 create+compile flat native", () => + { + var f = default(ExprTree); + var p = f.ParameterOf("b"); + f.RootIdx = f.Lambda>(f.New(ctor, f.Constant(a, typeof(ReadmeA)), p), p); + f.TryCompileFlat>(); + }); + Time("2 create+compile light FEC", () => + { + var p = Parameter(typeof(int), "n"); + Lambda>>(Lambda>(Add(p, Constant(1))), p).CompileFast(true); + }); + Time("2 create+compile flat native", () => + { + var f = default(ExprTree); + var p = f.ParameterOf("n"); + f.RootIdx = f.Lambda>>(f.Lambda>(f.MakeBinary(ExpressionType.Add, p, f.ConstantInt(1))), p); + f.TryCompileFlat>>(); + }); + Time("C create+compile light FEC", () => CreateComplexLightExpression().CompileFast(true)); + Time("C create+compile flat native", () => { var f = CreateComplexFlatExpression(); f.TryCompileFlat>(); }); + + var cLight = CreateComplexLightExpression(); + var cFlat = CreateComplexFlatExpression(); + Time("C light FEC", () => cLight.CompileFast(true)); + Time("C flat->light->FEC (CompileFast)", () => cFlat.CompileFast>()); + Time("C flat->light only", () => cFlat.ToLightExpression()); + Time("C flat native", () => cFlat.TryCompileFlat>()); + + var hoistedB = new ReadmeB(); + System.Linq.Expressions.Expression> hoisted = () => new ReadmeX(a, hoistedB); + var hFlat = hoisted.ToFlatExpression(); + var hLight = (FastExpressionCompiler.LightExpression.LambdaExpression)hFlat.ToLightExpression(); + Time("H light FEC", () => hLight.CompileFast>(true)); + Time("H flat native", () => hFlat.TryCompileFlat>()); + Time("floor: 2 bare DynamicMethods", () => + { + for (var k = 0; k < 2; ++k) + { + var dm = new System.Reflection.Emit.DynamicMethod(string.Empty, typeof(int), new[] { typeof(FastExpressionCompiler.LightExpression.ExpressionCompiler.ArrayClosure), typeof(int) }, typeof(FastExpressionCompiler.LightExpression.ExpressionCompiler.ArrayClosure), true); + var il = dm.GetILGenerator(); + il.Emit(System.Reflection.Emit.OpCodes.Ldc_I4_1); + il.Emit(System.Reflection.Emit.OpCodes.Ret); + dm.CreateDelegate(typeof(Func), null); + } + }); + } + + /// All README examples and the complex benchmark expression (MemberInit, Convert, ArrayIndex, NewArrayInit) compile natively, no fallback. + public void Flat_compiles_all_the_readme_examples_natively() + { + var a = new ReadmeA(); + var b = new ReadmeB(); + + // hoisted: () => new X(a, b), the closure fields of the C# compiler display class + System.Linq.Expressions.Expression> hoisted = () => new ReadmeX(a, b); + var hFlat = hoisted.ToFlatExpression(); + var hf = hFlat.TryCompileFlat>(); + Ensure(hf != null, "hoisted: native"); + var hx = hf(); + Ensure(hx.A == a && hx.B == b, "hoisted: result"); + + // manually composed, System expression converted to flat + var bp = SysExpr.Parameter(typeof(ReadmeB), "b"); + var manual = SysExpr.Lambda>( + SysExpr.New(typeof(ReadmeX).GetConstructors()[0], SysExpr.Constant(a, typeof(ReadmeA)), bp), bp); + var mFlat = manual.ToFlatExpression(); + var mf = mFlat.TryCompileFlat>(); + Ensure(mf != null && mf(b).A == a && mf(b).B == b, "manual: native"); + + // the complex expression + var cFlat = CreateComplexFlatExpression(); + var cf = cFlat.TryCompileFlat>(); + Ensure(cf != null, "complex: native"); + var input = new object[12]; + for (var i = 0; i < input.Length; ++i) + input[i] = i + ""; + var cx = (A)cf(input); + Ensure(cx.Sop == "11" && cx.Prop != null && cx.Bop != null, "complex: result"); + Ensure(cf(input) != cx, "complex: a new object on each call"); + + // the same complex expression made of the System expression + var cSys = CreateComplexExpression().ToFlatExpression(); + var cf2 = cSys.TryCompileFlat>(); + Ensure(cf2 != null && ((A)cf2(input)).Sop == "11", "complex from System: native"); + + // the bad cast throws the same as the System does + input[11] = 42; + var threw = false; + try { cf(input); } catch (InvalidCastException) { threw = true; } + Ensure(threw, "complex: the bad cast throws"); + + // n => () => n + 1 + var fe = default(ExprTree); + var n = fe.ParameterOf("n"); + fe.RootIdx = fe.Lambda>>(fe.Lambda>(fe.Add(n, fe.ConstantInt(1))), n); + Ensure(fe.TryCompileFlat>>()?.Invoke(41)() == 42, "n => () => n + 1: native"); + + // the array of ints and bools, the index, the element type fits + var fa = default(ExprTree); + var arr = fa.ParameterOf("arr"); + fa.RootIdx = fa.Lambda>( + fa.Add(fa.ArrayIndex(arr, fa.ConstantInt(1)), fa.ArrayIndex(fa.NewArrayInit(typeof(int), fa.ConstantInt(5), fa.ConstantInt(6)), fa.ConstantInt(0))), arr); + Ensure(fa.TryCompileFlat>()?.Invoke(new[] { 1, 2 }) == 7, "int array index and init"); + + // the constant bool lambda is folded without the compile, FEC does the same with its Interpreter + var ff = default(ExprTree); + ff.RootIdx = ff.Lambda>(ff.MakeBinary(ExpressionType.AndAlso, + ff.MakeBinary(ExpressionType.GreaterThan, ff.MakeBinary(ExpressionType.Add, ff.ConstantInt(2), ff.ConstantInt(3)), ff.ConstantInt(4)), + ff.Not(ff.MakeBinary(ExpressionType.Equal, ff.ConstantInt(1), ff.ConstantInt(2))))); + var folded = ff.TryCompileFlat>(); + Ensure(folded != null && folded() && ReferenceEquals(folded, FastExpressionCompiler.LightExpression.ExpressionCompiler.Interpreter.TrueFunc), "the constant bool is folded"); + + // the instance method of the int needs the address: not emitted natively, the Light fallback does it + var fs = default(ExprTree); + var ns = fs.ParameterOf("n"); + fs.RootIdx = fs.Lambda>(fs.Call(ns, typeof(int).GetMethod("ToString", Type.EmptyTypes)), ns); + Ensure(fs.TryCompileFlat>() == null, "the value type instance is not native"); + Ensure(fs.CompileFast>()(5) == "5", "the value type instance via the fallback"); + } + + /// The generated closures (outer variables read and assigned by the nested lambda and by the outer one afterwards) + /// must compile natively and agree with System.Linq on the repeated calls, so the shared StrongBox and the by-value capture are checked. + public void Can_fuzz_nested_lambdas_with_captured_variables_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var lambda = GenerateClosureLambda(seed); + var fe = lambda.ToFlatExpression(); + var native = fe.TryCompileFlat>>(); + Ensure(native != null, "closure: native compile"); + var expected = lambda.Compile(); + for (var arg = -2; arg <= 2; ++arg) + { + var n = native(arg); + var e = expected(arg); + var n2 = native(arg); // the second instance has its own variables + var e2 = expected(arg); + for (var y = -1; y <= 3; ++y) + { + Ensure(n(y) == e(y), $"closure: result for {arg},{y}"); + Ensure(n2(y + 1) == e2(y + 1), $"closure: the second instance for {arg},{y}"); + } + } + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GenerateClosureLambda(seed)}"); + } + + // x => { int a = 0, b = 0; f = y => ; a = ; f } + private static System.Linq.Expressions.Expression>> GenerateClosureLambda(int seed) + { + var rnd = new Random(seed); + var x = SysExpr.Parameter(typeof(int), "x"); + var y = SysExpr.Parameter(typeof(int), "y"); + var a = SysExpr.Variable(typeof(int), "a"); + var b = SysExpr.Variable(typeof(int), "b"); + var f = SysExpr.Variable(typeof(Func), "f"); + + SysExpr Gen(int depth, SysExpr[] leaves) + { + if (depth == 0 || rnd.Next(4) == 0) + return rnd.Next(5) == 0 ? SysExpr.Constant(rnd.Next(-3, 4)) : leaves[rnd.Next(leaves.Length)]; + switch (rnd.Next(5)) + { + case 0: return SysExpr.Add(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 1: return SysExpr.Subtract(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 2: return SysExpr.Multiply(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 3: return SysExpr.Condition(SysExpr.GreaterThan(Gen(depth - 1, leaves), Gen(depth - 1, leaves)), Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + default: // assign the captured variable, the value is the result + return SysExpr.Assign(rnd.Next(2) == 0 ? a : b, Gen(depth - 1, leaves)); + } + } + + var inner = SysExpr.Lambda>(Gen(3, new SysExpr[] { x, y, a, b }), y); + var body = SysExpr.Block(typeof(Func), new[] { a, b, f }, + SysExpr.Assign(f, inner), + SysExpr.Assign(a, Gen(2, new SysExpr[] { x, a, b })), + f); + return SysExpr.Lambda>>(body, x); + } + + /// The generated New, Call, Field and Property, MemberInit, and the reference Conditional must compile natively and agree with System.Linq. + public void Can_fuzz_objects_and_members_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var lambda = GenerateObjectLambda(seed); + var fe = lambda.ToFlatExpression(); + var native = fe.TryCompileFlat>(); + Ensure(native != null, "objects: native compile"); + var expected = lambda.Compile(); + for (var k = -2; k <= 2; ++k) + { + var p = new FlatPoint(k * 3, k + 7); + var nr = native(p, k); + var er = expected(p, k); + Ensure(nr.X == er.X && nr.Y == er.Y, $"objects: result for {k}"); + } + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GenerateObjectLambda(seed)}"); + } + + private static System.Linq.Expressions.Expression> GenerateObjectLambda(int seed) + { + var rnd = new Random(seed); + var t = typeof(FlatPoint); + var ctor = t.GetConstructor(new[] { typeof(int), typeof(int) }); + var p = SysExpr.Parameter(t, "p"); + var k = SysExpr.Parameter(typeof(int), "k"); + var q = SysExpr.Variable(t, "q"); + var v = SysExpr.Variable(typeof(int), "v"); + + SysExpr Ref(int depth) + { + if (depth == 0 || rnd.Next(3) == 0) + return rnd.Next(2) == 0 ? p : q; + switch (rnd.Next(3)) + { + case 0: return SysExpr.Property(Ref(depth - 1), "Self"); + case 1: return SysExpr.New(ctor, Int(depth - 1), Int(depth - 1)); + default: return SysExpr.Condition(SysExpr.GreaterThan(Int(depth - 1), Int(depth - 1)), Ref(depth - 1), Ref(depth - 1)); + } + } + + SysExpr Int(int depth) + { + if (depth == 0 || rnd.Next(4) == 0) + return rnd.Next(3) == 0 ? SysExpr.Constant(rnd.Next(-3, 4)) : rnd.Next(2) == 0 ? k : v; + switch (rnd.Next(8)) + { + case 0: return SysExpr.Add(Int(depth - 1), Int(depth - 1)); + case 1: return SysExpr.Multiply(Int(depth - 1), Int(depth - 1)); + case 2: return SysExpr.Field(Ref(depth - 1), "X"); + case 3: return SysExpr.Property(Ref(depth - 1), "Y"); + case 4: return SysExpr.Call(t.GetMethod("Twice"), Int(depth - 1)); + case 5: return SysExpr.Call(Ref(depth - 1), t.GetMethod("Sum"), Int(depth - 1)); + case 6: return SysExpr.Assign(v, Int(depth - 1)); + default: return SysExpr.Condition(SysExpr.Equal(Int(depth - 1), Int(depth - 1)), Int(depth - 1), Int(depth - 1)); + } + } + + var result = rnd.Next(2) == 0 + ? SysExpr.MemberInit(SysExpr.New(ctor, Int(2), Int(2)), SysExpr.Bind(t.GetProperty("Y"), Int(2)), SysExpr.Bind(t.GetField("X"), Int(2))) + : (SysExpr)SysExpr.New(ctor, Int(3), Int(3)); + var body = SysExpr.Block(t, new[] { q, v }, + SysExpr.Assign(q, p), // q must not be null when Ref reads it + SysExpr.Assign(v, k), + SysExpr.Assign(q, Ref(2)), + SysExpr.Assign(v, Int(2)), + result); + return SysExpr.Lambda>(body, p, k); + } + + public class FlatPoint + { + public int X; + public int Y { get; set; } + public FlatPoint(int x, int y) { X = x; Y = y; } + public FlatPoint Self => this; + public static int Twice(int v) => v * 2; + public int Sum(int k) => X + Y + k; + } + + /// The native subset with the reference types: New, static and instance Call, Field and Property, the reference-typed + /// parameter, local and result; the same tree with a closure constant must fall back. + public void Flat_compile_handles_new_call_and_member_access_natively() + { + var t = typeof(FlatPoint); + var ctor = t.GetConstructor(new[] { typeof(int), typeof(int) }); + var fe = default(ExprTree); + var p = fe.ParameterOf("p"); + var k = fe.ParameterOf("k"); + var q = fe.Variable(t, "q"); + // (p, k) => { var q = new FlatPoint(1, 2); q = p; new FlatPoint(q.Self.X + Twice(k), q.Sum(k)) } + var body = fe.Block(t, new[] { q }, + fe.Assign(q, fe.New(ctor, fe.ConstantInt(1), fe.ConstantInt(2))), + fe.Assign(q, p), + fe.New(ctor, + fe.Add(fe.Field(fe.Property(q, t.GetProperty("Self")), t.GetField("X")), fe.Call(t.GetMethod("Twice"), k)), + fe.Call(q, t.GetMethod("Sum"), k))); + fe.RootIdx = fe.Lambda>(body, p, k); + + var native = fe.TryCompileFlat>(); + Ensure(native != null, "native compile of New/Call/Member"); + var r = native(new FlatPoint(10, 20), 3); + Ensure(r.X == 10 + 6 && r.Y == 10 + 20 + 3, "native result"); + + // closure constant, string and null constants are native too; the nested lambda is outside the subset + var fe2 = default(ExprTree); + var p2 = fe2.ParameterOf("p"); + var held = new FlatPoint(7, 8); + fe2.RootIdx = fe2.Lambda>( + fe2.NewArrayInit(typeof(object), fe2.Constant(held, t), fe2.Constant("s"), fe2.ConstantNull(t), p2), p2); + var arrFn = fe2.TryCompileFlat>(); + Ensure(arrFn != null, "the array init is native"); + var arrRes = arrFn(held); + Ensure(arrRes.Length == 4 && arrRes[0] == held && (string)arrRes[1] == "s" && arrRes[2] == null && arrRes[3] == held, "the array init result"); + + var fe3 = default(ExprTree); + var p3 = fe3.ParameterOf("p"); + fe3.RootIdx = fe3.Lambda>( + fe3.Add(fe3.Field(fe3.Constant(held, t), t.GetField("X")), + fe3.Call(t.GetMethod("Twice"), fe3.Property(fe3.Constant(held, t), t.GetProperty("Y")))), + p3, fe3.ParameterOf("unused")); + var native3 = fe3.TryCompileFlat>(); + Ensure(native3 != null && native3(null, null) == 7 + 16, "closure constant native"); + + var fe4 = default(ExprTree); + var p4 = fe4.ParameterOf("p"); + fe4.RootIdx = fe4.Lambda>(fe4.Constant(held, t), p4); + Ensure(ReferenceEquals(fe4.TryCompileFlat>()(null), held), "closure constant is the same instance"); + var fe5 = default(ExprTree); + var p5 = fe5.ParameterOf("p"); + fe5.RootIdx = fe5.Lambda>(fe5.Block(fe5.Constant("s"), fe5.Constant("str")), p5); + Ensure(fe5.TryCompileFlat>()(null) == "str", "string constant native"); + var fe6 = default(ExprTree); + var p6 = fe6.ParameterOf("p"); + fe6.RootIdx = fe6.Lambda>(fe6.ConstantNull(t), p6); + Ensure(fe6.TryCompileFlat>()(null) == null, "null constant native"); + } + + /// The nested lambdas: no capture, the captured parameter and block variable (the shared StrongBox, so the mutation is seen + /// both ways), the capture through the middle lambda, and the closure constants inside the nested lambda. + public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() + { + // n => () => n + 1, the README expression + var fe1 = default(ExprTree); + var n1 = fe1.ParameterOf("n"); + fe1.RootIdx = fe1.Lambda>>(fe1.Lambda>(fe1.Add(n1, fe1.ConstantInt(1))), n1); + var f1 = fe1.TryCompileFlat>>(); + Ensure(f1 != null && f1(41)() == 42, "readme: n => () => n + 1"); + + // x => y => x + y + var fe2 = default(ExprTree); + var x2 = fe2.ParameterOf("x"); + var y2 = fe2.ParameterOf("y"); + fe2.RootIdx = fe2.Lambda>>(fe2.Lambda>(fe2.Add(x2, y2), y2), x2); + var f2 = fe2.TryCompileFlat>>(); + var add3 = f2(3); + var add10 = f2(10); + Ensure(add3(4) == 7 && add10(4) == 14 && add3(1) == 4, "x => y => x + y"); + + // n => () => n = n + 1: every outer call has its own n, the nested calls share it + var fe3 = default(ExprTree); + var n3 = fe3.ParameterOf("n"); + fe3.RootIdx = fe3.Lambda>>( + fe3.Lambda>(fe3.Assign(n3, fe3.Add(n3, fe3.ConstantInt(1)))), n3); + var f3 = fe3.TryCompileFlat>>(); + var inc = f3(10); + var inc0 = f3(0); + Ensure(inc() == 11 && inc() == 12 && inc0() == 1 && inc() == 13, "mutated captured parameter"); + + // n => { var f = () => n; n = 5; f }: the lambda sees the later assignment + var fe4 = default(ExprTree); + var n4 = fe4.ParameterOf("n"); + var f4v = fe4.Variable(typeof(Func), "f"); + fe4.RootIdx = fe4.Lambda>>( + fe4.Block(typeof(Func), new[] { f4v }, + fe4.Assign(f4v, fe4.Lambda>(n4)), + fe4.Assign(n4, fe4.ConstantInt(5)), + f4v), + n4); + var f4 = fe4.TryCompileFlat>>(); + Ensure(f4 != null && f4(1)() == 5, "the lambda sees the assignment after its creation"); + + // a => () => () => a + 1: the middle lambda passes the capture through + var fe5 = default(ExprTree); + var a5 = fe5.ParameterOf("a"); + fe5.RootIdx = fe5.Lambda>>>( + fe5.Lambda>>(fe5.Lambda>(fe5.Add(a5, fe5.ConstantInt(1)))), a5); + var f5 = fe5.TryCompileFlat>>>(); + Ensure(f5 != null && f5(1)()() == 2, "the capture through the middle lambda"); + + // p => { var v = p * 2; () => v + p + Constant(held).X } + var held = new FlatPoint(7, 8); + var fe6 = default(ExprTree); + var p6 = fe6.ParameterOf("p"); + var v6 = fe6.Variable(typeof(int), "v"); + fe6.RootIdx = fe6.Lambda>>( + fe6.Block(typeof(Func), new[] { v6 }, + fe6.Assign(v6, fe6.MakeBinary(ExpressionType.Multiply, p6, fe6.ConstantInt(2))), + fe6.Lambda>( + fe6.Add(fe6.Add(v6, p6), fe6.Field(fe6.Constant(held, typeof(FlatPoint)), typeof(FlatPoint).GetField("X"))))), + p6); + var f6 = fe6.TryCompileFlat>>(); + Ensure(f6 != null && f6(5)() == 10 + 5 + 7, "the captured block variable and the closure constant in the nested lambda"); + + // the never-assigned reference-type parameter is captured by value + var fe8 = default(ExprTree); + var s8 = fe8.ParameterOf("s"); + fe8.RootIdx = fe8.Lambda>>(fe8.Lambda>(s8), s8); + var f8 = fe8.TryCompileFlat>>(); + Ensure(f8 != null && f8("abc")() == "abc", "captured string by value"); + + // x => (y, z) => x + y + z: the nested lambda with two parameters and a by-value capture + var fe9 = default(ExprTree); + var x9 = fe9.ParameterOf("x"); + var y9 = fe9.ParameterOf("y"); + var z9 = fe9.ParameterOf("z"); + fe9.RootIdx = fe9.Lambda>>( + fe9.Lambda>(fe9.Add(fe9.Add(x9, y9), z9), y9, z9), x9); + var f9 = fe9.TryCompileFlat>>(); + Ensure(f9 != null && f9(1)(10, 100) == 111, "nested lambda with two parameters"); + + // the custom delegate type of the nested lambda is outside the subset and falls back + var fe7 = default(ExprTree); + var n7 = fe7.ParameterOf("n"); + fe7.RootIdx = fe7.Lambda>(fe7.Lambda(n7), n7); + Ensure(fe7.TryCompileFlat>() == null, "custom delegate is outside the subset"); + Ensure(fe7.CompileFast>()(3)() == 3, "custom delegate falls back"); + } + + public delegate int FlatCustomDelegate(); + + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, + /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. + public void Can_fuzz_optional_children_presence_in_flat_expression() + { + Gen.Int[0, (1 << 10) - 1].Sample( + bits => CheckOptionalChildren(bits), + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: bits => System.Convert.ToString(bits, 2).PadLeft(10, '0')); + } + + private static bool CheckOptionalChildren(int bits) + { + var swDefault = (bits & 1) != 0; + var swCases = (bits >> 1 & 3) % 3; + if (!swDefault && swCases == 0) swCases = 1; // switch needs a default or at least one case + var hasFinally = (bits & 8) != 0; + var hasCatch = (bits & 16) != 0 || !hasFinally; // try needs a catch or a finally + var catchVar = hasCatch && (bits & 32) != 0; + var catchFilter = hasCatch && (bits & 64) != 0; + var loopBreak = (bits & 128) != 0; + var loopContinue = (bits & 256) != 0; + var gotoValue = (bits & 512) != 0; + var labelDefault = gotoValue; // a non-void label needs its default value + + var p = SysExpr.Parameter(typeof(int), "p"); + var r = SysExpr.Variable(typeof(int), "r"); + var t = SysExpr.Variable(typeof(int), "t"); + var i = SysExpr.Variable(typeof(int), "i"); + var g = SysExpr.Variable(typeof(int), "g"); + var fin = SysExpr.Variable(typeof(int), "fin"); + SysExpr Int(int x) => SysExpr.Constant(x); + + var cases = new SysSwitchCase[swCases]; + for (var k = 0; k < cases.Length; ++k) + cases[k] = SysExpr.SwitchCase(SysExpr.Assign(r, Int(10 + k)), Int(k)); + var @switch = SysExpr.Switch(typeof(void), p, swDefault ? SysExpr.Assign(r, Int(7)) : null, null, cases); + + var tryBody = SysExpr.Condition(SysExpr.LessThan(p, Int(0)), SysExpr.Throw(SysExpr.New(typeof(Exception)), typeof(int)), p); + var filter = catchFilter ? SysExpr.Equal(Int(1), Int(1)) : null; + var handlers = !hasCatch ? Array.Empty() : new[] + { + catchVar ? SysExpr.Catch(SysExpr.Variable(typeof(Exception), "ex"), Int(-1), filter) : SysExpr.Catch(typeof(Exception), Int(-1), filter) + }; + var @finally = SysExpr.Assign(fin, Int(1)); + var @try = !hasFinally ? SysExpr.TryCatch(tryBody, handlers) + : hasCatch ? SysExpr.TryCatchFinally(tryBody, @finally, handlers) + : SysExpr.TryFinally(tryBody, @finally); + + var brk = loopBreak ? SysExpr.Label("brk") : null; + var cont = loopContinue ? SysExpr.Label("cont") : null; + var exit = SysExpr.Label("exit"); + var loop = SysExpr.Block( + SysExpr.Loop(SysExpr.Block( + SysExpr.Assign(i, SysExpr.Add(i, Int(1))), + cont != null ? SysExpr.IfThen(SysExpr.LessThan(i, Int(2)), SysExpr.Continue(cont)) : SysExpr.Empty(), + SysExpr.IfThen(SysExpr.GreaterThanOrEqual(i, Int(3)), loopBreak ? SysExpr.Break(brk) : SysExpr.Goto(exit))), brk, cont), + SysExpr.Label(exit)); + + var l = SysExpr.Label(gotoValue ? typeof(int) : typeof(void), "l"); + var gotoLabel = gotoValue + ? SysExpr.Block(SysExpr.Goto(l, Int(5)), SysExpr.Label(l, labelDefault ? Int(9) : null)) + : SysExpr.Block(typeof(int), SysExpr.Goto(l), SysExpr.Label(l), Int(3)); + + var lambda = SysExpr.Lambda>( + SysExpr.Block(typeof(int), new[] { r, t, i, g, fin }, + @switch, SysExpr.Assign(t, @try), loop, SysExpr.Assign(g, gotoLabel), + SysExpr.Add(SysExpr.Add(r, t), SysExpr.Add(SysExpr.Add(i, g), fin))), + p); + + var fe = lambda.ToFlatExpression(); + var back2 = ((System.Linq.Expressions.Expression>)fe.ToExpression()).ToFlatExpression(); + Ensure(fe.Equals(back2), "structural roundtrip: Equals"); + Ensure(fe.GetHashCode() == back2.GetHashCode(), "structural roundtrip: GetHashCode"); + var light = fe.ToLightExpression(); + Ensure(fe.Equals(light.ToFlatExpression()), "flat->light->flat roundtrip"); + var viaSys = default(ExprTree); + LightToFlatViaSystem(ref viaSys, light); + Ensure(viaSys.Equals(light.ToFlatExpression()), "direct light->flat equals via-system"); + + int Count(Func match) + { + var n = 0; + for (var k = 1; k < fe.Nodes.Count; ++k) + if (match(fe.Nodes[k])) ++n; + return n; + } + bool Is(ExprNode n, ExpressionType type, int childCount) => + n.Kind == ExprNodeKind.Expression && n.NodeType == type && n.ChildCount == childCount; + + Ensure(Count(n => Is(n, ExpressionType.Switch, 1 + (swDefault ? 1 : 0) + (swCases > 0 ? 1 : 0))) == 1, "switch children"); + Ensure(Count(n => Is(n, ExpressionType.Loop, 1 + (loopBreak ? 1 : 0) + (loopContinue ? 1 : 0))) == 1, "loop children"); + Ensure(Count(n => Is(n, ExpressionType.Try, 1 + (hasFinally ? 1 : 0) + (hasCatch ? 1 : 0))) == 1, "try children"); + Ensure(Count(n => n.Kind == ExprNodeKind.CatchBlock && n.ChildCount == 1 + (catchVar ? 1 : 0) + (catchFilter ? 1 : 0)) == (hasCatch ? 1 : 0), "catch children"); + Ensure(Count(n => Is(n, ExpressionType.Goto, 2)) == (gotoValue ? 1 : 0), "goto value"); + Ensure(Count(n => Is(n, ExpressionType.Label, 2)) == (labelDefault ? 1 : 0), "label default"); + + if (catchFilter) return true; // FEC does not support catch filters, the structure is checked above + + var fast = fe.ToLightExpression() is FastExpressionCompiler.LightExpression.LambdaExpression fl + ? fl.CompileFast>(ifFastFailedReturnNull: true) : null; + Ensure(fast != null, "CompileFast failed"); + var expected = lambda.Compile(); + // an uncaught throw (no catch) must happen in both + static int Run(Func f, int arg) { try { return f(arg); } catch (Exception) { return int.MinValue; } } + foreach (var arg in new[] { -1, 0, 1, 2, 5 }) + Ensure(Run(fast, arg) == Run(expected, arg), $"result for {arg}"); + return true; + } + + private static void Ensure(bool ok, string what) + { + if (!ok) throw new InvalidOperationException("Property failed: " + what); + } + private static FastExpressionCompiler.LightExpression.Expression> CreateGeneratedLightExpression(IntSpec spec) { var parameter = ParameterOf("p"); @@ -310,5 +996,15 @@ private bool EqBlock(FastExpressionCompiler.LightExpression.BlockExpression x, F } #else public void Can_property_test_generated_flat_expression_roundtrip_structurally() { } + public void Can_fuzz_optional_children_presence_in_flat_expression() { } + public void Can_compile_generated_flat_expression_natively() { } + public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } + public void Experiment_compare_flat_and_light_compile_phases() { } + public void Flat_compile_handles_new_call_and_member_access_natively() { } + public void Flat_compiles_all_the_readme_examples_natively() { } + public void Can_fuzz_nested_lambdas_with_captured_variables_natively() { } + public void Can_fuzz_objects_and_members_natively() { } + public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() { } + public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index f3636675..64692bc2 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -37,28 +37,24 @@ public int Run() Can_build_flat_expression_directly_with_light_expression_like_api(); Can_build_flat_expression_control_flow_directly(); Can_property_test_generated_flat_expression_roundtrip_structurally(); + Can_fuzz_optional_children_presence_in_flat_expression(); + Can_compile_generated_flat_expression_natively(); + Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); + Flat_compile_handles_new_call_and_member_access_natively(); + Flat_compiles_all_the_readme_examples_natively(); + Can_fuzz_nested_lambdas_with_captured_variables_natively(); + Can_fuzz_objects_and_members_natively(); + Flat_compile_handles_nested_lambdas_and_captured_variables_natively(); + Experiment_compare_flat_and_light_compile_phases(); + Flat_compiles_the_readme_expressions_and_compares_the_time(); Flat_lambda_parameter_ref_before_decl_preserves_identity(); Flat_lambda_multiple_parameter_refs_all_yield_same_identity(); Flat_block_variables_and_refs_yield_same_identity(); Flat_nested_lambda_captures_outer_parameter_identity(); - Flat_lambda_closure_parameter_usages_track_captured_outer_parameter_during_direct_construction(); - Flat_lambda_closure_parameter_usages_propagate_across_nested_lambdas_during_direct_construction(); - Flat_lambda_closure_parameter_usages_track_captured_block_variable_during_direct_construction(); - Flat_lambda_closure_parameter_usages_track_captures_from_expression_conversion(); Flat_out_of_order_decl_block_in_lambda_compiles_correctly(); Flat_enum_constant_stored_inline_roundtrip(); Flat_metadata_uses_16_bit_idx_storage(); - Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction(); - Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction(); - Flat_lambda_nodes_tracks_lambdas_from_expression_conversion(); - Flat_lambda_nodes_has_single_entry_for_root_only_lambda(); - Flat_blocks_with_variables_tracked_during_direct_construction(); - Flat_goto_and_label_nodes_tracked_during_direct_construction(); - Flat_try_catch_nodes_tracked_during_direct_construction(); - Flat_blocks_with_variables_tracked_from_expression_conversion(); - Flat_goto_and_label_nodes_tracked_from_expression_conversion(); - Flat_try_catch_nodes_tracked_from_expression_conversion(); Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal(); Flat_equal_nested_lambdas_with_captures_are_structurally_equal_and_hash_equal(); Flat_standalone_parameters_use_name_in_structural_equality(); @@ -1021,80 +1017,6 @@ public void Flat_nested_lambda_captures_outer_parameter_identity() Asserts.AreSame(sysOuter.Parameters[0], sysInner.Body); } - public void Flat_lambda_closure_parameter_usages_track_captured_outer_parameter_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - var inner = fe.Lambda>(x); - fe.RootIdx = fe.Lambda>>(inner, x); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); - Asserts.AreEqual(fe.Nodes[x].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - } - - public void Flat_lambda_closure_parameter_usages_propagate_across_nested_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - var inner = fe.Lambda>(x); - var middle = fe.Lambda>>(inner); - fe.RootIdx = fe.Lambda>>>(middle, x); - - Asserts.AreEqual(2, fe.LambdaClosureParameterUsages.Count); - - var foundInner = false; - var foundMiddle = false; - for (var i = 0; i < fe.LambdaClosureParameterUsages.Count; ++i) - { - ref var usage = ref fe.LambdaClosureParameterUsages[i]; - Asserts.AreEqual(fe.Nodes[x].ChildIdx, usage.ParameterId); - if (usage.LambdaIdx == inner) foundInner = true; - if (usage.LambdaIdx == middle) foundMiddle = true; - } - - Asserts.IsTrue(foundInner); - Asserts.IsTrue(foundMiddle); - } - - public void Flat_lambda_closure_parameter_usages_track_captured_block_variable_during_direct_construction() - { - var fe = default(ExprTree); - var v = fe.Variable(typeof(int), "v"); - var inner = fe.Lambda>(v); - fe.RootIdx = fe.Lambda>>( - fe.Block(typeof(Func), new[] { v }, - fe.Assign(v, fe.ConstantInt(42)), - inner)); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); - Asserts.AreEqual(fe.Nodes[v].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - } - - public void Flat_lambda_closure_parameter_usages_track_captures_from_expression_conversion() - { - var x = SysExpr.Parameter(typeof(int), "x"); - var sysLambda = SysExpr.Lambda>>( - SysExpr.Lambda>(x), - x); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(fe.Nodes[fe.LambdaClosureParameterUsages[0].ParameterIdx].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - - var nestedLambdaCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; ++i) - if (fe.LambdaNodes[i] != fe.RootIdx) - { - ++nestedLambdaCount; - Asserts.AreEqual(fe.LambdaNodes[i], fe.LambdaClosureParameterUsages[0].LambdaIdx); - } - - Asserts.AreEqual(1, nestedLambdaCount); - } - /// /// End-to-end compile-and-run test with a block containing two variables, /// verifying that out-of-order parameter decls and variable refs produce @@ -1160,7 +1082,6 @@ void Check(TEnum enumValue) where TEnum : Enum public void Flat_metadata_uses_16_bit_idx_storage() { Asserts.AreEqual(24, typeof(ExprNode).StructLayoutAttribute.Size); - Asserts.AreEqual(6, typeof(LambdaClosureParameterUsage).StructLayoutAttribute.Size); var fe = default(ExprTree); // Index 0 is reserved as the absent-child sentinel, so MaxValue-1 real nodes fill storage. @@ -1171,241 +1092,6 @@ public void Flat_metadata_uses_16_bit_idx_storage() Asserts.Throws(() => fe.ParameterOf("overflow")); } - /// - /// When building a flat expression directly, calling Lambda() for a nested lambda - /// and then for the root lambda should result in both indices recorded in LambdaNodes. - /// The root is identified by RootIdx; all others are nested. - /// - public void Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - - // Build: outer: x => () => x - var inner = fe.Lambda>(x); - fe.RootIdx = fe.Lambda>>(inner, x); - - // Both the root and nested lambda indices should be recorded - Asserts.AreEqual(2, fe.LambdaNodes.Count); - - // Check that inner and root are both in LambdaNodes - var foundInner = false; - var foundRoot = false; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - { - if (fe.LambdaNodes[i] == inner) foundInner = true; - if (fe.LambdaNodes[i] == fe.RootIdx) foundRoot = true; - } - Asserts.IsTrue(foundInner); - Asserts.IsTrue(foundRoot); - - // Nested lambdas are all LambdaNodes entries that are not the root - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(1, nestedCount); - } - - /// - /// When building a flat expression with multiple levels of nesting, - /// all lambda node indices are captured in LambdaNodes. - /// - public void Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - - // Build: outer: x => (() => (() => x)) - var innermost = fe.Lambda>(x); - var middle = fe.Lambda>>(innermost); - fe.RootIdx = fe.Lambda>>>(middle, x); - - // All three lambda nodes should be recorded - Asserts.AreEqual(3, fe.LambdaNodes.Count); - - // Count nested (non-root) lambdas - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(2, nestedCount); - } - - /// - /// When converting a System.Linq expression tree with nested lambdas via FromExpression, - /// the resulting ExprTree should have all lambda indices populated in LambdaNodes. - /// - public void Flat_lambda_nodes_tracks_lambdas_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - // Build: p => () => p using System.Linq.Expressions - var sysLambda = SysExpr.Lambda>>( - SysExpr.Lambda>(p), - p); - - var fe = sysLambda.ToFlatExpression(); - - // Both root and nested lambda indices should be recorded - Asserts.AreEqual(2, fe.LambdaNodes.Count); - - // The root lambda must be in the list - var foundRoot = false; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] == fe.RootIdx) { foundRoot = true; break; } - Asserts.IsTrue(foundRoot); - - // Exactly one nested lambda - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(1, nestedCount); - } - - /// - /// A flat expression with no nested lambdas (root-only) should have exactly one - /// entry in LambdaNodes (the root itself). - /// - public void Flat_lambda_nodes_has_single_entry_for_root_only_lambda() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - fe.RootIdx = fe.Lambda>(fe.Add(p, fe.ConstantInt(1)), p); - - Asserts.AreEqual(1, fe.LambdaNodes.Count); - Asserts.AreEqual(fe.RootIdx, fe.LambdaNodes[0]); - } - - /// - /// Block nodes with explicit variable declarations are recorded in BlocksWithVariables; - /// blocks without variables produce no entry. - /// - public void Flat_blocks_with_variables_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - var v = fe.Variable(typeof(int), "v"); - - // Block with one variable: should be tracked - var blockWithVar = fe.Block(typeof(int), new[] { v }, fe.Assign(v, p), v); - // Block without variables: should NOT be tracked - var blockNoVar = fe.Block(fe.Add(p, fe.ConstantInt(1))); - - fe.RootIdx = fe.Lambda>(fe.Block(blockWithVar, blockNoVar), p); - - Asserts.AreEqual(1, fe.BlocksWithVariables.Count); - Asserts.AreEqual(blockWithVar, fe.BlocksWithVariables[0]); - } - - /// - /// Goto and label expression nodes are recorded in GotoNodes and LabelNodes respectively. - /// - public void Flat_goto_and_label_nodes_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - var target = fe.Label(typeof(int), "done"); - - var gotoNode = fe.Goto(target, p, typeof(int)); - var labelNode = fe.Label(target, fe.ConstantInt(0)); - - fe.RootIdx = fe.Lambda>(fe.Block(gotoNode, labelNode), p); - - Asserts.AreEqual(1, fe.GotoNodes.Count); - Asserts.AreEqual(gotoNode, fe.GotoNodes[0]); - - Asserts.AreEqual(1, fe.LabelNodes.Count); - Asserts.AreEqual(labelNode, fe.LabelNodes[0]); - } - - /// - /// Try/catch, try/finally and try/fault node indices are all recorded in TryCatchNodes. - /// - public void Flat_try_catch_nodes_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - - var tryCatchNode = fe.TryCatch( - fe.Add(p, fe.ConstantInt(1)), - fe.Catch(typeof(Exception), fe.ConstantInt(-1))); - - var tryFinallyNode = fe.TryFinally( - fe.Add(p, fe.ConstantInt(2)), - fe.Default(typeof(void))); - - fe.RootIdx = fe.Lambda>( - fe.Block(tryCatchNode, tryFinallyNode), p); - - Asserts.AreEqual(2, fe.TryCatchNodes.Count); - - var foundTryCatch = false; - var foundTryFinally = false; - for (var i = 0; i < fe.TryCatchNodes.Count; i++) - { - if (fe.TryCatchNodes[i] == tryCatchNode) foundTryCatch = true; - if (fe.TryCatchNodes[i] == tryFinallyNode) foundTryFinally = true; - } - Asserts.IsTrue(foundTryCatch); - Asserts.IsTrue(foundTryFinally); - } - - /// - /// When converting a System.Linq expression tree, blocks with variables are - /// recorded in BlocksWithVariables; plain blocks are not. - /// - public void Flat_blocks_with_variables_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var v = SysExpr.Variable(typeof(int), "v"); - // block with variable - var sysBlock = SysExpr.Block(new[] { v }, SysExpr.Assign(v, p), v); - var sysLambda = SysExpr.Lambda>(sysBlock, p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.BlocksWithVariables.Count); - } - - /// - /// When converting a System.Linq expression tree with goto/label, both - /// GotoNodes and LabelNodes are populated. - /// - public void Flat_goto_and_label_nodes_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var target = SysExpr.Label(typeof(int), "done"); - var sysLambda = SysExpr.Lambda>( - SysExpr.Block( - SysExpr.Goto(target, p, typeof(int)), - SysExpr.Label(target, SysExpr.Constant(0))), - p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.GotoNodes.Count); - Asserts.AreEqual(1, fe.LabelNodes.Count); - } - - /// - /// When converting a System.Linq expression tree with a try/catch, - /// TryCatchNodes is populated. - /// - public void Flat_try_catch_nodes_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var sysLambda = SysExpr.Lambda>( - SysExpr.TryCatch( - SysExpr.Add(p, SysExpr.Constant(1)), - SysExpr.Catch(typeof(Exception), SysExpr.Constant(-1))), - p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.TryCatchNodes.Count); - } - public void Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal() { var x = Parameter(typeof(int), "x");