Skip to content

Commit

Permalink
[Java, C#, C++] Model resetCountToIndex() state machine transition.
Browse files Browse the repository at this point in the history
Previously, the access order checks did not work with the
`resetCountToIndex()` methods (generated on flyweights for repeating
groups). Now, the valid transitions using these methods are modelled in
the state machine and this is reflected in the code we generate for
Java, C# and C++.

Note that the `resetCountToIndex()` method does not change the
`limit`/`position` of the message; therefore, it is only valid to use it
when the `limit` aligns with a boundary between group elements.
  • Loading branch information
ZachBray committed Aug 18, 2023
1 parent e487b74 commit aa136ce
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 26 deletions.
93 changes: 84 additions & 9 deletions csharp/sbe-tests/FieldAccessOrderCheckTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,79 @@ public void AllowsEncodingAndDecodingEmptyGroupAndVariableLengthFieldsInSchemaDe
Assert.IsTrue(decoder.ToString().Contains("A=42|B=[]|D='abc'"));
}

[TestMethod]
public void AllowsEncoderToResetZeroGroupLengthToZero()
{
var encoder = new GroupAndVarLength();
encoder.WrapForEncodeAndApplyHeader(_buffer, Offset, _messageHeader);
encoder.A = 42;
encoder.BCount(0).ResetCountToIndex();
encoder.SetD("abc");
encoder.CheckEncodingIsComplete();

var decoder = new GroupAndVarLength();
decoder.WrapForDecodeAndApplyHeader(_buffer, Offset, _messageHeader);
Assert.AreEqual(42, decoder.A);
var bDecoder = decoder.B;
Assert.AreEqual(0, bDecoder.Count);
Assert.AreEqual("abc", decoder.GetD());
Assert.IsTrue(decoder.ToString().Contains("A=42|B=[]|D='abc'"));
}

[TestMethod]
public void AllowsEncoderToResetNonZeroGroupLengthToZeroBeforeCallingNext()
{
var encoder = new GroupAndVarLength();
encoder.WrapForEncodeAndApplyHeader(_buffer, Offset, _messageHeader);
encoder.A = 42;
encoder.BCount(2).ResetCountToIndex();
encoder.SetD("abc");
encoder.CheckEncodingIsComplete();

var decoder = new GroupAndVarLength();
decoder.WrapForDecodeAndApplyHeader(_buffer, Offset, _messageHeader);
Assert.AreEqual(42, decoder.A);
var bDecoder = decoder.B;
Assert.AreEqual(0, bDecoder.Count);
Assert.AreEqual("abc", decoder.GetD());
Assert.IsTrue(decoder.ToString().Contains("A=42|B=[]|D='abc'"));
}

[TestMethod]
public void AllowsEncoderToResetNonZeroGroupLengthToNonZero()
{
var encoder = new GroupAndVarLength();
encoder.WrapForEncodeAndApplyHeader(_buffer, Offset, _messageHeader);
encoder.A = 42;
var bEncoder = encoder.BCount(2);
bEncoder.Next().C = 43;
bEncoder.ResetCountToIndex();
encoder.SetD("abc");
encoder.CheckEncodingIsComplete();

var decoder = new GroupAndVarLength();
decoder.WrapForDecodeAndApplyHeader(_buffer, Offset, _messageHeader);
Assert.AreEqual(42, decoder.A);
var bDecoder = decoder.B;
Assert.AreEqual(1, bDecoder.Count);
Assert.AreEqual(43, bDecoder.Next().C);
Assert.AreEqual("abc", decoder.GetD());
Assert.IsTrue(decoder.ToString().Contains("A=42|B=[(C=43)]|D='abc'"));
}

[TestMethod]
public void DisallowsEncoderToResetGroupLengthMidGroupElement()
{
var encoder = new NestedGroups();
encoder.WrapForEncodeAndApplyHeader(_buffer, Offset, _messageHeader);
encoder.A = 42;
var bEncoder = encoder.BCount(2).Next();
bEncoder.C = 43;
var exception = Assert.ThrowsException<InvalidOperationException>(() => bEncoder.ResetCountToIndex());
Assert.IsTrue(exception.Message.Contains(
"Cannot reset count of repeating group \"b\" in state: V0_B_N_BLOCK"));
}

[TestMethod]
public void DisallowsEncodingGroupElementBeforeCallingNext()
{
Expand Down Expand Up @@ -2311,7 +2384,7 @@ public void DisallowsEncodingAsciiInsideGroupBeforeCallingNext5()
{
var bEncoder = EncodeUntilGroupWithAsciiInside();

Exception exception = Assert.ThrowsException<InvalidOperationException>(() =>
Exception exception = Assert.ThrowsException<InvalidOperationException>(() =>
bEncoder.SetC(Encoding.ASCII.GetBytes("EURUSD"), 0));

Assert.IsTrue(exception.Message.Contains("Cannot access field \"b.c\" in state: V0_B_N"));
Expand All @@ -2322,7 +2395,7 @@ public void DisallowsEncodingAsciiInsideGroupBeforeCallingNext6()
{
var bEncoder = EncodeUntilGroupWithAsciiInside();

Exception exception = Assert.ThrowsException<InvalidOperationException>(() =>
Exception exception = Assert.ThrowsException<InvalidOperationException>(() =>
bEncoder.SetC(Encoding.ASCII.GetBytes("EURUSD")));

Assert.IsTrue(exception.Message.Contains("Cannot access field \"b.c\" in state: V0_B_N"));
Expand Down Expand Up @@ -2693,7 +2766,7 @@ public void DisallowsEncodingElementOfEmptyGroup2()
encoder.HCount(0);

var ex = Assert.ThrowsException<InvalidOperationException>(() => dEncoder.E = 44);
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_H_0"));
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_H_DONE"));
}

[TestMethod]
Expand All @@ -2710,7 +2783,7 @@ public void DisallowsEncodingElementOfEmptyGroup3()
encoder.HCount(0);

var ex = Assert.ThrowsException<InvalidOperationException>(() => dEncoder.E = 44);
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_H_0"));
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_H_DONE"));
}

[TestMethod]
Expand All @@ -2726,7 +2799,7 @@ public void DisallowsEncodingElementOfEmptyGroup4()
bEncoder.FCount(0);

var ex = Assert.ThrowsException<InvalidOperationException>(() => dEncoder.E = 44);
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_B_1_F_0"));
Assert.IsTrue(ex.Message.Contains("Cannot access field \"b.d.e\" in state: V0_B_1_F_DONE"));
}


Expand All @@ -2738,7 +2811,7 @@ public void DisallowsEncodingElementOfEmptyGroup5()
encoder.A = 42;
var bEncoder = encoder.BCount(0);
var exception = Assert.ThrowsException<InvalidOperationException>(() => bEncoder.C = 43);
Assert.IsTrue(exception.Message.Contains("Cannot access field \"b.c\" in state: V1_B_0"));
Assert.IsTrue(exception.Message.Contains("Cannot access field \"b.c\" in state: V1_B_DONE"));
}

[TestMethod]
Expand Down Expand Up @@ -3003,7 +3076,8 @@ public void DisallowsIncompleteMessagesDueToMissingTopLevelGroup1()
encoder.BCount(0);
var exception = Assert.ThrowsException<InvalidOperationException>(encoder.CheckEncodingIsComplete);
StringAssert.Contains(exception.Message,
"Not fully encoded, current state: V0_B_0, allowed transitions: \"dCount(0)\", \"dCount(>0)\"");
"Not fully encoded, current state: V0_B_DONE, allowed transitions: " +
"\"b.resetCountToIndex()\", \"dCount(0)\", \"dCount(>0)\"");
}

[TestMethod]
Expand All @@ -3016,7 +3090,8 @@ public void DisallowsIncompleteMessagesDueToMissingTopLevelGroup2()
bEncoder.C = 2;
var exception = Assert.ThrowsException<InvalidOperationException>(encoder.CheckEncodingIsComplete);
StringAssert.Contains(exception.Message,
"Not fully encoded, current state: V0_B_1_BLOCK, allowed transitions: \"b.c(?)\", \"dCount(0)\", \"dCount(>0)\"");
"Not fully encoded, current state: V0_B_1_BLOCK, allowed transitions:" +
" \"b.c(?)\", \"b.resetCountToIndex()\", \"dCount(0)\", \"dCount(>0)\"");
}

[TestMethod]
Expand Down Expand Up @@ -3047,7 +3122,7 @@ public void DisallowsIncompleteMessagesDueToMissingNestedGroup1(int bCount, stri
[DataTestMethod]
[DataRow(1, 1, "V0_B_1_D_N")]
[DataRow(1, 2, "V0_B_1_D_N")]
[DataRow(2, 0, "V0_B_N_D_0")]
[DataRow(2, 0, "V0_B_N_D_DONE")]
[DataRow(2, 1, "V0_B_N_D_N")]
[DataRow(2, 2, "V0_B_N_D_N")]
public void DisallowsIncompleteMessagesDueToMissingNestedGroup2(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,13 +499,13 @@ public void onEnterRepeatingGroup(
final State nRemainingGroup = allocateState(groupPrefix + "N");
final State nRemainingGroupElement = allocateState(groupPrefix + "N_BLOCK");
final State oneRemainingGroupElement = allocateState(groupPrefix + "1_BLOCK");
final State emptyGroup = allocateState(groupPrefix + "0");
final State doneGroup = allocateState(groupPrefix + "DONE");

final Set<State> beginGroupStates = new HashSet<>(currentStates);

// fooCount(0)
final CodecInteraction emptyGroupInteraction = interactionFactory.determineGroupIsEmpty(token);
allocateTransitions(emptyGroupInteraction, beginGroupStates, emptyGroup);
allocateTransitions(emptyGroupInteraction, beginGroupStates, doneGroup);

// fooCount(N) where N > 0
final CodecInteraction nonEmptyGroupInteraction = interactionFactory.determineGroupHasElements(token);
Expand Down Expand Up @@ -548,8 +548,16 @@ public void onEnterRepeatingGroup(
);
walkSchemaLevel(oneRemainingCollector, groupFields, groupGroups, groupVarData);

final CodecInteraction resetCountToIndexInteraction = interactionFactory.resetCountToIndex(token);
currentStates.clear();
currentStates.add(emptyGroup);
currentStates.add(doneGroup);
currentStates.add(nRemainingGroup);
currentStates.addAll(nRemainingCollector.exitStates());
currentStates.addAll(oneRemainingCollector.exitStates());
allocateTransitions(resetCountToIndexInteraction, currentStates, doneGroup);

currentStates.clear();
currentStates.add(doneGroup);
currentStates.addAll(oneRemainingCollector.exitStates());
}
}
Expand Down Expand Up @@ -935,6 +943,41 @@ String exampleConditions()
}
}

/**
* When the number of elements in a repeating group is set to be its current extent.
*/
private static final class ResetCountToIndex extends CodecInteraction
{
private final String groupPath;
private final Token token;

private ResetCountToIndex(final String groupPath, final Token token)
{
assert groupPath != null;
assert token.signal() == Signal.BEGIN_GROUP;
this.groupPath = groupPath;
this.token = token;
}

@Override
public String groupQualifiedName()
{
return groupPath + token.name();
}

@Override
String exampleCode()
{
return groupPath + token.name() + ".resetCountToIndex()";
}

@Override
String exampleConditions()
{
return "";
}
}

/**
* When the length of a variable length field is accessed without adjusting the position.
*/
Expand Down Expand Up @@ -984,6 +1027,7 @@ public static final class HashConsingFactory
private final Map<Token, CodecInteraction> determineGroupHasElementsInteractions = new HashMap<>();
private final Map<Token, CodecInteraction> moveToNextElementInteractions = new HashMap<>();
private final Map<Token, CodecInteraction> moveToLastElementInteractions = new HashMap<>();
private final Map<Token, CodecInteraction> resetCountToIndexInteractions = new HashMap<>();
private final Map<Token, CodecInteraction> accessVarDataLengthInteractions = new HashMap<>();
private final Map<Token, String> groupPathsByField;
private final Set<Token> topLevelBlockFields;
Expand Down Expand Up @@ -1100,6 +1144,25 @@ public CodecInteraction moveToLastElement(final Token token)
t -> new MoveToLastElement(groupPathsByField.get(t), t));
}


/**
* Find or create a {@link CodecInteraction} to represent resetting the count
* of a repeating group to the current index.
*
* <p>For encoders, decoders, and codecs, this will be when the {@code resetCountToIndex}
* method is called, e.g., {@code myGroup.resetCountToIndex()}.
*
* <p>The supplied token must carry a {@link Signal#BEGIN_GROUP} signal.
*
* @param token the token identifying the group
* @return the {@link CodecInteraction} instance
*/
public CodecInteraction resetCountToIndex(final Token token)
{
return resetCountToIndexInteractions.computeIfAbsent(token,
t -> new ResetCountToIndex(groupPathsByField.get(t), t));
}

/**
* Find or create a {@link CodecInteraction} to represent accessing the length
* of a variable-length data field without advancing the codec position.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,33 @@ private static void generateAccessOrderListenerMethodForNextGroupElement(
.append(indent).append("}\n");
}

private static void generateAccessOrderListenerMethodForResetGroupCount(
final StringBuilder sb,
final AccessOrderModel accessOrderModel,
final String indent,
final Token token)
{
if (null == accessOrderModel)
{
return;
}

sb.append(indent).append("void onResetCountToIndex()\n")
.append(indent).append("{\n");

final AccessOrderModel.CodecInteraction resetCountToIndex =
accessOrderModel.interactionFactory().resetCountToIndex(token);

generateAccessOrderListener(
sb,
indent + " ",
"reset count of repeating group",
accessOrderModel,
resetCountToIndex);

sb.append(indent).append("}\n");
}

private void generateGroups(
final StringBuilder sb,
final List<Token> tokens,
Expand Down Expand Up @@ -733,6 +760,7 @@ private static void generateGroupClassHeader(
}

generateAccessOrderListenerMethodForNextGroupElement(sb, accessOrderModel, indent + INDENT, groupToken);
generateAccessOrderListenerMethodForResetGroupCount(sb, accessOrderModel, indent, groupToken);

final CharSequence onNextAccessOrderCall = null == accessOrderModel ? "" :
generateAccessOrderListenerCall(accessOrderModel, indent + TWO_INDENT, "onNextElementAccessed");
Expand Down Expand Up @@ -803,6 +831,7 @@ private static void generateGroupClassHeader(
sb.append("\n")
.append(indent).append(" inline std::uint64_t resetCountToIndex()\n")
.append(indent).append(" {\n")
.append(generateAccessOrderListenerCall(accessOrderModel, indent + TWO_INDENT, "onResetCountToIndex"))
.append(indent).append(" m_count = m_index;\n")
.append(indent).append(" ").append(dimensionsClassName)
.append(" dimensions(m_buffer, m_initialPosition, m_bufferLength, m_actingVersion);\n")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,19 +344,22 @@ private void generateGroupEnumerator(
final String indent)
{
generateAccessOrderListenerMethodForNextGroupElement(sb, accessOrderModel, indent + INDENT, groupToken);
generateAccessOrderListenerMethodForResetGroupCount(sb, accessOrderModel, indent + INDENT, groupToken);

sb.append(
indent + INDENT + "public int ActingBlockLength { get { return _blockLength; } }\n\n" +
indent + INDENT + "public int Count { get { return _count; } }\n\n" +
indent + INDENT + "public bool HasNext { get { return _index < _count; } }\n");
indent + INDENT + "public bool HasNext { get { return _index < _count; } }\n\n");

sb.append(String.format("\n" +
indent + INDENT + "public int ResetCountToIndex()\n" +
indent + INDENT + "{\n" +
"%s" +
indent + INDENT + INDENT + "_count = _index;\n" +
indent + INDENT + INDENT + "_dimensions.NumInGroup = (%s) _count;\n\n" +
indent + INDENT + INDENT + "return _count;\n" +
indent + INDENT + "}\n",
generateAccessOrderListenerCall(accessOrderModel, indent + TWO_INDENT, "OnResetCountToIndex"),
typeForNumInGroup));

sb.append(String.format("\n" +
Expand Down Expand Up @@ -1833,6 +1836,33 @@ private static void generateAccessOrderListenerMethodForNextGroupElement(
.append(indent).append("}\n");
}

private static void generateAccessOrderListenerMethodForResetGroupCount(
final StringBuilder sb,
final AccessOrderModel accessOrderModel,
final String indent,
final Token token)
{
if (null == accessOrderModel)
{
return;
}

sb.append(indent).append("private void OnResetCountToIndex()\n")
.append(indent).append("{\n");

final AccessOrderModel.CodecInteraction resetCountToIndex =
accessOrderModel.interactionFactory().resetCountToIndex(token);

generateAccessOrderListener(
sb,
indent + " ",
"reset count of repeating group",
accessOrderModel,
resetCountToIndex);

sb.append(indent).append("}\n");
}

private static void generateAccessOrderListenerMethodForVarDataLength(
final StringBuilder sb,
final AccessOrderModel accessOrderModel,
Expand Down
Loading

0 comments on commit aa136ce

Please sign in to comment.