@@ -210,7 +210,7 @@ def __init__(self, normalize=None):
210210 self .predicates = {} # name -> k of a proof-valued predicate
211211 self .atoms = {} # name -> TkpAtom
212212 self .probability_predicates = set ()
213- self .disjunction_rules = [] # (name, rule, expression , reads)
213+ self .disjunction_rules = [] # (name, rule, expr , reads, guards )
214214 self .probability_reads = [] # (owner rule, TkpProbability call)
215215
216216 def IsTkp (self , name ):
@@ -409,16 +409,20 @@ def Contributions(name, rule):
409409 return [(rule , argument )]
410410 return [(rule , None )]
411411
412- def CheckContributionBody (contribution_rule , read_names , name ):
413- """Refuses guards in a contribution body.
414-
415- The runtime compiles the VALUE EXPRESSION of a contribution only:
416- proof reads join by their variables and nothing else executes, so
417- a comparison, an unrelated predicate or a computation in the body
418- would be silently dropped, changing the meaning of the program.
419- Allowed: the aggregation machinery (_MultBodyAggAux), the
420- logica_value binding, and body restatements of the very reads of
421- the value expression."""
412+ def ContributionGuards (contribution_rule , read_names , name ):
413+ """Collects the RELATIONAL GUARDS of a contribution body.
414+
415+ The first slice of the constraints layer: a body conjunct that is
416+ a plain relation over contribution variables filters the keys of
417+ the contribution — `Reach(a, c) TKP= Step(a, s, c) :-
418+ StartCell(a, s)` seeds reachability from the start only. The
419+ guard relation must be materialized and structural (independent
420+ of the learned parameters: the jax seam captures its rows at
421+ probe time). Everything else in a body stays a loud error:
422+ comparisons, computations and unifications would need value-level
423+ masks — the rest of the constraints design — and silently
424+ dropping them would change the meaning of the program."""
425+ guards = []
422426 conjuncts = (contribution_rule .get ('body' ) or {}).get (
423427 'conjunction' , {}).get ('conjunct' , [])
424428 for conjunct in conjuncts :
@@ -428,20 +432,32 @@ def CheckContributionBody(contribution_rule, read_names, name):
428432 continue
429433 if world .normalize (predicate ) in read_names :
430434 continue
431- Error ('TKP: contribution of %s reads %s in its body; the '
432- 'runtime executes only the value expression, so a body '
433- 'guard would be silently ignored. Move the filter into '
434- 'a separate predicate feeding the proof reads.' % (
435- color .Warn (name ), color .Warn (predicate )), name )
435+ if world .IsTkp (predicate ):
436+ Error ('TKP: contribution of %s reads the proof-valued %s '
437+ 'in its body; proof reads belong in the value '
438+ 'expression.' % (color .Warn (name ),
439+ color .Warn (predicate )), name )
440+ fields = CallFields (conjunct ['predicate' ])
441+ variables = []
442+ for field in sorted (fields , key = FieldOrder ):
443+ variable = VariableName (fields [field ])
444+ if variable is None :
445+ Error ('TKP: the guard %s of %s must hold plain '
446+ 'variables.' % (color .Warn (predicate ),
447+ color .Warn (name )), name )
448+ variables .append (variable )
449+ guards .append ((predicate , variables ))
450+ continue
436451 unification = conjunct .get ('unification' )
437452 if (unification is not None and VariableName (
438453 unification ['left_hand_side' ]) == 'logica_value' ):
439454 continue
440- Error ('TKP: contribution of %s has a guard or computation in '
441- 'its body; the runtime executes only the value '
442- 'expression, so it would be silently ignored. Move the '
443- 'filter into a separate predicate feeding the proof '
444- 'reads.' % color .Warn (name ), name )
455+ Error ('TKP: contribution of %s has a comparison or computation '
456+ 'in its body; only relational guards over contribution '
457+ 'variables are supported. Move the rest into a separate '
458+ 'predicate feeding the proof reads.' % color .Warn (name ),
459+ name )
460+ return guards
445461
446462 for name , rule in rules :
447463 operator , argument = AggregationOperator (rule )
@@ -460,7 +476,7 @@ def CheckContributionBody(contribution_rule, read_names, name):
460476 if contributions and contributions [0 ][0 ] is not rule :
461477 # Multi-body form: contributions live in aux rules; the outer
462478 # rule itself must carry nothing beyond the machinery.
463- CheckContributionBody (rule , set (), name )
479+ ContributionGuards (rule , set (), name )
464480 for contribution_rule , expression in contributions :
465481 reads = []
466482 read_names = set ()
@@ -469,12 +485,25 @@ def CheckContributionBody(contribution_rule, read_names, name):
469485 for kind , read in reads :
470486 if kind == 'read' :
471487 read_names .add (world .normalize (CallName (read )))
472- CheckContributionBody (contribution_rule , read_names , name )
488+ guards = ContributionGuards (contribution_rule , read_names ,
489+ name )
473490 if expression is None :
474491 Error ('TKP: cannot find the contribution of %s.' %
475492 color .Warn (name ), name )
493+ known = set (ContributionHeadVariables (contribution_rule , name ))
494+ for kind , read in reads :
495+ if kind == 'read' :
496+ known .update (CallVariables (read , name ))
497+ for guard_name , guard_vars in guards :
498+ for variable in guard_vars :
499+ if variable not in known :
500+ Error ('TKP: the guard %s of %s binds %s, which is not a '
501+ 'contribution variable; a guard only filters '
502+ 'existing keys.' % (
503+ color .Warn (guard_name ), color .Warn (name ),
504+ color .Warn (variable )), name )
476505 world .disjunction_rules .append (
477- (name , contribution_rule , expression , reads ))
506+ (name , contribution_rule , expression , reads , guards ))
478507
479508 # 3. Probability reads: TkpProbability(...) in any expression.
480509 def FindProbabilityReads (node , owner ):
@@ -658,7 +687,7 @@ def __init__(self, name, k, contributions):
658687 super ().__init__ (name )
659688 self .k = k
660689 self .slots = k
661- self .tkp_contributions = contributions # (head_vars , tree, rule)
690+ self .tkp_contributions = contributions # (vars , tree, guards , rule)
662691
663692
664693class TkpProbabilityMember (TkpMemberBase ):
@@ -842,18 +871,18 @@ def CompileMember(self, plan, predicate):
842871 diamond = predicate + self .diamond_suffix
843872 source = (diamond
844873 if any (name == diamond for name , unused_rule ,
845- unused_expression , unused_reads
874+ unused_expression , unused_reads , unused_guards
846875 in self .world .disjunction_rules )
847876 else predicate )
848877 contributions = []
849- for name , rule , expression , unused_reads in (
878+ for name , rule , expression , unused_reads , guards in (
850879 self .world .disjunction_rules ):
851880 if name != source :
852881 continue
853882 head_vars = ContributionHeadVariables (rule , predicate )
854883 tree = BuildContributionTree (expression , self .world ,
855884 self .definitions , predicate )
856- contributions .append ((head_vars , tree , rule ))
885+ contributions .append ((head_vars , tree , guards , rule ))
857886 member = TkpDisjunctionMember (
858887 predicate , self .world .predicates [predicate ], contributions )
859888 member .key_fields , member .key_types = self .Signature (plan , predicate )
@@ -947,6 +976,8 @@ def __init__(self, runtime):
947976 self .domains = runtime .domains
948977 self .units = [] # (compiled members, repetitions, unit names)
949978 self .unit_names = set ()
979+ self .needed_guards = set ()
980+ self .guard_rows = {} # guard relation -> set of key tuples
950981 self .static_masks = {}
951982 self .cache = {}
952983 self .cache_order = []
@@ -957,11 +988,41 @@ def EnsureUnit(self, members, repetitions):
957988 if names in self .unit_names :
958989 return
959990 self .unit_names .add (names )
960- compiled = [(member , [(head_vars , tree ) for head_vars , tree ,
961- unused_rule in member .tkp_contributions ])
991+ compiled = [(member ,
992+ [(head_vars , tree , guards )
993+ for head_vars , tree , guards , unused_rule
994+ in member .tkp_contributions ])
962995 for member in members ]
996+ for unused_member , trees in compiled :
997+ for unused_vars , unused_tree , guards in trees :
998+ self .needed_guards .update (
999+ guard_name for guard_name , unused_guard_vars in guards )
9631000 self .units .append ((compiled , repetitions , ', ' .join (names )))
9641001
1002+ def CaptureGuards (self , state , tensors ):
1003+ """Rows of the guard relations, captured when concrete.
1004+
1005+ Guards are structural — independent of the learned parameters —
1006+ so the probe's concrete capture serves every later (possibly
1007+ traced) call. A guard whose relation never turns up concrete is a
1008+ loud error at solving time."""
1009+ onp = self .onp
1010+ for name in sorted (self .needed_guards - set (self .guard_rows )):
1011+ if name in state :
1012+ mask = state [name ][0 ]
1013+ else :
1014+ try :
1015+ mask = tensors [name ][0 ]
1016+ except Exception :
1017+ continue
1018+ try :
1019+ mask = onp .asarray (mask )
1020+ except Exception :
1021+ continue # Traced; a concrete caller captures it first.
1022+ self .guard_rows [name ] = set (
1023+ tuple (int (i ) for i in key )
1024+ for key in zip (* onp .nonzero (mask )))
1025+
9651026 def ThetaTensor (self , state , tensors ):
9661027 """The fact probabilities: a differentiable gather at the rows."""
9671028 if self .probability_predicate in state :
@@ -1028,8 +1089,19 @@ def Fingerprint(self, compiled, local):
10281089
10291090 def EvalMember (self , member , trees , view , theta ):
10301091 pools = {}
1031- for head_vars , tree in trees :
1092+ for head_vars , tree , guards in trees :
1093+ filters = []
1094+ for guard_name , guard_vars in guards :
1095+ rows = self .guard_rows .get (guard_name )
1096+ if rows is None :
1097+ Error ('TKP: the guard %s never materialized before '
1098+ 'solving; a guard must be a structural relation.' %
1099+ color .Warn (guard_name ), member .name )
1100+ filters .append ((guard_vars , rows ))
10321101 for assignment , proofs in self .EvalTree (tree , view , theta ):
1102+ if any (tuple (assignment [v ] for v in guard_vars ) not in rows
1103+ for guard_vars , rows in filters ):
1104+ continue
10331105 key = tuple (assignment [v ] for v in head_vars )
10341106 pools .setdefault (key , []).extend (proofs )
10351107 return {key : SparseCanonicalize (pool , theta , member .k )
@@ -1140,6 +1212,7 @@ def EvaluateDisjunction(state, tensors):
11401212 # stages run this function (recursion groups register through
11411213 # their own stage with their declared bound).
11421214 solver .EnsureUnit ([member ], 2 )
1215+ solver .CaptureGuards (state , tensors )
11431216 theta_tensor = solver .ThetaTensor (state , tensors )
11441217 theta = ConcreteTheta (theta_tensor )
11451218 return (solver .StaticMask (member , theta ), None )
@@ -1178,6 +1251,7 @@ def BackwardArray(cotangent, theta_values):
11781251 if hasattr (jnp , 'custom' ): # The logix seam.
11791252
11801253 def EvaluateLogix (state , tensors ):
1254+ solver .CaptureGuards (state , tensors )
11811255 theta_tensor = solver .ThetaTensor (state , tensors )
11821256 mask = solver .StaticMask (member , ConcreteTheta (theta_tensor ))
11831257 values = jnp .custom (
@@ -1210,6 +1284,7 @@ def PublishedBackward(theta_tensor, cotangent):
12101284 Published .defvjp (PublishedForward , PublishedBackward )
12111285
12121286 def EvaluateJax (state , tensors ):
1287+ solver .CaptureGuards (state , tensors )
12131288 theta_tensor = solver .ThetaTensor (state , tensors )
12141289 mask = solver .StaticMask (member , ConcreteTheta (theta_tensor ))
12151290 return mask , Published (theta_tensor )
0 commit comments