Skip to content

Commit aa70109

Browse files
committed
Use correct walk snapshots for UOM observation
UOM needs both the previous walk result and the current walk result after node override (that is, after user manually chooses a candidate) to make an observation, but since the nodes in a WalkResult are shared pointers to the Node instances in the grid, WalkResult does not capture an immutable snapshot. A new member function is introduced to enable getting a snapshot that has a real copy of the nodes, so as to capture the state of the grid before it is further mutated.
1 parent 5df30ff commit aa70109

4 files changed

Lines changed: 205 additions & 1 deletion

File tree

src/Engine/UserOverrideModelTest.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <string>
2525

2626
#include "UserOverrideModel.h"
27+
#include "gramambular2/reading_grid.h"
2728
#include "gtest/gtest.h"
2829

2930
namespace McBopomofo {
@@ -119,4 +120,159 @@ TEST(UserOverrideModelTest, LRUBehavior) {
119120
ASSERT_TRUE(v.empty());
120121
}
121122

123+
constexpr char kSampleData[] = R"(
124+
ㄐㄧ 機 -3.02367199
125+
ㄐㄧ 積 -3.72854036
126+
ㄐㄧ-ㄧㄡˊ 機油 -6.03662914
127+
ㄧㄡˊ 由 -3.00970678
128+
ㄧㄡˊ 油 -3.75900671
129+
)";
130+
131+
class SimpleLM : public Formosa::Gramambular2::LanguageModel {
132+
public:
133+
explicit SimpleLM(const char* input, bool readingIsFirstColumn = true) {
134+
std::stringstream sstream(input);
135+
while (sstream.good()) {
136+
std::string line;
137+
getline(sstream, line);
138+
if (line.empty() || line[0] == '#') {
139+
continue;
140+
}
141+
std::stringstream linestream(line);
142+
std::string col0;
143+
std::string col1;
144+
std::string col2;
145+
linestream >> col0;
146+
linestream >> col1;
147+
linestream >> col2;
148+
db_[readingIsFirstColumn ? col0 : col1].emplace_back(
149+
readingIsFirstColumn ? col1 : col0, std::stod(col2));
150+
}
151+
}
152+
153+
std::vector<Unigram> getUnigrams(const std::string& key) override {
154+
const auto f = db_.find(key);
155+
return f == db_.end() ? std::vector<Unigram>() : (*f).second;
156+
}
157+
158+
bool hasUnigrams(const std::string& key) override {
159+
return db_.find(key) != db_.end();
160+
}
161+
162+
protected:
163+
std::map<std::string, std::vector<Unigram>> db_;
164+
};
165+
166+
TEST(UserOverrideModelTest, WalkResultSnapshotTest) {
167+
// See https://github.com/openvanilla/McBopomofo/issues/885.
168+
// This is a kind of integration test for the following steps:
169+
// 1. Type ㄐㄧ ㄧㄡˊ -> 機由; this assumes P(機)P(由) > P(機油)
170+
// 2. Override with 機油 and tell UOM to observe
171+
// 3. Reset, type ㄐㄧ ㄧㄡˊ -> 機油 -- UOM should suggest the expected
172+
// override
173+
// 4. Reset, type ㄐㄧ -> 機
174+
// 5. Override with 積 and tell UOM to observe
175+
// 6. Reset, type ㄐㄧ ㄧㄡˊ -> 積由 -- this is expected due to UOM suggestion
176+
// 7. Override with 機油 and tell UOM to observe
177+
// 8. Reset, type ㄐㄧ -> 積 due to UOM suggestion
178+
// 9. Continue to type 由 -> walk result should be 積由, and the UOM should
179+
// suggest 機油 as the override candidate.
180+
181+
std::string sampleData(kSampleData);
182+
Formosa::Gramambular2::ReadingGrid grid(
183+
std::make_shared<SimpleLM>(sampleData.c_str()));
184+
UserOverrideModel uom(kCapacity, kHalflife);
185+
Formosa::Gramambular2::ReadingGrid::WalkResult walkBefore;
186+
Formosa::Gramambular2::ReadingGrid::WalkResult walkLatest;
187+
UserOverrideModel::Suggestion suggestion;
188+
std::vector<Formosa::Gramambular2::ReadingGrid::NodePtr>::const_iterator
189+
nodeIter;
190+
double timestamp = kFakeNow;
191+
192+
grid.insertReading("ㄐㄧ");
193+
grid.insertReading("ㄧㄡˊ");
194+
walkBefore = grid.walk();
195+
ASSERT_EQ(walkBefore.valuesAsStrings(),
196+
(std::vector<std::string>{"", ""}));
197+
198+
grid.overrideCandidate(1, "機油");
199+
walkLatest = grid.walk();
200+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{"機油"}));
201+
202+
nodeIter = walkLatest.findNodeAt(1, /*outCursorPastNode=*/nullptr);
203+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
204+
uom.observe(walkBefore, walkLatest, 1, timestamp);
205+
timestamp += 1.0;
206+
207+
grid.clear();
208+
grid.insertReading("ㄐㄧ");
209+
grid.insertReading("ㄧㄡˊ");
210+
walkLatest = grid.walk();
211+
suggestion = uom.suggest(walkLatest, 1, timestamp);
212+
timestamp += 1.0;
213+
ASSERT_EQ(suggestion.candidate, "機油");
214+
215+
grid.clear();
216+
grid.insertReading("ㄐㄧ");
217+
walkBefore = grid.walk();
218+
ASSERT_EQ(walkBefore.valuesAsStrings(), (std::vector<std::string>{""}));
219+
220+
grid.overrideCandidate(0, "");
221+
walkLatest = grid.walk();
222+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{""}));
223+
224+
nodeIter = walkLatest.findNodeAt(0, /*outCursorPastNode=*/nullptr);
225+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
226+
uom.observe(walkBefore, walkLatest, 0, timestamp);
227+
timestamp += 1.0;
228+
229+
grid.clear();
230+
grid.insertReading("ㄐㄧ");
231+
walkLatest = grid.walk();
232+
suggestion = uom.suggest(walkLatest, 0, timestamp);
233+
timestamp += 1.0;
234+
ASSERT_EQ(suggestion.candidate, "");
235+
236+
grid.overrideCandidate(0, "");
237+
grid.insertReading("ㄧㄡˊ");
238+
239+
// THIS MUST BE A COPY OF NODES, otherwise the observation below will not
240+
// capture the correct state of the grid.
241+
// See https://github.com/openvanilla/McBopomofo/issues/885.
242+
//
243+
// This is wrong:
244+
// walkBefore = grid.walk();
245+
//
246+
// The following is correct:
247+
walkBefore = grid.walk().copyWithFixedNodes();
248+
249+
ASSERT_EQ(walkBefore.valuesAsStrings(),
250+
(std::vector<std::string>{"", ""}));
251+
252+
grid.overrideCandidate(1, "機油");
253+
walkLatest = grid.walk();
254+
ASSERT_EQ(walkLatest.valuesAsStrings(), (std::vector<std::string>{"機油"}));
255+
256+
nodeIter = walkLatest.findNodeAt(1, /*outCursorPastNode=*/nullptr);
257+
ASSERT_NE(nodeIter, walkLatest.nodes.cend());
258+
uom.observe(walkBefore, walkLatest, 1, timestamp);
259+
timestamp += 1.0;
260+
261+
grid.clear();
262+
grid.insertReading("ㄐㄧ");
263+
walkLatest = grid.walk();
264+
suggestion = uom.suggest(walkLatest, 0, timestamp);
265+
timestamp += 1.0;
266+
ASSERT_EQ(suggestion.candidate, "");
267+
grid.overrideCandidate(0, "");
268+
grid.insertReading("ㄧㄡˊ");
269+
walkLatest = grid.walk();
270+
ASSERT_EQ(walkLatest.valuesAsStrings(),
271+
(std::vector<std::string>{"", ""}));
272+
273+
suggestion = uom.suggest(walkLatest, 1, timestamp);
274+
timestamp += 1.0;
275+
ASSERT_EQ(suggestion.candidate, "機油");
276+
}
277+
122278
} // namespace McBopomofo

src/Engine/gramambular2/reading_grid.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ class ReadingGrid {
111111
unigramIter_(unigrams_.begin()),
112112
overrideType_(OverrideType::kNone) {}
113113

114+
// An explicit copy ctor is needed since unigramIter_ must come from
115+
// this.unigram_, not copied.
116+
Node(const Node& o)
117+
: reading_(o.reading_),
118+
spanningLength_(o.spanningLength_),
119+
unigrams_(o.unigrams_),
120+
unigramIter_(unigrams_.begin()),
121+
overrideType_(o.overrideType_) {}
122+
114123
[[nodiscard]] const std::string& reading() const { return reading_; }
115124

116125
[[nodiscard]] size_t spanningLength() const { return spanningLength_; }
@@ -178,6 +187,24 @@ class ReadingGrid {
178187

179188
std::vector<std::string> valuesAsStrings() const;
180189
std::vector<std::string> readingsAsStrings() const;
190+
191+
// Makes a copy with the nodes also being copies instead of refernces to
192+
// those in the current grid.
193+
//
194+
// For performance reasons, nodes is a vector of shared ptrs to those
195+
// nodes in the referenced grid. If the intent is to have the walk capture
196+
// the current state of the grid before the grid mutates, the default
197+
// behavior will not be enough. Instead, use this to make sure that the
198+
// nodes are correctly copied.
199+
WalkResult copyWithFixedNodes() const {
200+
std::vector<NodePtr> copiedNodes;
201+
for (const auto& n : nodes) {
202+
copiedNodes.push_back(std::make_shared<Node>(*n));
203+
}
204+
205+
return WalkResult{copiedNodes, totalReadings, vertices, edges,
206+
elapsedMicroseconds};
207+
}
181208
};
182209

183210
WalkResult walk();

src/Engine/gramambular2/reading_grid_test.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,4 +817,18 @@ TEST(ReadingGridTest, FindInSpan2) {
817817
ASSERT_EQ(result->get()->value(), "高熱");
818818
}
819819

820+
TEST(ReadingGridTest, CopyWithFixedNodesMustNotContainDanglingUnigramIter) {
821+
Formosa::Gramambular2::ReadingGrid::WalkResult walkBefore;
822+
{
823+
Formosa::Gramambular2::ReadingGrid grid(std::make_shared<MockLM>());
824+
grid.insertReading("a");
825+
walkBefore = grid.walk().copyWithFixedNodes();
826+
}
827+
828+
// Accessing value() dereferences the walkBefore.unigramIter_, which must not
829+
// come from the nodes in the grid that is already gone.
830+
std::string val = walkBefore.nodes[0]->value();
831+
EXPECT_EQ(val, "a");
832+
}
833+
820834
} // namespace Formosa::Gramambular2

src/KeyHandler.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1777,14 +1777,21 @@ void KeyHandler::pinNode(
17771777
size_t originalCursor,
17781778
const InputStates::ChoosingCandidate::Candidate& candidate,
17791779
bool useMoveCursorAfterSelectionSetting) {
1780+
// Since WalkResult makes references to the current nodes, we must make a
1781+
// copy of the walk that *has a copy* of the current nodes to capture the
1782+
// current state. ReadingGrid::overrideCandidate() changes the state, and
1783+
// so having a simple copy of latestWalk_ (auto prevWalk = latestWalk_;)
1784+
// is NOT enough.
1785+
Formosa::Gramambular2::ReadingGrid::WalkResult prevWalk =
1786+
latestWalk_.copyWithFixedNodes();
1787+
17801788
size_t actualCursor = actualCandidateCursorIndex();
17811789
Formosa::Gramambular2::ReadingGrid::Candidate gridCandidate(
17821790
candidate.reading, candidate.value, "");
17831791
if (!grid_.overrideCandidate(actualCursor, gridCandidate)) {
17841792
return;
17851793
}
17861794

1787-
Formosa::Gramambular2::ReadingGrid::WalkResult prevWalk = latestWalk_;
17881795
walk();
17891796

17901797
// Update the user override model if warranted.

0 commit comments

Comments
 (0)