Skip to content

Commit 4461600

Browse files
committed
refactor(web): replace SearchQuotientSpur.addInput() with extended constructor
This has been factorized out of #14987 for ease of review. Build-bot: skip build:web Test-bot: skip
1 parent a65dcf4 commit 4461600

File tree

4 files changed

+54
-93
lines changed

4 files changed

+54
-93
lines changed

web/src/engine/predictive-text/worker-thread/src/main/correction/context-token.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class ContextToken {
128128
inputStartIndex: 0,
129129
bestProbFromSet: BASE_PROBABILITY
130130
});
131-
searchSpace = searchSpace.addInput([{sample: transform, p: BASE_PROBABILITY}], 1);
131+
searchSpace = new SearchQuotientSpur(searchSpace, [{sample: transform, p: BASE_PROBABILITY}], 1);
132132
});
133133

134134
this._searchSpace = searchSpace;
@@ -141,7 +141,7 @@ export class ContextToken {
141141
*/
142142
addInput(inputSource: TokenInputSource, distribution: Distribution<Transform>) {
143143
this._inputRange.push(inputSource);
144-
this._searchSpace = this._searchSpace.addInput(distribution, inputSource.bestProbFromSet);
144+
this._searchSpace = new SearchQuotientSpur(this._searchSpace, distribution, inputSource.bestProbFromSet);
145145
}
146146

147147
/**

web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
* manage the search-space(s) for text corrections within the engine.
88
*/
99

10+
import { LexicalModelTypes } from "@keymanapp/common-types";
11+
1012
import { SearchNode, SearchResult } from "./distance-modeler.js";
1113

14+
import Distribution = LexicalModelTypes.Distribution;
15+
import Transform = LexicalModelTypes.Transform;
16+
1217
let SPACE_ID_SEED = 0;
1318

1419
export function generateSpaceSeed(): number {
@@ -80,6 +85,14 @@ export interface SearchQuotientNode {
8085
*/
8186
readonly inputCount: number;
8287

88+
/**
89+
* Retrieves the sequence of inputs that led to this SearchSpace.
90+
*
91+
* THIS WILL BE REMOVED SHORTLY. (Once SearchQuotientNode takes on merging &
92+
* splitting)
93+
*/
94+
readonly inputSequence: Distribution<Transform>[];
95+
8396
/**
8497
* Determines the best example text representable by this batcher's portion of
8598
* the correction-search graph and its paths.

web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts

Lines changed: 33 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ export const QUEUE_NODE_COMPARATOR: Comparator<SearchNode> = function(arg1, arg2
2828
// Whenever a wordbreak boundary is crossed, a new instance should be made.
2929
export class SearchQuotientSpur implements SearchQuotientNode {
3030
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(QUEUE_NODE_COMPARATOR);
31-
private inputs: Distribution<Transform>;
32-
33-
readonly rootPath: SearchQuotientSpur;
31+
readonly inputs?: Distribution<Readonly<Transform>>;
3432

3533
private parentPath: SearchQuotientSpur;
3634
readonly spaceId: number;
@@ -58,45 +56,38 @@ export class SearchQuotientSpur implements SearchQuotientNode {
5856
*/
5957
private lowestCostAtDepth: number[];
6058

61-
/**
62-
* Clone constructor. Deep-copies its internal queues, but not search nodes.
63-
* @param instance
64-
*/
65-
constructor(instance: SearchQuotientSpur);
6659
/**
6760
* Constructs a fresh SearchSpace instance for used in predictive-text correction
6861
* and suggestion searches.
6962
* @param baseSpaceId
7063
* @param model
7164
*/
7265
constructor(model: LexicalModel);
73-
constructor(arg1: SearchQuotientSpur|LexicalModel) {
66+
constructor(space: SearchQuotientSpur, inputs: Distribution<Transform>, bestProbFromSet: number);
67+
constructor(arg1: LexicalModel | SearchQuotientSpur, inputs?: Distribution<Transform>, bestProbFromSet?: number) {
7468
this.spaceId = generateSpaceSeed();
7569

7670
if(arg1 instanceof SearchQuotientSpur) {
77-
const parentSpace = arg1;
78-
this.lowestCostAtDepth = parentSpace.lowestCostAtDepth.slice();
79-
this.rootPath = parentSpace.rootPath;
80-
this.parentPath = parentSpace;
71+
const parentNode = arg1 as SearchQuotientSpur;
72+
const logTierCost = -Math.log(bestProbFromSet);
8173

82-
return;
83-
}
74+
this.inputs = inputs;
75+
this.lowestCostAtDepth = parentNode.lowestCostAtDepth.concat(logTierCost);
76+
this.parentPath = parentNode;
77+
78+
this.addEdgesForNodes(parentNode.completedPaths);
8479

85-
const model = arg1;
86-
if(!model.traverseFromRoot) {
87-
throw new Error("The provided model does not implement the `traverseFromRoot` function, which is needed to support robust correction searching.");
80+
return;
8881
}
8982

90-
const rootNode = new SearchNode(model.traverseFromRoot(), this.spaceId, model.toKey ? model.toKey.bind(model) : null);
91-
this.selectionQueue.enqueue(rootNode);
83+
const model = arg1 as LexicalModel;
84+
this.selectionQueue.enqueue(new SearchNode(model.traverseFromRoot(), this.spaceId, t => model.toKey(t)));
9285
this.lowestCostAtDepth = [];
93-
this.rootPath = this;
94-
9586
this.completedPaths = [];
9687
}
9788

9889
/**
99-
* Retrieves the sequence of inputs
90+
* Retrieves the sequences of inputs that led to this SearchPath.
10091
*/
10192
public get inputSequence(): Distribution<Transform>[] {
10293
if(this.parentPath) {
@@ -141,76 +132,33 @@ export class SearchQuotientSpur implements SearchQuotientNode {
141132
return this.parentPath?.correctionsEnabled || this.inputs?.length > 1;
142133
}
143134

144-
/**
145-
* Extends the correction-search process embodied by this SearchPath by an extra
146-
* input character, according to the characters' likelihood in the distribution.
147-
* @param inputDistribution The fat-finger distribution for the incoming keystroke (or
148-
* just the raw keystroke if corrections are disabled)
149-
*/
150-
addInput(inputDistribution: Distribution<Transform>, bestProbFromSet: number): SearchQuotientSpur {
151-
const input = inputDistribution;
152-
153-
const childSpace = new SearchQuotientSpur(this);
154-
155-
childSpace.inputs = inputDistribution;
156-
const lastDepthCost = this.lowestCostAtDepth[this.lowestCostAtDepth.length - 1] ?? 0;
157-
const logTierCost = -Math.log(bestProbFromSet);
158-
childSpace.lowestCostAtDepth.push(lastDepthCost + logTierCost);
159-
160-
// With a newly-available input, we can extend new input-dependent paths from
161-
// our previously-reached 'extractedResults' nodes.
162-
let newlyAvailableEdges: SearchNode[] = [];
163-
let batches = this.completedPaths?.map(function(node) {
164-
let deletions = node.buildDeletionEdges(input, childSpace.spaceId);
165-
let substitutions = node.buildSubstitutionEdges(input, childSpace.spaceId);
166-
167-
// Skip the queue for the first pass; there will ALWAYS be at least one pass,
168-
// and queue-enqueing does come with a cost. Avoid the unnecessary overhead.
169-
return substitutions.flatMap(e => e.processSubsetEdge()).concat(deletions);
170-
});
171-
172-
childSpace.completedPaths = [];
173-
childSpace.returnedValues = {};
174-
175-
batches?.forEach(function(batch) {
176-
newlyAvailableEdges = newlyAvailableEdges.concat(batch);
177-
});
178-
179-
childSpace.selectionQueue.enqueueAll(newlyAvailableEdges);
180-
181-
return childSpace;
182-
}
183-
184135
public get currentCost(): number {
185136
const parentCost = this.parentPath?.currentCost ?? Number.POSITIVE_INFINITY;
186137
const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY;
187138

188139
return Math.min(localCost, parentCost);
189140
}
190141

191-
/**
192-
* Given an incoming SearchNode, this method will build all outgoing edges
193-
* from the node that correspond to processing this SearchPath instance's
194-
* input distribution.
195-
* @param currentNode
196-
*/
197-
private addEdgesForNodes(currentNode: SearchNode) {
198-
// Hard restriction: no further edits will be supported. This helps keep the search
199-
// more narrowly focused.
200-
const substitutionsOnly = currentNode.editCount == 2;
201-
202-
let deletionEdges: SearchNode[] = [];
203-
if(!substitutionsOnly) {
204-
deletionEdges = currentNode.buildDeletionEdges(this.inputs, this.spaceId);
205-
}
206-
const substitutionEdges = currentNode.buildSubstitutionEdges(this.inputs, this.spaceId);
142+
private addEdgesForNodes(baseNodes: ReadonlyArray<SearchNode>) {
143+
// With a newly-available input, we can extend new input-dependent paths from
144+
// our previously-reached 'extractedResults' nodes.
145+
let outboundNodes = baseNodes.map((node) => {
146+
// Hard restriction: no further edits will be supported. This helps keep the search
147+
// more narrowly focused.
148+
const substitutionsOnly = node.editCount == 2;
149+
150+
let deletionEdges: SearchNode[] = [];
151+
if(!substitutionsOnly) {
152+
deletionEdges = node.buildDeletionEdges(this.inputs, this.spaceId);
153+
}
154+
const substitutionEdges = node.buildSubstitutionEdges(this.inputs, this.spaceId);
207155

208-
// Skip the queue for the first pass; there will ALWAYS be at least one pass,
209-
// and queue-enqueing does come with a cost - avoid unnecessary overhead here.
210-
let batch = substitutionEdges.flatMap(e => e.processSubsetEdge()).concat(deletionEdges);
156+
// Skip the queue for the first pass; there will ALWAYS be at least one pass,
157+
// and queue-enqueing does come with a cost - avoid unnecessary overhead here.
158+
return substitutionEdges.flatMap(e => e.processSubsetEdge()).concat(deletionEdges);
159+
}).flat();
211160

212-
this.selectionQueue.enqueueAll(batch);
213-
// We didn't reach an end-node, so we just end the iteration and continue the search.
161+
this.selectionQueue.enqueueAll(outboundNodes);
214162
}
215163

216164
/**
@@ -233,7 +181,7 @@ export class SearchQuotientSpur implements SearchQuotientNode {
233181
const result = this.parentPath.handleNextNode();
234182

235183
if(result.type == 'complete') {
236-
this.addEdgesForNodes(result.finalNode);
184+
this.addEdgesForNodes([result.finalNode]);
237185
}
238186

239187
return {

web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ describe('getBestMatches', () => {
126126
{sample: {insert: 'n', deleteLeft: 0}, p: 0.25}
127127
];
128128

129-
const searchPath1 = searchPath.addInput(synthInput1, 1);
130-
const searchPath2 = searchPath1.addInput(synthInput2, .75);
131-
const searchPath3 = searchPath2.addInput(synthInput3, .75);
129+
const searchPath1 = new SearchQuotientSpur(searchPath, synthInput1, 1);
130+
const searchPath2 = new SearchQuotientSpur(searchPath1, synthInput2, .75);
131+
const searchPath3 = new SearchQuotientSpur(searchPath2, synthInput3, .75);
132132

133133
assert.notEqual(searchPath1.spaceId, searchPath.spaceId);
134134
assert.notEqual(searchPath2.spaceId, searchPath1.spaceId);
@@ -160,9 +160,9 @@ describe('getBestMatches', () => {
160160
{sample: {insert: 'n', deleteLeft: 0}, p: 0.25}
161161
];
162162

163-
const searchPath1 = searchPath.addInput(synthInput1, 1);
164-
const searchPath2 = searchPath1.addInput(synthInput2, .75);
165-
const searchPath3 = searchPath2.addInput(synthInput3, .75);
163+
const searchPath1 = new SearchQuotientSpur(searchPath, synthInput1, 1);
164+
const searchPath2 = new SearchQuotientSpur(searchPath1, synthInput2, .75);
165+
const searchPath3 = new SearchQuotientSpur(searchPath2, synthInput3, .75);
166166

167167
assert.notEqual(searchPath1.spaceId, searchPath.spaceId);
168168
assert.notEqual(searchPath2.spaceId, searchPath1.spaceId);

0 commit comments

Comments
 (0)