11using System ;
22using System . Buffers ;
33using System . Diagnostics ;
4+ using System . Runtime . CompilerServices ;
45using System . Threading . Tasks ;
56using RESPite . Messages ;
67
@@ -15,12 +16,43 @@ public RedisValue ReadRedisValue()
1516 reader . DemandScalar ( ) ;
1617 if ( reader . IsNull ) return RedisValue . Null ;
1718
18- return reader . Prefix switch
19+ switch ( reader . Prefix )
1920 {
20- RespPrefix . Boolean => reader . ReadBoolean ( ) ,
21- RespPrefix . Integer => reader . ReadInt64 ( ) ,
22- _ => reader . ReadByteArray ( ) ,
23- } ;
21+ case RespPrefix . Boolean :
22+ return reader . ReadBoolean ( ) ;
23+ case RespPrefix . Integer :
24+ return reader . ReadInt64 ( ) ;
25+ }
26+
27+ // bulk/simple/verbatim string. Only inline (non-streaming) scalars get the compact storage
28+ // kinds; streaming scalars fall through to ReadByteArray.
29+ if ( reader . IsInlineScalar )
30+ {
31+ var length = reader . ScalarLength ( ) ;
32+
33+ // Short payloads (<= 8 bytes) pack inline as a short-blob: allocation-free, and with *no*
34+ // eager numeric parse - any later (long)/(double)/etc. is deferred to the caller (Simplify
35+ // on demand), which is cheaper for the common case of values never interpreted as numbers.
36+ // Contiguous data (the common case) is taken straight from TryGetSpan - no stackalloc. Only a
37+ // scalar that straddles segments needs linearizing into the 8-byte stack buffer; the length
38+ // guard is what makes that fixed buffer safe, since Buffer() silently truncates an over-long
39+ // discontiguous payload.
40+ if ( length <= RedisValue . MaxInlineBytes )
41+ {
42+ return RedisValue . FromRaw ( reader . TryGetSpan ( out var buffer ) ?
43+ buffer : reader . Buffer ( stackalloc byte [ RedisValue . MaxInlineBytes ] ) ) ;
44+ }
45+
46+ // Longer payloads: prefer a compact numeric storage kind when the text is the *canonical*
47+ // representation of that number, so every projection (ToString, (byte[]), equality, hash)
48+ // still round-trips byte-for-byte; this also avoids the byte[] alloc. Canonical parsing needs
49+ // a contiguous span, so a discontiguous payload falls through to ReadByteArray.
50+ if ( reader . TryGetSpan ( out var span ) && TryReadCanonicalNumber ( span , out var number ) )
51+ {
52+ return number ;
53+ }
54+ }
55+ return reader . ReadByteArray ( ) ;
2456 }
2557
2658 public string DebugReadTruncatedString ( int maxChars )
@@ -184,7 +216,7 @@ public static RespPrefix GetRespPrefix(ReadOnlySpan<byte> frame)
184216 RespPrefix . SimpleError => ResultType . Error ,
185217 RespPrefix . Null => ResultType . Null ,
186218 RespPrefix . VerbatimString => ResultType . VerbatimString ,
187- RespPrefix . Push => ResultType . Push ,
219+ RespPrefix . Push => ResultType . Push ,
188220 _ => throw new ArgumentOutOfRangeException ( nameof ( prefix ) , prefix , null ) ,
189221 } ;
190222 }
@@ -208,4 +240,92 @@ internal bool AnyNull()
208240 public bool IsCompletedSuccessfully => task. Status is TaskStatus. RanToCompletion;
209241 }
210242#endif
243+
244+ private static readonly int MaxCanonicalLength = Math . Max ( Format . MaxInt64TextLen , Format . MaxDoubleTextLen ) ;
245+
246+ // Recognizes the canonical decimal text of an integer (signed Int64 or, for non-negative values up to
247+ // ulong.MaxValue, UInt64) or a finite double, returning a numeric-backed RedisValue only when re-formatting
248+ // the parsed value reproduces the exact input bytes. This keeps the optimization invisible to callers:
249+ // non-canonical spellings ("01234", "+5", "1.50", "1e3"), values beyond the ulong/double range, and the
250+ // special inf/nan tokens all return false and are kept as a byte[] payload by the caller.
251+ private static bool TryReadCanonicalNumber ( ReadOnlySpan < byte > span , out RedisValue value )
252+ {
253+ static bool Failure ( out RedisValue value )
254+ {
255+ value = default ;
256+ return false ;
257+ }
258+ // integer: canonical exactly when the round-trip text length matches (rules out leading zeros, a
259+ // leading '+', "-0", trailing junk, etc.) - so no need to re-emit and compare bytes
260+ if ( span . IsEmpty | span . Length > MaxCanonicalLength ) return Failure ( out value ) ;
261+
262+ // restrict to *just* basic number tokens, tracking the two facts that let us pick a single parse:
263+ // whether a '-' appeared (so we know whether the integer is signed), and whether a '.'/'e'/'E'
264+ // appeared (which rules out an integer entirely - only a double can be canonical)
265+ bool seenNegative = false , seenDotOrExp = false ;
266+ foreach ( var b in span )
267+ {
268+ switch ( b )
269+ {
270+ case ( byte ) '0' :
271+ case ( byte ) '1' :
272+ case ( byte ) '2' :
273+ case ( byte ) '3' :
274+ case ( byte ) '4' :
275+ case ( byte ) '5' :
276+ case ( byte ) '6' :
277+ case ( byte ) '7' :
278+ case ( byte ) '8' :
279+ case ( byte ) '9' :
280+ break ;
281+ case ( byte ) '-' :
282+ seenNegative = true ;
283+ break ;
284+ case ( byte ) '.' :
285+ case ( byte ) 'E' :
286+ case ( byte ) 'e' :
287+ seenDotOrExp = true ;
288+ break ;
289+ default :
290+ return Failure ( out value ) ;
291+ }
292+ }
293+
294+ if ( ! seenDotOrExp )
295+ {
296+ // pure integer text. For a non-negative value parse as *unsigned* so the full ulong range is
297+ // covered; the RedisValue(ulong) ctor demotes to Int64 storage when the value fits, so smaller
298+ // values still land as Int64 exactly as before. A negative value can only be Int64.
299+ if ( seenNegative )
300+ {
301+ if ( Format . TryParseInt64 ( span , out var i64 ) && Format . MeasureInt64 ( i64 ) == span . Length )
302+ {
303+ value = i64 ;
304+ return true ;
305+ }
306+ }
307+ else if ( Format . TryParseUInt64 ( span , out var u64 ) && Format . MeasureUInt64 ( u64 ) == span . Length )
308+ {
309+ value = u64 ;
310+ return true ;
311+ }
312+
313+ // note that all-digit text which isn't a canonical integer (oversize, leading zero, etc.) can't
314+ // be a canonical double either - any such value re-formats with an exponent - so we simply fall
315+ // through to the failure at the bottom rather than attempting a (futile) double parse
316+ }
317+ else if ( Format . TryParseDouble ( span , out var dbl ) )
318+ {
319+ if ( dbl == 0.0 ) dbl = Math . Abs ( dbl ) ; // prevent problems with -0 formatting
320+ Span < byte > formatted = stackalloc byte [ Format . MaxDoubleTextLen ] ;
321+ var len = Format . FormatDouble ( dbl , formatted ) ;
322+ if ( formatted . Slice ( 0 , len ) . SequenceEqual ( span ) )
323+ {
324+ value = dbl ;
325+ return true ;
326+ }
327+ }
328+
329+ return Failure ( out value ) ;
330+ }
211331}
0 commit comments