@@ -334,7 +334,7 @@ export class ContextTokenization {
334334 }
335335
336336 /**
337- * Given the existing tokenization and an incoming input `Transform`, this
337+ * Given this existing tokenization and an incoming input `Transform`, this
338338 * method precomputes how both the current, pre-application tokenization will
339339 * be altered and how the incoming Transform will be tokenized.
340340 *
@@ -351,158 +351,7 @@ export class ContextTokenization {
351351 transform : Transform ,
352352 edgeOptions ?: EdgeWindowOptions
353353 ) : TokenizationTransitionEdits {
354- // Step 4: now that our window's been properly updated, determine what the
355- // input's effects on the context is.
356- //
357- // Context does not slide within this function.
358- //
359- // Assumption: this alignment cannot fail; we KNOW there's a solid
360- // before-and-after relationship here, and we can base it on the results of
361- // a prior syncToSourceWindow call.
362- //
363- // We don't wish to do the full tokenization here - we only want to check
364- // over the last few tokens that might reasonably shift. We also want to
365- // batch effects.
366-
367- // Do not mutate the original transform; it can cause unexpected assertion
368- // effects in unit tests.
369- const edgeTransform = { ...transform , deleteRight : transform . deleteRight || 0 } ;
370- const edgeWindow = buildEdgeWindow ( this . tokens , edgeTransform , false , edgeOptions ) ;
371- const {
372- retokenizationText,
373- editBoundary,
374- sliceIndex : edgeSliceIndex
375- } = edgeWindow ;
376- // Prevent mutation of the original return property.
377- const stackedDeletes = edgeWindow . deleteLengths . slice ( ) ;
378-
379- const tokenize = determineModelTokenizer ( lexicalModel ) ;
380- const postTokenization = tokenize ( { left : retokenizationText + transform . insert , startOfBuffer : true , endOfBuffer : true } ) . left . map ( t => t . text ) ;
381- if ( postTokenization . length == 0 ) {
382- postTokenization . push ( '' ) ;
383- }
384- const { stackedInserts, firstInsertPostIndex } = traceInsertEdits ( postTokenization , transform ) ;
385-
386- // What does the edge's retokenization look like when we remove the inserted portions?
387- const retokenizedEdge = postTokenization . slice ( 0 , firstInsertPostIndex ) ;
388- const insertBoundaryToken = postTokenization [ firstInsertPostIndex ] ;
389-
390- // Note: requires that helpers have not mutated `stackedInserts`.
391- const uninsertedBoundaryToken = KMWString . substring ( insertBoundaryToken , 0 , KMWString . lastIndexOf ( insertBoundaryToken , stackedInserts [ 0 ] ) ) ;
392-
393- // Do not preserve empty tokens here, even if tokenization normally would produce one.
394- // It's redundant and replaceable for tokenization batching efforts.
395- if ( uninsertedBoundaryToken != '' ) {
396- retokenizedEdge . push ( uninsertedBoundaryToken ) ;
397- }
398-
399- // We've found the root token within the root context state to which deletes (and inserts)
400- // may be applied.
401- // We've also found the last post-application token to which transform changes contributed.
402- // How do these indices line up - we need to properly construct and index our transforms,
403- // but 'merge' and 'split' edits can mess up that indexing.
404-
405- const currentTokens = this . tokens ;
406- const preTokenization = currentTokens
407- . slice ( edgeSliceIndex , editBoundary . tokenIndex + 1 )
408- . map ( t => t . exampleInput ) ;
409-
410- // Determine the effects of splits & merges as applied to the original
411- // cached context state.
412- const { mergeOffset, splitOffset, editPath, merges, splits } = analyzePathMergesAndSplits (
413- preTokenization ,
414- postTokenization . slice ( 0 , firstInsertPostIndex + 1 )
415- ) ;
416-
417- /*
418- * Final steps: We can now safely index the transforms. Let's do it!
419- * 1. Determine the first index a Transform may align to
420- * 2. Build the transforms
421- *
422- * Notes:
423- * - text applied to the end of a 'merged' token at the tail: should have
424- * index 0, not -1.
425- * - pretokenization index will mismatch by -1: -SUM(merge size - 1)
426- * - Ex: can + ' + t => can't
427- * -1 0 0
428- * - text applied to the end of a 'split' token at the tail: should also
429- * have index 0, not 1.
430- * - posttokenization index will mismatch by +1: SUM(split size - 1)
431- * - new token after 'split': index 1
432- * - Ex: can' + ? => can + ' + ?
433- * 0 -1 0 1
434- *
435- * The first transform applies at the end of the retokenized zone and its
436- * associated index. The question: were there deletes that occurred?
437- */
438-
439- const lastEditedPreTokenIndex = editBoundary . tokenIndex - edgeSliceIndex ;
440- let shiftDeletes = false ;
441- // first popped entry == 0 - a delete no-op.
442- if ( stackedDeletes [ stackedDeletes . length - 1 ] == 0 ) {
443- // the boundary indices found by both methods above differ
444- if ( lastEditedPreTokenIndex + mergeOffset != firstInsertPostIndex + splitOffset ) {
445- shiftDeletes = true ;
446- }
447-
448- // there are no inserts, so we don't affect the boundary token we landed on.
449- if ( stackedDeletes . length > 1 && transform . insert == '' ) {
450- shiftDeletes = true ;
451- }
452- }
453-
454- if ( shiftDeletes ) {
455- // Do not add a zero-length delete if we're not actually altering the
456- // corresponding token at all.
457- stackedDeletes . pop ( ) ;
458- }
459-
460- // The first delete always applies to index 0. If the built edge window
461- // omits a context-final empty-string, adjust the tokenization indices
462- // accordingly.
463- const tailIndex = 0 - ( stackedDeletes . length - 1 ) + ( editBoundary . omitsEmptyToken ? - 1 : 0 ) ;
464- // Mutates stackedInserts, stackedDeletes.
465- const baseRemovedTokenCount = Math . max ( 0 , stackedDeletes . length - stackedInserts . length ) ;
466- const transformMap = assembleTransforms ( stackedInserts , stackedDeletes , tailIndex ) ;
467-
468- // If there's an empty transform in the 0 position and we already know we're
469- // dropping tokens - and only deleting - we're dropping an
470- // otherwise-untracked empty token - make sure it's included!
471- const droppedFinalTransform = baseRemovedTokenCount > 0 && transform . insert == '' && TransformUtils . isEmpty ( transformMap . get ( 0 ) ) ;
472- // Past that, if we have more delete entries than insert entries for our transforms, we
473- // dropped some tokens outright.
474- const removedTokenCount = baseRemovedTokenCount + ( droppedFinalTransform ? 1 : 0 ) ;
475-
476- // Final step: check for any unexpected boundary shifts not mappable to 'merge' / 'split'
477- // and not caused by transforms. All transforms always apply in sequence at the end.
478- const unmappedEdits : EditTuple < EditOperation > [ ] = [ ] ;
479- for ( let i = 0 ; i < editPath . length - transformMap . size ; i ++ ) {
480- const op = editPath [ i ] . op ;
481- switch ( op ) {
482- case 'merge' :
483- case 'split' :
484- // already calculated
485- // can fall through to the `continue;` line.
486- case 'match' :
487- continue ;
488- default :
489- // Should only be substitutions here.
490- // We may wish to add extra analysis in the future when supporting
491- // prediction from multiple competing tokenizations.
492- unmappedEdits . push ( editPath [ i ] as EditTuple < EditOperation > ) ;
493- }
494- }
495-
496- return {
497- alignment : {
498- edgeWindow : { ...edgeWindow , retokenization : retokenizedEdge } ,
499- merges,
500- splits,
501- unmappedEdits,
502- removedTokenCount
503- } ,
504- tokenizedTransform : transformMap ,
505- } ;
354+ return mapWhitespacedTokenization ( this . tokens , lexicalModel , transform , edgeOptions ) ;
506355 }
507356
508357 /**
@@ -763,6 +612,190 @@ interface RetokenizedEdgeWindow extends EdgeWindow {
763612 retokenization : string [ ] ;
764613}
765614
615+ export interface ContextTokenLike {
616+ exampleInput : string ;
617+ isPartial ?: boolean ;
618+ sourceRangeKey ?: string ;
619+ }
620+
621+ /**
622+ * Given an existing tokenization and an incoming input `Transform`, this
623+ * method precomputes how both the current, pre-application tokenization will
624+ * be altered and how the incoming Transform will be tokenized.
625+ *
626+ * This function is able to operate with a reduced interface, not requiring
627+ * the full ContextToken/ContextState/etc subsystem and its related
628+ * SearchQuotientNode requirements.
629+ *
630+ * Note that this method is designed for use with languages that employ
631+ * classical space-based wordbreaking. Do not use it for languages that need
632+ * dictionary-based wordbreaking support!
633+ * @param tokens
634+ * @param lexicalModel
635+ * @param transform
636+ * @param edgeOptions
637+ * @returns
638+ */
639+ export function mapWhitespacedTokenization (
640+ tokens : ContextTokenLike [ ] ,
641+ lexicalModel : LexicalModel ,
642+ transform : Transform ,
643+ edgeOptions ?: EdgeWindowOptions
644+ ) : TokenizationTransitionEdits {
645+ // Step 4: now that our window's been properly updated, determine what the
646+ // input's effects on the context is.
647+ //
648+ // Context does not slide within this function.
649+ //
650+ // Assumption: this alignment cannot fail; we KNOW there's a solid
651+ // before-and-after relationship here, and we can base it on the results of
652+ // a prior syncToSourceWindow call.
653+ //
654+ // We don't wish to do the full tokenization here - we only want to check
655+ // over the last few tokens that might reasonably shift. We also want to
656+ // batch effects.
657+
658+ // Do not mutate the original transform; it can cause unexpected assertion
659+ // effects in unit tests.
660+ const edgeTransform = { ...transform , deleteRight : transform . deleteRight || 0 } ;
661+ const edgeWindow = buildEdgeWindow ( tokens , edgeTransform , false , edgeOptions ) ;
662+ const {
663+ retokenizationText,
664+ editBoundary,
665+ sliceIndex : edgeSliceIndex
666+ } = edgeWindow ;
667+ // Prevent mutation of the original return property.
668+ const stackedDeletes = edgeWindow . deleteLengths . slice ( ) ;
669+
670+ const tokenize = determineModelTokenizer ( lexicalModel ) ;
671+ const postTokenization = tokenize ( { left : retokenizationText + transform . insert , startOfBuffer : true , endOfBuffer : true } ) . left . map ( t => t . text ) ;
672+ if ( postTokenization . length == 0 ) {
673+ postTokenization . push ( '' ) ;
674+ }
675+ const { stackedInserts, firstInsertPostIndex } = traceInsertEdits ( postTokenization , transform ) ;
676+
677+ // What does the edge's retokenization look like when we remove the inserted portions?
678+ const retokenizedEdge = postTokenization . slice ( 0 , firstInsertPostIndex ) ;
679+ const insertBoundaryToken = postTokenization [ firstInsertPostIndex ] ;
680+
681+ // Note: requires that helpers have not mutated `stackedInserts`.
682+ const uninsertedBoundaryToken = KMWString . substring ( insertBoundaryToken , 0 , KMWString . lastIndexOf ( insertBoundaryToken , stackedInserts [ 0 ] ) ) ;
683+
684+ // Do not preserve empty tokens here, even if tokenization normally would produce one.
685+ // It's redundant and replaceable for tokenization batching efforts.
686+ if ( uninsertedBoundaryToken != '' ) {
687+ retokenizedEdge . push ( uninsertedBoundaryToken ) ;
688+ }
689+
690+ // We've found the root token within the root context state to which deletes (and inserts)
691+ // may be applied.
692+ // We've also found the last post-application token to which transform changes contributed.
693+ // How do these indices line up - we need to properly construct and index our transforms,
694+ // but 'merge' and 'split' edits can mess up that indexing.
695+
696+ const currentTokens = tokens ;
697+ const preTokenization = currentTokens
698+ . slice ( edgeSliceIndex , editBoundary . tokenIndex + 1 )
699+ . map ( t => t . exampleInput ) ;
700+
701+ // Determine the effects of splits & merges as applied to the original
702+ // cached context state.
703+ const { mergeOffset, splitOffset, editPath, merges, splits } = analyzePathMergesAndSplits (
704+ preTokenization ,
705+ postTokenization . slice ( 0 , firstInsertPostIndex + 1 )
706+ ) ;
707+
708+ /*
709+ * Final steps: We can now safely index the transforms. Let's do it!
710+ * 1. Determine the first index a Transform may align to
711+ * 2. Build the transforms
712+ *
713+ * Notes:
714+ * - text applied to the end of a 'merged' token at the tail: should have
715+ * index 0, not -1.
716+ * - pretokenization index will mismatch by -1: -SUM(merge size - 1)
717+ * - Ex: can + ' + t => can't
718+ * -1 0 0
719+ * - text applied to the end of a 'split' token at the tail: should also
720+ * have index 0, not 1.
721+ * - posttokenization index will mismatch by +1: SUM(split size - 1)
722+ * - new token after 'split': index 1
723+ * - Ex: can' + ? => can + ' + ?
724+ * 0 -1 0 1
725+ *
726+ * The first transform applies at the end of the retokenized zone and its
727+ * associated index. The question: were there deletes that occurred?
728+ */
729+
730+ const lastEditedPreTokenIndex = editBoundary . tokenIndex - edgeSliceIndex ;
731+ let shiftDeletes = false ;
732+ // first popped entry == 0 - a delete no-op.
733+ if ( stackedDeletes [ stackedDeletes . length - 1 ] == 0 ) {
734+ // the boundary indices found by both methods above differ
735+ if ( lastEditedPreTokenIndex + mergeOffset != firstInsertPostIndex + splitOffset ) {
736+ shiftDeletes = true ;
737+ }
738+
739+ // there are no inserts, so we don't affect the boundary token we landed on.
740+ if ( stackedDeletes . length > 1 && transform . insert == '' ) {
741+ shiftDeletes = true ;
742+ }
743+ }
744+
745+ if ( shiftDeletes ) {
746+ // Do not add a zero-length delete if we're not actually altering the
747+ // corresponding token at all.
748+ stackedDeletes . pop ( ) ;
749+ }
750+
751+ // The first delete always applies to index 0. If the built edge window
752+ // omits a context-final empty-string, adjust the tokenization indices
753+ // accordingly.
754+ const tailIndex = 0 - ( stackedDeletes . length - 1 ) + ( editBoundary . omitsEmptyToken ? - 1 : 0 ) ;
755+ // Mutates stackedInserts, stackedDeletes.
756+ const baseRemovedTokenCount = Math . max ( 0 , stackedDeletes . length - stackedInserts . length ) ;
757+ const transformMap = assembleTransforms ( stackedInserts , stackedDeletes , tailIndex ) ;
758+
759+ // If there's an empty transform in the 0 position and we already know we're
760+ // dropping tokens - and only deleting - we're dropping an
761+ // otherwise-untracked empty token - make sure it's included!
762+ const droppedFinalTransform = baseRemovedTokenCount > 0 && transform . insert == '' && TransformUtils . isEmpty ( transformMap . get ( 0 ) ) ;
763+ // Past that, if we have more delete entries than insert entries for our transforms, we
764+ // dropped some tokens outright.
765+ const removedTokenCount = baseRemovedTokenCount + ( droppedFinalTransform ? 1 : 0 ) ;
766+
767+ // Final step: check for any unexpected boundary shifts not mappable to 'merge' / 'split'
768+ // and not caused by transforms. All transforms always apply in sequence at the end.
769+ const unmappedEdits : EditTuple < EditOperation > [ ] = [ ] ;
770+ for ( let i = 0 ; i < editPath . length - transformMap . size ; i ++ ) {
771+ const op = editPath [ i ] . op ;
772+ switch ( op ) {
773+ case 'merge' :
774+ case 'split' :
775+ // already calculated
776+ // can fall through to the `continue;` line.
777+ case 'match' :
778+ continue ;
779+ default :
780+ // Should only be substitutions here.
781+ // We may wish to add extra analysis in the future when supporting
782+ // prediction from multiple competing tokenizations.
783+ unmappedEdits . push ( editPath [ i ] as EditTuple < EditOperation > ) ;
784+ }
785+ }
786+
787+ return {
788+ alignment : {
789+ edgeWindow : { ...edgeWindow , retokenization : retokenizedEdge } ,
790+ merges,
791+ splits,
792+ unmappedEdits,
793+ removedTokenCount
794+ } ,
795+ tokenizedTransform : transformMap ,
796+ } ;
797+ }
798+
766799/**
767800 * Constructs a window on one side of the represented context that is aligned to
768801 * existing tokenization.
@@ -777,7 +810,7 @@ interface RetokenizedEdgeWindow extends EdgeWindow {
777810 * @returns
778811 */
779812export function buildEdgeWindow (
780- currentTokens : ContextToken [ ] ,
813+ currentTokens : ContextTokenLike [ ] ,
781814 // Requires deleteRight be explicitly set.
782815 transform : Transform & { deleteRight : number } ,
783816 applyAtFront : boolean ,
0 commit comments