diff --git a/graph/pipeline/run-souffle.sh b/graph/pipeline/run-souffle.sh index 5bfb4461..320a6646 100755 --- a/graph/pipeline/run-souffle.sh +++ b/graph/pipeline/run-souffle.sh @@ -860,7 +860,14 @@ while [ "$iter" -lt 50 ]; do cores="$( (command -v nproc >/dev/null 2>&1 && nproc) || sysctl -n hw.ncpu 2>/dev/null || echo 4 )" SOLVE_J="${AXIOMCODE_SOLVE_THREADS:-$(( cores < 8 ? cores : 8 ))}" fi + # AXIOM_DL_PROFILE=: solve this run in the souffle INTERPRETER with per-rule profiling + # written to (souffleprof reads it). Slower than the compiled engine, same answer; for + # finding the rules a solve spends its time in, as the java local-use hoist was found. + if [ -n "${AXIOM_DL_PROFILE:-}" ] && command -v souffle >/dev/null 2>&1; then + rc=0; souffle -I "$SRC" -j "$SOLVE_J" -p "$AXIOM_DL_PROFILE" -F "$FACTS" -D "$RAW" "$PROG" || rc=$? + else rc=0; "$BIN" -j "$SOLVE_J" -F "$FACTS" -D "$RAW" || rc=$? + fi if [ "$rc" -ne 0 ]; then if [ "$rc" -eq 132 ] && [ -z "$PACKAGED" ]; then rm -f "$BIN" diff --git a/graph/typescript/engine/config-resolution/entry-points.dl b/graph/typescript/engine/config-resolution/entry-points.dl index 168ff32a..638f4121 100644 --- a/graph/typescript/engine/config-resolution/entry-points.dl +++ b/graph/typescript/engine/config-resolution/entry-points.dl @@ -216,9 +216,7 @@ entry_point(m, "package_export") :- cfg_published_value(x), method_prov(m, "client"). // an instance method read as a value: `engine.produce`, typed through its receiver entry_point(m, "package_export") :- cfg_published_value(x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_type(q, _, t), method_in_scope(t, n, _, m), method_prov(m, "client"). diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index 6fe5e680..c4257044 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -396,15 +396,11 @@ expr_call_candidate(ce, m) :- callee_branch(ce, x), expr_shape(x, s), call_signature_in_scope(s, m). expr_call_candidate(ce, m) :- callee_branch(ce, x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_type(q, _, qt), field_is_callable(qt, n, m). expr_call_candidate(ce, m) :- callee_branch(ce, x), - expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), + property_access_recv(x, n, q), expr_shape(q, s), field_is_callable(s, n, m). diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index c049ce6d..d8b6b23b 100644 --- a/graph/typescript/engine/expression-resolution/expr-type.dl +++ b/graph/typescript/engine/expression-resolution/expr-type.dl @@ -158,9 +158,7 @@ expr_type(e, "client", t) :- expr_referenced("client", "NAMESPACE", t, e), // // Recurses on itself for free: the rule types `Local.Deep` from `Local`, and a third // segment types from that, so depth is not bounded here. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), namespace_member(prov, qt, n, "TYPE", t). @@ -381,9 +379,7 @@ expr_awaited_type(e, prov, t) :- expr_referenced("client", "VARIABLE", v, e), expr_awaited_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p), ref_awaited_target(r, prov, t). -expr_awaited_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_awaited_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f), @@ -401,26 +397,32 @@ property_access_name(e, n) :- expr_kind("client", "PROPERTY_ACCESS", _, e), expr_name("client", n, pn), n != "". +// ── property_access_recv(ExprHash, Name, ReceiverHash) ──────────────────── +// A property access, decomposed ONCE, below the typing fixpoint: the access, its +// name, its receiver. These same three atoms sat inline in 22 rules, most of them +// recursive through expr_type(q, …), and semi-naive evaluation re-anchored the +// PROPERTY_ACCESS scan every iteration — the souffle profile put expr_type's clauses +// at 17K tuples/s with all of the time recursive, on an answer that is pure syntax +// and never changes. Hoisted, each of those rules starts from its recursive delta +// and probes this table by receiver. +property_access_recv(e, n, q) :- expr_kind("client", "PROPERTY_ACCESS", _, e), + property_access_name(e, n), + expr_child("client", e, "RECEIVER", _, q). + // ── PROPERTY ACCESS — `a.b`, where the parser had nothing ────────────────── // The 123k UNKNOWN references. The qualifier is typed first, then the member is // looked up on it, and the member's own declared type becomes the access's type. // Both member tables are searched by the shape of the qualifier: a VALUE qualifier // searches instance members, a TYPE qualifier (a class name) searches statics. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type(f, prov, t). -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_static_type(q, _, qt), field_in_scope(qt, n, "true", f), field_type(f, prov, t). -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_shape(q, qs), field_in_scope(qs, n, _, f), field_type(f, prov, t). @@ -440,17 +442,13 @@ expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // method_return_type / _shape / _element already describe it, exactly as they describe // the result of a call. This only supplies the missing link from the access to the // member. -property_access_getter(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +property_access_getter(e, m) :- property_access_recv(e, n, q), expr_type(q, _, qt), method_in_scope(qt, n, "false", m), method_kind(_, "GETTER", _, m). // A STATIC getter, reached through a class name rather than a value, mirroring the // static field rule above. -property_access_getter(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +property_access_getter(e, m) :- property_access_recv(e, n, q), expr_static_type(q, _, qt), method_in_scope(qt, n, "true", m), method_kind(_, "GETTER", _, m). @@ -462,29 +460,21 @@ expr_shape(e, s) :- property_access_getter(e, m), expr_element_type(e, prov, t) :- property_access_getter(e, m), method_return_element(m, prov, t). -expr_shape(e, s) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_shape(e, s) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_shape(f, s). // A property access on a NAMESPACE — `fs.promises`, `ts.SyntaxKind`. The member // comes from the module's export table, not from a member table. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, mprov, m), module_export(mprov, m, n, "VARIABLE", v), var_type(v, prov, t). -expr_static_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_static_type(e, prov, t) :- property_access_recv(e, n, q), expr_namespace(q, prov, m), module_export(prov, m, n, "TYPE", t). -expr_callable(e, cm) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_callable(e, cm) :- property_access_recv(e, n, q), expr_namespace(q, _, m), module_export(_, m, n, "METHOD", cm). @@ -500,9 +490,7 @@ expr_callable(e, cm) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // same fixpoint, so reading it here would put that negation inside its own cycle. // The ancestor walk is the one method_in_scope (2) performs, with the shadowing // check expressed against the static table directly for the same reason. -expr_callable(e, m) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_callable(e, m) :- property_access_recv(e, n, q), expr_static_type(q, _, t), static_member_value(t, n, m). static_member_value(t, n, m) :- scope_sibling(t, sib), @@ -517,9 +505,7 @@ static_member_declared(t, n) :- scope_sibling(t, sib), // A property access on a namespace TYPE — `N.Inner` where N is a namespace // declaration rather than an imported module. -expr_static_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_static_type(e, prov, t) :- property_access_recv(e, n, q), expr_static_type(q, prov, ns), namespace_member(prov, ns, n, "TYPE", t). @@ -551,9 +537,7 @@ expr_element_shape(e, sh) :- expr_call_candidate(e, callee), expr_element_type(e, prov, t) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p), ref_element_target(r, prov, t). -expr_element_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_element_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f), diff --git a/graph/typescript/engine/resolution/generics.dl b/graph/typescript/engine/resolution/generics.dl index 1d11e6a3..77f3f519 100644 --- a/graph/typescript/engine/resolution/generics.dl +++ b/graph/typescript/engine/resolution/generics.dl @@ -48,9 +48,7 @@ expr_decl_ref(e, r) :- expr_referenced("client", "VARIABLE", v, e), var_type_ref(_, r, v). expr_decl_ref(e, r) :- expr_referenced("client", "PARAMETER", p, e), param_type_ref(_, r, p). -expr_decl_ref(e, r) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_decl_ref(e, r) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_type_ref(_, r, f). diff --git a/graph/typescript/engine/resolution/name-resolution.dl b/graph/typescript/engine/resolution/name-resolution.dl index afd1a6fc..10dc942f 100644 --- a/graph/typescript/engine/resolution/name-resolution.dl +++ b/graph/typescript/engine/resolution/name-resolution.dl @@ -109,9 +109,13 @@ global_name_demanded("String"). // ReadonlyArray's — `slice`, `every`, `map`, `filter`, `join`, `reduce`, `forEach`. global_name_demanded("ReadonlyArray"). -name_binds_any(mod, name, prov, k, ent) :- module_decl("client", _, _, _, _, mod), - global_name_demanded(name), - global_decl_visible(mod, name, k, prov, ent), +// ANCHORED ON global_decl_visible, which already holds exactly the (module, demanded +// name) pairs with a global to offer — its own clauses carry the module_decl and +// global_name_demanded gates. Written with the cross pair first, the plan enumerated +// |client modules| x |demanded names| and probed global_decl_visible per pair: 30s +// between this rule and name_binds' twin on a 1.1M-LOC subject, for 217K rows. +name_binds_any(mod, name, prov, k, ent) :- global_decl_visible(mod, name, k, prov, ent), + module_decl("client", _, _, _, _, mod), !name_bound_locally(mod, name). // ── name_binds(ModuleHash, Name, Prov, Kind, EntityHash) ──────────────────── @@ -125,9 +129,8 @@ name_binds(mod, local, prov, k, ent) :- import_binding("client", _, local, _, mo import_binds(ih, prov, k, ent). name_binds(mod, name, prov, k, ent) :- module_local_decl(prov, mod, name, k, ent), !decl_is_type_only(prov, k, ent). -name_binds(mod, name, prov, k, ent) :- module_decl("client", _, _, _, _, mod), - global_name_demanded(name), - global_decl_visible(mod, name, k, prov, ent), +name_binds(mod, name, prov, k, ent) :- global_decl_visible(mod, name, k, prov, ent), + module_decl("client", _, _, _, _, mod), !decl_is_type_only(prov, k, ent), !name_bound_locally(mod, name). @@ -210,13 +213,16 @@ qualified_name_binds(mod, ctn, prov, k, ent) :- qname_walk(mod, ctn, ctn, prov, // A namespace IMPORT qualifier — `import * as ts from "typescript"; ts.SourceFile`. // The leftmost segment is a module rather than a namespace declaration, so the // member comes from that module's export table instead of from namespace_member. -qualified_name_binds(mod, ctn, prov, k, ent) :- demanded_qualified_name(ctn), +// The qualifier is matched as an EQUALITY KEY, not as a substring test. An import's +// local name holds no dot, so "the demanded name starts with local followed by a dot" +// is exactly "local is one of the demanded name's dot-prefixes" — and qname_dot_prefix +// already enumerates those. Written with the substring test, no index applies and the +// body is |demanded names| x |import bindings| with two substr calls per pair: 29s for +// 805 rows on a 1.1M-LOC subject, the single most expensive rule of the solve. +qualified_name_binds(mod, ctn, prov, k, ent) :- qname_dot_prefix(ctn, local), import_binding("client", _, local, _, mod, ih), import_binds_namespace_any(ih, prov, target), l = strlen(local), - l < strlen(ctn), - substr(ctn, 0, l) = local, - substr(ctn, l, 1) = ".", seg = substr(ctn, l + 1, strlen(ctn) - l - 1), module_export_any(prov, target, seg, k, ent). @@ -236,12 +242,16 @@ qualified_name_binds(mod, ctn, prov, k, ent) :- demanded_qualified_name(ctn), // needed: qname_dot_prefix already enumerates the prefixes, and a prefix that is the // qualifier plus one segment has no interior dot by construction — the next prefix out // would be the one that contains it. +// `local` is joined as another dot-prefix of the SAME demanded name: two prefixes of +// one string nest, so "local is a shorter prefix of dotted ending at a dot" is implied +// by both being dot-prefixes of ctn with strlen(local) < strlen(dotted) — the substring +// tests the first version ran per (binding x prefix) pair are gone, and import_binding +// is probed by local through its index. qname_walk(mod, ctn, dotted, prov, k, ent) :- qname_dot_prefix(ctn, dotted), - import_binding("client", _, local, _, mod, ih), - import_binds_namespace_any(ih, prov, target), + qname_dot_prefix(ctn, local), l = strlen(local), l < strlen(dotted), - substr(dotted, 0, l) = local, - substr(dotted, l, 1) = ".", + import_binding("client", _, local, _, mod, ih), + import_binds_namespace_any(ih, prov, target), seg = substr(dotted, l + 1, strlen(dotted) - l - 1), module_export_any(prov, target, seg, k, ent). diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl index 496892d3..0cf14b85 100644 --- a/graph/typescript/engine/resolution/value-flow.dl +++ b/graph/typescript/engine/resolution/value-flow.dl @@ -111,9 +111,7 @@ field_flow_type(f, prov, t) :- field_initializer("client", e, f), // ── the receiver takes what its field holds ───────────────────────────────── // `this.repo.find()`: the property access is typed by the field's flowed type as well // as its declared one. -expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_type(e, prov, t) :- property_access_recv(e, n, q), expr_type(q, _, qt), field_in_scope(qt, n, "false", f), field_flow_type(f, prov, t). @@ -123,9 +121,7 @@ expr_type(e, prov, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), // fixpoint, whereas an arbitrary qualifier's type comes from expr_type and feeding that // into a negated relation would put the negation inside its own cycle. Java's // type-flow.dl draws the identical line and records the identical reason. -expr_exact_type(e, t) :- expr_kind("client", "PROPERTY_ACCESS", _, e), - property_access_name(e, n), - expr_child("client", e, "RECEIVER", _, q), +expr_exact_type(e, t) :- property_access_recv(e, n, q), expr_kind("client", "THIS_REFERENCE", _, q), expr_enclosing_type(q, encl), field_in_enclosing(encl, n, f), @@ -298,15 +294,11 @@ call_result_callable(w) :- expr_shape(w, s), call_signature_in_scope(s, _). // instance call `recv.name()` looks it up (callee-resolution rules 4 and 6). Without it a // method passed by reference had no edge from the function that passes it, where a free // function passed at the same position had one. -method_value(x, m) :- expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), +method_value(x, m) :- property_access_recv(x, n, q), expr_type(q, _, qt), method_in_scope(qt, n, "false", m), !method_is_accessor(m). -method_value(x, m) :- expr_kind("client", "PROPERTY_ACCESS", _, x), - property_access_name(x, n), - expr_child("client", x, "RECEIVER", _, q), +method_value(x, m) :- property_access_recv(x, n, q), expr_shape(q, s), method_in_scope(s, n, _, m), !method_is_accessor(m). diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 0a7e2778..3119a141 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -501,6 +501,7 @@ .decl property_written(c0:symbol) .decl update_operator(c0:symbol) .decl property_access_name(c0:symbol,c1:symbol) +.decl property_access_recv(c0:symbol,c1:symbol,c2:symbol) .decl qname_dot_prefix(c0:symbol,c1:symbol) .decl qname_scan(c0:symbol,c1:number) .decl qname_walk(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol) diff --git a/tests/cases/typescript/shape-chained-access/case.json b/tests/cases/typescript/shape-chained-access/case.json new file mode 100644 index 00000000..44d2f29b --- /dev/null +++ b/tests/cases/typescript/shape-chained-access/case.json @@ -0,0 +1,10 @@ +{"lang": "typescript", "src": ".", + "checks": [ + {"why": "a chained access typed through a parameter's inline object shape resolves: attachTo.parent is the shape's field, and .getHash() on it reaches the class method. A rule reordering in the field-access neighborhood silently degraded exactly this to ambiguous_unknown while every other suite check held; only the byte-identity gate saw it", + "run": ["impact", "Reg.getHash"], + "want": ["[resolved", "user.ts"], + "avoid": ["by name"]}, + {"why": "and path '*' names the caller through the same chain", + "run": ["path", "*", "Reg.getHash"], + "want": ["1 hop(s) use"]} + ]} diff --git a/tests/cases/typescript/shape-chained-access/src/registry.ts b/tests/cases/typescript/shape-chained-access/src/registry.ts new file mode 100644 index 00000000..82984703 --- /dev/null +++ b/tests/cases/typescript/shape-chained-access/src/registry.ts @@ -0,0 +1,5 @@ +export class Reg { + getHash(): string { + return "x"; + } +} diff --git a/tests/cases/typescript/shape-chained-access/src/user.ts b/tests/cases/typescript/shape-chained-access/src/user.ts new file mode 100644 index 00000000..7ea8c104 --- /dev/null +++ b/tests/cases/typescript/shape-chained-access/src/user.ts @@ -0,0 +1,5 @@ +import { Reg } from "./registry"; + +export function use(attachTo?: { readonly parent: Reg }): string { + return attachTo === undefined ? "" : attachTo.parent.getHash(); +}