7878 memoize the score of these nodes by context, so that the scores
7979 between contexts can be different.
8080
81- Verb contexts span from tree nodes tagged with "enable_prep_bonus"
82- (typically the SagnInnskot nonterminal), through their descendant nodes down
83- to those tagged with "begin_prep_scope" or "purge_prep", or noun phrase
84- nonterminal nodes ("Nl_*"). The preposition nodes that actually receive
85- different scores depending on the context are terminal nodes whose names
86- have the form fs_*.
81+ This is done by memoizing subtree scores on the tuple (node, context
82+ signature), where the context signature captures the two pieces of
83+ reducer state that a subtree score can depend on: the preposition
84+ bonus verbs in effect (established by nodes tagged "enable_prep_bonus",
85+ typically SagnInnskot) and the current verb (which an enclosed
86+ "enable_prep_bonus" node would capture). Nodes that reset both on
87+ entry - those tagged "begin_prep_scope", and noun phrase nonterminals
88+ ("Nl_*") - are context-independent and get a neutral signature,
89+ preserving full sharing of their memoized scores. The preposition
90+ nodes that actually receive different scores depending on the context
91+ are terminal nodes whose names have the form fs_*.
8792
8893"""
8994
@@ -121,7 +126,6 @@ class ResultDict(TypedDict, total=False):
121126BonusCache = Dict [Tuple [BIN_Terminal , str , BIN_Terminal , BIN_Token ], int ]
122127FinalsDict = Dict [int , Set [BIN_Terminal ]]
123128TokensDict = Dict [int , BIN_Token ]
124- KeyTuple = Tuple [Node , int ]
125129
126130# Reducer result dictionary with a null score
127131NULL_SC : ResultDict = {"sc" : 0 }
@@ -132,11 +136,6 @@ class ResultDict(TypedDict, total=False):
132136
133137_CASES_SET = BIN_Token .CASES_SET
134138
135- # Tags of nonterminals that allow us to stop copying nodes
136- # in the preposition unpacker
137- _PREP_SCOPE_SET = frozenset (
138- ("begin_prep_scope" , "purge_prep" , "no_prep" , "enable_prep_bonus" )
139- )
140139_CONTAINED_VERBS_SET = frozenset (("begin_prep_scope" , "purge_verb" ))
141140
142141# BÍN categories ('fl') of person and entity names
@@ -386,61 +385,60 @@ def visit_token(self, node: Node) -> ResultDict:
386385 return d
387386
388387 def go (self , root_node : Node ) -> ResultDict :
389- """Perform the reduction, but first split the tree underneath
390- nodes that have the enable_prep_bonus tag"""
391-
392- # Memoization/caching dict, keyed by node and memoization key
393- visited : Dict [KeyTuple , ResultDict ] = dict ()
394- # Current memoization key
395- current_key = 0
396- # Next memoization key to use
397- next_key = 0
398-
399- def enter_key_scope (node : Node ) -> bool :
400- """Return True for a node whose score should not be
401- memoized within the shared packed parse forest"""
402- if not node .is_completed or node .nonterminal is None :
403- return False
404- return node .nonterminal .has_tag ("enable_prep_bonus" )
405-
406- def exit_key_scope (node : Node ) -> bool :
407- """Return True if it is safe to resume memoization
408- of subtree scores from this node onwards"""
409- if not node .is_completed :
410- return False
411- nt = node .nonterminal
412- if nt is not None :
413- if nt .has_any_tag (_PREP_SCOPE_SET ):
414- # Entering a subtree that has its own scope:
415- # resume memoization until further notice
416- return True
417- if nt .is_noun_phrase :
418- # Once we've gone through a preposition node,
419- # it is safe to memoize the enclosed noun phrase subtree
420- return True
421- if node .is_empty :
422- # Explicitly nullable nonterminal with no child:
423- # always OK to memoize
424- return True
425- return False
388+ """Perform the reduction, scoring shared packed subtrees
389+ separately for each distinct verb context they occur in"""
390+
391+ # Memoization/caching dict, keyed by node and context signature
392+ visited : Dict [Tuple [Node , Any ], ResultDict ] = dict ()
393+ # Signature of a context-independent subtree
394+ NEUTRAL : Tuple [Any , Any ] = (None , None )
395+
396+ def vsig (vl : Optional [VerbList ]) -> Any :
397+ """Hashable identity signature of a verb list"""
398+ return (
399+ None
400+ if vl is None
401+ else tuple ((id (t ), id (tok )) for t , tok in vl )
402+ )
403+
404+ def context_sig (w : Node ) -> Any :
405+ """Return the part of the reducer state that the score of
406+ the subtree rooted at w can depend on. Subtrees that are
407+ shared between different verb contexts must be scored
408+ separately for each context, since preposition terminals
409+ within them receive different verb/preposition bonuses."""
410+ if w ._token is not None :
411+ # A token score depends only on the active
412+ # preposition bonus verbs (and only for fs terminals,
413+ # but the signature is cheap enough to include always)
414+ pb = self .get_prep_bonus ()
415+ return None if pb is None else vsig (pb )
416+ if w .is_completed :
417+ nt = w .nonterminal
418+ if nt is not None and (
419+ nt .has_tag ("begin_prep_scope" ) or nt .is_noun_phrase
420+ ):
421+ # This node resets both the prep bonus zone and the
422+ # current verb on entry, so its score is the same
423+ # in all contexts
424+ return NEUTRAL
425+ if w .is_empty :
426+ # Explicitly nullable nonterminal with no child
427+ return NEUTRAL
428+ # The score may depend both on the enclosing prep bonus zone
429+ # (for enclosed fs terminals) and on the current verb (which
430+ # an enclosed enable_prep_bonus node would capture)
431+ return (vsig (self .get_prep_bonus ()), vsig (self .get_current_verb ()))
426432
427433 def calc_score (w : Node ) -> ResultDict :
428- """Navigate from (w, current_key) where w is a node and current_key
429- is an integer navigation key, carefully controlling the memoization
430- of already visited nodes. When navigating into
431- nodes marked enable_prep_bonus, we create a new unique
432- navigation key, since such nodes - although stored in shared
433- packed form - may have different scores depending on the
434- enclosing (verb) context and thus should not share memoized results.
435- """
436- nonlocal current_key , next_key
437- # Has this (node, current_key) tuple been memoized?
438- v = visited .get ((w , current_key ))
434+ """Calculate the score of the subtree rooted at w within
435+ the current verb context, memoized on (node, context)"""
436+ sig = context_sig (w )
437+ # Has this (node, context) combination been memoized?
438+ v = visited .get ((w , sig ))
439439 if v is not None :
440440 # Yes: return the previously calculated result
441441 return v
442- # We have not seen this (node, current_key) combination before:
443- # reduce it, calculate its score and memoize it
444442 if w ._token is not None :
445443 # Return the score of this terminal option
446444 v = self .visit_token (w )
@@ -454,36 +452,21 @@ def calc_score(w: Node) -> ResultDict:
454452 scope .start_family (family_ix , prod )
455453 for ch in children :
456454 if ch is not None :
457- prev_key = current_key
458- if enter_key_scope (ch ):
459- # This child subtree has an enable_prep_bonus flag:
460- # make sure we navigate separately through it
461- # sincle enclosed prepositions may have different
462- # scores in other subtrees.
463- # Generate a new unique memoization key to use
464- # when navigating through this child subtree.
465- next_key += 1
466- current_key = next_key
467- elif current_key != 0 and exit_key_scope (ch ):
468- # We no longer need a separate memoization key
469- # for this child subtree
470- current_key = 0
471455 scope .add_child (family_ix , calc_score (ch ))
472- current_key = prev_key
473456 # Return a dict describing the winning family of children
474457 # (derivation) including an "sc" field for its score.
475458 # !!! TODO: We might be pruning the parse forest too
476- # !!! early here - there could be a different verb scope
477- # !!! above this node that would cause a different child
478- # !!! to be culled. However a test case to demonstrate this
479- # !!! has yet to be identified/created .
459+ # !!! early here - a node shared between contexts is
460+ # !!! culled by whichever context scores it first, even
461+ # !!! though a later context might prefer a different
462+ # !!! child family .
480463 v = scope .process (w )
481464 # The winning family is now the only remaining family
482465 # of children of this node; the others have been culled.
483466 else :
484467 v = NULL_SC
485- # Memoize the result for this (node, current_key ) combination
486- visited [(w , current_key )] = v
468+ # Memoize the result for this (node, context ) combination
469+ visited [(w , sig )] = v
487470 w .score = v ["sc" ]
488471 return v
489472
0 commit comments