Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal struct BitStack

private int _currentDepth;

public int CurrentDepth => _currentDepth;
public readonly int CurrentDepth => _currentDepth;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PushTrue()
Expand Down Expand Up @@ -110,13 +110,13 @@ public bool Pop()
}
else
{
inObject = PopFromArray();
inObject = PeekInArray();
}
return inObject;
}

[MethodImpl(MethodImplOptions.NoInlining)]
private bool PopFromArray()
private readonly bool PeekInArray()
Copy link
Member

@eiriktsarpalis eiriktsarpalis Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we peeking at in this case? It seems we're just returning a boolean so perhaps this could be expressed as a property, i.e. IsInArray or something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks at the top of the stack assuming the stack is backed by an array. IsInArray sounds like it's checking containment. I also like it being a method since it's symmetric with Push/PushToArray and Peek/PeekInArray. I'm open to better naming but I left it as is with more comments.

{
int index = _currentDepth - AllocationFreeMaxDepth - 1;
Debug.Assert(_array != null);
Expand All @@ -129,6 +129,8 @@ private bool PopFromArray()
return (_array[elementIndex] & (1 << extraBits)) != 0;
}

public readonly bool Peek() => _currentDepth <= AllocationFreeMaxDepth ? (_allocationFreeContainer & 1) != 0 : PeekInArray();

private void DoubleArray(int minSize)
{
Debug.Assert(_array != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand Down Expand Up @@ -36,13 +37,10 @@ private void ValidateWritingProperty()
{
if (!_options.SkipValidation)
{
// Make sure a new property is not attempted within an unfinalized string.
ValidateNotWithinUnfinalizedString();

if (!_inObject || _tokenType == JsonTokenType.PropertyName)
if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName)
{
Debug.Assert(_tokenType != JsonTokenType.StartObject);
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
OnValidateWritingPropertyFailed();
}
}
}
Expand All @@ -52,18 +50,28 @@ private void ValidateWritingProperty(byte token)
{
if (!_options.SkipValidation)
{
// Make sure a new property is not attempted within an unfinalized string.
ValidateNotWithinUnfinalizedString();

if (!_inObject || _tokenType == JsonTokenType.PropertyName)
if (_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName)
{
Debug.Assert(_tokenType != JsonTokenType.StartObject);
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
OnValidateWritingPropertyFailed();
}
UpdateBitStackOnStart(token);
}
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnValidateWritingPropertyFailed()
{
if (_enclosingContainer == EnclosingContainerType.PartialValue)
{
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(_enclosingContainer != EnclosingContainerType.Object || _tokenType == JsonTokenType.PropertyName);
ThrowInvalidOperationException(ExceptionResource.CannotWritePropertyWithinArray);
}

private void WritePropertyNameMinimized(ReadOnlySpan<byte> escapedPropertyName, byte token)
{
Debug.Assert(escapedPropertyName.Length < int.MaxValue - 5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ namespace System.Text.Json
{
public sealed partial class Utf8JsonWriter
{
/// <summary>
/// Assuming that the writer is currently in a valid state, this returns true if a JSON value is allowed at the current position.
/// <list type="bullet">
/// <item>
/// If <see cref="_enclosingContainer"/> is an array then writing a value is always allowed.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is an object then writing a value is allowed only if <see cref="_tokenType"/> is a property name.
/// Because we designed <see cref="EnclosingContainerType.Object"/> == <see cref="JsonTokenType.PropertyName"/>, we can just check for equality.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is none (the root level) then writing a value is allowed only if <see cref="_tokenType"/> is None (only
/// one value may be written at the root). This case is identical to the previous case.
/// </item>
/// <item>
/// If <see cref="_enclosingContainer"/> is a partial value, then it will never be a valid <see cref="_tokenType"/> by construction.
/// </item>
/// </list>
/// This method performs better without short circuiting (this often gets inlined so using simple branch free code seems to have some benefits).
/// </summary>
private bool CanWriteValue => _enclosingContainer == EnclosingContainerType.Array | (byte)_enclosingContainer == (byte)_tokenType;

private bool HasPartialCodePoint => PartialCodePointLength != 0;

private void ClearPartialCodePoint() => PartialCodePointLength = 0;
Expand All @@ -26,39 +48,48 @@ private void ValidateEncodingDidNotChange(SegmentEncoding currentSegmentEncoding

private void ValidateNotWithinUnfinalizedString()
{
if (_tokenType == StringSegmentSentinel)
if (_enclosingContainer == EnclosingContainerType.PartialValue)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);
}

private void ValidateWritingValue()
{
if (!CanWriteValue)
{
OnValidateWritingValueFailed();
}
}

[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnValidateWritingValueFailed()
{
Debug.Assert(!_options.SkipValidation);

// Make sure a new value is not attempted within an unfinalized string.
ValidateNotWithinUnfinalizedString();
if (_enclosingContainer == EnclosingContainerType.PartialValue)
{
ThrowInvalidOperationException(ExceptionResource.CannotWriteWithinString);
}

Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);

if (_inObject)
if (_enclosingContainer == EnclosingContainerType.Object)
{
if (_tokenType != JsonTokenType.PropertyName)
{
Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray);
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
}
Debug.Assert(_tokenType != JsonTokenType.PropertyName);
Debug.Assert(_tokenType != JsonTokenType.None && _tokenType != JsonTokenType.StartArray);
ThrowInvalidOperationException(ExceptionResource.CannotWriteValueWithinObject);
}
else
{
Debug.Assert(_tokenType != JsonTokenType.PropertyName);

// It is more likely for CurrentDepth to not equal 0 when writing valid JSON, so check that first to rely on short-circuiting and return quickly.
if (CurrentDepth == 0 && _tokenType != JsonTokenType.None)
{
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose, currentDepth: default, maxDepth: _options.MaxDepth, token: default, _tokenType);
}
Debug.Assert(CurrentDepth == 0 && _tokenType != JsonTokenType.None);
ThrowInvalidOperationException(ExceptionResource.CannotWriteValueAfterPrimitiveOrClose);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ public void WriteStringValueSegment(ReadOnlySpan<char> value, bool isFinalSegmen
{
JsonWriterHelper.ValidateValue(value);

if (_tokenType != Utf8JsonWriter.StringSegmentSentinel)
if (_enclosingContainer == EnclosingContainerType.PartialValue)
{
ValidateEncodingDidNotChange(SegmentEncoding.Utf16);
}
else
{
Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);
Expand All @@ -44,11 +48,7 @@ public void WriteStringValueSegment(ReadOnlySpan<char> value, bool isFinalSegmen
WriteStringSegmentPrologue();

PreviousSegmentEncoding = SegmentEncoding.Utf16;
_tokenType = Utf8JsonWriter.StringSegmentSentinel;
}
else
{
ValidateEncodingDidNotChange(SegmentEncoding.Utf16);
_enclosingContainer = EnclosingContainerType.PartialValue;
}

// The steps to write a string segment are to complete the previous partial code point
Expand All @@ -68,6 +68,8 @@ public void WriteStringValueSegment(ReadOnlySpan<char> value, bool isFinalSegmen

SetFlagToAddListSeparatorBeforeNextItem();
PreviousSegmentEncoding = SegmentEncoding.None;
EnclosingContainerType container = _bitStack.Peek() ? EnclosingContainerType.Object : EnclosingContainerType.Array;
_enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container;
_tokenType = JsonTokenType.String;
}
}
Expand Down Expand Up @@ -207,7 +209,11 @@ public void WriteStringValueSegment(ReadOnlySpan<byte> value, bool isFinalSegmen
{
JsonWriterHelper.ValidateValue(value);

if (_tokenType != Utf8JsonWriter.StringSegmentSentinel)
if (_enclosingContainer == EnclosingContainerType.PartialValue)
{
ValidateEncodingDidNotChange(SegmentEncoding.Utf8);
}
else
{
Debug.Assert(PreviousSegmentEncoding == SegmentEncoding.None);
Debug.Assert(!HasPartialCodePoint);
Expand All @@ -220,11 +226,7 @@ public void WriteStringValueSegment(ReadOnlySpan<byte> value, bool isFinalSegmen
WriteStringSegmentPrologue();

PreviousSegmentEncoding = SegmentEncoding.Utf8;
_tokenType = Utf8JsonWriter.StringSegmentSentinel;
}
else
{
ValidateEncodingDidNotChange(SegmentEncoding.Utf8);
_enclosingContainer = EnclosingContainerType.PartialValue;
}

// The steps to write a string segment are to complete the previous partial code point
Expand All @@ -244,6 +246,8 @@ public void WriteStringValueSegment(ReadOnlySpan<byte> value, bool isFinalSegmen

SetFlagToAddListSeparatorBeforeNextItem();
PreviousSegmentEncoding = SegmentEncoding.None;
EnclosingContainerType container = _bitStack.Peek() ? EnclosingContainerType.Object : EnclosingContainerType.Array;
_enclosingContainer = _bitStack.CurrentDepth == 0 ? EnclosingContainerType.None : container;
_tokenType = JsonTokenType.String;
}
}
Expand Down
Loading
Loading