Skip to content

Commit b0882e7

Browse files
committed
[Java] Ensure Java code generation for precedence checks always produces the same code without re-orderings.
1 parent 002af63 commit b0882e7

2 files changed

Lines changed: 100 additions & 10 deletions

File tree

sbe-tool/src/main/java/uk/co/real_logic/sbe/generation/common/FieldPrecedenceModel.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ public final class FieldPrecedenceModel
4545
private final CodecInteraction.CodecInteractionFactory interactionFactory =
4646
new CodecInteraction.CodecInteractionFactory(groupPathsByField, topLevelBlockFields);
4747
private final Map<CodecInteraction, List<TransitionGroup>> transitionsByInteraction = new LinkedHashMap<>();
48-
private final Map<State, List<TransitionGroup>> transitionsByState = new HashMap<>();
48+
private final Map<State, List<TransitionGroup>> transitionsByState = new TreeMap<>();
4949
private final TreeMap<Integer, State> versionWrappedStates = new TreeMap<>();
5050
private final State notWrappedState = allocateState("NOT_WRAPPED");
5151
private final String generatedRepresentationClassName;
5252
private State encoderWrappedState;
53-
private Set<State> terminalEncoderStates;
53+
private SortedSet<State> terminalEncoderStates;
5454

5555
private FieldPrecedenceModel(final String generatedRepresentationClassName)
5656
{
@@ -135,9 +135,7 @@ public void forEachTerminalEncoderState(final Consumer<State> consumer)
135135
*/
136136
public void forEachStateOrderedByStateNumber(final Consumer<State> consumer)
137137
{
138-
transitionsByState.keySet().stream()
139-
.sorted(Comparator.comparingInt((s) -> s.number))
140-
.forEach(consumer);
138+
transitionsByState.keySet().forEach(consumer);
141139
}
142140

143141
/**
@@ -273,7 +271,7 @@ private void findTransitions(
273271

274272
// Last writer (highest version) wins when there are multiple versions
275273
encoderWrappedState = versionWrappedState;
276-
terminalEncoderStates = transitionCollector.exitStates();
274+
terminalEncoderStates = new TreeSet<>(transitionCollector.exitStates());
277275
});
278276
}
279277

@@ -459,7 +457,7 @@ public void onVarData(final Token token)
459457
private final class TransitionCollector implements SchemaConsumer
460458
{
461459
private final String statePrefix;
462-
private final HashSet<State> currentStates;
460+
private final Set<State> currentStates;
463461
private final State blockState;
464462
private final Predicate<Token> filter;
465463

@@ -470,7 +468,7 @@ private TransitionCollector(
470468
final Predicate<Token> filter)
471469
{
472470
this.statePrefix = statePrefix;
473-
this.currentStates = new HashSet<>(fromStates);
471+
this.currentStates = new TreeSet<>(fromStates);
474472
this.blockState = blockState;
475473
this.filter = filter;
476474

@@ -585,7 +583,7 @@ private Set<State> exitStates()
585583
/**
586584
* A state in which a codec may reside.
587585
*/
588-
public static final class State
586+
public static final class State implements Comparable<State>
589587
{
590588
private final int number;
591589
private final String name;
@@ -615,6 +613,19 @@ public String name()
615613
return name;
616614
}
617615

616+
/**
617+
* {@inheritDoc}.
618+
*/
619+
public int compareTo(final State other)
620+
{
621+
final int result = Integer.compare(number, other.number);
622+
if (0 != result)
623+
{
624+
return result;
625+
}
626+
return name.compareTo(other.name);
627+
}
628+
618629
/**
619630
* {@inheritDoc}
620631
*/
@@ -642,7 +653,7 @@ private TransitionGroup(
642653
final State to)
643654
{
644655
this.interaction = interaction;
645-
this.from = new HashSet<>(from);
656+
this.from = new TreeSet<>(from);
646657
this.to = to;
647658
}
648659

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2013-2025 Real Logic Limited.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package uk.co.real_logic.sbe.generation.java;
17+
18+
import org.agrona.DirectBuffer;
19+
import org.agrona.MutableDirectBuffer;
20+
import org.agrona.generation.StringWriterOutputManager;
21+
import org.junit.jupiter.api.Test;
22+
import uk.co.real_logic.sbe.Tests;
23+
import uk.co.real_logic.sbe.generation.common.PrecedenceChecks;
24+
import uk.co.real_logic.sbe.ir.Ir;
25+
import uk.co.real_logic.sbe.xml.IrGenerator;
26+
import uk.co.real_logic.sbe.xml.MessageSchema;
27+
import uk.co.real_logic.sbe.xml.ParserOptions;
28+
29+
import java.io.InputStream;
30+
import java.util.Map;
31+
import java.util.TreeMap;
32+
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static uk.co.real_logic.sbe.xml.XmlSchemaParser.parse;
35+
36+
/**
37+
* Tests that generated code is deterministic when precedence checks are enabled.
38+
*/
39+
class JavaGeneratorDeterminismTest
40+
{
41+
private static final String BUFFER_NAME = MutableDirectBuffer.class.getName();
42+
private static final String READ_ONLY_BUFFER_NAME = DirectBuffer.class.getName();
43+
44+
@Test
45+
void shouldGenerateDeterministicOutputWithPrecedenceChecks() throws Exception
46+
{
47+
final Map<String, CharSequence> firstGeneration = generateWithPrecedenceChecks();
48+
final Map<String, CharSequence> secondGeneration = generateWithPrecedenceChecks();
49+
50+
assertEquals(firstGeneration.keySet(), secondGeneration.keySet());
51+
52+
for (final String fileName : firstGeneration.keySet())
53+
{
54+
final CharSequence firstSource = firstGeneration.get(fileName);
55+
final CharSequence secondSource = secondGeneration.get(fileName);
56+
assertEquals(firstSource, secondSource);
57+
}
58+
}
59+
60+
private Map<String, CharSequence> generateWithPrecedenceChecks() throws Exception
61+
{
62+
try (InputStream in = Tests.getLocalResource("field-order-check-schema.xml"))
63+
{
64+
final ParserOptions options = ParserOptions.builder().stopOnError(true).build();
65+
final MessageSchema schema = parse(in, options);
66+
final IrGenerator irg = new IrGenerator();
67+
final Ir ir = irg.generate(schema);
68+
final StringWriterOutputManager outputManager = new StringWriterOutputManager();
69+
outputManager.setPackageName(ir.applicableNamespace());
70+
final PrecedenceChecks.Context context = new PrecedenceChecks.Context()
71+
.shouldGeneratePrecedenceChecks(true);
72+
final PrecedenceChecks precedenceChecks = PrecedenceChecks.newInstance(context);
73+
final JavaGenerator generator = new JavaGenerator(
74+
ir, BUFFER_NAME, READ_ONLY_BUFFER_NAME, false, false, false, false, precedenceChecks, outputManager);
75+
generator.generate();
76+
return new TreeMap<>(outputManager.getSources());
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)