@@ -65,7 +65,13 @@ public struct Idx : IEquatable<Idx>
6565/// <summary>
6666/// Fat node in <see cref="ExpressionTree.Nodes"/>. Intrusive linked-list tree encoding:
6767/// <list type="table">
68- /// <item><term>Constant</term> <description>Info = boxed value; ConstantIndex ≥ 0 → value lives in ClosureConstants instead.</description></item>
68+ /// <item><term>Constant</term>
69+ /// <description>
70+ /// ExtraIdx.It == 0 (nil): value is in Info (boxed, or null for null constant).<br/>
71+ /// ExtraIdx.It > 0: value is ClosureConstants[ExtraIdx.It - 1] (1-based).<br/>
72+ /// ExtraIdx.It == -1: int32-fitting value (bool/byte/int/float/…) stored inline in ChildIdx.It bits — no boxing.
73+ /// </description>
74+ /// </item>
6975/// <item><term>Parameter</term> <description>Info = name (string or null).</description></item>
7076/// <item><term>Unary</term> <description>Info = MethodInfo (nullable), ChildIdx = operand.</description></item>
7177/// <item><term>Binary</term> <description>Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right.</description></item>
@@ -75,17 +81,25 @@ public struct Idx : IEquatable<Idx>
7581/// <item><term>Block</term> <description>ChildIdx = first expr, ExtraIdx = first variable (both chained via NextIdx).</description></item>
7682/// <item><term>Conditional</term><description>ChildIdx = test, ExtraIdx = ifTrue; ifFalse = ifTrue.NextIdx.</description></item>
7783/// </list>
84+ /// <para>
85+ /// Layout: 32 bytes on 64-bit (refs first eliminates 4-byte padding after NodeType).<br/>
86+ /// vs LightExpression heap objects (16-byte header + fields):<br/>
87+ /// Constant/Parameter: ~40 bytes heap | Binary/Unary: ~48–56 bytes heap
88+ /// </para>
7889/// </summary>
7990[ StructLayout ( LayoutKind . Sequential ) ]
80- public struct ExpressionNode
91+ public struct ExpressionNode // 32 bytes: Type(8)+Info(8)+NodeType(4)+NextIdx(4)+ChildIdx(4)+ExtraIdx(4)
8192{
82- public ExpressionType NodeType ;
93+ // Reference fields placed first to avoid 4-byte padding that would appear after NodeType.
8394 public Type Type ;
8495 public object Info ;
85- /// <summary>≥ 0: index into <see cref="ExpressionTree.ClosureConstants"/>. -1: value is inline in Info.</summary>
86- public int ConstantIndex ;
96+ public ExpressionType NodeType ;
8797 public Idx NextIdx ;
98+ /// <summary>First child node, or for Constant with ExtraIdx.It==-1: raw int32 value bits.</summary>
8899 public Idx ChildIdx ;
100+ /// <summary>
101+ /// Second child node; for Constant: 0=value in Info, positive=ClosureConstants index (1-based), -1=inline bits in ChildIdx.It.
102+ /// </summary>
89103 public Idx ExtraIdx ;
90104}
91105
@@ -116,40 +130,81 @@ private Idx AddNode(
116130 ExpressionType nodeType ,
117131 Type type ,
118132 object info = null ,
119- int constantIndex = - 1 ,
120133 Idx childIdx = default ,
121134 Idx extraIdx = default )
122135 {
123136 ref var n = ref Nodes . AddDefaultAndGetRef ( ) ;
124137 n . NodeType = nodeType ;
125138 n . Type = type ;
126139 n . Info = info ;
127- n . ConstantIndex = constantIndex ;
128140 n . ChildIdx = childIdx ;
129141 n . ExtraIdx = extraIdx ;
130142 n . NextIdx = Idx . Nil ;
131143 return Idx . Of ( Nodes . Count ) ; // Count already incremented by AddDefaultAndGetRef
132144 }
133145
134- // Primitives with stable identity — safe to keep inline (ConstantIndex == -1).
135- private static bool IsInlineable ( Type t ) =>
136- t == typeof ( int ) || t == typeof ( long ) || t == typeof ( double ) || t == typeof ( float ) ||
137- t == typeof ( bool ) || t == typeof ( string ) || t == typeof ( char ) ||
138- t == typeof ( byte ) || t == typeof ( short ) || t == typeof ( decimal ) ||
139- t == typeof ( DateTime ) || t == typeof ( Guid ) ;
146+ // Types whose value fits in 32 bits — stored inline in ChildIdx.It to avoid boxing.
147+ private static bool FitsInInt32 ( Type t ) =>
148+ t == typeof ( int ) || t == typeof ( uint ) || t == typeof ( bool ) || t == typeof ( float ) ||
149+ t == typeof ( byte ) || t == typeof ( sbyte ) || t == typeof ( short ) || t == typeof ( ushort ) ||
150+ t == typeof ( char ) ;
151+
152+ // Encode an inline value as its int32 bit pattern (only call when FitsInInt32 is true).
153+ private static int ToInt32Bits ( object value , Type t )
154+ {
155+ if ( t == typeof ( int ) ) return ( int ) value ;
156+ if ( t == typeof ( uint ) ) return ( int ) ( uint ) value ; // reinterpret bits
157+ if ( t == typeof ( bool ) ) return ( bool ) value ? 1 : 0 ;
158+ if ( t == typeof ( float ) ) return BitConverter . SingleToInt32Bits ( ( float ) value ) ;
159+ if ( t == typeof ( byte ) ) return ( byte ) value ;
160+ if ( t == typeof ( sbyte ) ) return ( sbyte ) value ;
161+ if ( t == typeof ( short ) ) return ( short ) value ;
162+ if ( t == typeof ( ushort ) ) return ( ushort ) value ;
163+ if ( t == typeof ( char ) ) return ( char ) value ;
164+ return 0 ; // unreachable
165+ }
166+
167+ // Decode int32 bit pattern back to a boxed value (only call when FitsInInt32 is true).
168+ internal static object FromInt32Bits ( int bits , Type t )
169+ {
170+ if ( t == typeof ( int ) ) return bits ;
171+ if ( t == typeof ( uint ) ) return ( uint ) bits ;
172+ if ( t == typeof ( bool ) ) return bits != 0 ;
173+ if ( t == typeof ( float ) ) return BitConverter . Int32BitsToSingle ( bits ) ;
174+ if ( t == typeof ( byte ) ) return ( byte ) bits ;
175+ if ( t == typeof ( sbyte ) ) return ( sbyte ) bits ;
176+ if ( t == typeof ( short ) ) return ( short ) bits ;
177+ if ( t == typeof ( ushort ) ) return ( ushort ) bits ;
178+ if ( t == typeof ( char ) ) return ( char ) bits ;
179+ return null ; // unreachable
180+ }
181+
182+ // Types not fitting in int32 but still safe to keep inline in Info (no special closure treatment needed).
183+ private static bool IsInfoInline ( Type t ) =>
184+ t == typeof ( string ) || t == typeof ( long ) || t == typeof ( double ) ||
185+ t == typeof ( decimal ) || t == typeof ( DateTime ) || t == typeof ( Guid ) ;
140186
141187 public Idx Constant ( object value , bool putIntoClosure = false )
142188 {
143189 if ( value == null )
144- return AddNode ( ExpressionType . Constant , typeof ( object ) , null ) ;
190+ return AddNode ( ExpressionType . Constant , typeof ( object ) ) ;
145191
146192 var type = value . GetType ( ) ;
147- if ( ! putIntoClosure && IsInlineable ( type ) )
148- return AddNode ( ExpressionType . Constant , type , value , constantIndex : - 1 ) ;
193+ if ( ! putIntoClosure )
194+ {
195+ if ( FitsInInt32 ( type ) )
196+ // ExtraIdx.It == -1 is the "inline bits" sentinel; ChildIdx.It holds the value.
197+ return AddNode ( ExpressionType . Constant , type ,
198+ childIdx : new Idx { It = ToInt32Bits ( value , type ) } ,
199+ extraIdx : new Idx { It = - 1 } ) ;
200+ if ( IsInfoInline ( type ) )
201+ return AddNode ( ExpressionType . Constant , type , info : value ) ;
202+ }
149203
150204 var ci = ClosureConstants . Count ;
151205 ClosureConstants . Add ( value ) ;
152- return AddNode ( ExpressionType . Constant , type , null , constantIndex : ci ) ;
206+ // ExtraIdx.It > 0 (1-based) identifies the closure constant slot.
207+ return AddNode ( ExpressionType . Constant , type , extraIdx : new Idx { It = ci + 1 } ) ;
153208 }
154209
155210 public Idx Constant < T > ( T value , bool putIntoClosure = false ) =>
@@ -271,9 +326,13 @@ private SysExpr ToSystemExpression(Idx nodeIdx, ref SmallMap16<int, SysParam, In
271326 {
272327 case ExpressionType . Constant :
273328 {
274- var value = node . ConstantIndex >= 0
275- ? ClosureConstants . GetSurePresentRef ( node . ConstantIndex )
276- : node . Info ;
329+ object value ;
330+ if ( node . ExtraIdx . It > 0 )
331+ value = ClosureConstants . GetSurePresentRef ( node . ExtraIdx . It - 1 ) ;
332+ else if ( node . ExtraIdx . It == - 1 )
333+ value = FromInt32Bits ( node . ChildIdx . It , node . Type ) ;
334+ else
335+ value = node . Info ;
277336 return SysExpr . Constant ( value , node . Type ) ;
278337 }
279338
@@ -411,7 +470,6 @@ public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b)
411470 if ( na . NodeType != nb . NodeType ) return false ;
412471 if ( na . Type != nb . Type ) return false ;
413472 if ( ! InfoEqual ( na . Info , nb . Info ) ) return false ;
414- if ( na . ConstantIndex != nb . ConstantIndex ) return false ;
415473 if ( na . NextIdx . It != nb . NextIdx . It ) return false ;
416474 if ( na . ChildIdx . It != nb . ChildIdx . It ) return false ;
417475 if ( na . ExtraIdx . It != nb . ExtraIdx . It ) return false ;
@@ -443,10 +501,14 @@ public string Dump()
443501 for ( var i = 0 ; i < NodeCount ; i ++ )
444502 {
445503 ref var n = ref Nodes . GetSurePresentRef ( i ) ;
504+ var constStr = n . NodeType == ExpressionType . Constant
505+ ? ( n . ExtraIdx . It > 0 ? $ "closure[{ n . ExtraIdx . It - 1 } ]" :
506+ n . ExtraIdx . It == - 1 ? $ "inline:{ FromInt32Bits ( n . ChildIdx . It , n . Type ) } " :
507+ $ "info:{ n . Info } ")
508+ : null ;
446509 sb . AppendLine (
447510 $ " [{ i + 1 } ] { n . NodeType , - 22 } type={ n . Type ? . Name , - 14 } " +
448- $ "info={ InfoStr ( n . Info ) , - 30 } " +
449- $ "ci={ n . ConstantIndex , 2 } " +
511+ $ "{ ( constStr != null ? $ "val={ constStr , - 28 } " : $ "info={ InfoStr ( n . Info ) , - 28 } ") } " +
450512 $ "child={ n . ChildIdx } extra={ n . ExtraIdx } next={ n . NextIdx } ") ;
451513 }
452514 if ( ClosureConstants . Count > 0 )
0 commit comments