Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions graph/pipeline/run-souffle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=<file>: solve this run in the souffle INTERPRETER with per-rule profiling
# written to <file> (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"
Expand Down
4 changes: 1 addition & 3 deletions graph/typescript/engine/config-resolution/entry-points.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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").
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
68 changes: 26 additions & 42 deletions graph/typescript/engine/expression-resolution/expr-type.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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),
Expand All @@ -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).
Expand All @@ -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).
Expand All @@ -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).

Expand All @@ -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),
Expand All @@ -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).

Expand Down Expand Up @@ -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),
Expand Down
4 changes: 1 addition & 3 deletions graph/typescript/engine/resolution/generics.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
38 changes: 24 additions & 14 deletions graph/typescript/engine/resolution/name-resolution.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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) ────────────────────
Expand All @@ -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).

Expand Down Expand Up @@ -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).

Expand All @@ -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).
16 changes: 4 additions & 12 deletions graph/typescript/engine/resolution/value-flow.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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),
Expand Down Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions graph/typescript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions tests/cases/typescript/shape-chained-access/case.json
Original file line number Diff line number Diff line change
@@ -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"]}
]}
5 changes: 5 additions & 0 deletions tests/cases/typescript/shape-chained-access/src/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class Reg {
getHash(): string {
return "x";
}
}
5 changes: 5 additions & 0 deletions tests/cases/typescript/shape-chained-access/src/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Reg } from "./registry";

export function use(attachTo?: { readonly parent: Reg }): string {
return attachTo === undefined ? "" : attachTo.parent.getHash();
}
Loading