diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts index ccb8540cd3..e4aebf1856 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts @@ -45,10 +45,12 @@ export class LegacyQuotientRoot extends SearchQuotientRoot { } this.processed.push(node); + const result = new TokenResultMapping(this, node); + this.saveResult(result); return { type: 'complete', cost: node.currentCost, - mapping: new TokenResultMapping(this, node) + mapping: result }; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts index 05cfd7c9d6..47cdcdb262 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts @@ -42,7 +42,6 @@ export class LegacyQuotientSpur extends SearchQuotientSpur { const codepointLength = space.codepointLength + insertLength - inputSample.deleteLeft; super(space, inputs, inputSource, codepointLength); - this.queueNodes(this.buildEdgesFromResults(space.previousResults)); this.insertLength = insertLength; this.leftDeleteLength = inputSample.deleteLeft; return; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts index 451353ba27..0b2396bd3c 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts @@ -23,20 +23,12 @@ const PATH_QUEUE_COMPARATOR: QueueComparator = (a, b) => { // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. -export class SearchQuotientCluster implements SearchQuotientNode { +export class SearchQuotientCluster extends SearchQuotientNode { private selectionQueue: PriorityQueue = new PriorityQueue(PATH_QUEUE_COMPARATOR); readonly spaceId: number; - // We use an array and not a PriorityQueue b/c batch-heapifying at a single point in time - // is cheaper than iteratively building a priority queue. - /** - * This tracks all paths that have reached the end of a viable input-matching path - even - * those of lower cost that produce the same correction as other paths. - * - * When new input is received, its entries are then used to append edges to the path in order - * to find potential paths to reach a new viable end. - */ - private completedPaths?: TokenResultMapping[] = []; + // We use an array and not a PriorityQueue b/c batch-heapifying at a single + // point in time is cheaper than iteratively building a priority queue. /** * Acts as a Map that prevents duplicating a correction-search path if reached @@ -60,6 +52,8 @@ export class SearchQuotientCluster implements SearchQuotientNode { * @param inboundPaths */ constructor(inboundPaths: SearchQuotientNode[]) { + super(); + if(inboundPaths.length == 0) { throw new Error("SearchQuotientCluster requires an array with at least one SearchQuotientNode"); } @@ -86,7 +80,6 @@ export class SearchQuotientCluster implements SearchQuotientNode { this.spaceId = generateSpaceSeed(); this.lowestPossibleSingleCost = lowestPossibleSingleCost; - this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => new TokenResultMapping(this, r)); this.selectionQueue.enqueueAll(inboundPaths); return; @@ -140,25 +133,21 @@ export class SearchQuotientCluster implements SearchQuotientNode { */ public handleNextNode(): PathResult { const bestPath = this.selectionQueue.dequeue(); - const currentResult = bestPath.handleNextNode(); + const baseResult = bestPath.handleNextNode(); this.selectionQueue.enqueue(bestPath); - let finalResult = currentResult; - if(currentResult.type == 'complete') { + let finalResult = baseResult; + if(baseResult.type == 'complete') { finalResult = { - ...currentResult, - mapping: new TokenResultMapping(this, currentResult.mapping) + ...baseResult, + mapping: new TokenResultMapping(this, baseResult.mapping) }; - this.completedPaths.push(finalResult.mapping); + this.saveResult(finalResult.mapping); } return finalResult; } - public get previousResults(): TokenResultMapping[] { - return this.completedPaths; - } - get model(): LexicalModelTypes.LexicalModel { return this.parents[0].model; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts index efd160c43c..f9e64cfc0e 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts @@ -80,23 +80,35 @@ export interface PathInputProperties { * Represents all or a portion of the dynamically-generated graph used to search * for predictive-text corrections. */ -export interface SearchQuotientNode extends CorrectionSearchable { +export abstract class SearchQuotientNode implements CorrectionSearchable { + /** + * Holds all `incomingNode` child buffers - buffers to hold nodes processed by + * this SearchCluster but not yet by child SearchSpaces. + */ + private childQueues: TokenResultMapping[][] = []; + + /** + * Marks all results that have already been returned from this instance of SearchPath. + * Should be deleted and cleared if any paths consider this one as a parent. + */ + private returnedValues?: {[resultKey: string]: TokenResultMapping} = {}; + /** * Returns an identifier uniquely identifying this search-batching structure * by correction-search results. */ - readonly spaceId: number; + abstract get spaceId(): number; /** * The active LexicalModel for use with correction-search. */ - readonly model: LexicalModel; + abstract get model(): LexicalModel; /** * Notes the SearchQuotientNode(s) whose correction-search paths are extended by this * SearchQuotientNode. */ - readonly parents: SearchQuotientNode[]; + abstract get parents(): SearchQuotientNode[]; /** * Retrieves the lowest-cost / lowest-distance edge from the batcher's search @@ -104,20 +116,20 @@ export interface SearchQuotientNode extends CorrectionSearchable; + abstract handleNextNode(): PathResult /** * Increases the editing range that will be considered for determining * correction distances. */ - increaseMaxEditDistance(): void; + abstract increaseMaxEditDistance(): void; /** * Reports the cost of the lowest-cost / lowest-distance edge held within the * batcher's search area. * @returns */ - readonly currentCost: number; + abstract get currentCost(): number; /** * Provides a heuristic for the base cost at this path's depth if the best @@ -127,19 +139,21 @@ export interface SearchQuotientNode extends CorrectionSearchable buf.push(result)); + return true; + } } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts index 681b40c6aa..2f68f632dd 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts @@ -11,10 +11,9 @@ import LexicalModel = LexicalModelTypes.LexicalModel; // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. -export class SearchQuotientRoot implements SearchQuotientNode { +export class SearchQuotientRoot extends SearchQuotientNode { readonly rootNode: SearchNode; readonly model: LexicalModel; - private readonly rootResult: TokenResultMapping; readonly lowestPossibleSingleCost: number = 0; @@ -25,19 +24,19 @@ export class SearchQuotientRoot implements SearchQuotientNode { private hasBeenProcessed: boolean = false; /** - * Constructs a fresh SearchQuotientRoot instance to be used as the root of - * the predictive-text correction / suggestion search process. + * Constructs a fresh SearchSpace instance for used in predictive-text correction + * and suggestion searches. * @param baseSpaceId * @param model */ constructor(model: LexicalModel) { + super(); if(!model.traverseFromRoot) { throw new Error("The active lexical model does not support traversal-based searching."); } this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t)); this.model = model; - this.rootResult = new TokenResultMapping(this, this.rootNode); } get spaceId(): number { @@ -75,10 +74,12 @@ export class SearchQuotientRoot implements SearchQuotientNode { this.hasBeenProcessed = true; + const result = new TokenResultMapping(this, this.rootNode); + this.saveResult(result); return { type: 'complete', cost: 0, - mapping: new TokenResultMapping(this, this.rootNode) + mapping: result }; } @@ -86,14 +87,6 @@ export class SearchQuotientRoot implements SearchQuotientNode { return this.hasBeenProcessed ? Number.POSITIVE_INFINITY : 0; } - get previousResults(): TokenResultMapping[] { - if(!this.hasBeenProcessed) { - return []; - } else { - return [this.rootResult]; - } - } - // Return a new array each time; avoid aliasing potential! get inputSegments(): InputSegment[] { return []; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts index f1d1f5091d..95330316ce 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts @@ -31,8 +31,15 @@ export const QUEUE_NODE_COMPARATOR: QueueComparator = function(arg1, // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. -export abstract class SearchQuotientSpur implements SearchQuotientNode { +export abstract class SearchQuotientSpur extends SearchQuotientNode { private selectionQueue: PriorityQueue = new PriorityQueue(QUEUE_NODE_COMPARATOR); + + /** + * Holds all incoming Nodes generated from a parent `SearchSpace` that have not yet been + * extended with this `SearchSpace`'s input. + */ + private incomingNodes: TokenResultMapping[] = []; + readonly inputs?: Distribution; readonly inputSource?: PathInputProperties; @@ -44,12 +51,6 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { public abstract readonly insertLength: number; public abstract readonly leftDeleteLength: number - /** - * Marks all results that have already been returned from this instance of SearchPath. - * Should be deleted and cleared if any paths consider this one as a parent. - */ - private returnedValues?: {[resultKey: string]: SearchNode} = {}; - /** * Provides a heuristic for the base cost at this path's depth if the best * individual input were taken here, regardless of whether or not that's possible. @@ -74,6 +75,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { inputSource: PathInputProperties | ProbabilityMass, codepointLength: number ) { + super(); this.spaceId = generateSpaceSeed(); // Coerce inputSource to TokenInputSource format. @@ -101,6 +103,9 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { this.inputs = inputs?.length > 0 ? inputs : null; this.inputCount = parentNode.inputCount + (this.inputs ? 1 : 0); this.codepointLength = codepointLength; + + this.queueNodes(this.buildEdgesFromResults(parentNode.previousResults)); + this.linkAndQueueFromParent(parentNode, this.incomingNodes); } public get model(): LexicalModel { @@ -334,6 +339,11 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { * @returns */ public handleNextNode(): PathResult { + if(this.incomingNodes.length > 0) { + this.buildEdgesFromResults(this.incomingNodes); + this.incomingNodes = []; + } + const parentCost = this.parentNode?.currentCost ?? Number.POSITIVE_INFINITY; const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY; @@ -346,6 +356,9 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { if(parentCost <= localCost) { const result = this.parentNode.handleNextNode(); + // The parent will insert the node into our queue. We don't need it, though + // any siblings certainly will. + this.incomingNodes = []; if(result.type == 'complete') { this.queueNodes(this.buildEdgesFromResults([result.mapping])); @@ -392,20 +405,20 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { // OK, we fully crossed a graph edge and have landed on a transition point; // time to build more edges / edge batches. + const result = new TokenResultMapping(this, currentNode); if(currentNode.spaceId == this.spaceId) { - if(this.returnedValues) { - if((this.returnedValues[currentNode.resultKey]?.currentCost ?? Number.POSITIVE_INFINITY) > currentNode.currentCost) { - this.returnedValues[currentNode.resultKey] = currentNode; - } else { - // Not a better cost, so reject it and move on to the next potential result. - return this.handleNextNode(); - } + // Verify that we don't already have a better-cost result for the + // correction already. + const isUnhandled = this.saveResult(result); + if(!isUnhandled) { + // Not a better cost, so reject it and move on to the next potential result. + return this.handleNextNode(); } return { type: 'complete', cost: currentNode.currentCost, - mapping: new TokenResultMapping(this, currentNode) + mapping: result }; } @@ -413,10 +426,6 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { return unmatchedResult; } - public get previousResults(): TokenResultMapping[] { - return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(this, v)); - } - public get inputSegments(): InputSegment[] { if(!this.parentNode) { return []; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts index 987aa30ab2..b88cdf6147 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts @@ -26,4 +26,4 @@ export { ModelCompositor } from './model-compositor.js'; export * from './predict-helpers.js'; export { default as TransformUtils } from './transformUtils.js' export { default as LMLayerWorker } from './index.js' -export * from './transform-subsets.js'; \ No newline at end of file +export * from './transform-subsets.js';