From bf2e7fea4b22fd8d2e6131a65188b8888192e524 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:36:02 -0700 Subject: [PATCH 1/3] csharp rules: the per-tier tally counts each tier once, not once per site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The souffle profile on a 2,250-file subject put 9.78s of a 36s solve — the single hottest rule — in call_chain_summary producing 7 rows: driven by call_class rows, the aggregate re-counted a tier once per SITE in it. call_class_kind(cls) drives it once per DISTINCT tier, the same fix the python engine's site_class_kind carries. Serial solve on identical facts: 36s -> 22s (-39%), every output relation sorted-identical. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/csharp/engine/call-edge-generation/call_chain.dl | 6 +++++- graph/csharp/souffle/decls_all.dl | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl index b44340cf..69647979 100644 --- a/graph/csharp/engine/call-edge-generation/call_chain.dl +++ b/graph/csharp/engine/call-edge-generation/call_chain.dl @@ -485,6 +485,10 @@ call_site_dropped(e) :- csite("client", e), !site_in_output(e). // call_chain_summary(Tier, Count) -- the per-tier tally, which is the one number a // run is judged on. +// One count per DISTINCT tier: driven by call_class rows, the aggregate re-counted the +// tier once per SITE in it (sites x tiers — 9.8s of a 36s solve for 7 rows, the single +// hottest rule of the C# profile). Same fix as the python engine's site_class_kind. +call_class_kind(cls) :- call_class("client", _, cls). call_chain_summary(cls, n) :- - call_class("client", _, cls), + call_class_kind(cls), n = count : { call_class("client", _, cls) }. diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl index 2bb9352f..b1faa46b 100644 --- a/graph/csharp/souffle/decls_all.dl +++ b/graph/csharp/souffle/decls_all.dl @@ -65,6 +65,7 @@ .decl call_callee_name(c0:symbol,c1:symbol,c2:symbol) .decl call_caller_unknown(c0:symbol,c1:symbol) .decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol) +.decl call_class_kind(c0:symbol) .decl call_chain_summary(c0:symbol,c1:number) .decl call_class(c0:symbol,c1:symbol,c2:symbol) .decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) From ee351632e3976c329160061383c94ba461f9a4bd Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:51:12 -0700 Subject: [PATCH 2/3] python rules, round three: the pairing key materialized, the leftover attribute prefixes adopted, the mro lookup hoisted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-two profile on a django-sized subject named three shapes, all familiar: - root_expr_at(p, rc, scope, line, e) holds each context's root expression WITH the keys the statement pairings join on. Written inline, iter_pair and with_pair each enumerated one side's roots and probed scope and span per candidate — 22s between them for ~6K rows; each pairing is now one indexed join. - five attribute-access clauses still carried the expr_node/expr_parent prefix inline — the context-test variants (STORE / not-STORE / not-callee) that round two's regex missed because atoms sit between the prefix parts. They now probe attr_access and keep their context atoms: instance_attr_method_value, expr_denotes_method, element_type_of and kin, ~27s of the profile. - replaced_method_of(p, c, n, def) hoists mro_lookup_decorated's pure-syntax member lookup below the fixpoint (12s re-derived every iteration for 783 rows). Gate on identical staged django facts, same machine: serial solve 193s -> 124s (-33%; cumulative with rounds one and two, 885s -> 124s, 7.1x), every output relation sorted-identical. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../engine/resolution/attribute-lookup.dl | 9 +++++-- .../python/engine/resolution/builtin-types.dl | 3 +-- graph/python/engine/resolution/iteration.dl | 27 ++++++++++--------- graph/python/engine/resolution/value-flow.dl | 6 ++--- graph/python/souffle/decls_all.dl | 2 ++ 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/graph/python/engine/resolution/attribute-lookup.dl b/graph/python/engine/resolution/attribute-lookup.dl index 4fbc0982..3c032d13 100644 --- a/graph/python/engine/resolution/attribute-lookup.dl +++ b/graph/python/engine/resolution/attribute-lookup.dl @@ -152,10 +152,15 @@ mro_lookup_data_shadow(p, t, n, f) :- // factory), while mro_lookup is upstream of it and uses negation — folding this in makes // that negation cyclic and souffle refuses the program. Consumed by // expression-resolution/callee-resolution.dl, which is downstream of both. +// The class-member lookup is pure syntax and was re-run inside the fixpoint every +// iteration (12s for 783 rows on a mid-size subject); materialized once, keyed (c, n), +// the rule starts from its recursive delta and probes it. +replaced_method_of(p, c, n, def) :- + method_owner(p, c, def), method_decl(p, n, _, _, _, def), + method_target_replaced(p, def). mro_lookup_decorated(p, t, n, r) :- mro_winner(p, t, n, c), - method_owner(p, c, def), method_decl(p, n, _, _, _, def), - method_target_replaced(p, def), + replaced_method_of(p, c, n, def), decorated_name_target(def, r). // ── mro_lookup_opaque(Prov, ReceiverType, Name) ────────────────────────────── diff --git a/graph/python/engine/resolution/builtin-types.dl b/graph/python/engine/resolution/builtin-types.dl index 47c72917..457f3453 100644 --- a/graph/python/engine/resolution/builtin-types.dl +++ b/graph/python/engine/resolution/builtin-types.dl @@ -472,8 +472,7 @@ expr_container_annotation_ref(p, src, r) :- // the same hop through ANY object whose type is known, not only self -- the fix that // #61 / #63 had to make three times over in the other typing files. expr_container_annotation_ref(p, src, r) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), type_ref_owner(p, f, "FIELD", r), type_ref(p, _, "FIELD_TYPE", _, _, r). diff --git a/graph/python/engine/resolution/iteration.dl b/graph/python/engine/resolution/iteration.dl index 4f32f14b..7a3e3279 100644 --- a/graph/python/engine/resolution/iteration.dl +++ b/graph/python/engine/resolution/iteration.dl @@ -36,12 +36,19 @@ iter_pair(p, tgt, src) :- for_statement_roots("FOR_TARGET", "FOR_ITERABLE"). for_statement_roots("ASYNC_FOR_TARGET", "ASYNC_FOR_ITERABLE"). +// ── root_expr_at(Prov, RootContext, Scope, Line, Expr) — the pairing key, ONCE ── +// Both statement pairings below match a target root against a source root by scope and +// line; written inline, each pairing enumerated every root of one context and probed +// scope/span per candidate (22s between them for ~6K rows). Materialized with the key +// in the columns, each pairing is one indexed join. +root_expr_at(p, rc, s, line, e) :- + expr_root_context(p, rc, e), !expr_has_parent(p, e), + expr_scope(p, s, _, e), expr_span(p, line, _, e). + iter_pair(p, tgt, src) :- for_statement_roots(trc, irc), - expr_root_context(p, trc, tgt), !expr_has_parent(p, tgt), - expr_root_context(p, irc, src), !expr_has_parent(p, src), - expr_scope(p, s, _, tgt), expr_scope(p, s, _, src), - expr_span(p, line, _, tgt), expr_span(p, line, _, src). + root_expr_at(p, trc, s, line, tgt), + root_expr_at(p, irc, s, line, src). // ── with_pair(Prov, TargetExprHash, ContextExprHash) — `with X() as y` (#128) ── // The SAME shape as a for statement, and it lives here for that reason: the parser emits @@ -52,10 +59,8 @@ with_statement_roots("ASYNC_WITH_TARGET", "ASYNC_WITH_CONTEXT"). with_pair(p, tgt, cm) :- with_statement_roots(trc, crc), - expr_root_context(p, trc, tgt), !expr_has_parent(p, tgt), - expr_root_context(p, crc, cm), !expr_has_parent(p, cm), - expr_scope(p, s, _, tgt), expr_scope(p, s, _, cm), - expr_span(p, line, _, tgt), expr_span(p, line, _, cm). + root_expr_at(p, trc, s, line, tgt), + root_expr_at(p, crc, s, line, cm). // The two names this needs, stated as facts rather than written into a rule body: one is a // dunder the language defines, the other the typing spelling for "the enclosing class". @@ -125,14 +130,12 @@ element_type_of(p, src, t) :- // self-only, here the ELEMENT type of a container held on an attribute is. The self // clauses stay as the special case; nothing about what an element type means changes. element_type_of(p, src, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_declared_element(p, f, t). element_type_of(p, src, t) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, src), n != "", - expr_parent(p, src, "ATTRIBUTE_OBJECT", _, obj), + attr_access(p, src, n, obj), expr_type(p, obj, ot), type_attr_field(p, ot, n, f), field_element_from_param(p, f, t). diff --git a/graph/python/engine/resolution/value-flow.dl b/graph/python/engine/resolution/value-flow.dl index c7b5c9a3..14ef74b5 100644 --- a/graph/python/engine/resolution/value-flow.dl +++ b/graph/python/engine/resolution/value-flow.dl @@ -230,9 +230,8 @@ type_attr_class_value(p, t, n, c) :- // never a single edge to the assigned value. `self.x = ...` is left to the field rules, // which see it with its write count. instance_attr_method_value(p, t, n, m) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", + attr_access(p, e, n, obj), expr_name_context(p, "STORE", e), - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), !expr_node(p, "SELF_REFERENCE", _, _, obj), expr_type(p, obj, t), assign_pair(p, e, val), @@ -242,10 +241,9 @@ instance_attr_method_value(p, t, n, m) :- // denotes what was assigned to it, beside the bound class method expr_denotes_bound_method // already gives it. expr_denotes_method(p, e, m) :- - expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "", + attr_access(p, e, n, obj), !expr_name_context(p, "STORE", e), !call_callee_expr(_, e), - expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj), expr_type(p, obj, t), instance_attr_method_value(p, t, n, m). diff --git a/graph/python/souffle/decls_all.dl b/graph/python/souffle/decls_all.dl index 16583b79..d6b069c6 100644 --- a/graph/python/souffle/decls_all.dl +++ b/graph/python/souffle/decls_all.dl @@ -260,6 +260,7 @@ .decl mro_tie(c0:symbol,c1:symbol,c2:symbol) .decl mro_lookup(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_data_shadow(c0:symbol,c1:symbol,c2:symbol,c3:symbol) +.decl replaced_method_of(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_decorated(c0:symbol,c1:symbol,c2:symbol,c3:symbol) .decl mro_lookup_opaque(c0:symbol,c1:symbol,c2:symbol) .decl mro_definer_after(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) @@ -402,6 +403,7 @@ .decl method_owner_declares_param(c0:symbol,c1:symbol,c2:symbol) .decl call_returns_type(c0:symbol,c1:symbol) .decl call_returns_element(c0:symbol,c1:symbol) +.decl root_expr_at(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl iter_pair(c0:symbol,c1:symbol,c2:symbol) .decl with_pair(c0:symbol,c1:symbol,c2:symbol) .decl with_target_declared(c0:symbol,c1:symbol,c2:symbol) From 36726032553ebfdee9ac600c4bc245ff5b7cbf8f Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:22:13 -0700 Subject: [PATCH 3/3] javascript rules: the two cross-product anchors that were half the solve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The souffle profile on a 257MB app put 3.1 of 6 minutes in ONE clause: carry_step anchored on expr_binding visited every USE of every variable and re-scanned the assignments for each. var_assigned_value(v, c) enumerates the assignments once, keyed by the variable, and carry_step is one indexed join. state_leak's instance-write clause crossed every (type, entry) pair with every assignment in the program — 33s for two rows; func_written_onto (t, f, owner) enumerates the assignment side once, keyed (type, func). Gate on identical staged facts, same machine: serial solve 280s -> 81s (-71%), every output relation sorted-identical. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- graph/javascript/engine/resolution/instance-state.dl | 6 +++++- graph/javascript/engine/resolution/value-flow.dl | 7 ++++++- graph/javascript/souffle/decls_all.dl | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/graph/javascript/engine/resolution/instance-state.dl b/graph/javascript/engine/resolution/instance-state.dl index 95409a3c..df747c53 100644 --- a/graph/javascript/engine/resolution/instance-state.dl +++ b/graph/javascript/engine/resolution/instance-state.dl @@ -106,10 +106,14 @@ state_outer_carry(t, k2, i2, d + 1) :- state_outer_carry(t, k, i, d), d < 3, k ! state_outer_carry(t, k2, i2, d + 1) :- state_outer_carry(t, k, i, d), d < 3, (k = "arr" ; k = "coll"), elem_value(i, k2, i2), k2 != "alloc". state_leak(t, f) :- state_enters(t, f, _), state_outer_carry(t, "func", f, _). // written onto an instance of T or onto T itself from outside its instance code -state_leak(t, f) :- state_enters(t, f, _), expr_kind(_, "ASSIGNMENT", _, a), expr_owner(_, em, _, a), !state_world(em, t), +// The assignment side enumerated ONCE, keyed (t, f): anchored on state_enters the +// clause crossed every (type, entry) pair with every assignment in the program — 33s +// for 2 rows on the profile. +func_written_onto(t, f, em) :- expr_kind(_, "ASSIGNMENT", _, a), expr_owner(_, em, _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_child(_, tgt, "ACCESS_TARGET", _, r), expr_value(r, k, t), (k = "inst" ; k = "ctor"), expr_child(_, a, "ASSIGNMENT_VALUE", _, val), expr_value(val, "func", f). +state_leak(t, f) :- state_enters(t, f, _), func_written_onto(t, f, em), !state_world(em, t). state_leak(t, f) :- state_enters(t, f, _), prop_value("ctor", t, _, "func", f). // an entry that says nothing about which instance it gives F to state_leak(t, f) :- state_enters(t, f, ce), state_entry_open(ce). diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl index d6f8c132..2c79e52b 100644 --- a/graph/javascript/engine/resolution/value-flow.dl +++ b/graph/javascript/engine/resolution/value-flow.dl @@ -493,8 +493,13 @@ carry_step(x, c) :- expr_kind(_, "BINARY", _, x), expr_operator(_, op, x), binar carry_step(x, c) :- expr_kind(_, "ASSIGNMENT", _, x), expr_child(_, x, "ASSIGNMENT_VALUE", _, c). carry_step(x, c) :- expr_kind(_, "SEQUENCE", _, x), sequence_last(x, c). carry_step(x, c) :- expr_binding(_, v, x), var_init(_, _, c, v). -carry_step(x, c) :- expr_binding(_, v, x), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op), +// An assignment writing variable v the value c, KEYED BY THE VARIABLE and enumerated +// once. Anchored on expr_binding the clause below visited every USE of every variable +// and re-scanned the assignments for each: 3.1 minutes of a 6-minute solve, half the +// profile, for 16.5K rows. +var_assigned_value(v, c) :- expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, op, a), assignment_op_writes_value(op), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt), expr_binding(_, v, tgt), expr_child(_, a, "ASSIGNMENT_VALUE", _, c). +carry_step(x, c) :- expr_binding(_, v, x), var_assigned_value(v, c). carry_rest(e, x) :- carry_part(e, x), !carries_param_call(x, _). // What the carrying return holds read context-insensitively: every caller's argument // merged (and nothing once the wrapper is over the fan cap). Only the fallbacks below diff --git a/graph/javascript/souffle/decls_all.dl b/graph/javascript/souffle/decls_all.dl index 2f25337d..b7b56647 100644 --- a/graph/javascript/souffle/decls_all.dl +++ b/graph/javascript/souffle/decls_all.dl @@ -384,6 +384,7 @@ .decl wrapper_param_replaced(c0:symbol, c1:number) .decl wrapper_param_written(c0:symbol, c1:number, c2:symbol) .decl carry_part(c0:symbol, c1:symbol) +.decl var_assigned_value(c0:symbol,c1:symbol) .decl carry_step(c0:symbol, c1:symbol) .decl carry_rest(c0:symbol, c1:symbol) .decl carried_value(c0:symbol, c1:symbol, c2:symbol) @@ -598,6 +599,7 @@ .decl state_entry_open(c0:symbol) .decl state_carry(c0:symbol, c1:symbol, c2:symbol, c3:number) .decl state_enters(c0:symbol, c1:symbol, c2:symbol) +.decl func_written_onto(c0:symbol,c1:symbol,c2:symbol) .decl state_leak(c0:symbol, c1:symbol) .decl state_outer_ref(c0:symbol, c1:symbol) .decl state_outer_carry(c0:symbol, c1:symbol, c2:symbol, c3:number)