nx_disctree.nx source
↩ module page · 231 lines · 8211 B
1// nx_disctree.nx -- discrimination tree term index.
2//
3// Per Vampire-displacement roadmap Phase 2. THE biggest perf win
4// for saturation provers: replaces the O(n_processed) iteration in
5// forward subsumption + demodulation candidate lookup with an O(d)
6// trie walk where d is the term depth. Vampire's hot path goes
7// from O(n^2) to O(n * d * branching) per saturation step.
8//
9// Standard "perfect discrimination tree" (Stickel 1989 lineage):
10// - Trie keyed on preorder linearization of terms.
11// - Each token is (sym, n_args); a STAR token marks variables.
12// - Edges from a node match by (sym, n_args).
13// - Insertion: walk the linearization, allocate edges + nodes
14// as needed; mark terminal node with the stored value.
15// - Lookup (find generalizations of Q): at each tree node, follow
16// EITHER the matching (sym, n_args) edge OR a STAR edge. STAR
17// consumes one whole subterm of Q (skip via pending-counter).
18//
19// API:
20// nx_dt_new() -> *DiscTree
21// nx_dt_insert(tree, term, value) -- attach value
22// nx_dt_find_generalizations(tree, query, results, *n)
23//
24// Bits-up nx_int. Variables in queries are not yet handled (TODO):
25// for v1 the caller passes ground queries. Variables in stored
26// terms ARE handled via the STAR edge.
27
28// nx_safety_envelope:
29// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
30// sil_target: SIL1
31// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
32// verdict: NOT_YET_EVALUATED
33
34import "nx_syscalls.nx"
35import "nx_runtime.nx"
36import "nx_tier.nx"
37import "nx_result.nx"
38import "nx_unify.nx"
39
40// 0 is reserved as wildcard -- real sym_ids start at NX_TPTP_SYM_BASE
41// (1000), so 0 is safe to use as the STAR sentinel.
42const NX_DT_STAR: nx_int = 0
43const NX_DT_MAX_TOKENS: nx_int = 256
44const NX_DT_MAX_RESULTS: nx_int = 64
45
46struct DTToken {
47 sym: nx_int,
48 n_args: nx_int,
49}
50const NX_DT_TOKEN_BYTES: nx_int = 16
51
52struct DTEdge {
53 sym: nx_int, // NX_DT_STAR for wildcard
54 n_args: nx_int,
55 child: *DTNode,
56 next: *DTEdge,
57}
58const NX_DT_EDGE_BYTES: nx_int = 32
59
60struct DTNode {
61 edges: *DTEdge,
62 has_value: nx_int,
63 value: nx_int,
64}
65const NX_DT_NODE_BYTES: nx_int = 24
66
67struct DiscTree {
68 root: *DTNode,
69}
70const NX_DT_BYTES: nx_int = 8
71
72// ===== Constructors =================================================
73func nx_dt_node_new() -> *DTNode {
74 let n: *DTNode = (sys_mmap(NX_DT_NODE_BYTES as i64)) as *DTNode
75 n.edges = 0 as *DTEdge
76 n.has_value = 0
77 n.value = 0
78 return n
79}
80
81func nx_dt_new() -> *DiscTree {
82 let t: *DiscTree = (sys_mmap(NX_DT_BYTES as i64)) as *DiscTree
83 t.root = nx_dt_node_new()
84 return t
85}
86
87// Find or create the child node reached by the (sym, n_args) edge.
88func nx_dt_node_descend(node: *DTNode, sym: nx_int, n_args: nx_int) -> *DTNode {
89 var e: *DTEdge = node.edges
90 while ((e as nx_int) != 0) {
91 if e.sym == sym {
92 if e.n_args == n_args { return e.child }
93 }
94 e = e.next
95 }
96 // Not found -- allocate.
97 let new_child: *DTNode = nx_dt_node_new()
98 let new_edge: *DTEdge = (sys_mmap(NX_DT_EDGE_BYTES as i64)) as *DTEdge
99 new_edge.sym = sym
100 new_edge.n_args = n_args
101 new_edge.child = new_child
102 new_edge.next = node.edges
103 node.edges = new_edge
104 return new_child
105}
106
107// ===== Linearization ===============================================
108// Emit preorder tokens for term t into out_tokens; advance *out_n.
109func nx_dt_linearize(t: *Term, out_tokens: *DTToken, out_n: *nx_int) {
110 if out_n[0] >= NX_DT_MAX_TOKENS { return }
111 let slot: *DTToken = ((out_tokens as nx_int) + (out_n[0] * NX_DT_TOKEN_BYTES)) as *DTToken
112 if t.kind == NX_TERM_VAR {
113 slot.sym = NX_DT_STAR
114 slot.n_args = 0
115 out_n[0] = out_n[0] + 1
116 return
117 }
118 slot.sym = t.sym
119 slot.n_args = t.n_args
120 out_n[0] = out_n[0] + 1
121 if t.kind == NX_TERM_CONST { return }
122 var i: nx_int = 0
123 while i < t.n_args {
124 nx_dt_linearize(nx_term_arg(t, i), out_tokens, out_n)
125 i = i + 1
126 }
127}
128
129// ===== Insert =====================================================
130func nx_dt_insert(tree: *DiscTree, t: *Term, value: nx_int) {
131 let tokens: *DTToken = (sys_mmap((NX_DT_MAX_TOKENS * NX_DT_TOKEN_BYTES) as i64)) as *DTToken
132 let n_p: *nx_int = (sys_mmap(8)) as *nx_int
133 n_p[0] = 0
134 nx_dt_linearize(t, tokens, n_p)
135
136 var cur: *DTNode = tree.root
137 var i: nx_int = 0
138 while i < n_p[0] {
139 let tok: *DTToken = ((tokens as nx_int) + (i * NX_DT_TOKEN_BYTES)) as *DTToken
140 cur = nx_dt_node_descend(cur, tok.sym, tok.n_args)
141 i = i + 1
142 }
143 cur.has_value = 1
144 cur.value = value
145}
146
147// ===== Skip a subterm in the linearized stream =====================
148// Starting at `cursor`, consume tokens until exactly one whole subterm
149// has been consumed. Each token "takes 1, gives n_args" -- start with
150// pending=1, decrement per token + add token's arity. When pending
151// hits 0, return the position past the consumed subterm.
152func nx_dt_skip_subterm(tokens: *DTToken, n: nx_int, cursor: nx_int) -> nx_int {
153 var pending: nx_int = 1
154 var pos: nx_int = cursor
155 while pos < n {
156 if pending == 0 { return pos }
157 let tok: *DTToken = ((tokens as nx_int) + (pos * NX_DT_TOKEN_BYTES)) as *DTToken
158 pending = pending - 1
159 pending = pending + tok.n_args
160 pos = pos + 1
161 }
162 return pos
163}
164
165// ===== Find generalizations =======================================
166func nx_dt_walk(node: *DTNode, tokens: *DTToken, n_tokens: nx_int,
167 cursor: nx_int, results: *nx_int, n_results: *nx_int) {
168 if cursor >= n_tokens {
169 if node.has_value == 1 {
170 if n_results[0] < NX_DT_MAX_RESULTS {
171 results[n_results[0]] = node.value
172 n_results[0] = n_results[0] + 1
173 }
174 }
175 return
176 }
177 let qtok: *DTToken = ((tokens as nx_int) + (cursor * NX_DT_TOKEN_BYTES)) as *DTToken
178
179 var e: *DTEdge = node.edges
180 while ((e as nx_int) != 0) {
181 if e.sym == NX_DT_STAR {
182 // Wildcard edge: consume one whole subterm of the query.
183 let skip_to: nx_int = nx_dt_skip_subterm(tokens, n_tokens, cursor)
184 nx_dt_walk(e.child, tokens, n_tokens, skip_to, results, n_results)
185 }
186 if e.sym != NX_DT_STAR {
187 if e.sym == qtok.sym {
188 if e.n_args == qtok.n_args {
189 nx_dt_walk(e.child, tokens, n_tokens, cursor + 1, results, n_results)
190 }
191 }
192 }
193 e = e.next
194 }
195}
196
197func nx_dt_find_generalizations(tree: *DiscTree, query: *Term,
198 results: *nx_int, n_results: *nx_int) {
199 let tokens: *DTToken = (sys_mmap((NX_DT_MAX_TOKENS * NX_DT_TOKEN_BYTES) as i64)) as *DTToken
200 let n_q_p: *nx_int = (sys_mmap(8)) as *nx_int
201 n_q_p[0] = 0
202 nx_dt_linearize(query, tokens, n_q_p)
203 n_results[0] = 0
204 nx_dt_walk(tree.root, tokens, n_q_p[0], 0, results, n_results)
205}
206
207// Delete every leaf with stored value == `target` -- recursively
208// walks the entire tree. Used by saturation backward subsumption to
209// clean up entries pointing at tombstoned clauses, preventing the
210// monotonic memory growth that would otherwise OOM on long-running
211// problems (e.g. pel012's 118-clause Tseitin output).
212//
213// Implementation: the leaves don't get physically removed (that would
214// require parent-pointer bookkeeping); instead has_value is set to 0
215// so the lookup walker ignores them. Memory footprint stays the
216// same per individual deletion, but the search-time cost of those
217// branches drops to "follow edge, check has_value, skip".
218func nx_dt_delete_value_node(node: *DTNode, target: nx_int) {
219 if node.has_value == 1 {
220 if node.value == target { node.has_value = 0 }
221 }
222 var e: *DTEdge = node.edges
223 while ((e as nx_int) != 0) {
224 nx_dt_delete_value_node(e.child, target)
225 e = e.next
226 }
227}
228
229func nx_dt_delete_value(tree: *DiscTree, target: nx_int) {
230 nx_dt_delete_value_node(tree.root, target)
231}