9696from typing_extensions import TypedDict , Required
9797
9898from collections import defaultdict
99+ from types import MappingProxyType
99100
100101from tokenizer .definitions import BIN_Tuple
101102
@@ -126,9 +127,13 @@ class ResultDict(TypedDict, total=False):
126127BonusCache = Dict [Tuple [BIN_Terminal , str , BIN_Terminal , BIN_Token ], int ]
127128FinalsDict = Dict [int , Set [BIN_Terminal ]]
128129TokensDict = Dict [int , BIN_Token ]
130+ # Winning family index and per-family lists of (child, context signature)
131+ DecisionTuple = Tuple [int , List [List [Tuple [Node , Any ]]]]
129132
130- # Reducer result dictionary with a null score
131- NULL_SC : ResultDict = {"sc" : 0 }
133+ # Reducer result dictionary with a null score, shared between
134+ # empty nodes; wrapped in a read-only proxy so that it cannot
135+ # be corrupted by accidental modification
136+ NULL_SC : ResultDict = cast (ResultDict , MappingProxyType ({"sc" : 0 }))
132137
133138_VERB_PREP_BONUS = 7 # Give 7 extra points for a verb/preposition match
134139_VERB_PREP_PENALTY = - 2 # Subtract 2 points for a non-match
@@ -196,43 +201,35 @@ def add_child(self, ix: int, rd: ResultDict) -> None:
196201 if key == "sl" :
197202 self .reducer .set_current_verb (rd ["sl" ])
198203
199- def process (self , node : Node ) -> ResultDict :
204+ def process (self , node : Node ) -> Tuple [ ResultDict , int ] :
200205 """After accumulating scores for all possible productions
201206 of this nonterminal (families of children), find the
202- highest scoring one and reduce the tree to that child only"""
207+ highest scoring one and return its result dict along with
208+ its family index. The actual reduction of the tree is
209+ deferred to a separate pass, once all contexts in which
210+ this node occurs have been scored."""
203211 try :
204212
205213 csc = self .sc
206214 if not csc :
207215 # Empty node
208- return NULL_SC
216+ return NULL_SC , 0
209217
210218 nt = node .nonterminal if node .is_completed else None
211219
212220 if len (csc ) == 1 :
213221 # Not ambiguous: only one result, do a shortcut
214- # Will raise an exception if not exactly one value
215- [sc ] = csc .values ()
222+ # Will raise an exception if not exactly one item
223+ [( ix , sc ) ] = csc .items ()
216224 else :
217- # Eliminate all families except the best scoring one
218- # Sort in decreasing order by score, using the family index
219- # as a tie-breaker for determinism
220- s = sorted (csc .items (), key = lambda x : (x [1 ]["sc" ], - x [0 ]), reverse = True )
221- # This is the best scoring family
222- # (and the one with the lowest index
223- # if there are many with the same score)
224- ix , sc = s [0 ]
225- # If the node nonterminal is marked as "no_reduce",
226- # we leave the child families in place. This feature
227- # is used in query processing.
228- if nt is None or not nt .no_reduce :
229- # And now for the key action of the reducer:
230- # Eliminate all other families
231- node .reduce_to (ix )
225+ # Find the best scoring family, using the lowest
226+ # family index as a tie-breaker for determinism
227+ ix , sc = max (csc .items (), key = lambda x : (x [1 ]["sc" ], - x [0 ]))
232228
233229 if nt is not None :
234- # We will be adjusting the result: make sure we do so on
235- # a separate dict copy (we don't want to clobber the child's dict)
230+ # Adjust the winning family's score. Note that sc is this
231+ # node's own accumulator dict (created in add_child()),
232+ # not a child's memoized dict, so we can modify it freely.
236233 # Get score adjustment for this nonterminal, if any
237234 # (This is the $score(+/-N) pragma from Greynir.grammar)
238235 sc ["sc" ] += self .reducer ._score_adj .get (nt , 0 )
@@ -264,7 +261,7 @@ def process(self, node: Node) -> ResultDict:
264261 sc .pop ("so" , None ) # Simpler than if "so" in sc: del sc["so"]
265262 sc .pop ("sl" , None )
266263
267- return sc
264+ return sc , ix
268265
269266 finally :
270267 # Make sure we pop everything that was pushed in __init__()
@@ -430,10 +427,18 @@ def context_sig(w: Node) -> Any:
430427 # an enclosed enable_prep_bonus node would capture)
431428 return (vsig (self .get_prep_bonus ()), vsig (self .get_current_verb ()))
432429
433- def calc_score (w : Node ) -> ResultDict :
430+ # Winning family index and per-family child lists (with the
431+ # context signatures they were scored under), keyed like visited.
432+ # This records the scoring pass's decisions so that the actual
433+ # reduction can be deferred until all contexts have been scored.
434+ decisions : Dict [Tuple [Node , Any ], DecisionTuple ] = dict ()
435+
436+ def calc_score (w : Node , sig : Any ) -> ResultDict :
434437 """Calculate the score of the subtree rooted at w within
435- the current verb context, memoized on (node, context)"""
436- sig = context_sig (w )
438+ the current verb context, memoized on (node, context).
439+ The sig parameter is context_sig(w), computed by the caller.
440+ This pass does not modify the forest; it only scores it
441+ and records the winning family of each (node, context)."""
437442 # Has this (node, context) combination been memoized?
438443 v = visited .get ((w , sig ))
439444 if v is not None :
@@ -447,31 +452,66 @@ def calc_score(w: Node) -> ResultDict:
447452 # of children, i.e. multiple possible derivations:
448453 # Init container for family results
449454 scope = _ReductionScope (self , w )
455+ fam_children : List [List [Tuple [Node , Any ]]] = []
450456 # Go through each family and calculate its score
451457 for family_ix , (prod , children ) in enumerate (w ._families ):
452458 scope .start_family (family_ix , prod )
459+ this_fam : List [Tuple [Node , Any ]] = []
453460 for ch in children :
454461 if ch is not None :
455- scope .add_child (family_ix , calc_score (ch ))
456- # Return a dict describing the winning family of children
457- # (derivation) including an "sc" field for its score.
458- # !!! TODO: We might be pruning the parse forest too
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.
463- v = scope .process (w )
464- # The winning family is now the only remaining family
465- # of children of this node; the others have been culled.
462+ chsig = context_sig (ch )
463+ this_fam .append ((ch , chsig ))
464+ scope .add_child (family_ix , calc_score (ch , chsig ))
465+ fam_children .append (this_fam )
466+ # Obtain a dict describing the winning family of children
467+ # (derivation), including an "sc" field for its score,
468+ # along with the winning family index
469+ v , chosen_ix = scope .process (w )
470+ decisions [(w , sig )] = (chosen_ix , fam_children )
466471 else :
467472 v = NULL_SC
468473 # Memoize the result for this (node, context) combination
469474 visited [(w , sig )] = v
470475 w .score = v ["sc" ]
471476 return v
472477
473- # Start the scoring and reduction process at the root
474- return calc_score (root_node )
478+ # Nodes already reduced in the reduction pass. A node shared
479+ # between contexts can only be physically reduced one way; the
480+ # first context to reach it in the winning tree decides.
481+ reduced : Set [Node ] = set ()
482+
483+ def apply_reduction (w : Node , sig : Any ) -> None :
484+ """Second pass: walk the winning tree, reducing each node
485+ to the family that won in the context the node is actually
486+ used in, as recorded by the scoring pass"""
487+ if w in reduced :
488+ return
489+ reduced .add (w )
490+ entry = decisions .get ((w , sig ))
491+ if entry is None :
492+ # Token or empty node: nothing to reduce
493+ return
494+ chosen_ix , fam_children = entry
495+ nt = w .nonterminal if w .is_completed else None
496+ if nt is not None and nt .no_reduce :
497+ # Leave the child families of this nonterminal in place;
498+ # this feature is used in query processing
499+ for fam in fam_children :
500+ for ch , chsig in fam :
501+ apply_reduction (ch , chsig )
502+ else :
503+ # The key action of the reducer:
504+ # eliminate all families except the winning one
505+ w .reduce_to (chosen_ix )
506+ for ch , chsig in fam_children [chosen_ix ]:
507+ apply_reduction (ch , chsig )
508+
509+ # First pass: score the forest without modifying it
510+ root_sig = context_sig (root_node )
511+ result = calc_score (root_node , root_sig )
512+ # Second pass: reduce the forest to the winning tree
513+ apply_reduction (root_node , root_sig )
514+ return result
475515
476516
477517class OptionFinder (ParseForestNavigator ):
0 commit comments