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 79d1c8d387..ccb8540cd3 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 @@ -48,7 +48,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot { return { type: 'complete', cost: node.currentCost, - mapping: new TokenResultMapping(node, this) + mapping: new TokenResultMapping(this, node) }; } @@ -57,7 +57,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot { } get previousResults(): TokenResultMapping[] { - return this.processed.map((n) => new TokenResultMapping(n, this)); + return this.processed.map((n) => new TokenResultMapping(this, n)); } split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][] { 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 72e79d667d..05cfd7c9d6 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,7 @@ export class LegacyQuotientSpur extends SearchQuotientSpur { const codepointLength = space.codepointLength + insertLength - inputSample.deleteLeft; super(space, inputs, inputSource, codepointLength); - this.queueNodes(this.buildEdgesForNodes(space.previousResults.map(r => r.node))); + this.queueNodes(this.buildEdgesFromResults(space.previousResults)); this.insertLength = insertLength; this.leftDeleteLength = inputSample.deleteLeft; return; @@ -52,19 +52,19 @@ export class LegacyQuotientSpur extends SearchQuotientSpur { return new LegacyQuotientSpur(parentNode, inputs, inputSource) as this; } - protected buildEdgesForNodes(baseNodes: ReadonlyArray) { + protected buildEdgesFromResults(priorResults: ReadonlyArray) { // With a newly-available input, we can extend new input-dependent paths from // our previously-reached 'extractedResults' nodes. - let outboundNodes = baseNodes.map((node) => { + let outboundNodes = priorResults.map((result) => { // Hard restriction: no further edits will be supported. This helps keep the search // more narrowly focused. - const substitutionsOnly = node.editCount == 2; + const substitutionsOnly = result.editCount == 2; let deletionEdges: SearchNode[] = []; if(!substitutionsOnly) { - deletionEdges = node.buildDeletionEdges(this.inputs, this.spaceId); + deletionEdges = result.buildDeletionEdges(this.inputs, this.spaceId); } - const substitutionEdges = node.buildSubstitutionEdges(this.inputs, this.spaceId); + const substitutionEdges = result.buildSubstitutionEdges(this.inputs, this.spaceId); // Skip the queue for the first pass; there will ALWAYS be at least one pass, // and queue-enqueing does come with a cost - avoid unnecessary overhead here. @@ -84,12 +84,12 @@ export class LegacyQuotientSpur extends SearchQuotientSpur { const result = super.handleNextNode(); if(result.type == 'complete') { - const currentNode = result.mapping.node; + const parentResult = result.mapping; // Forbid a raw edit-distance of greater than 2. // Note: .knownCost is not scaled, while its contribution to .currentCost _is_ scaled. - if(currentNode.editCount < 2) { - let insertionEdges = currentNode.buildInsertionEdges(); + if(parentResult.editCount < 2) { + let insertionEdges = parentResult.buildInsertionEdges(); this.queueNodes(insertionEdges); } } 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 02844356d2..a12621c648 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 @@ -12,7 +12,6 @@ import { QueueComparator, PriorityQueue } from '@keymanapp/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { PathResult } from './correction-searchable.js'; -import { SearchNode } from './distance-modeler.js'; import { LegacyQuotientRoot } from './legacy-quotient-root.js'; import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js'; import { SearchQuotientSpur } from './search-quotient-spur.js'; @@ -37,7 +36,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { * 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?: SearchNode[] = []; + private completedPaths?: TokenResultMapping[] = []; /** * Acts as a Map that prevents duplicating a correction-search path if reached @@ -87,8 +86,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { this.spaceId = generateSpaceSeed(); this.lowestPossibleSingleCost = lowestPossibleSingleCost; - this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => r.node); - this.completedPaths.forEach((p) => p.spaceId = this.spaceId); + this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => new TokenResultMapping(this, r)); this.selectionQueue.enqueueAll(inboundPaths); return; @@ -146,16 +144,14 @@ export class SearchQuotientCluster implements SearchQuotientNode { this.selectionQueue.enqueue(bestPath); if(currentResult.type == 'complete') { - const node = currentResult.mapping.node; - node.spaceId = this.spaceId; - this.completedPaths?.push(node); + this.completedPaths.push(new TokenResultMapping(this, currentResult.mapping)); } return currentResult; } public get previousResults(): TokenResultMapping[] { - return this.completedPaths?.map((n) => new TokenResultMapping(n, this)) ?? []; + return this.completedPaths; } get model(): LexicalModelTypes.LexicalModel { 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 fa7768df5b..681b40c6aa 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 @@ -37,7 +37,7 @@ export class SearchQuotientRoot implements SearchQuotientNode { this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t)); this.model = model; - this.rootResult = new TokenResultMapping(this.rootNode, this); + this.rootResult = new TokenResultMapping(this, this.rootNode); } get spaceId(): number { @@ -78,7 +78,7 @@ export class SearchQuotientRoot implements SearchQuotientNode { return { type: 'complete', cost: 0, - mapping: new TokenResultMapping(this.rootNode, this) + mapping: new TokenResultMapping(this, this.rootNode) }; } 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 3c08d98403..f1d1f5091d 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 @@ -321,7 +321,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { return Math.min(localCost, parentCost); } - protected abstract buildEdgesForNodes(baseNodes: ReadonlyArray): SearchNode[]; + protected abstract buildEdgesFromResults(baseNodes: ReadonlyArray): SearchNode[]; protected queueNodes(nodes: SearchNode[]) { this.selectionQueue.enqueueAll(nodes); @@ -348,7 +348,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { const result = this.parentNode.handleNextNode(); if(result.type == 'complete') { - this.queueNodes(this.buildEdgesForNodes([result.mapping.node])); + this.queueNodes(this.buildEdgesFromResults([result.mapping])); } return { @@ -405,7 +405,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { return { type: 'complete', cost: currentNode.currentCost, - mapping: new TokenResultMapping(currentNode, this) + mapping: new TokenResultMapping(this, currentNode) }; } @@ -414,7 +414,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { } public get previousResults(): TokenResultMapping[] { - return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(v, this)); + return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(this, v)); } public get inputSegments(): InputSegment[] { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts index c34a5e6b9b..0024f98794 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts @@ -10,29 +10,27 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CorrectionResultMapping } from "./correction-result-mapping.js"; import { SearchNode, TraversableToken } from "./distance-modeler.js"; import { SearchQuotientNode } from "./search-quotient-node.js"; +import Distribution = LexicalModelTypes.Distribution; import LexiconTraversal = LexicalModelTypes.LexiconTraversal; import ProbabilityMass = LexicalModelTypes.ProbabilityMass; import Transform = LexicalModelTypes.Transform; export function initTokenResultFilterer() { - const priorReturns: Map = new Map(); + const priorReturns: Map = new Map(); const closure = (searchResult: TokenResultMapping) => { - const node = searchResult.node; - - if(node.isFullReplacement) { + if(searchResult.isFullReplacement) { // If the entry's 'match' fully replaces the input string, we consider it // unreasonable and ignore it. Also, if we've reached this point... // we can(?) assume that everything thereafter is as well. return false; } - if((priorReturns.get(node.resultKey)?.currentCost ?? Number.MAX_VALUE) > searchResult.totalCost) { - priorReturns.set(node.resultKey, node); + if((priorReturns.get(searchResult.matchString)?.totalCost ?? Number.MAX_VALUE) > searchResult.totalCost) { + priorReturns.set(searchResult.matchString, searchResult); return true; } else { @@ -43,13 +41,29 @@ export function initTokenResultFilterer() { return closure; } -export class TokenResultMapping implements CorrectionResultMapping { +export class TokenResultMapping { readonly matchingSpace: SearchQuotientNode; - readonly node: SearchNode; + private readonly node: SearchNode; + readonly spaceId: number; - constructor(node: SearchNode, finalQuotientNode: SearchQuotientNode) { + constructor(finalQuotientNode: SearchQuotientNode, node: SearchNode); + constructor(finalQuotientNode: SearchQuotientNode, mapping: TokenResultMapping); + constructor(finalQuotientNode: SearchQuotientNode, mappingBase: SearchNode | TokenResultMapping) { + if(!mappingBase) { + throw new Error("Result-mapping parameters may not be null"); + } this.matchingSpace = finalQuotientNode; - this.node = node; + + if(mappingBase instanceof SearchNode) { + this.node = mappingBase; + this.spaceId = mappingBase.spaceId; + if(mappingBase.spaceId != this.matchingSpace.spaceId) { + throw new Error("QuotientNode and SearchNode .spaceId values provided to TokenResultMapping constructor should be equal"); + } + } else { + this.node = mappingBase.node; + this.spaceId = this.matchingSpace.spaceId; + } } get matchedResult(): Readonly { @@ -68,6 +82,13 @@ export class TokenResultMapping implements CorrectionResultMapping { return this.node.resultKey; } + get editCount(): number { + return this.node.editCount; + } + + get isFullReplacement(): boolean { + return this.node.isFullReplacement; + } /** * Gets the number of Damerau-Levenshtein edits needed to reach the node's * matchString from the output induced by the input sequence used to reach it. @@ -100,4 +121,16 @@ export class TokenResultMapping implements CorrectionResultMapping { get finalTraversal(): LexiconTraversal { return this.node.currentTraversal; } + + buildInsertionEdges(): SearchNode[] { + return this.node.buildInsertionEdges(); + } + + buildDeletionEdges(dist: Distribution, edgeId: number): SearchNode[] { + return this.node.buildDeletionEdges(dist, edgeId); + } + + buildSubstitutionEdges(dist: Distribution, edgeId: number): SearchNode[] { + return this.node.buildSubstitutionEdges(dist, edgeId); + } } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index e9aa0f1ca3..773f8a86b2 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -394,7 +394,7 @@ export function determineSuggestionAlignment( export function buildAndMapPredictions( transition: ContextTransition, tokenization: ContextTokenization, - match: TokenResultMapping + match: Readonly ): CorrectionPredictionTuple[] { const model = transition.final.model; const searchSpace = tokenization.tail.searchModule; @@ -537,7 +537,7 @@ export async function correctAndEnumerate( const correctionPredictionMap: Record> = {}; for await(const match of getBestMatches(searchModules, timer, initTokenResultFilterer())) { // Corrections obtained: now to predict from them! - const tokenization = tokenizations.find(t => t.spaceId == match.node.spaceId); + const tokenization = tokenizations.find(t => t.spaceId == match.spaceId); // If our 'match' results in fully deleting the new token, reject it and try again. if(match.matchSequence.length == 0 && match.inputSequence.length != 0) { @@ -549,7 +549,7 @@ export async function correctAndEnumerate( continue; } - if(match.node.editCount > 0 && !searchModules.find(s => s.correctionsEnabled)) { + if(match.editCount > 0 && !searchModules.find(s => s.correctionsEnabled)) { continue; } diff --git a/web/src/test/auto/headless/engine/predictive-text/helpers/analyzeQuotientNodeResults.ts b/web/src/test/auto/headless/engine/predictive-text/helpers/analyzeQuotientNodeResults.ts index 72c5608a7b..dd1450e49a 100644 --- a/web/src/test/auto/headless/engine/predictive-text/helpers/analyzeQuotientNodeResults.ts +++ b/web/src/test/auto/headless/engine/predictive-text/helpers/analyzeQuotientNodeResults.ts @@ -57,7 +57,7 @@ export function analyzeQuotientNodeResults( let result: PathResult = node.handleNextNode(); while(result.type != 'none') { if(result.type == 'complete') { - const resultKey = result.mapping.node.resultKey; + const resultKey = result.mapping.matchString; if(expectedResults.find((entry) => entry == resultKey)) { matchMap.set(resultKey, (matchMap.get(resultKey) ?? 0) + 1); } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts index 2ad86f9a29..2bcb416197 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts @@ -21,7 +21,7 @@ function buildTestTimer() { } describe('getBestMatches', () => { - const checkRepeatableResults_teh = async (iter: AsyncGenerator) => { + const checkRepeatableResults_teh = async (iter: AsyncGenerator, any, any>) => { const firstIterResult = await iter.next(); // {value: , done: } assert.isFalse(firstIterResult.done);