diff --git a/.gitattributes b/.gitattributes
index a36bf919..83b87289 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -29,3 +29,7 @@
*.jpg binary
*.pdf binary
*.sqlite binary
+
+# A torture fixture whose CRLF line endings ARE the trap it sets (a BOM and CRLF in one
+# page); normalising it would remove the case the suite asserts.
+parser/src/test-data/web/torture/quirks.html -text
diff --git a/.github/workflows/build-engines.yml b/.github/workflows/build-engines.yml
index bd8b39bf..08bbdfdd 100644
--- a/.github/workflows/build-engines.yml
+++ b/.github/workflows/build-engines.yml
@@ -73,7 +73,9 @@ jobs:
id="$(bash graph/pipeline/run-souffle.sh --language "$lang" --print-engine-id)"
echo "$lang: $id"; printf '%s' "$id" > "gen/$lang.id"
bash graph/pipeline/run-souffle.sh --language "$lang" --emit-program "gen/$lang.dl"
- souffle -I graph -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; }
+ # -j makes souffle EMIT the parallel loops (without it the C++ has none); whether a
+ # platform's binary runs them in parallel is decided at compile time below.
+ souffle -I graph -j 8 -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; }
awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "gen/$lang.gen.log"
done
# the query programs: self-contained (no #include), keyed by dl_program.py itself
@@ -89,6 +91,33 @@ jobs:
bash .github/scripts/query-smoke.sh expect gen/queries
cp .github/scripts/query-smoke.sh gen/queries/smoke.sh
cp -r /usr/include/souffle gen/souffle
+ # the seqlock fix (see souffle_overlay in run-souffle.sh): the write-entry RMW
+ # must be seq_cst or a weakly-ordered CPU lets data stores pass the version-odd
+ # store and readers validate garbage. Patched here so every platform's binary
+ # is built from the same fixed headers; the engine id carries +seqlock-fix-4,
+ # so these headers must carry the SAME patches run-souffle's overlay applies —
+ # a binary labeled fix-4 but built from lesser headers is the exact mislabeling
+ # the salt exists to prevent.
+ # the header layout differs by install (deb: souffle/utility; brew: souffle/souffle/utility),
+ # so the patched files are FOUND, not assumed — a miss fails the build here, loudly
+ PU=$(find gen/souffle -name ParallelUtil.h | head -1); [ -n "$PU" ]
+ sed -i 's/version\.fetch_or(0x1, std::memory_order_acquire)/version.fetch_or(0x1, std::memory_order_seq_cst)/g' "$PU"
+ grep -q 'fetch_or(0x1, std::memory_order_seq_cst)' "$PU"
+ ! grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$PU"
+ # fix-3: the publication fences, applied by the same anchored patch the local
+ # overlay uses (extracted from run-souffle.sh so the two cannot drift)
+ BT=$(find gen/souffle -name BTree.h | head -1); [ -n "$BT" ]
+ awk '/^import glob, os, sys$/,/^PYEOF$/' graph/pipeline/run-souffle.sh | sed '$d' > /tmp/fences.py
+ python3 /tmp/fences.py "$BT"
+ [ "$(grep -c 'seqlock-fix-3' "$BT")" = "4" ]
+ # fix-4: the speculative-fetch guards, applied by the same extracted patch
+ FW=$(find gen/souffle -name ConcurrentFlyweight.h | head -1); [ -n "$FW" ]
+ RT=$(find gen/souffle -name RecordTableImpl.h | head -1); [ -n "$RT" ]
+ [ "$(grep -c 'seqlock-fix-4' "$FW")" = "1" ]
+ [ "$(grep -c 'seqlock-fix-4' "$RT")" = "1" ]
+ [ "$(grep -c 'seqlock-fix-4' "$BT")" = "1" ]
+ BD=$(find gen/souffle -name BTreeDelete.h | head -1); [ -n "$BD" ]
+ [ "$(grep -c 'seqlock-fix-4' "$BD")" = "1" ]
# one key for the whole set; a partial match restores the previous set
echo "key=$(cat gen/*.id gen/queries/*.id | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
# The compile flags live in THIS file and ENGINE_ID does not cover them, so its
@@ -135,7 +164,15 @@ jobs:
for lang in $LANGUAGES; do
if cmp -s "gen/$lang.id" "engines/$lang/ENGINE_ID"; then echo "$lang: cached, rules unchanged"; continue; fi
mkdir -p "engines/$lang"
- c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
+ # OpenMP on BOTH architectures: the arm64 crash mode was two weak-ordering
+ # holes in the vendored headers (write-entry RMW + unfenced node publication),
+ # both patched above and validated 10/10 under load on linux-arm64 and
+ # darwin-arm64 (seqlock-fix-3). libgomp links STATICALLY so the binary runs on
+ # machines with no gcc runtime. The .parallel marker beside the binary is what
+ # run-souffle.sh reads to pass a real -j at run time.
+ OMP="-fopenmp -Wl,-Bstatic,-lgomp,-Bdynamic"
+ c++ -std=c++17 -O3 -w $OMP -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
+ [ -n "$OMP" ] && touch "engines/$lang/axiomcode-engine-$lang.parallel"
cp "gen/$lang.id" "engines/$lang/ENGINE_ID"
done
mkdir -p engines/queries
@@ -169,8 +206,9 @@ jobs:
fc /b gen\%%L.id engines\%%L\ENGINE_ID >nul 2>&1
if errorlevel 1 (
if not exist engines\%%L mkdir engines\%%L
- cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /I gen gen\%%L.cpp /Fe:engines\%%L\axiomcode-engine-%%L.exe
+ cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /openmp /I gen gen\%%L.cpp /Fe:engines\%%L\axiomcode-engine-%%L.exe
if errorlevel 1 exit /b 1
+ type nul > engines\%%L\axiomcode-engine-%%L.exe.parallel
copy /y gen\%%L.id engines\%%L\ENGINE_ID
) else (
echo %%L: cached, rules unchanged
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cb6710f5..a9920765 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,7 +52,7 @@ permissions:
contents: read
env:
- # bin/axiomcode and the parser need Node ≥ 22.5.
+ # bin/axiomcode and the parser need Node ≥ 22.13 (node:sqlite without a flag).
NODE_VERSION: '22'
SOUFFLE_VERSION: '2.5'
SOUFFLE_SHA512: '6b86e554f6aa5abf8a8b55d8312ae37c0957c5bd6c9edeea89246db9406f645ec5e600b84fe6636b1c163da556f0da6c3d2dad46c1083413f2fcf4f95b9ac62c'
@@ -371,6 +371,16 @@ jobs:
if: matrix.lang == 'csharp'
run: dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle
+ # `--library auto` decompiles a dependency that ships no sources jar; the Java query case for it needs Vineflower
+ - name: fetch the decompiler `--library auto` uses for Java class jars
+ if: matrix.lang == 'java'
+ run: mvn -q dependency:get -Dartifact=org.vineflower:vineflower:1.10.1
+
+ # `--library auto` compiles a NuGet package by decompiling it; the query case for it needs the decompiler
+ - name: install the decompiler `--library auto` uses for NuGet packages
+ if: matrix.lang == 'csharp'
+ run: dotnet tool install -g ilspycmd --version 8.2.0.7535
+
- name: cache the Soufflé package
uses: actions/cache@v4
with:
diff --git a/README.md b/README.md
index 166aaf3d..c388a185 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@
-
+
@@ -125,7 +125,7 @@ AxiomCode Graph is two parts. The **engine** (`@axiomcode/code-graph` on npm) pa
graph; it also provides the `axiomcode` command and an MCP server. The **plugin** (`plugins/axiomcode/`) is the
agent-facing frontend: a skill, four MCP tools, and hooks. Install the engine first.
-Requirements: **Node ≥ 22.5** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also
+Requirements: **Node ≥ 22.13** and **Python 3** (`python3`, or `python` / `py` on Windows). On Windows, also
[Git for Windows](https://git-scm.com/download/win): the CLI runs under its bash. The engine ships as a prebuilt
binary for macOS (Apple Silicon and Intel), Linux (x64 and arm64) and Windows x64, and `npm install` takes the one for
your platform. No Soufflé and no compiler are needed, with one exception:
@@ -255,8 +255,8 @@ looked up again in the graph before you see it; the `verified:` line is that che
### Support for agents
-Every agent below gets the four MCP tools and the skill; the hooks, which add the graph's edges to the agent's
-own file reads and searches, run where the last column says so.
+Every agent below gets the four MCP tools and the skill; the hooks, which keep the graph current and report what an
+edit breaks, run where the last column says so. No hook annotates the agent's own reads and searches.
| Agent | Install | Uninstall | Hooks |
|---|---|---|---|
@@ -308,12 +308,12 @@ about one change had to read 95 of 43,793 methods, and every true direct caller
## CLI commands
-Four questions, each answered as numbered places with the code of the function each one sits in. The MCP server
-offers the same four as tools: `find(question)`, `impact(name)`, `path(start, end)` and `tests()`.
+Three questions, each answered as numbered places with the code of the function each one sits in. The MCP server
+offers the same three as tools: `impact(name)`, `path(start, end)` and `tests()`. Finding where code lives is
+left to your own search: bring the name you found to these commands.
| command | what it answers |
|---|---|
-| `axiomcode find ""` | where the code for a task lives, when you have it in words and not yet a name |
| `axiomcode impact ` | who calls it, what a change to it reaches, and the tests that exercise it |
| `axiomcode impact` | the same for the declarations your uncommitted edits changed; the answer starts with `your edits:` |
| `axiomcode path ` | how A reaches B: every hop of the call chain, with the code at each call |
@@ -334,9 +334,9 @@ command's usage.
The graph stays current on its own. Every file the parser reads is recorded with its hash at build time; after an
edit, a shell command, a finished turn, at session start, and before a query, anything that differs starts one
background rebuild per repository, with the language, `--src` and `--library` of the graph it replaces. Every
-command keeps reading the previous graph until the new one is indexed and swapped in. A query waits up to
-`AXIOMCODE_FRESH_WAIT` seconds (default 10) for it, then answers from the previous graph with a `graph refresh:` line
-naming the files it predates. The MCP server also checks every repository it has answered for once 15 minutes have
+command keeps reading the previous graph until the new one is indexed and swapped in. A query answers from the
+previous graph at once, with a `graph refresh:` line naming the files it predates and the rows in them marked;
+`--fresh` waits for the rebuild instead, and `AXIOMCODE_FRESH_WAIT` (seconds, default 0) lets every query wait that long. The MCP server also checks every repository it has answered for once 15 minutes have
passed since its last update (`AXIOMCODE_REFRESH_INTERVAL`, seconds; 0 turns it off), which catches edits made while
a session sits idle. The graph records when and why it was built in `index_meta` (`refreshed_at`, `refresh_reason`).
`AXIOMCODE_NO_REFRESH=1` turns the rebuilds off, not the check: an answer from a graph older than an edit still
diff --git a/bin/axiomcode b/bin/axiomcode
index 6b902cf0..99f3f668 100755
--- a/bin/axiomcode
+++ b/bin/axiomcode
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# axiomcode — ask a repository's call graph. `axiomcode --help` prints the dispatcher's help
-# (plugins/axiomcode/skills/axiomcode/scripts/axiomcode): index, find, impact, path and tests.
+# (plugins/axiomcode/skills/axiomcode/scripts/axiomcode): index, context, impact, path and tests.
# ─────────────────────────────────────────────────────────────────────────────
# INTERNAL COMMANDS, not advertised: the build, the engine suites and the MCP server, which
# the dispatcher, the test suites and the agent manifests call.
@@ -98,16 +98,17 @@ PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}"
QUERY="$ROOT/plugins/axiomcode/skills/axiomcode/scripts/axiomcode"
# `tests` is a query verb (the tests an edit reaches); `test` is THIS command's engine suite. The query
# surface advertises `tests`; `test` is internal.
-# The list is read from the frontend's own dispatch table, the lines ` [|]) exec …` that its `--verbs`
-# prints, so the two entry points still cannot drift -- but read here, in this shell, instead of by a second bash, a
-# sed, a tr and two greps on every query.
+# THE LIST IS THE SUPPORTED SURFACE: the `# axiomcode ` lines of the frontend's own leading help (what its
+# `--verbs` prints), so the two entry points cannot drift -- read here, in this shell, instead of by a second bash.
+# The frontend still dispatches internal verbs (context, changed, test-impact) for the hooks and the suites, which
+# call it directly; this command refuses them, so the installed surface is only what the help teaches.
QVERBS=()
load_verbs(){
[ ${#QVERBS[@]} -gt 0 ] && return 0; [ -f "$QUERY" ] || return 0
- local l v x re='^ ([a-z|-]*)\) *exec '
+ local l re='^# axiomcode ([a-z][a-z-]*)'
while IFS= read -r l || [ -n "$l" ]; do
- [[ $l =~ $re ]] || continue; v="${BASH_REMATCH[1]}"
- for x in ${v//|/ }; do QVERBS+=("$x"); done
+ [ "${l:0:1}" = '#' ] || { [ -n "${QVERBS[*]:-}" ] && break; continue; }
+ [[ $l =~ $re ]] && QVERBS+=("${BASH_REMATCH[1]}")
done < "$QUERY"
}
query_verbs(){ load_verbs; [ ${#QVERBS[@]} -gt 0 ] && printf '%s\n' "${QVERBS[@]}"; return 0; }
@@ -294,6 +295,13 @@ case "$cmd" in
echo "▶ parsing $src → IR"
"$0" parser "$src" "$ir" ${popts[@]+"${popts[@]}"} > "$int/parse.log" 2>&1 || { echo "❌ parser failed — see $int/parse.log" >&2; tail -5 "$int/parse.log" >&2; exit 1; }
head -1 "$int/parse.log"
+ # WHAT AN INSTALLED PYTHON LIBRARY HANDS BACK to the objects the client gives it (graph/python/libsum): read from
+ # the project's environment, cached by package and version, staged beside the client IR. Never fatal -- without
+ # it the engine sees no library hand-backs, exactly as before. AXIOMCODE_LIB_CALLBACKS=0 turns it off.
+ if [ -d "$ir/python" ] && [ "${AXIOMCODE_LIB_CALLBACKS:-1}" != 0 ]; then
+ python3 "$ROOT/graph/python/libsum/libsum.py" --src "$src" --out "$ir/python" > "$int/libsum.log" 2>&1 \
+ && tail -1 "$int/libsum.log" || echo "⚠️ library callback summaries failed (see $int/libsum.log); continuing without them" >&2
+ fi
# which languages the IR holds — one folder each — and which of them have an engine
found=(); for d in "$ir"/*/; do l="$(basename "$d")"; [ -d "$ROOT/graph/$l/engine" ] && found+=("$l"); done
[ ${#found[@]} -gt 0 ] || { echo "❌ no language with an engine in the IR (folders: $(ls "$ir" | tr '\n' ' '))" >&2; exit 1; }
@@ -324,16 +332,61 @@ case "$cmd" in
# a caller that checks it is no worse off. Only the languages that produced a graph are
# listed at the end, so the output stays a list of files that exist.
solved=(); failed=()
- for l in "${langs[@]}"; do
- echo "▶ solving $l → $out/$l/graph.sqlite"
- if "$0" engine --language "$l" --client-ir "$ir/$l" --out "$out/$l" --intermediate "$int" --library "$libroots" \
- --meta "source_version=$version" --meta "source_dir=$(cd "$src" && pwd)" ${rest[@]+"${rest[@]}"}; then
- solved+=("$l"); [ -z "$progress" ] || echo "$l ok $out/$l/graph.sqlite" >> "$progress"
+ # Each language gets its OWN intermediate: run-souffle writes fixed names there
+ # (souffle-program.cpp, .souffle-gen.log), so two concurrent solves into one
+ # directory would overwrite each other's program mid-compile.
+ solve_one(){ local sl="$1"
+ "$0" engine --language "$sl" --client-ir "$ir/$sl" --out "$out/$sl" --intermediate "$int/$sl" --library "$libroots" \
+ --meta "source_version=$version" --meta "source_dir=$(cd "$src" && pwd)" ${rest[@]+"${rest[@]}"}
+ }
+ record(){ local rl="$1" rrc="$2"
+ if [ "$rrc" -eq 0 ]; then
+ solved+=("$rl"); [ -z "$progress" ] || echo "$rl ok $out/$rl/graph.sqlite" >> "$progress"
else
- failed+=("$l"); echo "❌ $l failed; continuing with the remaining languages" >&2
- [ -z "$progress" ] || echo "$l failed" >> "$progress"
+ failed+=("$rl"); echo "❌ $rl failed; continuing with the remaining languages" >&2
+ [ -z "$progress" ] || echo "$rl failed" >> "$progress"
fi
- done
+ }
+ # THE FIRST (LARGEST) LANGUAGE SOLVES ALONE, streaming, exactly as before: its graph is the one a
+ # --progress caller publishes first (#1555), and nothing may compete with it for cores or
+ # interleave its log. Only the languages AFTER it run concurrently — they used to wait in line
+ # behind each other for no reason: their solves share nothing (own IR, own output, own
+ # intermediate). Memory is the tradeoff (each is its own Soufflé process), so the width is
+ # modest by default and AXIOMCODE_SOLVE_JOBS raises or lowers it; 1 restores the strict line.
+ l="${langs[0]}"
+ echo "▶ solving $l → $out/$l/graph.sqlite"
+ rc=0; solve_one "$l" || rc=$?; record "$l" "$rc"
+ if [ ${#langs[@]} -gt 1 ]; then
+ others=("${langs[@]:1}")
+ JOBS="${AXIOMCODE_SOLVE_JOBS:-2}"
+ case "$JOBS" in (*[!0-9]*|'') JOBS=2;; esac; [ "$JOBS" -ge 1 ] || JOBS=1
+ if [ "$JOBS" -eq 1 ]; then
+ for l in "${others[@]}"; do
+ echo "▶ solving $l → $out/$l/graph.sqlite"
+ rc=0; solve_one "$l" || rc=$?; record "$l" "$rc"
+ done
+ else
+ # Waves of $JOBS (bash 3.2 has no `wait -n`): start a wave, wait for all of it, replay each
+ # log whole so the build log never interleaves, then record in the wave's order.
+ i=0
+ while [ $i -lt ${#others[@]} ]; do
+ wave=("${others[@]:$i:$JOBS}"); i=$((i + JOBS))
+ wavepids=()
+ for l in "${wave[@]}"; do
+ echo "▶ solving $l → $out/$l/graph.sqlite (concurrent)"
+ { solve_one "$l" > "$int/solve-$l.log" 2>&1; echo $? > "$int/solve-$l.rc"; } &
+ wavepids+=($!)
+ done
+ for wp in "${wavepids[@]}"; do wait "$wp" || true; done
+ for l in "${wave[@]}"; do
+ cat "$int/solve-$l.log" 2>/dev/null
+ rc="$(cat "$int/solve-$l.rc" 2>/dev/null || echo 1)"
+ rm -f "$int/solve-$l.log" "$int/solve-$l.rc"
+ record "$l" "$rc"
+ done
+ done
+ fi
+ fi
for l in ${solved[@]+"${solved[@]}"}; do echo "$out/$l/graph.sqlite"; done
if [ ${#failed[@]} -gt 0 ]; then
echo "❌ ${#failed[@]} of ${#langs[@]} languages failed: ${failed[*]}" >&2
@@ -347,7 +400,7 @@ case "$cmd" in
run_suite(){ local s="$1"; shift; echo "══ $s"; bash "$ROOT/graph/test/$s/run-tests.sh" "$@" || rc=1; }
case "$which" in
java|typescript|python|javascript|csharp) run_suite "$which" "$@";;
- parser) for t in java-extractor-tests typescript-tests python-tests javascript-tests csharp-tests gradle-tests services-tests discovery-tests; do
+ parser) for t in java-extractor-tests typescript-tests python-tests javascript-tests csharp-tests gradle-tests services-tests web-tests web-torture discovery-tests; do
[ -f "$ROOT/parser/src/test/$t.ts" ] || continue; echo "══ parser: $t"; (cd "$ROOT/parser" && npx tsx "src/test/$t.ts") || rc=1; done;;
all) for l in java typescript python javascript csharp; do run_suite "$l" "$@"; done; "$0" test parser || rc=1;;
*) die "test: unknown suite $which (java | typescript | python | javascript | csharp | parser | all)";;
diff --git a/bin/axiomcode.js b/bin/axiomcode.js
index 72a1fc1a..3f54b1e5 100755
--- a/bin/axiomcode.js
+++ b/bin/axiomcode.js
@@ -37,6 +37,14 @@ if (args.length === 1 && args[0] === '--version') {
process.exit(0);
}
+// THE NODE FLOOR, SAID BEFORE ANYTHING RUNS. The graph is written with node:sqlite, which Node ships without a flag from
+// 22.13 (and 23.4); on 22.5-22.12 the index failed minutes in with "No such built-in module: node:sqlite". Asked by
+// version, not by loading the module, which would print Node's experimental-feature warning on every command.
+const [nodeMaj, nodeMin] = process.versions.node.split('.').map(Number);
+if (!(nodeMaj > 23 || (nodeMaj === 23 && nodeMin >= 4) || (nodeMaj === 22 && nodeMin >= 13))) {
+ fail(`axiomcode needs Node 22.13 or newer (it writes the graph with node:sqlite); this is Node ${process.versions.node}.`);
+}
+
const { bash, error } = findBash();
if (error) fail(error);
const py = findPython();
diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md
index 2c17b391..a3f36c76 100644
--- a/graph/bundle/SCHEMA.md
+++ b/graph/bundle/SCHEMA.md
@@ -442,7 +442,7 @@ One row per place a call is written (or, for a synthesised edge, the construct t
- **typescript** — end_line / end_column come from the expression row; the call-site row itself records only the start.
- **javascript** — caller_id is the parser's enclosing method, or the module initializer for top-level code. end_line / end_column come from the expression row. `require()` is a module edge, not a call site.
- **typescript** — PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor's name is on the callee's methods row. Filter them out with kind NOT IN (…) when counting calls.
-- **python** — PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.
+- **python** — PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.
- **python** — The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class's TYPE hash for METACLASS_CREATION, positioned at the class declaration.
### `call_edges`
@@ -467,6 +467,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
| `new` | csharp | An object creation. The target is the constructed type's constructor, and it is never dispatched. |
| `ctor_delegate` | csharp | `: this(...)` or `: base(...)`. No name is written, so the target is structural. |
| `primary_ctor_base` | csharp | SYNTHESISED. A primary constructor's base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference. |
+| `implicit_base_ctor` | csharp | SYNTHESISED. The base class's parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site. |
| `delegate` | csharp | A call through a delegate value: `handler(x)` or `handler.Invoke(x)`. |
| `operator` | csharp | A user-defined operator invoked by operator syntax. |
| `conversion` | csharp | A user-defined conversion. An implicit one has no syntax at the call site. |
@@ -531,9 +532,13 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h
| `DECORATOR_*` | python | Applying an unparenthesised decorator; the suffix is the parser's decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash. |
| `METACLASS_CREATION` | python | A class statement invokes its metaclass's `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base's `__init_subclass__`. No written call; the site is the class's type hash. |
| `PROPERTY_READ` | python | Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression. |
+| `PROPERTY_WRITE` | python | Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression. |
| `CONTEXT_MANAGER` | python | `with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression. |
| `ITERATION_PROTOCOL` | python | `for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression. |
| `SUBSCRIPT_PROTOCOL` | python | `x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver's class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call. |
+| `BUILTIN_PROTOCOL` | python | `repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument's class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call. |
+| `OPERATOR_PROTOCOL` | python | An operator runs a dunder of its operands: `a + b` the left operand's `__add__` and the right one's `__radd__`, a comparison the left slot and the right one's mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand's `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression. |
+| `TRUTH_PROTOCOL` | python | A truth test runs the tested value's `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression. |
**`call_edges.tier` values**
diff --git a/graph/bundle/build.ts b/graph/bundle/build.ts
index 072e9594..84c9fcb9 100644
--- a/graph/bundle/build.ts
+++ b/graph/bundle/build.ts
@@ -529,6 +529,21 @@ export async function buildCore(inp: BuildInputs): Promise {
}
}
}
+ // a field access whose site is no expression: a module variable read in a type (`typeof X`)
+ if (A.ir.fieldSites && fieldSites.size > 0) {
+ const L = A.ir.fieldSites;
+ const src = await clientSource(inp.clientIrDir, L.file);
+ if (src) {
+ const h = src.header;
+ const ci = h.col(L.id), cl = h.col(L.startLine), cel = h.col(L.endLine);
+ for await (const r of rowsOf(src)) {
+ const fa = fieldSites.get(r[ci] ?? '');
+ if (!fa) continue;
+ const file = fileOf(L.fileVia, h, r);
+ for (const fr of fa) { fill(fr, 6, file); fill(fr, 7, int(r[cl])); fill(fr, 9, int(r[cel])); }
+ }
+ }
+ }
// a site the IR writes no name for, where the engine derived the accessor it calls (#1441)
if (A.raw.siteNames) {
for (const [site, name] of await readSource(rawDir, A.raw.siteNames)) {
diff --git a/graph/bundle/cli.ts b/graph/bundle/cli.ts
index adb2061c..ad975fad 100644
--- a/graph/bundle/cli.ts
+++ b/graph/bundle/cli.ts
@@ -95,7 +95,7 @@ export async function main(argv = process.argv.slice(2)): Promise {
log(`▶ wrote ${dbPath}${a.debug ? ' (+ csv/*.csv, --debug)' : ''}`);
} else {
fs.rmSync(dbPath, { force: true });
- console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.5) — graph.sqlite NOT written; csv/*.csv is complete`);
+ console.error(` ! node ${process.versions.node} has no node:sqlite (needs ≥ 22.13) — graph.sqlite NOT written; csv/*.csv is complete`);
}
log(`▶ bundle complete in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
}
diff --git a/graph/bundle/languages.ts b/graph/bundle/languages.ts
index 286ec18a..e4ec00d9 100644
--- a/graph/bundle/languages.ts
+++ b/graph/bundle/languages.ts
@@ -166,6 +166,8 @@ export interface LanguageAdapter {
callSites?: CallSitesIR;
decorators?: DecoratorsIR;
localSites?: LocalSitesIR;
+ /** field access sites that are not expressions (TypeScript: a `typeof X` type reference), positioned from this table */
+ fieldSites?: LocalSitesIR;
/** absent where the front end writes no skipped-files report */
skipped?: SkippedIR;
};
@@ -277,6 +279,11 @@ const TYPESCRIPT: LanguageAdapter = {
startLine: 'startLine', startColumn: 'startColumn',
fileVia: { column: 'tsModuleLinkHash', through: 'modules' },
},
+ // a module variable read in a TYPE (`typeof X`): the field_access site is the type reference
+ fieldSites: {
+ file: 'all-typescript-type-references.csv', id: 'tsTypeReferenceUniqueHash', startLine: 'startLine', endLine: 'endLine',
+ fileVia: { column: 'tsModuleLinkHash', through: 'modules' },
+ },
skipped: { file: 'skipped-typescript-files.csv', filePath: 'filePath', reason: 'reason', detail: 'detail' },
},
};
@@ -337,6 +344,9 @@ const JAVASCRIPT: LanguageAdapter = {
typeAncestors: { file: 'resolution-type-ancestor.csv', columns: [0, 1] },
entryPoints: { file: 'entry-point.csv', columns: [0, 1] },
entryReachable: { file: 'entry-reachable.csv', columns: [0] },
+ // site, caller, variable, provenance, tier, access: a module variable an identifier reads,
+ // directly or through an import binding (the field is the variable's own hash)
+ fieldAccess: { file: 'field-access.csv', columns: [0, 1, 2, 3, 4, 5] },
},
ir: {
// No signature and no owner qualified name: JavaScript declares neither.
diff --git a/graph/bundle/node-sqlite.d.ts b/graph/bundle/node-sqlite.d.ts
index a2d35d8c..618db642 100644
--- a/graph/bundle/node-sqlite.d.ts
+++ b/graph/bundle/node-sqlite.d.ts
@@ -1,5 +1,5 @@
/**
- * The slice of `node:sqlite` this package uses. Node ships the module from 22.5; the
+ * The slice of `node:sqlite` this package uses. Node ships the module unflagged from 22.13; the
* installed @types/node predates it, so the four members used here are declared locally
* rather than pulling a newer type package for one import.
*/
diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts
index 2e9af322..3ff97670 100644
--- a/graph/bundle/schema.ts
+++ b/graph/bundle/schema.ts
@@ -358,6 +358,7 @@ export const VOCAB: readonly VocabSpec[] = [
{ table: 'call_edges', column: 'kind', value: 'new', languages: C, meaning: 'An object creation. The target is the constructed type\'s constructor, and it is never dispatched.' },
{ table: 'call_edges', column: 'kind', value: 'ctor_delegate', languages: C, meaning: '`: this(...)` or `: base(...)`. No name is written, so the target is structural.' },
{ table: 'call_edges', column: 'kind', value: 'primary_ctor_base', languages: C, meaning: 'SYNTHESISED. A primary constructor\'s base invocation, written in the heritage clause: `class D(int a) : B(a)`. There is no call syntax anywhere in the body. FromExpr is the heritage type reference.' },
+ { table: 'call_edges', column: 'kind', value: 'implicit_base_ctor', languages: C, meaning: 'SYNTHESISED. The base class\'s parameterless constructor that a constructor with no `: base(...)` / `: this(...)` runs before its body, or that the implicit constructor of a class declaring none runs at its `new`. FromExpr is the heritage type reference, or the `new` site.' },
{ table: 'call_edges', column: 'kind', value: 'delegate', languages: C, meaning: 'A call through a delegate value: `handler(x)` or `handler.Invoke(x)`.' },
{ table: 'call_edges', column: 'kind', value: 'operator', languages: C, meaning: 'A user-defined operator invoked by operator syntax.' },
{ table: 'call_edges', column: 'kind', value: 'conversion', languages: C, meaning: 'A user-defined conversion. An implicit one has no syntax at the call site.' },
@@ -667,9 +668,13 @@ export const VOCAB: readonly VocabSpec[] = [
{ table: 'call_edges', column: 'kind', value: 'DECORATOR_*', languages: P, meaning: 'Applying an unparenthesised decorator; the suffix is the parser\'s decorator kind: BARE, ATTRIBUTE, SUBSCRIPT, EXPRESSION (and CALL/ATTRIBUTE_CALL when the factory expression is not itself a call site). The site is the decorator hash.' },
{ table: 'call_edges', column: 'kind', value: 'METACLASS_CREATION', languages: P, meaning: 'A class statement invokes its metaclass\'s `__new__` / `__init__` at import time, whether the metaclass is written on the statement (`class X(metaclass=M)`) or inherited from a base, and the nearest base\'s `__init_subclass__`. No written call; the site is the class\'s type hash.' },
{ table: 'call_edges', column: 'kind', value: 'PROPERTY_READ', languages: P, meaning: 'Reading `obj.attr` where `attr` is a `@property` runs the getter; reading `Cls.attr` where the METACLASS defines `attr` as a property runs that getter. No written call; the site is the attribute-access expression.' },
+ { table: 'call_edges', column: 'kind', value: 'PROPERTY_WRITE', languages: P, meaning: 'Assigning `obj.attr = v` where `attr` is a `@property` with a setter runs the setter; `del obj.attr` runs its deleter. No written call; the site is the attribute-access expression.' },
{ table: 'call_edges', column: 'kind', value: 'CONTEXT_MANAGER', languages: P, meaning: '`with expr:` runs `__enter__` / `__exit__` (or the async pair). No written call; the site is the context-manager expression.' },
{ table: 'call_edges', column: 'kind', value: 'ITERATION_PROTOCOL', languages: P, meaning: '`for x in expr:` (and comprehensions) runs `__iter__` / `__next__` (or the async pair). No written call; the site is the iterated expression.' },
{ table: 'call_edges', column: 'kind', value: 'SUBSCRIPT_PROTOCOL', languages: P, meaning: '`x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver\'s class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call.' },
+ { table: 'call_edges', column: 'kind', value: 'BUILTIN_PROTOCOL', languages: P, meaning: '`repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument\'s class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call.' },
+ { table: 'call_edges', column: 'kind', value: 'OPERATOR_PROTOCOL', languages: P, meaning: 'An operator runs a dunder of its operands: `a + b` the left operand\'s `__add__` and the right one\'s `__radd__`, a comparison the left slot and the right one\'s mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand\'s `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression.' },
+ { table: 'call_edges', column: 'kind', value: 'TRUTH_PROTOCOL', languages: P, meaning: 'A truth test runs the tested value\'s `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression.' },
// entry_points.reason
{ table: 'entry_points', column: 'reason', value: 'main', languages: ['java', 'csharp'], meaning: 'A static `main`. C#: a static `Main`, or the method top-level statements compile to.' },
@@ -748,7 +753,7 @@ export const NOTES: readonly NoteSpec[] = [
{ language: 'typescript', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`.' },
{ language: 'typescript', table: 'type_instantiated', note: 'Every row has how = `new`. Not restricted to client provenance: a type the library constructs is still a type that exists at run time, and dropping it would narrow the envelope unsoundly.' },
{ language: 'typescript', table: 'call_sites', note: 'PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor\'s name is on the callee\'s methods row. Filter them out with kind NOT IN (…) when counting calls.' },
- { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, CONTEXT_MANAGER, ITERATION_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' },
+ { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' },
{ language: 'python', table: 'call_sites', note: 'The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class\'s TYPE hash for METACLASS_CREATION, positioned at the class declaration.' },
{ language: 'python', table: 'call_edges', note: 'A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row.' },
{ language: 'java', table: 'call_edges', note: 'A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform\'s own subclasses, are the other possible targets. Stage the library to replace the label with the real method.' },
diff --git a/graph/bundle/write.ts b/graph/bundle/write.ts
index 77724016..d00899fe 100644
--- a/graph/bundle/write.ts
+++ b/graph/bundle/write.ts
@@ -20,10 +20,11 @@ export function writeCoreCsv(graphDir: string, core: CoreTables, log: (s: string
}
}
-/** Node ≥ 22.5 ships `node:sqlite`. */
+/** Whether `node:sqlite` loads here. Asked of the module, not the version: Node 22.5–22.12 and 23.0–23.3 ship it only
+ * behind --experimental-sqlite, and a version test passed them, so the import that followed failed with "No such
+ * built-in module: node:sqlite" on Node 22.11. */
export function sqliteAvailable(): boolean {
- const [maj, min] = process.versions.node.split('.').map(Number);
- return maj! > 22 || (maj === 22 && min! >= 5);
+ try { require('node:sqlite'); return true; } catch { return false; }
}
function createSql(t: TableSpec, prefix = ''): string {
@@ -42,6 +43,35 @@ export interface SqliteInputs {
log: (s: string) => void;
}
+// One INSERT per row pays the statement dispatch and argument binding once per row — half the
+// whole bundle stage on a 1.4GB graph (63.6s of 127s profiled, 4,461-file subject). A multi-row
+// VALUES list amortizes both; 128 rows per statement keeps the flattened argument array well
+// under sqlite's bind limit at the widest table.
+const BATCH = Math.max(1, Number(process.env.AXIOM_BUNDLE_BATCH ?? 128) || 128);
+type DbLike = { prepare(sql: string): { run(...a: (string | number | null)[]): unknown } };
+function batchInsert(db: DbLike, table: string, arity: number, verb: string): { push(r: (string | number | null)[]): void; flush(): void } {
+ const one = `(${Array.from({ length: arity }, () => '?').join(', ')})`;
+ if (BATCH === 1) {
+ // the pre-batching path, byte for byte: one prepared statement, one run per row
+ const single = db.prepare(`${verb} INTO ${table} VALUES ${one}`);
+ return { push(r) { single.run(...r.slice(0, arity).map((v) => v ?? null)); }, flush() {} };
+ }
+ const full = db.prepare(`${verb} INTO ${table} VALUES ${Array.from({ length: BATCH }, () => one).join(', ')}`);
+ let buf: (string | number | null)[] = [];
+ let n = 0;
+ return {
+ push(r) {
+ for (let i = 0; i < arity; i++) buf.push(r[i] ?? null);
+ if (++n === BATCH) { full.run(...buf); buf = []; n = 0; }
+ },
+ flush() {
+ if (n === 0) return;
+ const rest = db.prepare(`${verb} INTO ${table} VALUES ${Array.from({ length: n }, () => one).join(', ')}`);
+ rest.run(...buf); buf = []; n = 0;
+ },
+ };
+}
+
export async function writeSqlite(inp: SqliteInputs): Promise {
// Loaded lazily so the CSV path works on a Node without the module.
const { DatabaseSync } = await import('node:sqlite');
@@ -55,8 +85,9 @@ export async function writeSqlite(inp: SqliteInputs): Promise {
for (const t of CORE_TABLES) {
db.exec(createSql(t));
const rows = (inp.core as unknown as Record)[t.name] ?? [];
- const ins = db.prepare(`INSERT OR IGNORE INTO ${t.name} VALUES (${t.columns.map(() => '?').join(', ')})`);
- for (const r of rows) ins.run(...(r as (string | number | null)[]));
+ const ins = batchInsert(db, t.name, t.columns.length, 'INSERT OR IGNORE');
+ for (const r of rows) ins.push(r as (string | number | null)[]);
+ ins.flush();
inp.log(` sqlite ${t.name}: ${rows.length} rows`);
}
db.exec('COMMIT;');
@@ -68,12 +99,13 @@ export async function writeSqlite(inp: SqliteInputs): Promise {
const name = `ext_${e.relation}`;
db.exec(`CREATE TABLE ${name} (${cols.join(', ')});`);
if (e.arity === 0) continue;
- const ins = db.prepare(`INSERT INTO ${name} VALUES (${cols.map(() => '?').join(', ')})`);
+ const ins = batchInsert(db, name, e.arity, 'INSERT');
let n = 0;
for await (const r of readRaw(path.join(inp.rawDir, e.file))) {
if (r.length !== e.arity) continue; // a torn row is dropped, as the engine's own staging does
- ins.run(...r); n++;
+ ins.push(r); n++;
}
+ ins.flush();
if (n > 0) inp.log(` sqlite ${name}: ${n} rows`);
}
db.exec('COMMIT;');
diff --git a/graph/csharp/engine/call-edge-generation/call_chain.dl b/graph/csharp/engine/call-edge-generation/call_chain.dl
index b44340cf..c0e3de06 100644
--- a/graph/csharp/engine/call-edge-generation/call_chain.dl
+++ b/graph/csharp/engine/call-edge-generation/call_chain.dl
@@ -286,6 +286,52 @@ call_chain_edge(tr, caller, "-", ctor, "lib", "boundary_lib", "primary_ctor_base
method_decl("lib", _, _, _, ctor),
type_ctor("client", gk, caller).
+// ── SYNTHESISED EDGES: THE IMPLICIT `base()` ────────────────────────────────
+// A constructor that writes neither `: base(..)` nor `: this(..)` runs its base
+// class's parameterless constructor before its own body, and a class that declares
+// no constructor at all gets one that does the same. Neither is written anywhere, so
+// without these edges a base constructor that every derived class runs has no caller.
+//
+// implicit_base_ctor(TypeGroup, Ctor): the constructor `base()` runs for a type --
+// its base class's constructor that takes no argument, or, where the base declares
+// no constructor either, the one the base's own implicit constructor runs.
+implicit_base_ctor(gk, ctor) :-
+ type_extends("client", gk, bgk),
+ type_ctor("client", bgk, ctor), ctor_accepts_argc("client", ctor, "0").
+implicit_base_ctor(gk, ctor) :-
+ type_extends("client", gk, bgk),
+ !type_ctor("client", bgk, _), type_group("client", _, bgk),
+ implicit_base_ctor(bgk, ctor).
+
+// A constructor that hands off with `: this(..)` or `: base(..)`, or a primary
+// constructor whose heritage passes arguments: the base constructor it runs is the
+// one written there, already an edge.
+ctor_has_initializer(m) :- call_is_base_ctor("client", e), expr_ultimate_method("client", e, m).
+ctor_has_initializer(m) :- call_is_this_ctor("client", e), expr_ultimate_method("client", e, m).
+ctor_has_initializer(m) :-
+ heritage_has_ctor_args("client", t, _), type_group("client", t, gk), type_ctor("client", gk, m).
+
+// The FromExpr is the base class's entry in the heritage clause, as for a primary
+// constructor's base invocation: the closest thing to where the call is written.
+implicit_base_site(gk, tr) :-
+ type_extends("client", gk, bgk),
+ heritage_resolves("client", gk, pos, bgk),
+ heritage_of_entity("client", gk, pos, _, _, tr), tr != "".
+
+// (1) A declared constructor with no initializer.
+call_chain_edge(tr, m, "-", ctor, "client", "known_edge", "implicit_base_ctor") :-
+ type_ctor("client", gk, m), method_decl("client", _, _, _, m),
+ !ctor_has_initializer(m),
+ implicit_base_ctor(gk, ctor),
+ implicit_base_site(gk, tr).
+
+// (2) `new T()` where T declares no constructor: the implicit one runs T's base's.
+// The edge sits on the `new` site itself, beside its known_implicit_ctor row.
+call_chain_edge(e, caller, "-", ctor, "client", "known_edge", "implicit_base_ctor") :-
+ call_ctor_implicit("client", e, gk),
+ implicit_base_ctor(gk, ctor),
+ call_from_expr("client", e, caller).
+
// ── ACCESSOR EDGES: A PROPERTY READ IS A CALL ───────────────────────────────
// `x.Name` invokes get_Name, `a[i]` an indexer accessor, `e += h` an add accessor.
// These have no cs_call_site row -- the parser cannot know whether `x.Name` is a
@@ -485,6 +531,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/engine/containment/type-nesting.dl b/graph/csharp/engine/containment/type-nesting.dl
index e584db41..8ac8b94d 100644
--- a/graph/csharp/engine/containment/type-nesting.dl
+++ b/graph/csharp/engine/containment/type-nesting.dl
@@ -149,3 +149,27 @@ module_in_scope(prov, mod, outer) :-
// The GLOBAL namespace ("") is always in scope.
module_in_scope(prov, mod, "") :- module_decl(prov, _, _, _, mod).
+
+// module_ns_chain(Prov, ModuleHash, Namespace) -- the namespaces a name written in this
+// file is read from: the file's own namespaces and every dotted prefix of them, as
+// text. A prefix that holds no type of its own (`Acme` over `Acme.Shop`) still
+// qualifies a name written below it, so this is not ns_encloses, which needs one.
+module_ns_chain(prov, mod, ns) :- module_namespace_own(prov, mod, ns), ns != "".
+module_ns_chain(prov, mod, p) :- module_namespace_own(prov, mod, inner), ns_prefix(prov, inner, p).
+
+// ── A USING WRITTEN INSIDE A NAMESPACE ─────────────────────────────────────
+// `namespace Shop.Tests; using Results;` imports Shop.Results. C# reads the name in
+// a using directive from the namespace the directive sits in, innermost outward,
+// exactly as it reads any other name there. Taken as written it names a namespace
+// that does not exist, and every type the file names through it reads as a library
+// type.
+//
+// The IR does not record whether a using sits inside the namespace or above it.
+// The language decides it instead: a using whose name is no namespace at all only
+// compiles inside a namespace that has it as a child. So the relative reading is
+// taken only when the name as written names nothing, and only for a child that
+// exists -- a file whose namespaces have no such child gains nothing.
+module_in_scope(prov, mod, full) :-
+ using_namespace(prov, mod, n), !ns_exists(prov, n),
+ module_ns_chain(prov, mod, e), full = cat(e, cat(".", n)),
+ ns_exists(prov, full).
diff --git a/graph/csharp/engine/expression-resolution/expr-type.dl b/graph/csharp/engine/expression-resolution/expr-type.dl
index 5c962379..fe5e35e7 100644
--- a/graph/csharp/engine/expression-resolution/expr-type.dl
+++ b/graph/csharp/engine/expression-resolution/expr-type.dl
@@ -459,6 +459,25 @@ expr_type(prov, e, gk) :-
expr_type(prov, q, qgk),
member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk).
+// A STATIC member read through its TYPE NAME: `Context.Current.Factory.Create()`.
+// The qualifier `Context` names a type, not a value, so it has no expr_type and the
+// two clauses above cannot start from it: the getter of `Current` is an edge
+// (properties.dl reads the type name), and these give its RESULT a type, so that
+// `.Factory` and every call after it resolve. ref_names_type already refuses a name
+// that also binds a value, which is the language's member-over-type rule.
+expr_type(prov, e, gk) :-
+ expr_kind(prov, e, "MEMBER_ACCESS"),
+ expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr),
+ expr_written_name(prov, nameExpr, n),
+ ref_names_type(prov, q, qgk),
+ member_lookup(prov, qgk, n, "property", p), property_type(prov, p, gk).
+expr_type(prov, e, gk) :-
+ expr_kind(prov, e, "MEMBER_ACCESS"),
+ expr_qualifier_child(prov, e, q), expr_member_name_child(prov, e, nameExpr),
+ expr_written_name(prov, nameExpr, n),
+ ref_names_type(prov, q, qgk),
+ member_lookup(prov, qgk, n, "field", f), field_type(prov, f, gk).
+
// ── A GENERIC ARGUMENT, SUBSTITUTED FOR THE PARAMETER IT FILLS ──────────────
// `IWrap w; w.Value.Flag` -- `Value` is declared `T`, and without binding
// T to Settings the access resolves and the type of the RESULT is the type parameter,
diff --git a/graph/csharp/engine/projections/expressions.dl b/graph/csharp/engine/projections/expressions.dl
index 46e28c76..8a82e1ba 100644
--- a/graph/csharp/engine/projections/expressions.dl
+++ b/graph/csharp/engine/projections/expressions.dl
@@ -117,6 +117,15 @@ expr_written_name("lib", e, n) :-
lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, n, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e),
n != "".
+// ── expr_type_argc(Prov, ExprHash, TypeArgumentCount) ──────────────────────
+// The type arguments written on a name: `Cache` is the name `Cache` at arity 1.
+// A generic name used as a receiver or qualifier is resolved at this arity, since
+// `Cache` and `Cache` are two types.
+expr_type_argc("client", e, tac) :-
+ cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e).
+expr_type_argc("lib", e, tac) :-
+ lib_cs_expression(_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, tac, _, _, _, _, _, _, _, _, _, _, _, _, e).
+
// ── expr_literal(Prov, ExprHash, LiteralKind, LiteralValue) ─────────────────
expr_literal("client", e, lk, lv) :-
cs_expression(_, _, _, _, _, _, _, _, _, _, lk, lv, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e),
diff --git a/graph/csharp/engine/projections/variables.dl b/graph/csharp/engine/projections/variables.dl
index 27be6bf5..841c1f86 100644
--- a/graph/csharp/engine/projections/variables.dl
+++ b/graph/csharp/engine/projections/variables.dl
@@ -61,6 +61,10 @@ var_initializer("lib", v, e) :-
// The initializer IS a value of the variable's type.
var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "LOCAL", _, v).
var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "CONST", _, v).
+// A `using` resource -- `using (var w = new Writer())` and `using var w = ...;` -- is
+// the disposable itself, so its initializer is a value of its type exactly as a
+// local's is. Without it every call on such a resource has an untyped receiver.
+var_value_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "USING", _, v).
// The initializer is a COLLECTION and the variable takes its element type.
var_iterated_initializer(prov, v, e) :- var_initializer(prov, v, e), var_decl(prov, _, "FOREACH", _, v).
diff --git a/graph/csharp/engine/resolution/extensions.dl b/graph/csharp/engine/resolution/extensions.dl
index 23516414..5a09820c 100644
--- a/graph/csharp/engine/resolution/extensions.dl
+++ b/graph/csharp/engine/resolution/extensions.dl
@@ -118,6 +118,25 @@ call_recv_type_name(prov, e, tn) :-
call_recv_type_name(prov, e, tn) :-
call_receiver_expr(prov, e, r),
ref_denotes(prov, r, "LAMBDA_PARAMETER", p), param_type_name(prov, p, tn, _).
+// AN IMPLICITLY TYPED LAMBDA PARAMETER takes its type from the delegate it is
+// converted to (lambda-parameters.dl): `Configure(s => s.AddRules())` with
+// `Configure(Action c)` makes `s` an IServiceCollection. Where
+// that type is in source, param_type already says so; where it is unstaged, the
+// NAME is all there is, and without it an extension written for
+// `this IServiceCollection` cannot match the lambda that calls it.
+call_recv_type_name(prov, e, tn) :-
+ call_receiver_expr(prov, e, r),
+ ref_denotes(prov, r, "LAMBDA_PARAMETER", p),
+ lambda_param_arg_ref(prov, p, ar), type_ref_name(prov, ar, tn), tn != "".
+// AN IMPLICITLY TYPED LOCAL made by `new T(..)` holds a T, resolved or not:
+// `var services = new ServiceCollection(); services.AddRules();`. The name is the
+// one written at the creation (external-types.dl labels the boundary from the same
+// fact); without it the extension rungs have nothing to compare.
+call_recv_type_name(prov, e, tn) :-
+ call_receiver_expr(prov, e, r),
+ ref_denotes(prov, r, "LOCAL_VARIABLE", v), var_is_implicit(prov, v),
+ var_value_initializer(prov, v, init), expr_kind(prov, init, "OBJECT_CREATION"),
+ call_callee_name(prov, init, tn), tn != "".
// A receiver that is itself a call: the callee's declared return type name. The ROOT
// reference's name where there is one (`List`, not `List- `), which is how every
// other receiver is named; the written text only where no reference was recorded.
diff --git a/graph/csharp/engine/resolution/local-binding.dl b/graph/csharp/engine/resolution/local-binding.dl
index f2ec9440..e7bc391f 100644
--- a/graph/csharp/engine/resolution/local-binding.dl
+++ b/graph/csharp/engine/resolution/local-binding.dl
@@ -220,7 +220,8 @@ ref_unbound(prov, e, n) :-
!ref_names_type(prov, e, _).
// A reference that names a TYPE rather than a value: the receiver of a static call.
-// Resolved through type_name_resolves in the file's scope.
+// Resolved through type_name_resolves in the file's scope, at the arity written on
+// it: `Cache
.Get()` names Cache`1, not a non-generic Cache.
//
// NOT WHERE THE NAME ALSO BINDS TO A VALUE. C# allows a member and a type to share a
// name (the language's own "Color Color" rule) and prefers the MEMBER in an
@@ -235,10 +236,10 @@ ref_binds_value(prov, e) :- ref_denotes(prov, e, _, _).
ref_binds_value(prov, e) :- local_binds(prov, e, _).
ref_names_type(prov, e, gk) :-
- expr_written_name(prov, e, n),
+ expr_written_name(prov, e, n), expr_type_argc(prov, e, ar),
expr_ultimate_module(prov, e, mod),
!ref_binds_value(prov, e),
- type_name_resolves(prov, mod, n, "0", gk).
+ type_name_resolves(prov, mod, n, ar, gk).
// ── IMPLICIT `this` ─────────────────────────────────────────────────────────
// An unqualified instance member reference has an implicit `this` receiver whose
diff --git a/graph/csharp/engine/resolution/type-resolution.dl b/graph/csharp/engine/resolution/type-resolution.dl
index fb90ddc4..f4d1b803 100644
--- a/graph/csharp/engine/resolution/type-resolution.dl
+++ b/graph/csharp/engine/resolution/type-resolution.dl
@@ -89,6 +89,26 @@ type_name_cand(prov, mod, qn, ar, gk, 1) :-
type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk),
!type_is_top_level(prov, gk).
+// ── A QUALIFIED NAME READ FROM ITS NAMESPACE ────────────────────────────────
+// `Reflection.Info.Create(..)` written inside namespace Shop names
+// Shop.Reflection.Info: the first segment of a qualified name is a
+// simple name, looked up in the enclosing namespaces like any other. Matched only
+// in full, such a receiver stays untyped and every call through it unresolved.
+//
+// Keyed from the DECLARATION side, so nothing is enumerated per written name: each
+// type's qualified name is split at each dot into the namespace before it and the
+// dotted tail after it, and the tail is a candidate in every file whose namespace
+// chain holds that prefix. A tail with no dot is a simple name, which ranks 2-5
+// already answer.
+type_qn_tail(prov, qn, p, s) :-
+ type_decl(prov, _, qn, _, _, _),
+ i = range(1, strlen(qn)), substr(qn, i, 1) = ".",
+ p = substr(qn, 0, i), s = substr(qn, i + 1, strlen(qn) - i - 1),
+ contains(".", s).
+type_name_cand(prov, mod, s, ar, gk, 1) :-
+ type_qn_tail(prov, qn, p, s), module_ns_chain(prov, mod, p),
+ type_decl(prov, _, qn, ar, _, t), type_group(prov, t, gk).
+
// ── A CLIENT FILE NAMING A STAGED LIBRARY TYPE ──────────────────────────────
// Every clause above is SINGLE-PROVENANCE: it joins a module and a declaration
// under one `prov`, so a client module and a library namespace are never brought
@@ -211,10 +231,10 @@ type_ref_demand(prov, mod, n, ar) :-
// a type name and produced `external:s.Describe` -- an external target named after a
// local variable, which is a wrong answer rather than a missing one. The !ref_denotes
// term is what makes the guard "unbound by anyone".
-type_ref_demand(prov, mod, n, "0") :-
+type_ref_demand(prov, mod, n, ar) :-
call_receiver_expr(prov, e, r),
expr_ref_unknown(prov, r),
- expr_written_name(prov, r, n),
+ expr_written_name(prov, r, n), expr_type_argc(prov, r, ar),
!ref_denotes(prov, r, _, _),
call_module(prov, e, mod).
diff --git a/graph/csharp/souffle/decls_all.dl b/graph/csharp/souffle/decls_all.dl
index 2bb9352f..89a6ff36 100644
--- a/graph/csharp/souffle/decls_all.dl
+++ b/graph/csharp/souffle/decls_all.dl
@@ -67,6 +67,7 @@
.decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol)
.decl call_chain_summary(c0:symbol,c1:number)
.decl call_class(c0:symbol,c1:symbol,c2:symbol)
+.decl call_class_kind(c0:symbol)
.decl call_context(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
.decl call_ctor_implicit(c0:symbol,c1:symbol,c2:symbol)
.decl call_ctor_target(c0:symbol,c1:symbol,c2:symbol)
@@ -264,6 +265,7 @@
.decl ctl_route_c(c0:symbol,c1:symbol,c2:symbol)
.decl ctl_route_tok(c0:symbol,c1:symbol,c2:symbol)
.decl ctor_accepts_argc(c0:symbol,c1:symbol,c2:symbol)
+.decl ctor_has_initializer(c0:symbol)
.decl ctor_implicit_type(c0:symbol,c1:symbol)
.decl decl_return_param(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl delegate_arg_expr(c0:symbol,c1:symbol)
@@ -358,6 +360,7 @@
.decl expr_target_any(c0:symbol,c1:symbol,c2:symbol)
.decl expr_target_type(c0:symbol,c1:symbol,c2:symbol)
.decl expr_type(c0:symbol,c1:symbol,c2:symbol)
+.decl expr_type_argc(c0:symbol,c1:symbol,c2:symbol)
.decl expr_type_owner(c0:symbol,c1:symbol,c2:symbol)
.decl expr_type_via_ref(c0:symbol,c1:symbol,c2:symbol)
.decl expr_ultimate_method(c0:symbol,c1:symbol,c2:symbol)
@@ -486,8 +489,10 @@
.decl implements_member(c0:symbol,c1:symbol,c2:symbol)
.decl implements_params_conflict(c0:symbol,c1:symbol)
.decl implements_shape(c0:symbol,c1:symbol,c2:symbol)
+.decl implicit_base_ctor(c0:symbol,c1:symbol)
.decl implicit_base_group(c0:symbol,c1:symbol)
.decl implicit_base_name(c0:symbol)
+.decl implicit_base_site(c0:symbol,c1:symbol)
.decl implicit_new_untargeted(c0:symbol,c1:symbol,c2:symbol)
.decl indexer_access(c0:symbol,c1:symbol,c2:symbol)
.decl indexer_getter(c0:symbol,c1:symbol,c2:symbol)
@@ -662,6 +667,7 @@
.decl module_lang(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl module_namespace_own(c0:symbol,c1:symbol,c2:symbol)
.decl module_namespace_style(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl module_ns_chain(c0:symbol,c1:symbol,c2:symbol)
.decl module_parse_errors(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl module_regime(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl module_target(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
@@ -947,6 +953,7 @@
.decl type_parent(c0:symbol,c1:symbol,c2:symbol)
.decl type_placement(c0:symbol,c1:symbol,c2:symbol)
.decl type_primary_ctor(c0:symbol,c1:symbol,c2:symbol)
+.decl type_qn_tail(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_ref(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol)
.decl type_ref_arg(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_ref_argc(c0:symbol,c1:symbol,c2:symbol)
diff --git a/graph/java/engine/expression-resolution/expr-type.dl b/graph/java/engine/expression-resolution/expr-type.dl
index 94366240..054c50df 100644
--- a/graph/java/engine/expression-resolution/expr-type.dl
+++ b/graph/java/engine/expression-resolution/expr-type.dl
@@ -649,16 +649,10 @@ recv_decl_ref(recv, retRef, recvType) :- applicable_candidate(recv, innerCallee)
// declared-type clause above yields a reference carrying no arguments. The initializer's callee
// return type carries them. The field analogue is unnecessary: Java has no `var` fields, so a
// field always writes its type.
-recv_decl_ref(recv, retRef, t) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv),
- expr_ultimate_method("client", recv, m),
- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local),
- java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr),
+recv_decl_ref(recv, retRef, t) :- local_use_init(recv, initExpr),
applicable_candidate(initExpr, callee),
method_return_type_resolves(_, callee, retRef, t).
-recv_decl_ref(recv, retRef, t) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv),
- expr_ultimate_method("client", recv, m),
- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local),
- java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr),
+recv_decl_ref(recv, retRef, t) :- local_use_init(recv, initExpr),
applicable_candidate(initExpr, callee),
lib_type_reference(_, "METHOD_RETURN", _, _, _, _, _, "0", _, _, _, _, _, _, _, callee, "METHOD", retRef),
lib_type_ref_resolves(_, retRef, t).
diff --git a/graph/java/engine/resolution/external-types.dl b/graph/java/engine/resolution/external-types.dl
index 77697803..4b83b6a1 100644
--- a/graph/java/engine/resolution/external-types.dl
+++ b/graph/java/engine/resolution/external-types.dl
@@ -178,9 +178,8 @@ expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, nam
// with `Format source()` unstaged), the local is that external type exactly as `Format f` would
// be. Without it the receiver has no external type at all and the call is a declared unknown.
// java_local_variable (19): 11 isVarInferred 13 method 18 hash
-expr_type("external", e, x) :- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, e),
- expr_ultimate_method("client", e, m),
- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, "true", _, m, _, _, _, _, local),
+expr_type("external", e, x) :- local_use(e, local),
+ java_local_variable(_, _, _, _, _, _, _, _, _, _, _, "true", _, _, _, _, _, _, local),
local_init_expr(local, init),
expr_type("external", init, x).
expr_type("external", e, x) :- java_expression("CAST_EXPRESSION", _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e),
diff --git a/graph/java/engine/resolution/generic-chain.dl b/graph/java/engine/resolution/generic-chain.dl
index 5cf2ba5f..a9e35fae 100644
--- a/graph/java/engine/resolution/generic-chain.dl
+++ b/graph/java/engine/resolution/generic-chain.dl
@@ -100,10 +100,7 @@ expr_arg_binding(call, nested, nestedParam, arg) :- call_site(call, _, recv),
// the method it actually appears in — same guard the recv_decl_ref clauses in expr-type.dl use.
// java_expression (25): 11 name 15 referenceKind 25 hash. java_local_variable (19): 1 name 14 method 19 hash.
expr_arg_binding(recv, bt, param, arg) :-
- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv),
- expr_ultimate_method("client", recv, m),
- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local),
- java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr),
+ local_use_init(recv, initExpr),
expr_arg_binding(initExpr, bt, param, arg).
// ── INHERITED generics with a CONCRETE super-arg (c9): `class DogBox extends Box`.
diff --git a/graph/java/engine/resolution/local-flow.dl b/graph/java/engine/resolution/local-flow.dl
index 0d935863..8680ccab 100644
--- a/graph/java/engine/resolution/local-flow.dl
+++ b/graph/java/engine/resolution/local-flow.dl
@@ -33,6 +33,20 @@
// java_local_variable (19): 0 name 13 method 18 hash
// ============================================================================
+// A USE of a local: the LOCAL_VARIABLE reference joined to its declaration, ONCE, below the
+// typing fixpoint. These same three atoms sat inline in five recursive rules, and semi-naive
+// evaluation re-ran the full java_expression scan every iteration — 163 iterations on a large
+// subject, ~40% of the whole solve between them. Hoisted, each of those rules starts from its
+// recursive delta and probes this table.
+local_use(use, local) :-
+ java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, use),
+ expr_ultimate_method("client", use, m),
+ java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local).
+// ...and the uses whose local is initialized by a call, with that initializer.
+local_use_init(use, initExpr) :-
+ local_use(use, local),
+ java_expression("METHOD_INVOCATION", _, "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, initExpr).
+
// The initializer expression of a local (LOCAL_VAR_INITIALIZER root, owned by the local).
local_init_expr(local, e) :-
java_expression(_, "ROOT", "LOCAL_VAR_INITIALIZER", "LOCAL_VARIABLE", _, local, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e).
@@ -53,7 +67,5 @@ local_flow_type(local, t) :- local_assign_value(local, e), expr_type(_, e, t).
// PROPAGATE: a LOCAL_VARIABLE receiver reference takes the flow-type(s) of its local
// (POSITIVE — resolves the concrete method directly; the cap handles the residual fan).
expr_type("client", recv, t) :-
- java_expression(_, _, _, _, _, _, _, _, _, _, name, _, _, _, "LOCAL_VARIABLE", _, _, _, _, _, _, _, _, _, recv),
- expr_ultimate_method("client", recv, m),
- java_local_variable(name, _, _, _, _, _, _, _, _, _, _, _, _, m, _, _, _, _, local),
+ local_use(recv, local),
local_flow_type(local, t).
diff --git a/graph/java/souffle/decls_all.dl b/graph/java/souffle/decls_all.dl
index 57f51bdd..605f86b2 100644
--- a/graph/java/souffle/decls_all.dl
+++ b/graph/java/souffle/decls_all.dl
@@ -475,6 +475,8 @@
.decl cha_wide_base(c0:symbol)
.decl cha_override_low(c0:symbol,c1:symbol)
.decl local_init_expr(c0:symbol,c1:symbol)
+.decl local_use(c0:symbol,c1:symbol)
+.decl local_use_init(c0:symbol,c1:symbol)
.decl local_assign_value(c0:symbol,c1:symbol)
.decl local_flow_type(c0:symbol,c1:symbol)
.decl param_flow_type(c0:symbol,c1:symbol)
diff --git a/graph/javascript/README.md b/graph/javascript/README.md
index 6a26b2ab..1336eecd 100644
--- a/graph/javascript/README.md
+++ b/graph/javascript/README.md
@@ -103,6 +103,7 @@ arrays. That is the engine, entirely.
| module graph | `resolution/module-graph.dl` | one export surface for both systems, keyed by name with `default` for `module.exports = X`; a CommonJS default value's properties ARE its members |
| hierarchy | `resolution/type-hierarchy.dl` | ONE closure — every heritage form inherits members, there is no `implements` |
| value flow | `resolution/value-flow.dl` | the may-analysis above |
+| instance state | `resolution/instance-state.dl` | an `("alloc", new-expression)` value beside `("inst", T)`, and the facts that let impact walk a callback or dependency given to ONE instance (a constructor option, a subscription) only from callers whose receiver may be that instance |
| arrays | `resolution/arrays.dl` | the one platform type modelled: `push`, `[i]`, `map`, `forEach`, `for..of`, `T[]`; `Map` / `Set` as collections, including an instance of a class that extends one (#619) |
| ambient | `resolution/ambient.dl` | platform names as values, so a site reached through one is classified from the value, not the syntax |
| JSDoc types | `resolution/reference-types.dl` | `@param`/`@type`/`@returns`, `import()` types, typedef aliases, wrappers |
diff --git a/graph/javascript/engine/call-edge-generation/calls.dl b/graph/javascript/engine/call-edge-generation/calls.dl
index a257ba73..5745b29d 100644
--- a/graph/javascript/engine/call-edge-generation/calls.dl
+++ b/graph/javascript/engine/call-edge-generation/calls.dl
@@ -72,6 +72,36 @@ unresolved_covered(ce) :- call_site("client", ck, _, _, _, _, ce, _, _), call_ki
unresolved_receiver(ce, cn, ck, "other_form") :- call_site("client", ck, cn, _, _, _, ce, _, _),
!call_resolved(ce), !unresolved_covered(ce).
+// ── library_receiver(CallExpr, Specifier) ───────────────────────────────────
+// An unresolved member call whose receiver is a PACKAGE'S value: a binding imported
+// from a package no staged IR declares, a member of one, what calling or constructing
+// one returns, and a const holding any of those — `request(app).get('/x').expect(200)`,
+// `new grpc.Metadata().get(k)`, `const md = new Metadata(); md.get(k)`. The receiver's
+// origin is known, so the site is not an untyped `x.get()`: a project method of the
+// same name is not what it calls. It stays unresolved (the package is not staged), and
+// consumers list it apart from the name matches on unknown receivers rather than
+// among them. Read off the source, never the value flow, so it cannot invent a value.
+// A call handed a project INSTANCE is left out: a wrapper, a proxy or a mock helper
+// may return the very object it was given (`mocked(repo).get(1)`).
+// A package is named by a bare specifier: a relative or absolute path that reached no
+// file (`require('./not-here')`) is a missing PROJECT module, and an alias (`@/x`, `~/x`,
+// `#x`) may map onto the project's own source.
+package_import(imp, spec) :- import_decl(_, spec, _, _, _, _, out, _, imp), import_outcome_is_package(out),
+ !match("[.#~/].*", spec), !match("@/.*", spec).
+library_value(e, spec) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), var_import(_, imp, v),
+ package_import(imp, spec).
+library_value(e, spec) :- expr_kind(_, "MODULE_EDGE_CALL", _, e), expr_module_edge(_, imp, e), package_import(imp, spec).
+library_value(e, spec) :- expr_kind(_, k, _, e), access_kind_reads_member(k), expr_child(_, e, "ACCESS_TARGET", _, r),
+ library_value(r, spec).
+library_value(e, spec) :- expr_kind(_, k, _, e), (k = "CALL" ; k = "NEW"), expr_child(_, e, role, _, c),
+ (role = "CALLEE" ; role = "RECEIVER"), library_value(c, spec), !call_passes_project_instance(e).
+library_value(e, spec) :- expr_kind(_, "AWAIT", _, e), expr_child(_, e, _, _, c), library_value(c, spec).
+library_value(e, spec) :- expr_kind(_, "IDENTIFIER", _, e), expr_binding(_, v, e), !var_import(_, _, v),
+ var_init("client", _, i, v), !variable_reassigned(v), library_value(i, spec).
+call_passes_project_instance(e) :- call_arg(e, _, a), expr_value(a, "inst", _).
+library_receiver(ce, spec) :- call_site("client", ck, _, "SYNTACTIC", _, _, ce, _, _), call_kind_is_member_form(ck),
+ !call_resolved(ce), expr_child(_, ce, "RECEIVER", _, r), library_value(r, spec).
+
// ── unresolved_value_callee(CallExpr, Binding) ──────────────────────────────
// An unresolved call whose callee is a VALUE, not a name: `cb()` on a parameter,
// `f()` on a loop variable or a local, `table[k]()`, `eval(s)`. Nothing about its
@@ -140,6 +170,29 @@ call_chain_edge(ce, caller, "-", m, "client", "multi_inferred", kind) :-
wrapper_call_target(ce, m), !call_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind).
variable_reassigned(v) :- expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, tgt),
expr_binding(_, v, tgt).
+// ── a call of what a PROJECT wrapper returned runs what that site was handed ──
+// `export default promisable(eachItem, 3)`, `const add = curry2(function add(a, b) {…})`:
+// the wrapper returns a closure that calls its parameter, and every export of the module
+// is that closure. The closure's call of the parameter is read context-insensitively, so
+// it held every function any of the wrapper's sites passed (past the fan cap: nothing),
+// and a test calling the export reached the closure and stopped there. The site is
+// already a value of its own, ("wrap", site), beside the closure (value-flow.dl), and a
+// call through it takes its RESULT from that site's argument alone; the same site says
+// what the call RUNS: the function it was handed at the forwarded position, what the
+// wrapper writes into that parameter, or what a wrapped argument itself runs
+// (`memoize(once(f))`). One of a set, beside the edge to the closure the call keeps, and
+// under the same fan cap: a callee that may hold more wrapped values than --dispatch-cap
+// (a parameter every wrapped export is passed to) runs one of them, and naming all of
+// them says nothing.
+wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "func", g).
+wrap_runs(s, g) :- wrap_site_arg(s, _, arg), expr_value(arg, "wrap", s2), wrap_runs(s2, g).
+wrap_runs(s, g) :- wrap_site(s, h, pos), wrapper_param_written(h, pos, o), expr_value(o, "func", g).
+wrapped_call_target(ce, g) :- callee_value(ce, "wrap", s), wrap_runs(s, g), method_prov(g, "client"),
+ !call_site(_, "FUNCTION_CALL_BIND", _, _, _, _, ce, _, _).
+wrapped_target_count(ce, n) :- wrapped_call_target(ce, _), n = count : { wrapped_call_target(ce, _) }.
+wrapped_over_cap(ce) :- wrapped_target_count(ce, n), dispatch_cap(c), n > to_number(c).
+call_chain_edge(ce, caller, "-", g, "client", "multi_inferred", kind) :-
+ wrapped_call_target(ce, g), !call_over_cap(ce), !wrapped_over_cap(ce), call_from(ce, caller), invocation_site(ce, kind).
// `(c ? a : b)()`, `(0, cb)()`, `make()()`: the callee is computed by an expression.
// `new Function(s)()` / `Function(s)()` runs code built at run time.
unresolved_value_callee(ce, "dynamic_code") :- value_callee_unresolved(ce, c), callee_builds_code(c).
diff --git a/graph/javascript/engine/call-edge-generation/field_access.dl b/graph/javascript/engine/call-edge-generation/field_access.dl
new file mode 100644
index 00000000..066a5aee
--- /dev/null
+++ b/graph/javascript/engine/call-edge-generation/field_access.dl
@@ -0,0 +1,60 @@
+// ============================================================================
+// CALL-EDGE-GEN · A MODULE VARIABLE READ BY NAME, AS A DATA EDGE
+//
+// `CFG.name`, `use(SCHEMA)`, `${TOKENS.store}`: the identifier is bound by the binder
+// (expr_binding) to a variable -- the module's own top-level one, or an import binding.
+// This engine follows an import to the VALUE the export holds (import_value), which is
+// what a call needs, and never to the exported VARIABLE, which is what "who reads this
+// const" needs. So a read of an imported const was answered by name, and the name
+// matched every same-named const in every sibling package.
+//
+// The export surface is walked here once more, keeping the variable: export_decl names
+// it (`export const x` and `export { x }` carry target kind VARIABLE), and a re-export
+// (`export * from`, `export { x } from`) passes it on exactly as module_export_value
+// passes a value on. A variable holding a function or a class is left out: its uses
+// are calls and constructions, which call edges already carry.
+//
+// field_access(Site, Caller, Variable, "client", "known_edge", Access), the shape the
+// Java and TypeScript engines export, so the bundle reads one relation for all three.
+// ============================================================================
+
+js_module_var(v) :- var_decl("client", _, reg, _, _, _, v), !var_owner_method("client", _, v),
+ !js_not_a_data_binding(reg),
+ !var_init("client", "FUNCTION", _, v), !var_init("client", "CLASS", _, v),
+ !var_import("client", _, v).
+js_not_a_data_binding("IMPORT_BINDING").
+js_not_a_data_binding("FUNCTION_DECLARATION_HOISTED").
+js_not_a_data_binding("CLASS_TDZ").
+js_not_a_data_binding("CATCH_PARAMETER").
+
+// ── export_var(Module, ExportedName, Variable) — the surface, by variable ───
+export_var(mod, n, v) :- export_decl("client", n, _, _, _, "VARIABLE", v, _, mod, _), n != "", js_module_var(v).
+export_var(mod, n, v) :- export_decl("client", en, _, "EXPORT_ALL", _, _, _, _, mod, x), export_all_is_spread(en),
+ export_reexport(_, _, imp, x), import_module(imp, src),
+ export_var(src, n, v), n != "default".
+export_var(mod, en, v) :- export_decl("client", en, ln, form, _, _, _, _, mod, x), form != "EXPORT_ALL",
+ export_reexport(_, _, imp, x), import_module(imp, src), ln != "",
+ export_var(src, ln, v).
+// ── import_var(ImportHash, Variable) — what a named import binding stands for ─
+import_var(imp, v) :- import_decl("client", _, _, bf, n, _, _, _, imp), import_binding_is_named(bf), n != "",
+ !contains(".", n), import_module(imp, mod), export_var(mod, n, v).
+
+// ── the site ────────────────────────────────────────────────────────────────
+js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", v, e), js_module_var(v).
+js_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER", _, e), expr_binding("client", lv, e),
+ var_import("client", imp, lv), import_var(imp, v).
+// A plain `=` never reads the old value; `+=` and `++` read and then write.
+js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_child(_, a, "ASSIGNMENT_TARGET", _, e).
+js_var_written(e) :- js_var_read_target(e, _), expr_kind(_, "UNARY", _, u), expr_operator(_, op, u), update_operator(op),
+ expr_child(_, u, _, _, e).
+js_var_write_only(e) :- js_var_read_target(e, _), expr_kind(_, "ASSIGNMENT", _, a), expr_operator(_, "=", a),
+ expr_child(_, a, "ASSIGNMENT_TARGET", _, e).
+js_var_access(e, "write") :- js_var_written(e), js_var_write_only(e).
+js_var_access(e, "readwrite") :- js_var_written(e), !js_var_write_only(e).
+js_var_access(e, "read") :- js_var_read_target(e, _), !js_var_written(e).
+// the caller: the callable the expression is written in, else the module initializer
+js_var_read_from(e, m) :- js_var_read_target(e, _), expr_owner("client", m, _, e), m != "".
+js_var_read_from(e, init) :- js_var_read_target(e, _), expr_owner("client", "", mod, e), module_init(_, init, mod).
+
+field_access(e, caller, v, "client", "known_edge", acc) :- js_var_read_target(e, v),
+ js_var_read_from(e, caller), js_var_access(e, acc).
diff --git a/graph/javascript/engine/resolution/fan-cap.dl b/graph/javascript/engine/resolution/fan-cap.dl
index af6bdd93..ad69acdb 100644
--- a/graph/javascript/engine/resolution/fan-cap.dl
+++ b/graph/javascript/engine/resolution/fan-cap.dl
@@ -80,14 +80,48 @@ direct_export_func(mod, n, m) :- export_decl(_, n, _, _, _, _, _, se, mod, _), s
// shares its name with hundreds of unrelated sites and must not be capped by them,
// while a module-level `each` or `merge` called under its own name from everywhere
// is exactly the generic utility the cap exists for.
+//
+// The PROGRAM's arguments are decided by the program's sites alone. A suite calls the
+// API it tests over and over, and counted with the program those calls made exactly the
+// functions under test "generic utilities": their parameters went untracked, so a
+// callback the program hands a constructor and stores on the instance (`this.decode =
+// options.decode`) was lost, and the program's own call through that field resolved only
+// to the fallback beside it. So there are two verdicts:
+// method_is_hot every site counts (as before): decides whether a TEST's
+// arguments flow in
+// method_is_hot_in_program test files' sites do not count: decides whether the
+// PROGRAM's arguments flow in
+// Nothing that flowed before stops flowing (a test handing the program a plugin it calls
+// back still reaches it). A function hot only because of its tests now receives what the
+// program passes it and still nothing from the tests: their callbacks would be the widest
+// values it holds, and with them the call through the stored field would be over the
+// dispatch cap and answer nothing.
param_flow_capped_at(n) :- dispatch_cap(s), n = to_number(s).
direct_call_count(m, n) :- direct_call_site(_, m), n = count : { direct_call_site(_, m) }.
method_is_hot(m, n) :- direct_call_count(m, n), param_flow_capped_at(c), n > c.
name_call_count(cn, n) :- call_site(_, _, cn, _, _, _, _, _, _), cn != "", n = count : { call_site(_, _, cn, _, _, _, _, _, _) }.
+program_direct_site(ce, m) :- direct_call_site(ce, m), !site_in_test_module(ce).
+program_direct_count(m, n) :- program_direct_site(_, m), n = count : { program_direct_site(_, m) }.
+method_is_hot_in_program(m, n) :- program_direct_count(m, n), param_flow_capped_at(c), n > c.
+// The NAME count is the overcount for module-level functions. A test's member call that
+// only shares the name (`lib.add(1, 2)` on an export that is a different function, a
+// curried closure) stays in it, as before; a test's bare call or `new` of the name
+// (`build(...)`, `new lib.Codec(...)`) denotes the function itself and leaves it.
+test_names_itself(ce) :- site_in_test_module(ce), call_site(_, ck, _, _, _, _, ce, _, _), call_kind_is_callee_form(ck).
+test_names_itself(ce) :- site_in_test_module(ce), call_site(_, "CONSTRUCTOR_CALL", _, _, _, _, ce, _, _).
+program_name_site(m, ce) :- module_level_function(m, cn), call_site(_, _, cn, _, _, _, ce, _, _), !test_names_itself(ce).
+program_name_count(m, n) :- program_name_site(m, _), n = count : { program_name_site(m, _) }.
+// ── test_module / site_in_test_module ──────────────
+// By path, as runners find their files: a `test`/`tests`/`__tests__`/`spec(s)` directory,
+// or a `.test.` / `.spec.` file.
+test_module(mod) :- module_file("client", fp, mod),
+ match("(.*/)?(tests?|__tests__|specs?)/.*|.*[.](test|spec)[.][cm]?[jt]sx?", fp).
+site_in_test_module(ce) :- expr_owner(_, _, mod, ce), test_module(mod).
module_level_function(m, n) :- method_decl(_, n, "FUNCTION_DECLARATION", _, _, "", mod, m), method_scopes(_, _, _, init, m), module_init(_, init, mod).
module_level_function(m, n) :- var_decl(_, n, _, _, _, mod, v), function_binding_value(v, m), method_decl(_, _, _, _, _, "", mod, m),
method_scopes(_, _, _, init, m), module_init(_, init, mod).
method_is_hot(m, n) :- module_level_function(m, cn), name_call_count(cn, n), param_flow_capped_at(c), n > c.
+method_is_hot_in_program(m, n) :- program_name_count(m, n), param_flow_capped_at(c), n > c.
// ── param_fan_capped(ParamHash, SiteCount) — countable ──────────────────────
-param_fan_capped(p, n) :- method_is_hot(m, n), param_decl(_, _, _, m, p).
+param_fan_capped(p, n) :- method_is_hot_in_program(m, n), param_decl(_, _, _, m, p).
diff --git a/graph/javascript/engine/resolution/instance-state.dl b/graph/javascript/engine/resolution/instance-state.dl
new file mode 100644
index 00000000..df747c53
--- /dev/null
+++ b/graph/javascript/engine/resolution/instance-state.dl
@@ -0,0 +1,133 @@
+// ============================================================================
+// Resolution · INSTANCE STATE (what ONE object was given, not what its class was)
+//
+// `new Bus({ validate: check })` and `b.on(handleA)` hand a callback to ONE bus. The
+// flow layer keys an instance by its class, ("inst", Bus), so `this.v` inside
+// `Bus.emit` holds every callback any bus was ever given, and the call edge
+// emit -> check is right for SOME bus. The edge is right; what is wrong is reading it
+// as "every caller of emit reaches check": a caller that emits on a bus built without
+// `validate` does not. A context-insensitive call graph cannot say that, so this file
+// says it beside the graph, as facts the impact walk reads (dl/impact.dl):
+//
+// state_world(M, T) callable M runs with an instance of T as its `this`:
+// an instance member of T, or a function nested in one
+// state_gate(M, F, T) every call M makes to F reaches F only through what
+// the receiver instance was given
+// state_gate_alloc(T, F, S) the allocation S (a `new T(...)` expression) is one
+// that was given F
+// state_call_alloc(C, M, S) C calls the T member M on a receiver that may be S
+// state_call_open(C, M) C calls M on a receiver whose allocation is unknown
+//
+// ── THE ALLOCATION VALUE ────────────────────────────────────────────────────
+// ("alloc", NewExpr) is a value of its own, carried BESIDE ("inst", T) wherever the
+// instance goes (a variable, a parameter, a field, a return, `x ?? new T()`), exactly
+// as ("wrap", site) travels beside a wrapper's closure (value-flow.dl). Nothing
+// resolves through it: members are still read off ("inst", T). It is minted only for
+// a class whose instance code CALLS A VALUE — a parameter (`f(e)`, `handler(env)`) or
+// a property of `this` that the class does not declare as a method (`this.v?.(e)`) —
+// which is the shape whose targets depend on what the instance was given. Every
+// other class carries no allocation, and so costs nothing here.
+//
+// ── WHEN A CALLBACK IS GATED ────────────────────────────────────────────────
+// F is gated in T when the only way F enters T's instance code is as an argument of
+// a call made from OUTSIDE that code on a T member (the constructor or a method) —
+// an ENTRY — and every such entry's receiver has a known allocation. It is not gated
+// (every caller keeps it, as before) when F:
+// · is declared inside T's instance code;
+// · is read inside that code through a name bound outside it (an import, a
+// module-level variable, a function declaration), or returned to it by a call
+// to a function declared outside it;
+// · is written onto an instance of T, or onto the static side of T, from outside;
+// · reaches T whose hierarchy has a client class above or below it, or whose own
+// code builds another T (`new T()`, `new this.constructor()`): what one instance
+// holds could then flow into another;
+// · enters through an entry whose receiver's allocation is unknown.
+// The allocations are the closed-world answer the engine gives for parameters too: a
+// receiver's allocations are the `new` expressions that reach it through resolved
+// flow. A receiver with none (a documented `@param {Bus}`, a library's return) is open.
+// ============================================================================
+
+// ── the instance code of a class ────────────────────────────────────────────
+state_world(m, t) :- method_decl("client", _, k, _, "false", t, _, m), t != "", k != "STATIC_BLOCK".
+state_world(m, t) :- type_ctor("client", m, t).
+state_world(m, t) :- method_enclosing(m, e), state_world(e, t).
+state_type_declares(t, n) :- method_decl(_, n, _, _, _, t, _, _), n != "".
+
+// ── the classes whose instance code calls a value ───────────────────────────
+state_type(t) :- state_world(em, t), call_site("client", ck, n, "SYNTACTIC", _, em, ce, _, _), call_kind_is_member_form(ck),
+ expr_child(_, ce, "RECEIVER", _, r), expr_kind(_, "THIS", _, r), n != "", !state_type_declares(t, n).
+state_type(t) :- state_world(em, t), call_site("client", ck, _, _, _, em, ce, _, _), call_kind_is_callee_form(ck),
+ expr_child(_, ce, "CALLEE", _, c), expr_param(_, _, c).
+
+// ── the allocation value, minted at `new T(...)` ────────────────────────────
+expr_value(e, "alloc", e) :- expr_kind("client", "NEW", _, e), new_constructs(e, t), state_type(t).
+state_alloc_type(s, t) :- expr_value(s, "alloc", s), new_constructs(s, t).
+
+// ── eligibility: nothing one instance holds can move into another ───────────
+state_type_mixed(t) :- state_type(t), type_super(t, s), type_decl("client", _, _, _, _, s).
+state_type_mixed(t) :- state_type(t), type_super(u, t), type_decl("client", _, _, _, _, u).
+state_type_mixed(t) :- state_type(t), state_world(em, t), expr_kind(_, "NEW", _, e), expr_owner(_, em, _, e), new_constructs(e, t).
+state_type_mixed(t) :- state_type(t), state_world(em, t), own_class_new(em, _).
+state_type_ok(t) :- state_type(t), !state_type_mixed(t).
+
+// ── entries: a call from outside the instance code onto a member of T ───────
+state_entry(t, ce) :- state_type_ok(t), expr_resolves_to_method(ce, m), state_world(m, t),
+ call_site("client", _, _, _, _, em, ce, _, _), !state_world(em, t).
+state_entry_alloc(ce, s) :- state_entry(t, ce), expr_kind(_, "NEW", _, ce), expr_value(ce, "alloc", s), state_alloc_type(s, t).
+state_entry_alloc(ce, s) :- state_entry(t, ce), expr_child(_, ce, "RECEIVER", _, r), expr_value(r, "alloc", s), state_alloc_type(s, t).
+state_entry_open(ce) :- state_entry(_, ce), !state_entry_alloc(ce, _).
+
+// What an entry's arguments carry: the values themselves and, a few levels down, what
+// their properties and elements hold (`{ validate: check }`, `[a, b]`). Depth-bounded:
+// a carry is evidence FOR gating, so a callback found deeper than this is simply not
+// counted as entering through the entry, and then it is not gated at all.
+state_carry(ce, k, i, 0) :- state_entry(_, ce), call_arg(ce, _, a), expr_value(a, k, i), k != "alloc".
+state_carry(ce, k2, i2, d + 1) :- state_carry(ce, k, i, d), d < 3, k != "func", prop_value(k, i, _, k2, i2), k2 != "alloc".
+state_carry(ce, k2, i2, d + 1) :- state_carry(ce, k, i, d), d < 3, (k = "arr" ; k = "coll"), elem_value(i, k2, i2), k2 != "alloc".
+state_enters(t, f, ce) :- state_entry(t, ce), state_carry(ce, "func", f, _).
+
+// ── leaks: F reaches the instance code some other way ───────────────────────
+// declared inside it
+state_leak(t, f) :- state_enters(t, f, _), state_world(f, t).
+// a value the instance code has from anywhere but an entry: a name bound outside it
+// (an import, a module-level variable or function, a class it constructs itself: `dep ??
+// new DefaultDep()` names DefaultDep, whose prototype holds `run`), or what a function
+// declared outside it returns. What those hold, a few levels down, is F leaking in. A
+// literal the code writes itself needs no rule: a function in it is either named (above)
+// or written inline, and then it is declared inside the instance code.
+state_outer_ref(t, e) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, e), expr_binding(_, v, e),
+ var_owner_method(_, vm, v), !state_world(vm, t).
+state_outer_ref(t, e) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, e), expr_binding(_, v, e),
+ !var_owner_method(_, _, v).
+state_outer_ref(t, ce) :- state_type_ok(t), state_world(em, t), expr_owner(_, em, _, ce), expr_kind(_, "CALL", _, ce),
+ expr_resolves_to_method(ce, g), !state_world(g, t).
+state_outer_carry(t, k, i, 0) :- state_outer_ref(t, e), expr_value(e, k, i), k != "alloc".
+state_outer_carry(t, k2, i2, d + 1) :- state_outer_carry(t, k, i, d), d < 3, k != "func", prop_value(k, i, _, k2, i2), k2 != "alloc".
+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
+// 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).
+
+// ── the gate ────────────────────────────────────────────────────────────────
+state_gated(t, f) :- state_enters(t, f, _), !state_leak(t, f).
+state_gate_alloc(t, f, s) :- state_gated(t, f), state_enters(t, f, ce), state_entry_alloc(ce, s).
+state_gate(m, f, t) :- state_gated(t, f), state_world(m, t), expr_resolves_to_method(ce, f),
+ call_site("client", _, _, _, _, m, ce, _, _).
+
+// ── who calls the instance code, on which allocation ────────────────────────
+state_gated_type(t) :- state_gated(t, _).
+state_call_alloc(c, m, s) :- state_gated_type(t), state_entry(t, ce), expr_resolves_to_method(ce, m),
+ call_site("client", _, _, _, _, c, ce, _, _), state_entry_alloc(ce, s).
+state_call_open(c, m) :- state_gated_type(t), state_entry(t, ce), expr_resolves_to_method(ce, m),
+ call_site("client", _, _, _, _, c, ce, _, _), state_entry_open(ce).
+state_world_of_gated(m, t) :- state_gated_type(t), state_world(m, t).
diff --git a/graph/javascript/engine/resolution/value-flow.dl b/graph/javascript/engine/resolution/value-flow.dl
index d6f8c132..e78863e2 100644
--- a/graph/javascript/engine/resolution/value-flow.dl
+++ b/graph/javascript/engine/resolution/value-flow.dl
@@ -264,8 +264,15 @@ param_value(p, "inst", t) :- param_type_name(_, _, tr, p), type_ref_type(tr, t).
// values on every export, a 20-minute solve — because a generic library is exactly
// the shape whose parameters unify everything. What a library does with a callback it
// is handed is a MODEL (resolution/frameworks.dl), stated per library, not inferred.
-param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _),
- method_decl("client", _, _, _, _, _, _, m),
+// The program's sites pass their arguments unless the PROGRAM calls the function from
+// more places than the cap; a test file's sites only while every site together stays
+// under it (fan-cap.dl).
+param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce),
+ !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m),
+ param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p),
+ expr_value(arg, k, i).
+param_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce),
+ !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m),
param_decl(_, _, ppos, m, p), to_number(ppos) = pos, !param_is_rest(_, p),
expr_value(arg, k, i).
// THE ONE LIBRARY EXCEPTION: a UMD wrapper (#710). `(function (root, factory) {
@@ -283,8 +290,12 @@ param_value(p, "func", f) :- call_site(_, "IIFE_CALL", _, _, _, _, ce, _, _), ca
expr_value(arg, "func", f).
// A rest parameter IS an array; the arguments at and past its position are its elements.
param_value(p, "arr", p) :- param_is_rest(_, p).
-elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !method_is_hot(m, _),
- method_decl("client", _, _, _, _, _, _, m),
+elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), !site_in_test_module(ce),
+ !method_is_hot_in_program(m, _), method_decl("client", _, _, _, _, _, _, m),
+ param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos,
+ expr_value(arg, k, i).
+elem_value(p, k, i) :- call_arg(ce, pos, arg), expr_resolves_to_method(ce, m), site_in_test_module(ce),
+ !method_is_hot(m, _), method_decl("client", _, _, _, _, _, _, m),
param_decl(_, _, ppos, m, p), param_is_rest(_, p), to_number(ppos) <= pos,
expr_value(arg, k, i).
// A default value: `cb = () => {}`. The parser gives the parameter the default's
@@ -493,8 +504,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 7f73c459..bc489cb6 100644
--- a/graph/javascript/souffle/decls_all.dl
+++ b/graph/javascript/souffle/decls_all.dl
@@ -87,6 +87,10 @@
.decl call_resolved(c0:symbol)
.decl unresolved_receiver(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl unresolved_covered(c0:symbol)
+.decl package_import(c0:symbol, c1:symbol)
+.decl library_value(c0:symbol, c1:symbol)
+.decl call_passes_project_instance(c0:symbol)
+.decl library_receiver(c0:symbol, c1:symbol)
.decl value_callee_open(c0:symbol, c1:symbol)
.decl unresolved_value_callee(c0:symbol, c1:symbol)
.decl callee_builds_code(c0:symbol)
@@ -108,6 +112,10 @@
.decl wrapper_holder_call(c0:symbol, c1:symbol)
.decl wrapper_runs(c0:symbol, c1:symbol)
.decl wrapper_call_target(c0:symbol, c1:symbol)
+.decl wrap_runs(c0:symbol, c1:symbol)
+.decl wrapped_call_target(c0:symbol, c1:symbol)
+.decl wrapped_target_count(c0:symbol, c1:number)
+.decl wrapped_over_cap(c0:symbol)
.decl live_export_variable(c0:symbol, c1:symbol)
.decl this_type_open(c0:symbol)
@@ -232,6 +240,18 @@
.decl var_owner_method(c0:symbol, c1:symbol, c2:symbol)
.decl var_type_name(c0:symbol, c1:symbol, c2:symbol, c3:symbol)
.decl var_pattern_root(c0:symbol, c1:symbol, c2:symbol)
+
+// ── call-edge-generation/field_access.dl ──
+.decl js_module_var(c0:symbol)
+.decl js_not_a_data_binding(c0:symbol)
+.decl export_var(c0:symbol, c1:symbol, c2:symbol)
+.decl import_var(c0:symbol, c1:symbol)
+.decl js_var_read_target(c0:symbol, c1:symbol)
+.decl js_var_written(c0:symbol)
+.decl js_var_write_only(c0:symbol)
+.decl js_var_access(c0:symbol, c1:symbol)
+.decl js_var_read_from(c0:symbol, c1:symbol)
+.decl field_access(c0:symbol, c1:symbol, c2:symbol, c3:symbol, c4:symbol, c5:symbol)
.decl var_binding_form(c0:symbol, c1:symbol, c2:symbol)
// ── resolution/module-graph.dl ──
@@ -368,6 +388,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)
@@ -445,6 +466,14 @@
.decl param_fan_capped(c0:symbol, c1:number)
.decl import_binding_is_default_like(c0:symbol)
.decl name_call_count(c0:symbol, c1:number)
+.decl test_module(c0:symbol)
+.decl site_in_test_module(c0:symbol)
+.decl program_direct_site(c0:symbol, c1:symbol)
+.decl program_direct_count(c0:symbol, c1:number)
+.decl method_is_hot_in_program(c0:symbol, c1:number)
+.decl test_names_itself(c0:symbol)
+.decl program_name_site(c0:symbol, c1:symbol)
+.decl program_name_count(c0:symbol, c1:number)
.decl module_level_function(c0:symbol, c1:symbol)
.decl ambient_collection(c0:symbol)
.decl coll_method_call(c0:symbol, c1:symbol, c2:symbol)
@@ -569,3 +598,27 @@
// ── resolution/ambient.dl (the global object) ──
.decl global_object_name(c0:symbol)
.decl global_name_resolution(c0:symbol)
+
+// ── resolution/instance-state.dl ──
+.decl state_world(c0:symbol, c1:symbol)
+.decl state_type_declares(c0:symbol, c1:symbol)
+.decl state_type(c0:symbol)
+.decl state_alloc_type(c0:symbol, c1:symbol)
+.decl state_type_mixed(c0:symbol)
+.decl state_type_ok(c0:symbol)
+.decl state_entry(c0:symbol, c1:symbol)
+.decl state_entry_alloc(c0:symbol, c1:symbol)
+.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)
+.decl state_gated(c0:symbol, c1:symbol)
+.decl state_gate_alloc(c0:symbol, c1:symbol, c2:symbol)
+.decl state_gate(c0:symbol, c1:symbol, c2:symbol)
+.decl state_gated_type(c0:symbol)
+.decl state_call_alloc(c0:symbol, c1:symbol, c2:symbol)
+.decl state_call_open(c0:symbol, c1:symbol)
+.decl state_world_of_gated(c0:symbol, c1:symbol)
diff --git a/graph/javascript/souffle/export_manifest.tsv b/graph/javascript/souffle/export_manifest.tsv
index 1e8b5aac..d62ccfa3 100644
--- a/graph/javascript/souffle/export_manifest.tsv
+++ b/graph/javascript/souffle/export_manifest.tsv
@@ -4,6 +4,7 @@ client_calls_client client-to-client-calls.csv
client_calls_lib client-to-lib-calls.csv
client_calls_lib_detail client-to-lib-navigation.csv
unresolved_receiver unresolved-receiver.csv
+library_receiver library-receiver.csv
method_has_unknown_call method-has-unknown-call.csv
unresolved_value_callee unresolved-value-callee.csv
entry_point entry-point.csv
@@ -42,3 +43,9 @@ package_entry package-entry.csv
import_staged_package_unreached import-staged-package-unreached.csv
member_write_refused member-write-refused.csv
jsx_renders jsx-renders.csv
+state_gate state-gate.csv
+state_gate_alloc state-gate-alloc.csv
+state_call_alloc state-call-alloc.csv
+state_call_open state-call-open.csv
+state_world_of_gated state-world.csv
+field_access field-access.csv
diff --git a/graph/pipeline/run-souffle.sh b/graph/pipeline/run-souffle.sh
index c26c8774..0347b7a5 100755
--- a/graph/pipeline/run-souffle.sh
+++ b/graph/pipeline/run-souffle.sh
@@ -34,7 +34,7 @@ JDK_DEPTH=1 # max JDK-hop depth engine-ii expands. FORCED (always applied). De
# hop 1; deeper JDK expansion only traces internal plumbing (the explosion source).
LIB_DEPTH="" # max external-lib-hop depth. OPTIONAL — empty = UNCAPPED (dive deep into the client's
# real deps, no sink catalog there). Set with --lib-depth to bound a runaway library.
-DISPATCH_CAP="${DISPATCH_CAP:-20}" # fan-width cap on virtual dispatch. DEFAULT 20 (measured):
+DISPATCH_CAP="${DISPATCH_CAP:-}" # fan-width cap on virtual dispatch. DEFAULT 20, off for Python (below) (measured):
# on cassandra 6.0-alpha2 it lifts change-impact precision d2 0.782->0.920 and
# d3 0.569->0.753 at ZERO recall cost against the must-have bytecode set (recall
# 1.000/0.996/0.988 unchanged), and halves fabricated false positives. Cap 10 is
@@ -81,6 +81,9 @@ PKG="$(cd "$SRC/.." && pwd)" # the package root: package.json, node_modules, p
. "$SRC/pipeline/compile-lock.sh"
# Rules are PER-LANGUAGE and live under graph//; the executor itself is shared.
LANG_ARG="${LANG_ARG:-java}"
+# Python's cap is off by default: a Python site over the cap is dropped outright, and for test selection a missed
+# test costs more than an extra one. Every other language keeps 20. An explicit --dispatch-cap still wins.
+if [ -z "$DISPATCH_CAP" ]; then [ "$LANG_ARG" = python ] && DISPATCH_CAP=off || DISPATCH_CAP=20; fi
ENG="$SRC/$LANG_ARG/engine"; ENG2="$SRC/$LANG_ARG/engine-ii"; DL="$SRC/$LANG_ARG/souffle"; TPL="$SRC/$LANG_ARG/templates"
[ -d "$ENG" ] || { echo "no rule set for --language=$LANG_ARG (looked in $ENG)" >&2; exit 1; }
# shellcheck source=souffle-include.sh
@@ -201,6 +204,7 @@ write_program(){
_SU=()
t+="#include \"$LANG_ARG/souffle/decls_base.dl\"$nl#include \"$LANG_ARG/souffle/decls_all.dl\"$nl"
map_rels_su "$TPL/client-ir.map" || return 1
+ if [ -f "$TPL/client-extra.map" ]; then map_rels_su "$TPL/client-extra.map" || return 1; fi
map_rels_su "$TPL/lib.map" sig || return 1
for r in $LIB_BODY; do _SU+=("$r"); done
_SU+=(jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world)
@@ -254,7 +258,8 @@ write_program(){
# lost on either side shows up as a difference. Every .input and .output line must be exactly
# the expected one, each exactly once, with nothing extra. Exit status only: no pipe to lose.
verify_program(){
- awk -v cmap="$TPL/client-ir.map" -v lmap="$TPL/lib.map" -v man="$DL/export_manifest.tsv" \
+ local xmap=""; [ -f "$TPL/client-extra.map" ] && xmap="$TPL/client-extra.map"
+ awk -v cmap="$TPL/client-ir.map" -v xmap="$xmap" -v lmap="$TPL/lib.map" -v man="$DL/export_manifest.tsv" \
-v libsig=" $LIB_SIG " -v extra="$LIB_BODY jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world" '
function want(line) { if (!(line in need)) { need[line] = 1; n++ } }
function inp(r) { want(".input " r "(IO=file, filename=\"" r ".facts\", delimiter=\"\\t\", rfc4180=true)") }
@@ -269,7 +274,7 @@ verify_program(){
close(path)
}
BEGIN {
- maprels(cmap, 0); maprels(lmap, 1)
+ maprels(cmap, 0); maprels(lmap, 1); if (xmap != "") maprels(xmap, 0)
m = split(extra, e, " "); for (i = 1; i <= m; i++) if (e[i] != "") inp(e[i])
while ((rc = (getline l < man)) > 0) {
sub(/^\t+/, "", l); k = index(l, "\t"); p = l; f = ""
@@ -318,14 +323,14 @@ engine_id_of(){
done < "$prog"
hin="$(mktemp "${TMPDIR:-/tmp}/axiom-engine-id.XXXXXX")" && [ -f "$hin" ] \
|| { echo "❌ engine id: mktemp failed" >&2; return 1; }
- if ! printf 'souffle=%s\n' "$SOUFFLE_VERSION" > "$hin" || ! cat "$prog" ${incs[@]+"${incs[@]}"} >> "$hin"; then
+ if ! printf 'souffle=%s+seqlock-fix-4\n' "$SOUFFLE_VERSION" > "$hin" || ! cat "$prog" ${incs[@]+"${incs[@]}"} >> "$hin"; then
echo "❌ engine id: writing the hash input failed" >&2; rm -f "$hin"; return 1
fi
# The expected size comes from the SOURCE files (wc's last line is their total), not from a
# second read through cat, so a cat that loses bytes cannot agree with itself.
want="$(wc -c "$prog" ${incs[@]+"${incs[@]}"})"; have="$(wc -c < "$hin")"
want="${want##*$'\n'}"; want="${want#"${want%%[![:space:]]*}"}"; want="${want%% *}"
- case "$want" in ""|*[!0-9]*) want=-1;; *) want=$(( want + ${#SOUFFLE_VERSION} + 9 ));; esac # 9 = "souffle=" + "\n"
+ case "$want" in ""|*[!0-9]*) want=-1;; *) want=$(( want + ${#SOUFFLE_VERSION} + 23 ));; esac # 23 = "souffle=" + "+seqlock-fix-4" + "\n"
if [ "${have//[[:space:]]/}" != "$want" ]; then
echo "❌ engine id: the hash input is ${have//[[:space:]]/} bytes, expected $want (short write)" >&2; rm -f "$hin"; return 1
fi
@@ -379,8 +384,270 @@ case "${AXIOM_ENGINE_MARCH:-native}" in
portable) MARCH_FLAG=();;
*) MARCH_FLAG=("-march=${AXIOM_ENGINE_MARCH:-native}");;
esac
+# ── PARALLEL SOLVE ────────────────────────────────────────────────────────────
+# Soufflé emits parallel loops only when the program is GENERATED with -j — without it
+# the C++ holds zero parallel sections, which is why every engine before this was
+# sequential by construction. Compiled WITHOUT OpenMP the same generated code runs
+# serially (pfor degrades to for), so generation always asks for the parallel loops and
+# OpenMP at COMPILE time decides the flavor. Measured on a 6,139-file Java subject:
+# 147s serial -> 59s at -j8; the outputs are equal as sets (row order shifts between
+# flavors; the bundle loads rows into sqlite, which keeps no order).
+# GATED PER PLATFORM. darwin-arm64's nondeterministic parallel segfaults — a different
+# rule each crash, g++/libgomp and apple-clang/libomp alike — were two weak-ordering
+# holes, both closed by the header overlay below: the write-entry RMW (seqlock-fix-1)
+# and the unfenced publication of freshly split btree nodes (seqlock-fix-2), which is
+# why the crashes needed memory pressure (a recycled page holds garbage where a fresh
+# one holds zeros). darwin-arm64 now runs parallel by default, like linux-x64; Linux
+# enables OpenMP when its toolchain takes -fopenmp. AXIOM_SOLVE_PARALLEL=0 forces serial
+# anywhere; =1 forces the attempt anywhere (still needs a toolchain with -fopenmp).
+# The flavor is part of the CACHE NAME, never shared between flavors: the two binaries
+# answer with different row orders, and a cache hit must reproduce the flavor that ran
+# yesterday, not whichever compiled first.
+# THE SEQLOCK FIX (overlay). Soufflé's OptimisticReadWriteLock enters its write phase
+# with fetch_or(..., memory_order_acquire): the version-odd store may become visible
+# AFTER the write section's data stores on a weakly-ordered CPU, so a reader can read a
+# half-mutated node and still pass validate() against the stale even version. On x86's
+# TSO stores never reorder, which is why this only ever fired on arm64 (nondeterministic
+# segfaults in a different rule each run, any toolchain, Soufflé 2.5 and master alike).
+# seq_cst on the entry RMW pins the odd version BEFORE any data store; on x86 a locked
+# RMW is already a full barrier, so the change costs nothing there.
+# THE PUBLICATION FIX (fix-2, BTree.h). The residual arm64 crash under memory pressure:
+# a split publishes a freshly built node with PLAIN pointer stores — insert_inner's
+# `getChildren()[pos + 1] = newNode` and grow_parent's `*root = new_root` — and on a
+# weakly-ordered CPU that pointer can become visible BEFORE the node's own field stores,
+# including its lock's start_write. An optimistic reader then descends into memory whose
+# lock reads as free and whose children are whatever the allocator left there: zeros on a
+# fresh page (survivable), garbage on recycled memory — which is exactly the observed
+# "under memory pressure, a different rule each run" (TSAN: 66 atomic-vs-plain write pairs,
+# all BTree.h insert/split against a node's lock word). A release fence before each
+# publication orders every initialization store first; the reader's dereference is
+# address-dependent, which arm64 orders by itself. The engine id carries
+# "+seqlock-fix-4", so no unpatched cache entry or package is ever taken for a patched one.
+souffle_overlay(){
+ local inner="$1" overlay="$CACHE_ROOT/include-seqlock-fix-4"
+ local hdr="$overlay/souffle/utility/ParallelUtil.h"
+ if [ ! -f "$hdr" ]; then
+ rm -rf "$overlay.tmp.$$"
+ mkdir -p "$overlay.tmp.$$"
+ cp -R "$inner/." "$overlay.tmp.$$/" || return 1
+ local h="$overlay.tmp.$$/souffle/utility/ParallelUtil.h"
+ [ -f "$h" ] || return 1
+ # three write-entry RMWs: start_write (two), try_start_write, try_upgrade_to_write
+ sed -i.bak 's/version\.fetch_or(0x1, std::memory_order_acquire)/version.fetch_or(0x1, std::memory_order_seq_cst)/g' "$h" && rm -f "$h.bak"
+ grep -q 'fetch_or(0x1, std::memory_order_seq_cst)' "$h" || return 1
+ grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$h" && return 1
+ # fix-3: a release fence before EVERY store that makes a new node reachable — the child
+ # links fix-2 fenced, and the parent-pointer publications it missed (split's reparenting,
+ # grow_parent's this->parent/sibling->parent, insert_inner's late newNode->parent, which
+ # is reordered to before the link). The lock-parents walk reads exactly those pointers.
+ # ...and the MINGW-COMPILE guards: souffle maps __builtin_popcountll to MSVC's
+ # __popcnt64 and pulls intrin.h for ALL of _WIN32, which breaks g++ on MinGW — the
+ # one toolchain a Windows developer who clones this repository compiles with. Both
+ # are MSVC-only concerns, so the guards narrow to _MSC_VER; every other platform's
+ # preprocessed output is bit-identical.
+ local b="$overlay.tmp.$$/souffle/datastructure/BTree.h"
+ [ -f "$b" ] || return 1
+ python3 - "$b" <<'PYEOF' || return 1
+import glob, os, sys
+p = sys.argv[1]; s = open(p).read()
+root = os.path.dirname(os.path.dirname(p))
+F = "std::atomic_thread_fence(std::memory_order_release); // publication fence (seqlock-fix-3)"
+# split(): fill the sibling completely (children, counts), fence, THEN reparent — each
+# children[j]->parent = other makes the new sibling reachable from an existing node.
+a1 = """ // move child pointers
+ if (this->inner) {
+ // move pointers to sibling
+ auto* other = static_cast(sibling);
+ for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) {
+ other->children[j] = getChildren()[i];
+ other->children[j]->parent = other;
+ other->children[j]->position = static_cast(j);
+ }
+ }
+
+ // update number of elements
+ this->numElements = split_point;
+ sibling->numElements = maxKeys - split_point - 1;"""
+r1 = """ // move child pointers: the sibling's own fields first (private until reparented)
+ if (this->inner) {
+ auto* other = static_cast(sibling);
+ for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) {
+ other->children[j] = getChildren()[i];
+ }
+ }
+ sibling->numElements = maxKeys - split_point - 1;
+ """ + F + """
+ if (this->inner) {
+ auto* other = static_cast(sibling);
+ for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) {
+ other->children[j]->parent = other;
+ other->children[j]->position = static_cast(j);
+ }
+ }
+
+ // update number of elements
+ this->numElements = split_point;"""
+# grow_parent(): new_root is fully filled above; the parent stores are its first publication.
+a2 = """ // link this and the sibling node to new root
+ this->parent = new_root;"""
+r2 = """ // link this and the sibling node to new root
+ """ + F + """
+ this->parent = new_root;"""
+a3 = " // switch root node\n *root = new_root;"
+r3 = " // switch root node\n " + F + "\n *root = new_root;"
+# insert_inner(): give newNode its parent and position BEFORE the link that publishes it.
+a4 = """ keys[pos] = key;
+ getChildren()[pos + 1] = newNode;
+ newNode->parent = this;
+ newNode->position = static_cast(pos) + 1;"""
+r4 = """ keys[pos] = key;
+ newNode->parent = this;
+ newNode->position = static_cast(pos) + 1;
+ """ + F + """
+ getChildren()[pos + 1] = newNode;"""
+# spec-descent-guard (seqlock-fix-4): the insert descent dereferences the child it read
+# under a still-unvalidated optimistic lease; a concurrent split exposes a null slot.
+# CAPTURED on darwin-arm64: ldapr of the child's seqlock version at address null+8,
+# inside btree::insert, java -j8 under souffle-g memory load. A null child is a failed
+# validation and restarts the insert -- never a dereference. Applied to BTree.h and
+# its BTreeDelete.h twin.
+d = """ // get next pointer
+ auto next = cur->getChild(idx);
+
+ // get lease on next level
+ auto next_lease = next->lock.start_read();"""
+dr = """ // get next pointer
+ auto next = cur->getChild(idx);
+
+ // spec-descent-guard (seqlock-fix-4): a concurrent split can expose a
+ // null child slot under an optimistic lease; treat it as a failed
+ // validation, never dereference it.
+ if (next == nullptr) {
+ return insert(k, hints);
+ }
+
+ // get lease on next level
+ auto next_lease = next->lock.start_read();"""
+if s.count(d) != 1: sys.exit(1)
+s = s.replace(d, dr)
+bd = os.path.join(os.path.dirname(p), "BTreeDelete.h")
+t = open(bd).read()
+if t.count(d) != 1: sys.exit(1)
+open(bd, "w").write(t.replace(d, dr))
+for a, r in ((a1, r1), (a2, r2), (a3, r3), (a4, r4)):
+ if s.count(a) != 1: sys.exit(1)
+ s = s.replace(a, r)
+open(p, 'w').write(s)
+# mingw-compile guards, in every copy the include layout holds
+m1 = "#if _WIN64\n#define __builtin_popcountll __popcnt64\n#else\n#define __builtin_popcountll __popcnt\n#endif"
+m1r = "#if defined(_MSC_VER)\n" + m1 + "\n#endif // _MSC_VER (mingw-compile): MinGW has the gcc builtins"
+m2 = "#ifdef _WIN32\n#include "
+m2r = "#if defined(_MSC_VER) // (mingw-compile): intrin.h only for MSVC\n#include "
+hit1 = hit2 = 0
+for q in glob.glob(root + "/**/MiscUtil.h", recursive=True) + glob.glob(root + "/**/PiggyList.h", recursive=True):
+ t = open(q).read(); n = 0
+ if m1 in t: t = t.replace(m1, m1r); hit1 += 1; n = 1
+ if m2 in t: t = t.replace(m2, m2r); hit2 += 1; n = 1
+ if n: open(q, "w").write(t)
+if hit1 < 1 or hit2 < 1: sys.exit(1)
+# spec-fetch-guard (seqlock-fix-4): an optimistic BTree reader may hand the flyweight a
+# SPECULATIVELY-READ index before validating its seqlock. A torn or stale index must come
+# back as a harmless empty value that the validation then discards -- never a dereference
+# of an unassigned (null) or out-of-range slot. Observed as a segfault in a varying rule,
+# javascript -j8 on darwin-arm64 under memory-pressure load (4 hits in two runs).
+fw = glob.glob(root + "/**/ConcurrentFlyweight.h", recursive=True)[0]
+t = open(fw).read()
+fa = """ const Key& fetch(const lane_id H, const index_type Idx) const {
+ const auto Lane = Lanes.guard(H);
+ assert(Idx < SlotCount.load(std::memory_order_relaxed));
+ return Slots[Idx]->first;
+ }"""
+fr = """ const Key& fetch(const lane_id H, const index_type Idx) const {
+ const auto Lane = Lanes.guard(H);
+ // spec-fetch-guard (seqlock-fix-4): tolerate a speculative index, see overlay notes.
+ if (Idx >= SlotCount.load(std::memory_order_relaxed) || Slots[Idx] == nullptr) {
+ static const Key Empty{};
+ return Empty;
+ }
+ return Slots[Idx]->first;
+ }"""
+if t.count(fa) != 1: sys.exit(1)
+open(fw, "w").write(t.replace(fa, fr))
+rt = glob.glob(root + "/**/RecordTableImpl.h", recursive=True)[0]
+t = open(rt).read()
+ua = """ const RamDomain* unpack(RamDomain Index) const override {
+ return fetch(Index).data();
+ }"""
+ur = """ const RamDomain* unpack(RamDomain Index) const override {
+ // spec-fetch-guard (seqlock-fix-4): an empty generic record has a null data();
+ // give a speculative reader zeroed storage to misread instead.
+ const auto& R = fetch(Index);
+ if (R.data() == nullptr) {
+ static const RamDomain Zeros[64] = {};
+ return Zeros;
+ }
+ return R.data();
+ }"""
+if t.count(ua) != 1: sys.exit(1)
+open(rt, "w").write(t.replace(ua, ur))
+PYEOF
+ [ "$(grep -c 'seqlock-fix-3' "$b")" = "4" ] || return 1
+ [ "$(grep -c 'seqlock-fix-4' "$b")" = "1" ] || return 1
+ [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/BTreeDelete.h")" = "1" ] || return 1
+ [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/ConcurrentFlyweight.h")" = "1" ] || return 1
+ [ "$(grep -c 'seqlock-fix-4' "$overlay.tmp.$$/souffle/datastructure/RecordTableImpl.h")" = "1" ] || return 1
+ mv "$overlay.tmp.$$" "$overlay" 2>/dev/null || true # a concurrent run may have won; theirs is identical
+ rm -rf "$overlay.tmp.$$"
+ fi
+ [ -f "$hdr" ] && printf '%s' "$overlay"
+}
+OMP_FLAG=(); PAR_SUFFIX=""
+probe_openmp(){
+ # the flags this platform needs for a working OpenMP compile, or nothing.
+ # Linux: -fopenmp everywhere. Darwin: Apple clang only lowers the pragmas with the
+ # frontend flag plus Homebrew's libomp (-Xpreprocessor defines _OPENMP without
+ # lowering anything — a silently sequential binary, which is how this stayed hidden).
+ case "$(uname -s)" in
+ Linux|MINGW*|MSYS*|CYGWIN*)
+ # MSYS2/Git-Bash `c++` is g++, which takes -fopenmp exactly as Linux does
+ if printf 'int main(){return 0;}' | c++ -fopenmp -x c++ -o /dev/null - 2>/dev/null; then
+ printf '%s' "-fopenmp"; return 0
+ fi ;;
+ Darwin)
+ local omp
+ for omp in /opt/homebrew/opt/libomp /usr/local/opt/libomp; do
+ [ -f "$omp/lib/libomp.dylib" ] || continue
+ if printf 'int main(){return 0;}' | c++ -Xclang -fopenmp -I "$omp/include" -L "$omp/lib" -lomp -x c++ -o /dev/null - 2>/dev/null; then
+ printf '%s' "-Xclang -fopenmp -I $omp/include -L $omp/lib -lomp"; return 0
+ fi
+ done ;;
+ esac
+ return 1
+}
+# Default: Linux x86_64 only. The seqlock entry fix above repairs the diagnosed
+# ordering hole (quiet-machine runs went clean), but a residual crash mode remains on
+# arm64 under memory pressure, so weakly-ordered CPUs stay serial by default until it
+# is found; AXIOM_SOLVE_PARALLEL=1 opts any machine in for experiments.
+case "${AXIOM_SOLVE_PARALLEL:-}" in
+ 0) ;;
+ 1) if _OMP="$(probe_openmp)"; then
+ OMP_FLAG=($_OMP); PAR_SUFFIX="-par"
+ fi ;;
+ "") # Parallel by default on every platform with evidence behind it. The two arm64
+ # weak-ordering holes (write-entry RMW; unfenced node publication) are closed by
+ # the seqlock-fix-3 overlay, validated under load: darwin-arm64 10/10, linux-arm64
+ # 10/10 (GCP), linux-x64 5/5 (GCP) — all relation-identical to serial. x64 is TSO,
+ # where neither hole is observable. Windows local compiles probe like the rest
+ # (MSYS g++); packaged win32 engines carry /openmp from build-engines.yml, gated
+ # by its own release validation. AXIOM_SOLVE_PARALLEL=0 is the one-variable
+ # rollback to serial anywhere, no rebuild.
+ if _OMP="$(probe_openmp)"; then
+ # shellcheck disable=SC2206 — the probe emits simple flags, split wanted
+ OMP_FLAG=($_OMP); PAR_SUFFIX="-par"
+ fi ;;
+esac
EXE=""; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) EXE=".exe";; esac
-BIN="$CACHE_DIR/souffle-engine-$LANG_ARG-$ENGINE_ID$EXE"
+BIN="$CACHE_DIR/souffle-engine-$LANG_ARG-$ENGINE_ID$PAR_SUFFIX$EXE"
# The platform string, in npm's spelling (process.platform-process.arch), because that is
# how the engine packages are named: darwin-arm64, linux-x64, linux-arm64, win32-x64.
@@ -456,14 +723,16 @@ elif [ -z "$PACKAGED" ] && [ -n "${COMPILE_LOCK:-}" ]; then
# line blocks from stderr; on a real failure, dump the full log and fail. c++ -w
# silences the deprecation warnings in souffle's own headers. Compile to a .tmp then
# atomically rename, so a concurrent/aborted run never leaves a half-written binary.
- if ! souffle -I "$SRC" -g "$INT/souffle-program.cpp" "$PROG" 2> "$INT/.souffle-gen.log"; then
+ if ! souffle -I "$SRC" -j 8 -g "$INT/souffle-program.cpp" "$PROG" 2> "$INT/.souffle-gen.log"; then
cat "$INT/.souffle-gen.log" >&2; exit 1
fi
awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "$INT/.souffle-gen.log" >&2
[ -s "$INT/souffle-program.cpp" ] || { echo "❌ souffle wrote no C++ for $PROG" >&2; exit 1; }
CXX_PLATFORM=""
case "$(uname -s)" in CYGWIN*) CXX_PLATFORM="-Wa,-mbig-obj";; esac
- if ! c++ -std=c++17 -O3 ${MARCH_FLAG[@]+"${MARCH_FLAG[@]}"} -w $CXX_PLATFORM -I "$INNER" "$INT/souffle-program.cpp" -o "$BIN.tmp.$$"; then
+ OVERLAY="$(souffle_overlay "$INNER" || true)"
+ [ -n "$OVERLAY" ] || { echo "❌ could not prepare the patched soufflé headers (seqlock fix)" >&2; exit 1; }
+ if ! c++ -std=c++17 -O3 ${MARCH_FLAG[@]+"${MARCH_FLAG[@]}"} ${OMP_FLAG[@]+"${OMP_FLAG[@]}"} -w $CXX_PLATFORM -I "$OVERLAY" -I "$INNER" "$INT/souffle-program.cpp" -o "$BIN.tmp.$$"; then
rm -f "$BIN.tmp.$$"; echo "❌ compiling the engine failed" >&2; exit 1
fi
# VERIFY, THEN PUBLISH. The cache entry is trusted by name alone from now on, so nothing may
@@ -549,7 +818,7 @@ while IFS=$'\t' read -r rel csv; do
if [ -f "$CLIENT/$csv.csv" ]; then awk -F'\t' 'NR==1{n=NF; next} NF==n{print; next} {bad++} END{if(bad>0) printf " ! dropped %d malformed row(s) from %s\n", bad, FILENAME > "/dev/stderr"}' "$CLIENT/$csv.csv" > "$FACTS/$rel.facts"
else : > "$FACTS/$rel.facts"; fi
CLIENT_INPUTS="$CLIENT_INPUTS$rel"$'\n'
-done < <(read_map "$TPL/client-ir.map")
+done < <(read_map "$TPL/client-ir.map"; if [ -f "$TPL/client-extra.map" ]; then read_map "$TPL/client-extra.map"; fi)
# --- LIB: signature relations, concatenated across every module of every root ---
# CACHED. This concatenation reads the ENTIRE library IR (the JDK alone is 2.0 GB in,
@@ -771,7 +1040,22 @@ while [ "$iter" -lt 50 ]; do
# the instructions it uses (a shared cache, a CI cache keyed too coarsely), it dies
# with SIGILL (exit 132) before solving anything. Never leave it there to kill every
# later run the same way: drop the cache entry, so the next run recompiles, and say so.
- rc=0; "$BIN" -F "$FACTS" -D "$RAW" || rc=$?
+ # -j is passed ALWAYS (a serial binary ignores it silently — verified); more than one
+ # thread only for a binary of the parallel flavor: one this run compiled with OpenMP,
+ # or a packaged/cached one whose builder left a .parallel marker beside it.
+ SOLVE_J=1
+ if [ -n "$PAR_SUFFIX" ] || [ -f "$BIN.parallel" ]; then
+ 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"
@@ -810,7 +1094,7 @@ done
# here (it's in the shared cache), and facts must stay per-run (never shared) so concurrent
# analyses of different projects don't collide. Runs only on success (set -e bails earlier
# on failure, leaving the facts for debugging).
-rm -rf "$FACTS" "$INT/souffle-program.cpp"
+[ "${AXIOM_KEEP_FACTS:-0}" = "1" ] || rm -rf "$FACTS" "$INT/souffle-program.cpp"
SOLVE_EPOCH=$(date +%s)
echo "Elapsed (solve): $((SOLVE_EPOCH-START_EPOCH))s"
diff --git a/graph/python/engine/call-edge-generation/call_chain.dl b/graph/python/engine/call-edge-generation/call_chain.dl
index 622c5a81..ade21045 100644
--- a/graph/python/engine/call-edge-generation/call_chain.dl
+++ b/graph/python/engine/call-edge-generation/call_chain.dl
@@ -156,6 +156,40 @@ method_returns_method("client", m, r) :-
expr_call_candidate(site, g),
method_returns_method("client", g, r).
+// A function returning a member it LOOKED UP BY NAME: `def get_visitor(self, node): return
+// getattr(self, f"visit_{type(node).__name__}", None)`, called as `f = self.get_visitor(node);
+// f(node)`. The lookup is resolved where it is written (name-resolution.dl's getattr clauses),
+// and the method hands that set back exactly as it would hand back a bare name.
+method_returns_method("client", m, r) :-
+ method_return_value_expr("client", m, e),
+ call_of_expr(e, site), call_name(site, "getattr"),
+ expr_denotes_method("client", e, r).
+
+// ── a call that hands back one of its arguments (py_returns_arg, resolution/builtins.dl) ──
+// Matched through the import that binds the callee, never by the bare name:
+// `functools.update_wrapper(...)` / `t.cast(...)` where the receiver is the name an `import`
+// bound, or `update_wrapper(...)` bound by `from functools import update_wrapper`. A local
+// function that happens to share the name does neither.
+py_passthrough_arg(site, pos) :-
+ call_name(site, fn), call_receiver_object(site, obj),
+ expr_binding("client", rb, ctx, obj), ctx != "STORE", binding_lookup("client", rb, rb2),
+ import_binding("client", rb2, i), import_decl("client", k, mod, _, i),
+ (k = "MODULE_IMPORT" ; k = "MODULE_IMPORT_ALIAS"), // `import typing as t` is the alias kind
+ py_returns_arg(path, pos), cat(mod, cat(".", fn)) = path.
+py_passthrough_arg(site, pos) :-
+ call_callee_is_value(site), call_callee_expr(site, callee),
+ expr_binding("client", rb, ctx, callee), ctx != "STORE", binding_lookup("client", rb, rb2),
+ import_binding("client", rb2, i), import_decl("client", _, path, _, i),
+ py_returns_arg(path, pos).
+// the expression a value really is, through any number of such calls
+py_passthrough_root(e, e) :- method_return_value_expr("client", _, e).
+py_passthrough_root(e, x) :-
+ py_passthrough_root(e, c), call_of_expr(c, site), py_passthrough_arg(site, pos),
+ call_arg(site, pos, x).
+method_returns_method("client", m, r) :-
+ method_return_value_expr("client", m, e), py_passthrough_root(e, x), x != e,
+ expr_names_method("client", x, r).
+
// ── decorator_hits_lib(SiteKey, LibMethodHash) ───────────────────────────────
// `@abstractmethod` names `abc.abstractmethod`, which HAS Python source and is in the
// staged stdlib IR — it is a library boundary, not a blind spot. A BARE decorator has no
@@ -320,6 +354,25 @@ iter_protocol_edge(src, caller, m) :-
expr_type("client", src, t), iter_protocol_target(t, m),
expr_ultimate_method("client", src, caller).
+// ── property_write_edge(WriteExprHash, CallerMethodHash, AccessorMethodHash) ──
+// The store and delete halves of the property protocol: `obj.x = v` calls x's setter,
+// `del obj.x` its deleter (type_property_accessor, resolution/attribute-lookup.dl). Same
+// shape as a read, keyed on the access's own name context.
+property_write_edge(e, caller, m) :-
+ expr_node("client", "ATTRIBUTE_ACCESS", _, n, e),
+ expr_name_context("client", "STORE", e),
+ expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj),
+ expr_type("client", obj, t),
+ type_property_accessor("client", t, n, "PROPERTY_SETTER", m),
+ expr_ultimate_method("client", e, caller).
+property_write_edge(e, caller, m) :-
+ expr_node("client", "ATTRIBUTE_ACCESS", _, n, e),
+ expr_name_context("client", "DEL", e),
+ expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj),
+ expr_type("client", obj, t),
+ type_property_accessor("client", t, n, "PROPERTY_DELETER", m),
+ expr_ultimate_method("client", e, caller).
+
// ── property_read_edge(ReadExprHash, CallerMethodHash, GetterMethodHash) ─────
property_read_edge(e, caller, getter) :-
expr_node("client", "ATTRIBUTE_ACCESS", _, n, e),
@@ -426,9 +479,12 @@ site_class(site, "ambiguous_unknown") :-
// WHY a site is unresolved. This is the difference between a report and a shrug, and it
// is what makes an ambiguous_unknown auditable rather than a dumping ground.
site_reason(site, r) :- call_unresolvable(site, r), site_class(site, "ambiguous_unknown").
+// decorator_expr first: it binds d from site, so the annotation probe is keyed — written
+// annotation-first the planner crossed every annotation with every ambiguous site (25s
+// on a 526k-LOC subject for a diagnostic label).
site_reason(site, "unmodelled_decorator") :-
site_class(site, "ambiguous_unknown"),
- annotation_on("client", _, _, _, _, d), decorator_expr("client", site, d).
+ decorator_expr("client", site, d), annotation_on("client", _, _, _, _, d).
// The APPLICATION of a parenthesised decorator whose factory return the engine cannot
// type — a library factory such as functools.wraps. Named, not shrugged at.
site_reason(site, "decorator_factory_result_untyped") :-
@@ -626,6 +682,99 @@ subscript_protocol_edge(sub, caller, m) :-
mro_lookup("client", t, "__class_getitem__", m),
expr_ultimate_method("client", sub, caller).
+// ── BUILTIN PROTOCOL — `repr(x)` runs type(x).__repr__ ─────────────────────────
+// A builtin function that exists to call one dunder of its argument: repr, str, len,
+// hash, bool, iter, next, abs, format, reversed. The written call resolves to the builtin
+// (C code, boundary_lib), and the dunder it runs -- a client method, often the one a test
+// exists to check -- had no caller: `assert repr(v) == "<...>"` reached nothing of v's.
+// Only the BARE builtin name (bound to nothing in the client, so it is the builtin), with
+// exactly one argument (`str(b, "utf-8")` decodes and runs no __str__), on an argument the
+// engine can type. str() falls back to __repr__ and bool() to __len__ when the class
+// defines no __str__ / __bool__ of its own, as CPython's type slots do. The builtin names
+// and their dunders are catalogued in resolution/builtins.dl (builtin_protocol_slot).
+builtin_protocol_arg(e, fn, x) :-
+ call_of_expr(e, site), call_callee_is_value(site), call_callee_expr(site, callee),
+ expr_binding("client", b, _, callee), binding_lookup_unresolved("client", b, fn, _),
+ builtin_protocol_slot(fn, _),
+ call_arg(site, "0", x), !call_arg(site, "1", _), !call_kwarg(site, _, _).
+// The edge's site is the ARGUMENT expression, as an iteration's is the iterated one: the
+// call expression is already the written call to the builtin.
+builtin_protocol_edge(x, caller, m) :-
+ builtin_protocol_arg(_, fn, x), builtin_protocol_slot(fn, d),
+ expr_type("client", x, t), mro_lookup("client", t, d, m),
+ expr_ultimate_method("client", x, caller).
+builtin_protocol_edge(x, caller, m) :-
+ builtin_protocol_arg(_, fn, x), builtin_protocol_fallback(fn, d, d2),
+ expr_type("client", x, t), !type_has_member(t, d), mro_lookup("client", t, d2, m),
+ expr_ultimate_method("client", x, caller).
+type_has_member(t, d) :- builtin_protocol_fallback(_, d, _), mro_lookup("client", t, d, _).
+
+// ── OPERATOR PROTOCOL — `a + b`, `a == b`, `x in c`, `-a` run a dunder ────────
+// An operator compiles to BINARY_OP / COMPARE_OP / CONTAINS_OP / UNARY_*, never to a CALL,
+// so there is no written site, and the dunder it runs -- a client `__eq__`, `__or__`,
+// `__contains__` -- had no caller at all: a test asserting `a == b` or composing two
+// strategies with `|` reached nothing of theirs. Same channel as the protocol edges above,
+// its own kind. The site is the operator expression. Both halves of the protocol are
+// emitted, because both can run: the left operand's slot, and the right operand's
+// reflected one when the left is missing it or returns NotImplemented (a comparison's
+// mirror, `a < b` -> b.__gt__). The operators and their dunders are catalogued in
+// resolution/builtins.dl. A chained comparison (`a < b < c`) carries several operators in
+// one node and matches no catalogue row, so it emits nothing rather than a guess.
+operator_protocol_edge(e, caller, m) :-
+ expr_operator("client", op, e), operator_protocol_slot(op, d, _),
+ expr_parent("client", e, "OPERAND_LEFT", _, l), expr_type("client", l, t),
+ mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller).
+operator_protocol_edge(e, caller, m) :-
+ expr_operator("client", op, e), operator_protocol_slot(op, _, d),
+ expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t),
+ mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller).
+// `a != b` on a class with no __ne__ of its own runs __eq__ through object.__ne__.
+operator_protocol_edge(e, caller, m) :-
+ expr_operator("client", op, e), operator_protocol_slot(op, d, _), operator_protocol_fallback(d, d2),
+ expr_parent("client", e, "OPERAND_LEFT", _, l), expr_type("client", l, t),
+ !operator_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", e, caller).
+operator_type_has(t, d) :- operator_protocol_fallback(d, _), mro_lookup("client", t, d, _).
+// `x in c` runs the RIGHT operand's __contains__, or iterates it when it has none.
+operator_protocol_edge(e, caller, m) :-
+ expr_operator("client", op, e), membership_operator(op), membership_protocol_slot(d),
+ expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t),
+ mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller).
+operator_protocol_edge(e, caller, m) :-
+ expr_operator("client", op, e), membership_operator(op), membership_protocol_fallback(d, d2),
+ expr_parent("client", e, "OPERAND_RIGHT", _, r), expr_type("client", r, t),
+ !membership_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", e, caller).
+membership_type_has(t, d) :- membership_protocol_fallback(d, _), mro_lookup("client", t, d, _).
+// `-a`, `+a`, `~a`
+operator_protocol_edge(e, caller, m) :-
+ expr_node("client", "UNARY_OPERATION", _, _, e), expr_operator("client", op, e), unary_protocol_slot(op, d),
+ expr_parent("client", e, "UNARY_OPERAND", _, x), expr_type("client", x, t),
+ mro_lookup("client", t, d, m), expr_ultimate_method("client", e, caller).
+
+// ── TRUTH PROTOCOL — `if x:` runs type(x).__bool__, or __len__ ───────────────
+// Truth testing has no written call either: `if x:`, `while x:`, `assert x`, `not x`, the
+// condition of `a if x else b` and of a comprehension, and an operand of `and` / `or` all
+// run x's __bool__ -- or __len__ when the class defines no __bool__ (data model,
+// object.__bool__). The left operand of `and`/`or` is always tested; the right one only
+// when the whole expression is (`return a or b` hands b back untested). The site is the
+// tested expression.
+truth_tested(x) :-
+ truth_test_root(rc), expr_root_context("client", rc, x), !expr_has_parent("client", x).
+truth_tested(x) :- truth_test_role(role), expr_parent("client", _, role, _, x).
+truth_tested(x) :-
+ expr_node("client", "UNARY_OPERATION", _, _, e), expr_operator("client", op, e), truth_negation_operator(op),
+ expr_parent("client", e, "UNARY_OPERAND", _, x).
+truth_tested(x) :- expr_node("client", "BOOLEAN_OPERATION", _, _, e), expr_parent("client", e, "OPERAND_LEFT", _, x).
+truth_tested(x) :-
+ expr_node("client", "BOOLEAN_OPERATION", _, _, e), truth_tested(e), expr_parent("client", e, "OPERAND_RIGHT", _, x).
+
+truth_protocol_edge(x, caller, m) :-
+ truth_tested(x), truth_protocol_fallback(d, _), expr_type("client", x, t),
+ mro_lookup("client", t, d, m), expr_ultimate_method("client", x, caller).
+truth_protocol_edge(x, caller, m) :-
+ truth_tested(x), truth_protocol_fallback(d, d2), expr_type("client", x, t),
+ !truth_type_has(t, d), mro_lookup("client", t, d2, m), expr_ultimate_method("client", x, caller).
+truth_type_has(t, d) :- truth_protocol_fallback(d, _), mro_lookup("client", t, d, _).
+
// ── THE THREE PROTOCOL EDGES ARE TIERED BY TARGET COUNT, like every other edge ──
//
// A property read, a context-manager entry and an iteration all reach their target
@@ -665,9 +814,13 @@ subscript_protocol_edge(sub, caller, m) :-
// from the graph rather than being re-tiered. A dropped edge is worse than a mislabelled
// one, and the golden caught it.
protocol_edge(e, d, caller, m) :- property_read_edge(e, caller, m), method_decl(_, d, _, _, _, m).
+protocol_edge(e, d, caller, m) :- property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m).
protocol_edge(e, d, caller, m) :- with_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
protocol_edge(e, d, caller, m) :- iter_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
protocol_edge(e, d, caller, m) :- subscript_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
+protocol_edge(e, d, caller, m) :- builtin_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
+protocol_edge(e, d, caller, m) :- operator_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
+protocol_edge(e, d, caller, m) :- truth_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m).
// Aggregate in its own stratum, mirroring site_client_target_count above.
protocol_edge_target_count(e, d, n) :-
@@ -728,6 +881,12 @@ call_chain_edge(e, caller, "-", getter, "client", cls, "PROPERTY_READ") :-
property_read_edge(e, caller, getter), method_decl(_, d, _, _, _, getter),
protocol_edge_class(e, d, cls).
+// PROPERTY WRITE — `obj.x = v` runs x's setter and `del obj.x` its deleter. Likewise no
+// call site, and its own kind so it is never counted as a written call.
+call_chain_edge(e, caller, "-", m, "client", cls, "PROPERTY_WRITE") :-
+ property_write_edge(e, caller, m), method_decl(_, d, _, _, _, m),
+ protocol_edge_class(e, d, cls).
+
// CONTEXT MANAGER — the same shape: an edge with no call site, its own kind so it can
// never be mistaken for a written call.
call_chain_edge(cm, caller, "-", m, "client", cls, "CONTEXT_MANAGER") :-
@@ -746,6 +905,22 @@ call_chain_edge(sub, caller, "-", m, "client", cls, "SUBSCRIPT_PROTOCOL") :-
subscript_protocol_edge(sub, caller, m), method_decl(_, d, _, _, _, m),
protocol_edge_class(sub, d, cls).
+// BUILTIN PROTOCOL — likewise. Its own kind: the written call is to the builtin, and
+// this edge is the dunder that builtin runs, never a second written call.
+call_chain_edge(x, caller, "-", m, "client", cls, "BUILTIN_PROTOCOL") :-
+ builtin_protocol_edge(x, caller, m), method_decl(_, d, _, _, _, m),
+ protocol_edge_class(x, d, cls).
+
+// OPERATOR PROTOCOL — an operator's dunder. Its own kind: there is no written call.
+call_chain_edge(e, caller, "-", m, "client", cls, "OPERATOR_PROTOCOL") :-
+ operator_protocol_edge(e, caller, m), method_decl(_, d, _, _, _, m),
+ protocol_edge_class(e, d, cls).
+
+// TRUTH PROTOCOL — the __bool__ (or __len__) a truth test runs. Its own kind likewise.
+call_chain_edge(x, caller, "-", m, "client", cls, "TRUTH_PROTOCOL") :-
+ truth_protocol_edge(x, caller, m), method_decl(_, d, _, _, _, m),
+ protocol_edge_class(x, d, cls).
+
// A site whose caller could not be determined AT ALL would vanish from every rule
// above. That must be impossible (the parser guarantees a non-empty owner), so it is
// asserted rather than assumed: any such site is emitted with caller "-" so the
@@ -819,7 +994,10 @@ method_has_unknown_call(caller, site) :-
// ── call_chain_summary(EdgeStatus, N) ────────────────────────────────────────
// The conservation ledger, as a relation: the tier counts must sum to the site count.
+// One count per DISTINCT class: driven by site_class rows, the aggregate re-counted the
+// class for every site in it (sites x class-size on a large subject, 15s for a ledger).
+site_class_kind(cls) :- site_class(_, cls).
call_chain_summary(cls, n) :-
- site_class(_, cls),
+ site_class_kind(cls),
n = count : { site_class(_, cls) }.
call_chain_summary("_total_sites", n) :- n = count : { invocation_site(_, _) }, n > 0.
diff --git a/graph/python/engine/config-resolution/knobs.dl b/graph/python/engine/config-resolution/knobs.dl
index 19231f11..27c24871 100644
--- a/graph/python/engine/config-resolution/knobs.dl
+++ b/graph/python/engine/config-resolution/knobs.dl
@@ -165,6 +165,14 @@ py_parametrize_deco("parametrize").
py_parametrize_argnames_kw("argnames").
py_parametrize_indirect_kw("indirect").
py_parametrize_indirect_all("True").
+// The VALUES the runner hands in: the decorator's second argument (or `argvalues=`), a
+// fixture's `params=`, a row wrapped as `pytest.param(v, ..., id=...)`, and, inside a fixture
+// declared with params, the attribute `request.param` it reads the current one from.
+py_parametrize_argvalues_kw("argvalues").
+py_fixture_params_kw("params").
+py_param_row_wrapper("param").
+py_fixture_request_param("request").
+py_request_param_attr("param").
// ── py_argnames_sep / py_argnames_lead: how "a, b" separates argument names ──
// The runner splits the string on "," and strips each piece. Souffle cannot split, so a
diff --git a/graph/python/engine/expression-resolution/expr-type.dl b/graph/python/engine/expression-resolution/expr-type.dl
index 8385f783..ed530619 100644
--- a/graph/python/engine/expression-resolution/expr-type.dl
+++ b/graph/python/engine/expression-resolution/expr-type.dl
@@ -152,23 +152,30 @@ expr_type(p, e, t) :-
// of any length follows by recursion, since the object's type is derived the same way.
// Same two field-type sources and the same declared_dispatch widening as the self clauses,
// so no new precision assumption is introduced.
-expr_type(p, e, t) :-
+
+// ── attr_access(p, e, n, obj): a named attribute access and its object, ONCE ─────────
+// This three-atom prefix sat inline in 13 recursive clauses of the typing fixpoint, and
+// semi-naive evaluation re-ran the expr_node scan every iteration — 79 iterations on a
+// 526k-LOC subject, ~20% of the solve between them. Hoisted below the fixpoint (pure
+// syntax), each clause starts from its recursive delta and probes this table.
+attr_access(p, e, n, obj) :-
expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj).
+
+expr_type(p, e, t) :-
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
field_declared_type(p, f, d),
declared_dispatch(p, d, t).
expr_type(p, e, t) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
field_type_from_param(p, f, d),
declared_dispatch(p, d, t).
expr_type(p, e, t) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
field_type_from_construction(p, f, t).
@@ -179,8 +186,7 @@ expr_type(p, e, t) :-
// an attribute written through `self` in a method does not exist on the class object,
// so typing `Cls.x` from it would name something the class never holds.
expr_type(p, e, t) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type_class_object(p, obj, ot),
type_attr_field(p, ot, n, f),
field_decl(p, n, _, "CLASS_BODY_ASSIGN", f),
@@ -212,8 +218,7 @@ expr_type(p, e, t) :-
// Same widening as everywhere else: the declared type goes out through declared_dispatch,
// so an overriding subclass is not dropped and the annotation is not treated as exact.
property_read_of(p, e, obj, n, g) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_property_getter(p, ot, n, g).
// A USER-WRITTEN DATA DESCRIPTOR reaches the same two clauses (issue #326). `@property`
@@ -221,8 +226,7 @@ property_read_of(p, e, obj, n, g) :-
// agree; type_descriptor_getter (resolution/attribute-lookup.dl) is deliberately shaped
// like type_property_getter so this is one more clause rather than a parallel path.
property_read_of(p, e, obj, n, g) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_descriptor_getter(p, ot, n, g).
// (i3a) the getter's declared `-> X`.
@@ -430,6 +434,19 @@ binding_value_type(p, b, t) :-
assign_pair(p, tgt, val),
expr_type(p, val, t).
+// ── AN IMPORTED MODULE-LEVEL VALUE IS THE BINDING IT NAMES (#1140) ───────────
+// `from order_service import order_service` resolves to the exporting module's
+// own binding row (import_resolved_target kind VARIABLE), and that binding's
+// value type is derived above from its assignment. Without this clause the
+// type stopped at the module boundary: the same `order_service.cancel()` was a
+// known edge next to the assignment and a name match one import away. The
+// write-count trade-offs are inherited, not re-decided -- a multi-write export
+// arrives as the union and the tier follows from the target count as usual.
+binding_value_type(p, b, t) :-
+ import_binding(p, b, i),
+ import_resolved_target(p, "VARIABLE", b2, i),
+ binding_value_type(p, b2, t).
+
// ── A CONDITIONAL EXPRESSION IS EITHER BRANCH ────────────────────────────────
// `w = HtmlWriter() if flag else PlainWriter()`. CONDITIONAL_EXPRESSION was present in
// the IR with BODY / CONDITION / ORELSE children and NO rule read it, so a ternary had
@@ -568,6 +585,15 @@ binding_declared_nominal(p, bind, d) :-
binding_declared_ref(p, bind, r),
type_ref_resolved(p, d, r),
!type_is_structural(p, d).
+// `x: Optional[Order] = ...` / `x: Order | None = ...` IS an Order (or None) — the rule the
+// parameter, return and field annotations already have (resolution/annotations.dl,
+// "Optional[X] IS X"), which the local annotation alone was missing: a local written
+// with the commonest modern spelling stayed untyped while the same annotation on a
+// parameter resolved.
+binding_declared_nominal(p, bind, t) :-
+ type_ref_owner(p, bind, "BINDING", r),
+ type_ref(p, k, "VARIABLE_ANNOTATION", _, _, r), annotation_optional_kind(k),
+ type_ref_element(p, r, t), !type_is_structural(p, t).
// The FK is resolved on only 14 of 479 variable annotations, so the name-based lookup
// beside it is what carries this -- the same two clauses the parameter path uses, with
// the same kind restriction. A SUBSCRIPT or a UNION names no single type and is served by
@@ -645,8 +671,7 @@ expr_type_untypable(p, e, "unresolved_name") :-
// (2b) the same hop through ANY object, not only self.
expr_type_untypable(p, e, "attribute_untyped") :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
field_type_unknown(p, f), !expr_any_type(p, e).
@@ -656,8 +681,7 @@ expr_type_untypable(p, e, "attribute_untyped") :-
// one wants the field annotated or its writes typed, the other wants the object typed.
// Reporting them as one reason would point at the wrong layer.
expr_type_untypable(p, e, "attribute_object_untyped") :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
!expr_any_type(p, obj),
!expr_any_type(p, e).
@@ -674,8 +698,7 @@ expr_type_untypable(p, e, "attribute_object_untyped") :-
// `handler` is a METHOD is a call through a bound method, which resolution/value-flow.dl
// owns; labelling it "absent" would be wrong rather than merely unhelpful.
expr_type_untypable(p, e, "attribute_absent_on_type") :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
!type_attr_field(p, ot, n, _),
!mro_lookup(p, ot, n, _),
@@ -699,8 +722,7 @@ expr_type_untypable(p, e, "attribute_absent_on_type") :-
// and no amount of field typing will reach it. Merging them would put the largest single
// shape in the residual behind a name that suggests the wrong fix.
expr_type_untypable(p, e, "class_attribute_absent") :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type_class_object(p, obj, ot),
!type_attr_field(p, ot, n, _),
!mro_lookup(p, ot, n, _),
diff --git a/graph/python/engine/framework-behavior/dispatch.dl b/graph/python/engine/framework-behavior/dispatch.dl
index b542838d..f7277835 100644
--- a/graph/python/engine/framework-behavior/dispatch.dl
+++ b/graph/python/engine/framework-behavior/dispatch.dl
@@ -292,9 +292,15 @@ py_fixture_request(req, name) :- py_fixture_param(req, name),
// below. Only in a requester's own body: a helper function calling it is not collected,
// and which test calls the helper is a call edge's question, not this rule's.
.decl py_fixture_value_call(req:symbol, e:symbol)
-py_fixture_value_call(req, e) :- py_fixture_requester(req),
- call_decl("client", "METHOD_CALL", fn, _, e, site), py_fixture_value_method(fn),
- call_context("client", _, req, _, _, site).
+// SELECTIVE ATOM FIRST: there is one fixture-value method name and, on most projects,
+// few or no calls to it — while there are tens of thousands of requesters. Written
+// requester-first, the planner walked every requester's whole call context before the
+// one-tuple filter said no: 295s to derive ZERO rows on a 526k-LOC subject. This order
+// costs what the answer costs.
+py_fixture_value_call(req, e) :- py_fixture_value_method(fn),
+ call_decl("client", "METHOD_CALL", fn, _, e, site),
+ call_context("client", _, req, _, _, site),
+ py_fixture_requester(req).
// (a) the name written as a string literal;
py_fixture_request(req, name) :- py_fixture_value_call(req, e),
call_arg(e, "0", a), expr_node("client", "LITERAL", _, name, a), name != "".
@@ -320,7 +326,7 @@ py_argnames_sep_at(h, s, i) :- py_parametrize_argnames(h, s), py_argnames_sep(se
// A whole token: led by the string's start, a separator or a space, and followed by its
// end, a separator or a space, so `server` is not found inside `server_url`.
.decl py_argnames_found(h:symbol, name:symbol, i:number)
-py_argnames_found(h, p, i) :- py_fixture_value_param(h, p), py_parametrize_argnames(h, s),
+py_argnames_found(h, p, i) :- py_argnames_demand(h, p), py_parametrize_argnames(h, s),
strlen(s) > strlen(p), i = range(0, strlen(s) - strlen(p) + 1), substr(s, i, strlen(p)) = p.
.decl py_argnames_led(h:symbol, name:symbol, i:number)
py_argnames_led(h, p, 0) :- py_argnames_found(h, p, 0).
@@ -340,10 +346,15 @@ py_value_row_kind("LIST").
py_argnames_pos(h, p, k) :- py_argnames_name_at(h, p, i), py_parametrize_argnames(h, s),
k = count : { py_argnames_sep_at(h, s, j), j < i }.
// ... or, for argnames written as a list or tuple of strings, its element position there.
-py_argnames_pos(h, p, k) :- py_fixture_value_param(h, p),
+py_argnames_pos(h, p, k) :- py_argnames_demand(h, p),
decorator_expr("client", de, h), call_arg(de, "0", a),
expr_parent("client", a, "ELEMENT", ks, el), expr_node("client", "LITERAL", _, p, el),
k = to_number(ks).
+// the argument names whose position is asked for: one a getfixturevalue call reads, and
+// every argument the decorator supplies directly (its values type the parameter, below)
+.decl py_argnames_demand(h:symbol, p:symbol)
+py_argnames_demand(h, p) :- py_fixture_value_param(h, p).
+py_argnames_demand(h, p) :- py_parametrize_direct(req, p), py_parametrize_on(req, h).
.decl py_fixture_value_param(h:symbol, p:symbol)
py_fixture_value_param(h, p) :- py_fixture_value_call(req, e),
call_arg(e, "0", a), expr_node("client", "NAME_REFERENCE", _, p, a),
@@ -356,6 +367,70 @@ py_fixture_request(req, name) :- py_fixture_value_call(req, e),
expr_parent("client", tup, "ELEMENT", to_string(k), el),
expr_node("client", "LITERAL", _, name, el), name != "".
+// ── PARAMETRIZE AND FIXTURE PARAMS: the VALUES the runner hands an argument ──
+// @pytest.mark.parametrize("field_cls", [String, Integer]) -> field_cls is one of the classes
+// @pytest.mark.parametrize("obj, n", [(Box(), 1), pytest.param(Bag(), 2)])
+// @pytest.fixture(params=(Converter, BaseConverter))
+// def converter_cls(request): return request.param -> one of the classes
+// The runner calls the test once per row with the row's values in place of the argument,
+// so each value is an argument reaching the parameter exactly as one written at a call
+// site is (resolution/value-flow.dl types it). Only values WRITTEN in the decorator, or in
+// a display a single-write name in this module (or one it imports the name from) holds;
+// a value list built by a call is not read.
+.decl py_pz_rows(h:symbol, rows:symbol)
+py_pz_rows(h, a) :- py_parametrize_on(_, h), decorator_expr("client", de, h), call_arg(de, "1", a).
+py_pz_rows(h, a) :- py_parametrize_on(_, h), decorator_expr("client", de, h), call_kwarg(de, k, a), py_parametrize_argvalues_kw(k).
+py_pz_rows(h, a) :- py_fixture_deco_on(_, h), decorator_expr("client", de, h), call_kwarg(de, k, a), py_fixture_params_kw(k).
+// the display a rows expression is: written in place, or held by a single-write name
+.decl py_pz_display(rows:symbol, d:symbol)
+py_pz_display(a, a) :- py_pz_rows(_, a), py_value_row_kind(k), expr_node("client", k, _, _, a).
+py_pz_display(a, d) :- py_pz_rows(_, a),
+ expr_binding("client", b, ctx, a), ctx != "STORE", binding_lookup("client", b, b2),
+ py_pz_name_display(b2, d).
+.decl py_pz_name_display(b:symbol, d:symbol)
+py_pz_name_display(b, d) :- binding_rebinding("client", "1", _, _, b),
+ expr_binding("client", b, "STORE", tgt), assign_pair("client", tgt, d),
+ py_value_row_kind(k), expr_node("client", k, _, _, d).
+// `from tests.base import ALL_FIELDS`: the module-level binding of that name in the module
+py_pz_name_display(b, d) :- import_binding("client", b, i),
+ import_alias("client", orig, _, i), orig != "", import_relative("client", _, pkg, i), pkg != "",
+ module_decl("client", _, pkg, _, mod),
+ binding_decl("client", orig, _, "MODULE_LEVEL", _, mb), binding_owner("client", mod, _, mb),
+ binding_rebinding("client", "1", _, _, mb),
+ expr_binding("client", mb, "STORE", tgt), assign_pair("client", tgt, d),
+ py_value_row_kind(k), expr_node("client", k, _, _, d).
+// a row, with `pytest.param(v, ..., id=...)` read as its positional values
+.decl py_pz_row(h:symbol, row:symbol)
+py_pz_row(h, r) :- py_pz_rows(h, a), py_pz_display(a, d), expr_parent("client", d, "ELEMENT", _, r).
+.decl py_pz_param_call(row:symbol)
+py_pz_param_call(r) :- py_pz_row(_, r), call_of_expr(r, site), call_name(site, w), py_param_row_wrapper(w).
+// the value at one argument's position in a row: the whole row (one argument name), else
+// the row's k-th element
+.decl py_pz_single(h:symbol)
+py_pz_single(h) :- py_parametrize_argnames(h, s), !py_argnames_sep_at(h, s, _).
+py_pz_single(h) :- py_fixture_deco_on(_, h).
+.decl py_pz_value(h:symbol, k:number, v:symbol)
+py_pz_value(h, 0, r) :- py_pz_single(h), py_pz_row(h, r), !py_pz_param_call(r).
+py_pz_value(h, 0, v) :- py_pz_single(h), py_pz_row(h, r), py_pz_param_call(r), call_arg(r, "0", v).
+py_pz_value(h, k, v) :- !py_pz_single(h), py_pz_row(h, r), !py_pz_param_call(r),
+ py_value_row_kind(rk), expr_node("client", rk, _, _, r),
+ expr_parent("client", r, "ELEMENT", ks, v), k = to_number(ks).
+py_pz_value(h, k, v) :- !py_pz_single(h), py_pz_row(h, r), py_pz_param_call(r),
+ call_arg(r, ks, v), k = to_number(ks).
+// the parameter each value reaches: a test's directly parametrized argument ...
+.decl py_pz_param_value(ph:symbol, v:symbol)
+py_pz_param_value(ph, v) :- py_parametrize_direct(req, name), py_parametrize_on(req, h),
+ !py_pz_single(h), py_argnames_pos(h, name, k), py_pz_value(h, k, v),
+ param_decl("client", name, _, _, req, ph).
+py_pz_param_value(ph, v) :- py_parametrize_direct(req, name), py_parametrize_on(req, h),
+ py_parametrize_argname(req, h, name), py_pz_single(h), py_pz_value(h, 0, v),
+ param_decl("client", name, _, _, req, ph).
+// ... and, in a fixture declared with params=, what `request.param` evaluates to
+.decl py_pz_request_param(e:symbol, v:symbol)
+py_pz_request_param(e, v) :- py_fixture_deco_on(f, h), py_pz_value(h, 0, v),
+ py_fixture_request_param(rq), param_decl("client", rq, _, _, f, ph),
+ py_request_param_attr(pa), attr_access("client", e, pa, obj), expr_names_param("client", obj, ph).
+
// (0) a fixture a CLASS declares serves the tests of that class and of its subclasses,
// and nothing else; for them it wins over the module's fixture of the name. Read as a
// plain same-module fixture it was also handed to a sibling class's tests, and the
@@ -406,6 +481,53 @@ py_fixture_requested(m) :- py_fixture_injection(_, m, _).
framework_edge(from, to, "fixture_injection", name, "by_name") :-
py_fixture_injection(from, to, name).
+// ── 2a. THE SAME LOOKUP, FROM SYNTAX ALONE ──────────────────────────────────────
+// resolution/value-flow.dl types a parameter by the fixture serving it, so it needs the
+// runner's lookup INSIDE the resolution fixpoint. py_fixture_injection cannot be read
+// there: a fixture a conftest star-imports is found through module_member_method, which
+// is resolution, and the nearest-conftest MAX over it would then be a cyclic aggregate
+// souffle refuses to stratify. This chain keeps the runner's order (class, own module,
+// nearest conftest by its declaring file) and leaves out the two clauses that need
+// resolution — a star-imported fixture and a pytest_plugins one. Those still reach their
+// tests through the injection edge; they only go untyped.
+.decl py_fixs_in_file(fix:symbol, file:symbol)
+py_fixs_in_file(fix, p) :- py_fixture_decl(fix, _), method_file("client", p, fix).
+.decl py_fixs_same_module(req:symbol, name:symbol, fix:symbol)
+py_fixs_same_module(req, name, fix) :- py_fixture_request(req, name),
+ py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _),
+ !py_fixture_class_shadowed(req, name),
+ method_file("client", p, req), py_fixs_in_file(fix, p).
+.decl py_fixs_conftest_cand(req:symbol, name:symbol, fix:symbol, depth:number)
+py_fixs_conftest_cand(req, name, fix, dl) :- py_fixture_request(req, name),
+ py_fixture_decl(fix, name), fix != req, !py_fixture_class(fix, _),
+ py_fixs_in_file(fix, cp), py_fixture_scope_file(c),
+ strlen(cp) >= strlen(c), substr(cp, strlen(cp) - strlen(c), strlen(c)) = c,
+ d = substr(cp, 0, strlen(cp) - strlen(c)),
+ method_file("client", p, req),
+ strlen(p) > strlen(d), substr(p, 0, strlen(d)) = d, dl = strlen(d).
+.decl py_fixs_nearest(req:symbol, name:symbol, depth:number)
+py_fixs_nearest(req, name, m) :- py_fixs_conftest_cand(req, name, _, _),
+ m = max dl : { py_fixs_conftest_cand(req, name, _, dl) }.
+.decl py_fixs_injection(from:symbol, to:symbol, name:symbol)
+py_fixs_injection(req, fix, name) :- py_fixture_in_class(req, name, fix).
+py_fixs_injection(req, fix, name) :- py_fixs_same_module(req, name, fix).
+py_fixs_injection(req, fix, name) :- py_fixs_conftest_cand(req, name, fix, dl),
+ !py_fixture_class_shadowed(req, name), !py_fixs_same_module(req, name, _),
+ py_fixs_nearest(req, name, dl).
+
+// ── 2b. FIXTURE VALUE (what the runner hands the parameter) ────────────────────
+// py_fixture_value_type(Fixture, Type) — the value a fixture hands over: its return or
+// yield value's type, or its declared return. Consumed by resolution/value-flow.dl, where
+// the runner's call `test(fixture_value)` is one more argument reaching a parameter.
+.decl py_fixture_value_type(fix:symbol, t:symbol)
+py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _),
+ method_return_value_expr("client", fix, e), expr_type("client", e, t).
+py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _),
+ method_declared_return_type("client", fix, d), declared_dispatch("client", d, t).
+py_fixture_value_type(fix, t) :- py_fixture_decl(fix, _),
+ expr_node("client", "YIELD", _, _, y), expr_ultimate_method("client", y, fix),
+ expr_parent("client", y, "YIELD_VALUE", _, v), expr_type("client", v, t).
+
// ─────────────────────────────────────────────────────────────────────────────
// 3. URL DISPATCH (route table -> view)
// ─────────────────────────────────────────────────────────────────────────────
diff --git a/graph/python/engine/framework-behavior/library-callbacks.dl b/graph/python/engine/framework-behavior/library-callbacks.dl
new file mode 100644
index 00000000..eef361c8
--- /dev/null
+++ b/graph/python/engine/framework-behavior/library-callbacks.dl
@@ -0,0 +1,371 @@
+// ============================================================================
+// FRAMEWORK-BEHAVIOR · LIBRARY CALLBACKS (the hop a dependency makes back into the client)
+//
+// ── THE GAP ─────────────────────────────────────────────────────────────────
+// The engine reads a dependency's signatures, never its bodies. So when a library calls
+// BACK into client code, the call graph stops at the library frame: a test client's
+// `client.get("/")` runs the `open()` a client subclass overrides and then the
+// application object it was built around (`app(environ, start_response)`); a CLI test
+// runner's `runner.invoke(cli)` runs `cli.main()`, which runs the command's callback; an
+// `ExitStack.enter_context(cm)` runs `cm.__enter__`. Measured on a 16-repository Python
+// oracle, a library frame is the first broken hop behind most test-impact misses on the
+// web-framework subject: the test calls the library, the library calls the code under
+// change, and the graph has only the first half.
+//
+// ── WHERE THE KNOWLEDGE COMES FROM ──────────────────────────────────────────
+// graph/python/libsum/libsum.py reads the installed library's SOURCE once (cached by the
+// installed distributions and the client's imports, never solved here) and stages these tables
+// beside the client IR, for the library classes and functions the client can name:
+// py_lib_callback(Callee, Root, Pos, Path, Member, Via, Type)
+// calling the library callable Callee hands back `Member` on the object that arrived
+// as its parameter Root (written by the client at position Pos, -1 = the receiver
+// `self`), after following the attribute Path ("" = the object itself); Via is the
+// library function where that invocation is written; Type the class the library declares that
+// object to be, "" when it declares none
+// py_lib_ancestor(Class, Ancestor) a library class's library ancestors
+// py_lib_field_init(Class, Field, Param, Pos)
+// constructing Class stores the argument Param on the new object as Field
+// py_lib_alias(Alias, Canonical)
+// a name a package re-exports
+// py_lib_decorates(Decorator, Form, Class, Field)
+// `@Decorator` (DIRECT) or `@Decorator(...)` (FACTORY) turns a def into a Class whose Field
+// holds it; Class "" when the def itself comes back
+// No row for a library written in C: a declared blind spot, never a guess.
+//
+// ── WHAT IT EMITS, AND WHY NOT AS A CALL ────────────────────────────────────
+// lib_callback_edge(Caller, Target, Callee, Member, Via, How)
+// Caller the client method containing the call into the library
+// Target the client method the library is summarised to call back
+// Callee the library callable the client called (canonical name)
+// How receiver Target is a member of the object the call was made on
+// (a client subclass overriding what the library's own code calls)
+// receiver_field ... of an object the library holds in a field of it
+// argument ... of an object (or the callable itself) passed as an argument
+// argument_field ... of an object held in a field of an argument
+// The contract of this phase holds: it only ADDS a relation. The hop is a library's
+// behaviour summarised from its source, joined to the client by a member NAME on a type
+// the client handed over; a resolved call is stronger knowledge, so call_chain_edge is
+// untouched and a consumer opts in (impact walks it as its own hop, `library callback`).
+// No rule here names a library or a framework: every name comes from the staged tables.
+// ============================================================================
+
+// ── the dotted path a client expression spells through an unlinked import ───
+// `Client` after `from webkit.testing import Client`, `clikit.command` after `import clikit`: the import's path,
+// then each attribute read off it. Only an import that linked to nothing in the client starts one.
+.decl lcb_path_of(e:symbol, p:symbol)
+lcb_path_of(e, p) :-
+ expr_binding("client", b, ctx, e), ctx != "STORE",
+ binding_lookup("client", b, b2), import_unlinked(b2, p).
+lcb_path_of(e, cat(p, cat(".", n))) :-
+ expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), n != "",
+ expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), lcb_path_of(obj, p), strlen(p) < 200.
+
+// ── its canonical name, following the package's re-exports ──────────────────
+.decl lcb_named(p:symbol)
+lcb_named(p) :- lcb_path_of(_, p).
+.decl lcb_canon(p:symbol, c:symbol)
+lcb_canon(p, c) :- lcb_named(p), py_lib_alias(p, c).
+lcb_canon(p, p) :- lcb_named(p), !py_lib_alias(p, _).
+.decl lcb_expr_names(e:symbol, c:symbol)
+lcb_expr_names(e, c) :- lcb_path_of(e, p), lcb_canon(p, c).
+
+// ── a client class whose base is a library class ─────────────────────────────
+// Written `class AppClient(Client)` with `Client` imported from a dependency that was not
+// staged: the base resolves to nothing and type_base_unresolved records it by text. Here it
+// is named by the import the text binds to, and inherited through client subclasses.
+.decl lcb_direct_ext_base(t:symbol, c:symbol)
+lcb_direct_ext_base(t, c) :-
+ type_base_slot("client", t, _, "NAME", _, _, bh), base_expr("client", bh, e), lcb_expr_names(e, c).
+// a dotted base, `class Store(abc.Mapping)` after `from collections import abc`: the attribute chain names it
+lcb_direct_ext_base(t, c) :-
+ type_base_slot("client", t, _, "DOTTED_NAME", _, _, bh), base_expr("client", bh, e), lcb_expr_names(e, c).
+lcb_direct_ext_base(t, c) :-
+ type_base_slot("client", t, _, "SUBSCRIPT", _, _, bh), base_expr("client", bh, e),
+ expr_parent("client", e, "SUBSCRIPT_OBJECT", _, obj), lcb_expr_names(obj, c).
+.decl lcb_ext_class(t:symbol, c:symbol)
+lcb_ext_class(t, c) :- lcb_direct_ext_base(t, c).
+lcb_ext_class(t, c) :- type_ancestor("client", t, a), lcb_direct_ext_base(a, c).
+
+// a client type and every client subclass of it: the object a library calls back may be a
+// subclass of the type the call site was written against
+.decl lcb_self_type(t:symbol, s:symbol)
+lcb_self_type(t, t) :- lcb_ext_class(t, _).
+lcb_self_type(t, s) :- lcb_ext_class(t, _), type_ancestor("client", s, t).
+
+.decl lcb_has_rows(c:symbol)
+lcb_has_rows(c) :- py_lib_callback(c, _, _, _, _, _, _).
+
+// ── lcb_entry(Site, Callee, SelfType) — a client call into a summarised library callable ──
+// SelfType is the client type the library's `self` is, or "" when the receiver is not a
+// client object.
+.decl lcb_entry(site:symbol, callee:symbol, selfT:symbol)
+// (a) a member the client type inherits from its library base and does not define itself:
+// `client.get("/")` on an AppClient-like subclass, `self.get(...)` inside one
+lcb_entry(site, q, t) :-
+ call_receiver_object(site, obj), expr_type("client", obj, t),
+ call_name(site, n), n != "",
+ !expr_call_candidate(site, _),
+ lcb_ext_class(t, c), q = cat(c, cat(".", n)), lcb_has_rows(q).
+// (b) super().n(...) whose MRO leaves the client for a library base
+lcb_entry(site, q, a) :-
+ call_recv_kind(site, "SUPER"), call_super_anchor(site, _, a),
+ call_name(site, n), n != "",
+ !expr_call_candidate(site, _),
+ lcb_ext_class(a, c), q = cat(c, cat(".", n)), lcb_has_rows(q).
+// (c) a library function or class called by a name the client imported
+lcb_entry(site, c, "") :-
+ call_callee_is_value(site), call_callee_expr(site, callee),
+ lcb_expr_names(callee, c), lcb_has_rows(c).
+// (d) a member of a library object the client built: `runner = CliRunner(); runner.invoke(cli)`, or one a
+// fixture or a helper returned. Its `self` is a library object, so it has no client type; what the client
+// gave its constructor is reached through the allocation (lcb_lobj_entry below).
+lcb_entry(site, q, "") :- lcb_lobj_entry(site, q, _).
+// (e) constructing a client class whose constructor is the library base's: its __init__ may
+// call members the client overrides
+lcb_entry(site, c, t) :-
+ call_constructs_type(site, t), lcb_ext_class(t, c),
+ !type_constructor("client", t, _), lcb_has_rows(c).
+
+// (f) a hand-back to a member the client object does not define but INHERITS from a library class: the
+// library calls `cli.main()` on the command it was given, `main` is the library base's own, and that
+// method's hand-backs apply to the same object (its make_context override). Same site, same caller.
+lcb_entry(site, q2, t) :-
+ lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _),
+ expr_type("client", a, t), !mro_lookup("client", t, n, _),
+ lcb_ext_class(t, c), q2 = cat(c, cat(".", n)), lcb_has_rows(q2).
+lcb_entry(site, q2, t) :-
+ lcb_entry(site, q, t), t != "", py_lib_callback(q, "self", _, "", n, _, _),
+ !mro_lookup("client", t, n, _),
+ lcb_ext_class(t, c), q2 = cat(c, cat(".", n)), lcb_has_rows(q2).
+
+// ── lcb_ext_instance(Expr, Class, Alloc) — an expression holding an object of a library class ──
+// Alloc is the call site that constructed it. Followed through a single-binding local, a client function's
+// return value, and the fixture a test parameter is filled from.
+.decl lcb_ext_instance(e:symbol, c:symbol, alloc:symbol)
+lcb_ext_instance(e, c, site) :-
+ call_of_expr(e, site), call_callee_is_value(site), call_callee_expr(site, callee),
+ lcb_expr_names(callee, c), lcb_is_class(c).
+lcb_ext_instance(e, c, al) :-
+ expr_binding("client", bref, ctx, e), ctx != "STORE",
+ binding_lookup("client", bref, bind),
+ binding_rebinding("client", "1", _, _, bind),
+ expr_binding("client", bind, "STORE", tgt),
+ assign_pair("client", tgt, val),
+ lcb_ext_instance(val, c, al).
+lcb_ext_instance(e, c, al) :-
+ call_of_expr(e, site), expr_call_candidate(site, m),
+ method_return_value_expr("client", m, r), lcb_ext_instance(r, c, al).
+lcb_ext_instance(e, c, al) :-
+ expr_names_param("client", e, ph), param_decl("client", pn, _, _, req, ph),
+ py_fixs_injection(req, fix, pn), method_return_value_expr("client", fix, r), lcb_ext_instance(r, c, al).
+lcb_ext_instance(e, c, al) :-
+ expr_names_param("client", e, ph), param_decl("client", pn, _, _, req, ph),
+ py_fixs_injection(req, fix, pn),
+ expr_node("client", "YIELD", _, _, y), expr_ultimate_method("client", y, fix),
+ expr_parent("client", y, "YIELD_VALUE", _, v), lcb_ext_instance(v, c, al).
+.decl lcb_is_class(c:symbol)
+lcb_is_class(c) :- py_lib_field_init(c, _, _, _).
+lcb_is_class(c) :- py_lib_callback(c, "self", _, _, _, _, _).
+
+// ── LIBRARY OBJECTS THE CLIENT HOLDS ────────────────────────────────────────
+// lcb_lobj(Expr, Class, Obj): Expr holds a library object of Class, identified by Obj -- the call site that
+// constructed it, or the client function a library decorator wrapped into it (`@command() def greet()` makes
+// `greet` a Command whose `callback` is the def). Its fields are what the client gave it.
+.decl lcb_lobj(e:symbol, c:symbol, obj:symbol)
+lcb_lobj(e, c, al) :- lcb_ext_instance(e, c, al).
+lcb_lobj(e, c, m) :- lcb_names_def(e, m), lcb_deco_obj(m, c, _).
+
+// the def a name denotes, decorated or not (the decorated name's VALUE is what the decorator returned)
+.decl lcb_names_def(e:symbol, m:symbol)
+lcb_names_def(e, m) :- expr_binding("client", bref, ctx, e), ctx != "STORE", binding_lookup("client", bref, b),
+ method_declaring_binding("client", b, m).
+lcb_names_def(e, m) :- expr_binding("client", bref, ctx, e), ctx != "STORE", binding_lookup("client", bref, b),
+ import_binds_method("client", b, m).
+lcb_names_def(e, m) :- expr_attr_names_method("client", e, m).
+
+// the library callable a decorator expression applies: `@q` (direct) or `@q(...)` (factory)
+.decl lcb_deco_callable(d:symbol, q:symbol, form:symbol)
+lcb_deco_callable(d, q, "DIRECT") :- decorator_expr("client", e, d), !call_of_expr(e, _), lcb_expr_names(e, q).
+lcb_deco_callable(d, q, "FACTORY") :- decorator_expr("client", e, d), call_of_expr(e, site),
+ call_callee_expr(site, callee), lcb_expr_names(callee, q).
+lcb_deco_callable(d, q, "FACTORY") :- decorator_expr("client", e, d), call_of_expr(e, site),
+ call_receiver_object(site, obj), expr_type("client", obj, t), call_name(site, n), !mro_lookup("client", t, n, _),
+ lcb_ext_class(t, c), q = cat(c, cat(".", n)).
+// a decorator that hands the function straight back (registers it, records an option on it)
+.decl lcb_deco_identity(d:symbol)
+lcb_deco_identity(d) :- lcb_deco_callable(d, q, form), py_lib_decorates(q, form, "", "").
+lcb_deco_identity(d) :- decorator_builtin("client", bk, d), decorator_transparent(bk).
+// the def m enters application stage k unchanged: every decorator applied before it handed it straight back
+.decl lcb_deco_blocked(m:symbol, k:number)
+lcb_deco_blocked(m, k) :- deco_at(m, k, _), deco_at(m, k2, d2), k2 < k, !lcb_deco_identity(d2), !deco_stage(m, k2 + 1, m).
+.decl lcb_deco_obj(m:symbol, c:symbol, f:symbol)
+lcb_deco_obj(m, c, f) :- deco_at(m, k, d), !lcb_deco_blocked(m, k), lcb_deco_callable(d, q, form),
+ py_lib_decorates(q, form, c, f), c != "".
+
+// ── entries on a library object, and what its fields hand back ──────────────
+.decl lcb_lobj_entry(site:symbol, callee:symbol, obj:symbol)
+lcb_lobj_entry(site, q, o) :-
+ call_receiver_object(site, recv), lcb_lobj(recv, c, o),
+ call_name(site, n), n != "",
+ !expr_call_candidate(site, _),
+ q = cat(c, cat(".", n)), lcb_has_rows(q).
+// a library object handed to a library callable that calls one of the object's own (library) members
+lcb_lobj_entry(site, q2, o) :-
+ lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _), lcb_lobj(a, c, o),
+ q2 = cat(c, cat(".", n)), lcb_has_rows(q2).
+
+// what a library object's field holds that offers member n: the argument its construction was given, or the
+// function a decorator wrapped (called as `__call__`)
+.decl lcb_lobj_field_target(o:symbol, f:symbol, n:symbol, m:symbol)
+lcb_lobj_field_target(al, f, n, m) :- lcb_alloc_field(al, f, a), lcb_member_of_expr(a, n, m).
+lcb_lobj_field_target(m, f, "__call__", m) :- lcb_deco_obj(m, _, f), lcb_field_wanted(m, f).
+.decl lcb_field_wanted(o:symbol, f:symbol)
+lcb_field_wanted(o, f) :- lcb_lobj_entry(_, q, o), py_lib_callback(q, "self", _, f, _, _, _), f != "".
+lcb_field_wanted(o, f) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, f, _, _, _), f != "", lcb_lobj(a, _, o),
+ lcb_entry(site, q, _).
+
+// what the constructor of a library object stored in a field, as the client wrote it at the allocation
+.decl lcb_alloc_field(al:symbol, f:symbol, a:symbol)
+lcb_alloc_field(al, f, a) :-
+ lcb_field_wanted(al, f), call_callee_expr(al, callee), lcb_expr_names(callee, c),
+ py_lib_field_init(c, f, _, pos), call_arg(al, ps, a), to_number(ps) = to_number(pos).
+lcb_alloc_field(al, f, a) :-
+ lcb_field_wanted(al, f), call_callee_expr(al, callee), lcb_expr_names(callee, c),
+ py_lib_field_init(c, f, pn, _), call_kwarg(al, pn, a).
+
+// ── the argument the client wrote for a summarised parameter ─────────────────
+.decl lcb_arg(site:symbol, callee:symbol, root:symbol, a:symbol)
+lcb_arg(site, q, r, a) :-
+ lcb_entry(site, q, _), py_lib_callback(q, r, pos, _, _, _, _), r != "self",
+ to_number(pos) >= 0, call_arg(site, ps, a), to_number(ps) = to_number(pos).
+lcb_arg(site, q, r, a) :-
+ lcb_entry(site, q, _), py_lib_callback(q, r, _, _, _, _, _), r != "self",
+ call_kwarg(site, r, a).
+
+// ── what a library-held field of a client object holds ───────────────────────
+// lcb_field_holds(T, Field, Expr): an object of client type T was built with Expr as the
+// argument the library constructor stores in Field (the construction of T or of a client
+// subclass, or T's own super().__init__(...) call)
+.decl lcb_ctor_store(t:symbol, f:symbol, param:symbol, pos:symbol)
+lcb_ctor_store(t, f, pn, pos) :- lcb_ext_class(t, c), py_lib_field_init(c, f, pn, pos).
+.decl lcb_field_holds(t:symbol, f:symbol, e:symbol)
+lcb_field_holds(t, f, a) :-
+ lcb_ctor_store(t, f, _, pos), lcb_self_type(t, s), call_constructs_type(site, s),
+ call_arg(site, ps, a), to_number(ps) = to_number(pos).
+lcb_field_holds(t, f, a) :-
+ lcb_ctor_store(t, f, pn, _), lcb_self_type(t, s), call_constructs_type(site, s),
+ call_kwarg(site, pn, a).
+lcb_field_holds(t, f, a) :-
+ lcb_ctor_store(t, f, _, pos), call_recv_kind(site, "SUPER"), call_super_anchor(site, _, t),
+ call_name(site, "__init__"), call_arg(site, ps, a), to_number(ps) = to_number(pos).
+lcb_field_holds(t, f, a) :-
+ lcb_ctor_store(t, f, pn, _), call_recv_kind(site, "SUPER"), call_super_anchor(site, _, t),
+ call_name(site, "__init__"), call_kwarg(site, pn, a).
+
+// ── the client member an object expression offers under a name ───────────────
+// the type the expression has, or a client subclass of it; `__call__` on an expression that
+// IS a function or a bound method is that function
+.decl lcb_member_of_expr(e:symbol, n:symbol, m:symbol)
+lcb_member_of_expr(e, n, m) :-
+ lcb_wanted(e, n), expr_type("client", e, t), lcb_sub(t, s), mro_lookup("client", s, n, m),
+ method_owner("client", _, m).
+lcb_member_of_expr(e, "__call__", m) :- lcb_wanted(e, "__call__"), expr_denotes_method("client", e, m).
+lcb_member_of_expr(e, "__call__", m) :- lcb_wanted(e, "__call__"), expr_names_method("client", e, m).
+.decl lcb_sub(t:symbol, s:symbol)
+lcb_sub(t, t) :- lcb_wanted(e, _), expr_type("client", e, t).
+lcb_sub(t, s) :- lcb_wanted(e, _), expr_type("client", e, t), type_ancestor("client", s, t).
+
+// the member of a client object's own field, as the client wrote it (`self.app = app`)
+.decl lcb_member_of_field(t:symbol, f:symbol, n:symbol, m:symbol)
+lcb_member_of_field(t, f, n, m) :-
+ lcb_wanted_field(t, f, n), field_decl("client", f, owner, _, fh),
+ (owner = t ; type_ancestor("client", t, owner)),
+ field_type("client", fh, x), mro_lookup("client", x, n, m), method_owner("client", _, m).
+
+// demand: only the (expression, member) pairs a summary asks about are looked up
+.decl lcb_wanted(e:symbol, n:symbol)
+lcb_wanted(a, n) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, "", n, _, _), lcb_entry(site, q, _).
+lcb_wanted(e, n) :- lcb_entry(_, q, t), t != "", py_lib_callback(q, "self", _, f, n, _, _), f != "",
+ lcb_field_holds(t, f, e).
+lcb_wanted(a, n) :- lcb_alloc_field(al, f, a), lcb_lobj_entry(_, q, al), py_lib_callback(q, "self", _, f, n, _, _).
+lcb_wanted(e, n) :- lcb_alloc_field(al, f, e), lcb_lobj(a, _, al), lcb_arg(_, q, r, a), py_lib_callback(q, r, _, f, n, _, _).
+lcb_wanted(e, n) :- lcb_arg(site, q, r, a), py_lib_callback(q, r, _, f, n, _, _), f != "", lcb_entry(site, q, _),
+ expr_type("client", a, t), lcb_field_holds(t, f, e).
+.decl lcb_wanted_field(t:symbol, f:symbol, n:symbol)
+lcb_wanted_field(s, f, n) :- lcb_entry(_, q, t), t != "", py_lib_callback(q, "self", _, f, n, _, _), f != "",
+ !contains(".", f), lcb_self_type(t, s).
+lcb_wanted_field(t, f, n) :- lcb_arg(site, q, r, a), lcb_entry(site, q, _), py_lib_callback(q, r, _, f, n, _, _), f != "",
+ !contains(".", f), expr_type("client", a, t).
+
+// ── the edges ───────────────────────────────────────────────────────────────
+// lcb_hit(Site, Caller, Target, Callee, Root, Path, Member, Via, How): one summary row joined to the client object
+// the call actually hands over.
+.decl lcb_hit(site:symbol, caller:symbol, m:symbol, q:symbol, r:symbol, f:symbol, n:symbol, via:symbol, how:symbol)
+// the receiver itself: a member the client type (or a subclass) defines over the library's
+lcb_hit(site, caller, m, q, "self", "", n, via, "receiver") :-
+ lcb_entry(site, q, t), t != "", site_caller(site, caller),
+ py_lib_callback(q, "self", _, "", n, via, _),
+ lcb_self_type(t, s), mro_lookup("client", s, n, m), method_owner("client", _, m).
+// a field of the receiver the library constructor filled from a client argument
+lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :-
+ lcb_entry(site, q, t), t != "", site_caller(site, caller),
+ py_lib_callback(q, "self", _, f, n, via, _), f != "",
+ lcb_field_holds(t, f, e), lcb_member_of_expr(e, n, m).
+// a field of a library object the client holds (built by it, or made by a library decorator around its def)
+lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :-
+ lcb_lobj_entry(site, q, o), site_caller(site, caller),
+ py_lib_callback(q, "self", _, f, n, via, _), f != "",
+ lcb_lobj_field_target(o, f, n, m).
+lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :-
+ lcb_arg(site, q, r, a), site_caller(site, caller), lcb_entry(site, q, _),
+ py_lib_callback(q, r, _, f, n, via, _), f != "",
+ lcb_lobj(a, _, o), lcb_lobj_field_target(o, f, n, m).
+// a field of the receiver the client itself assigned
+lcb_hit(site, caller, m, q, "self", f, n, via, "receiver_field") :-
+ lcb_entry(site, q, t), t != "", site_caller(site, caller),
+ py_lib_callback(q, "self", _, f, n, via, _), f != "",
+ lcb_self_type(t, s), lcb_member_of_field(s, f, n, m).
+// an argument, or the callable passed as one
+lcb_hit(site, caller, m, q, r, "", n, via, "argument") :-
+ lcb_arg(site, q, r, a), site_caller(site, caller),
+ py_lib_callback(q, r, _, "", n, via, _),
+ lcb_member_of_expr(a, n, m).
+// a field of an argument
+lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :-
+ lcb_arg(site, q, r, a), site_caller(site, caller),
+ py_lib_callback(q, r, _, f, n, via, _), f != "",
+ expr_type("client", a, t), lcb_field_holds(t, f, e), lcb_member_of_expr(e, n, m).
+lcb_hit(site, caller, m, q, r, f, n, via, "argument_field") :-
+ lcb_arg(site, q, r, a), site_caller(site, caller),
+ py_lib_callback(q, r, _, f, n, via, _), f != "",
+ expr_type("client", a, t), lcb_member_of_field(t, f, n, m).
+
+// ── WHEN THE OBJECT CANNOT BE TOLD: THE TYPE THE LIBRARY DECLARES ────────────
+// A row whose object the client side could not name (a loader passed through **options, a command a helper
+// returns typed only as the library's base) is still a hand-back to SOME object of the type the library declares
+// there. The client classes that can be that object are its client subclasses; the one whose member runs is the one
+// that defines it. A class-hierarchy answer, used only for a row no client object answered at that site.
+.decl lcb_row_hit(site:symbol, q:symbol, r:symbol, f:symbol, n:symbol)
+lcb_row_hit(site, q, r, f, n) :- lcb_hit(site, _, _, q, r, f, n, _, _).
+.decl lcb_cha_wanted(ty:symbol, n:symbol)
+lcb_cha_wanted(ty, n) :- lcb_entry(_, q, _), py_lib_callback(q, r, _, f, n, _, ty), ty != "", (r != "self" ; f != "").
+.decl lcb_cha(ty:symbol, n:symbol, m:symbol)
+lcb_cha(ty, n, m) :- lcb_cha_wanted(ty, n), lcb_ext_class(s, ty), mro_lookup("client", s, n, m), method_owner("client", _, m).
+lcb_cha(ty, n, m) :- lcb_cha_wanted(ty, n), lcb_ext_class(s, c), py_lib_ancestor(c, ty),
+ mro_lookup("client", s, n, m), method_owner("client", _, m).
+
+// lib_callback_edge(Caller, Target, Callee, Member, Via, How): the client method Caller calls the library
+// callable Callee, which is summarised (from the library's source) to invoke Member -- written in the library
+// function Via -- on an object that resolves to the client method Target. How: receiver | receiver_field |
+// argument | argument_field | declared_type. A library's behaviour joined by a member name, not a resolved call.
+lib_callback_edge(caller, m, q, n, via, how) :- lcb_hit(_, caller, m, q, _, _, n, via, how).
+lib_callback_edge(caller, m, q, n, via, "declared_type") :-
+ lcb_entry(site, q, _), site_caller(site, caller),
+ py_lib_callback(q, r, _, f, n, via, ty), ty != "", (r != "self" ; f != ""),
+ !lcb_row_hit(site, q, r, f, n), lcb_cha(ty, n, m).
+
+// lib_callback_site(Site, Callee): a client call site the engine left unresolved that enters the library callable
+// Callee, which the library's own source declares (a member the client type inherits from its library base, a
+// super() call past the client classes, a member of a library object the client built). Not a call to any
+// same-named client method, so a consumer must not guess one by name.
+lib_callback_site(site, q) :- lcb_entry(site, q, _), q != "".
diff --git a/graph/python/engine/resolution/annotations.dl b/graph/python/engine/resolution/annotations.dl
index 0d671aa9..2b9087a4 100644
--- a/graph/python/engine/resolution/annotations.dl
+++ b/graph/python/engine/resolution/annotations.dl
@@ -199,6 +199,25 @@ type_ref_element(p, parentRef, t) :-
annotation_owner_module(p, owner, ok, mod),
type_name_in_module(p, mod, tn, t).
+// ── union_operand(Prov, UnionRefHash, OperandRefHash) — every operand of a union ──
+// `|` is LEFT-associative, so `A | B | None` is `(A | B) | None`: A and B are not
+// children of the annotation but grandchildren, under a nested UNION_PEP604, and the
+// depth-1 clauses above saw only that nested union (which names no type) and None. Every
+// rule reading "Optional[X] IS X" therefore lost the members of any union with three or
+// more operands, on a parameter, a return, a field or a local alike.
+union_operand(p, r, c) :-
+ type_ref(p, k, _, _, _, r), annotation_optional_kind(k), type_ref_nesting(p, r, _, _, c).
+union_operand(p, r, c) :-
+ union_operand(p, r, u), type_ref(p, "UNION_PEP604", _, _, _, u), type_ref_nesting(p, u, _, _, c).
+// the operands BELOW depth 1 (depth 1 is the clauses above), resolved, and by name
+type_ref_element(p, r, t) :-
+ union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1", type_ref_resolved(p, t, c).
+type_ref_element(p, r, t) :-
+ union_operand(p, r, c), type_ref_nesting(p, _, _, d, c), d != "1",
+ type_ref(p, _, "GENERIC_ARGUMENT", tn, _, c), tn != "",
+ type_ref_owner(p, owner, ok, r), annotation_owner_module(p, owner, ok, mod),
+ type_name_in_module(p, mod, tn, t).
+
// ── annotation_owner_module(Prov, OwnerHash, OwnerKind, ModuleHash) ──────────
// The module an annotation was written in, whatever kind of declaration owns it.
annotation_owner_module(p, ph, "METHOD_PARAM", mod) :-
diff --git a/graph/python/engine/resolution/attribute-lookup.dl b/graph/python/engine/resolution/attribute-lookup.dl
index 4fbc0982..7b44fe2d 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) ──────────────────────────────
@@ -312,6 +317,15 @@ type_call_target(p, t, m) :- mro_lookup(p, t, "__call__", m).
type_property_getter(p, t, n, m) :-
mro_lookup(p, t, n, m), method_kind(p, "PROPERTY_GETTER", _, m).
+// ── type_property_accessor(Prov, TypeHash, Name, Kind, MethodHash) ───────────
+// `@x.setter` and `@x.deleter` are the other two halves: `obj.x = v` runs the setter and
+// `del obj.x` the deleter, and like the getter neither has a call site. They share the
+// getter's name and binding, so they are found on the class that WINS the name in the
+// receiver's MRO, by their method kind, rather than through mro_lookup's one answer.
+type_property_accessor(p, t, n, k, m) :-
+ mro_winner(p, t, n, c), method_owner(p, c, m), method_decl(p, n, _, _, _, m),
+ method_kind(p, k, _, m), (k = "PROPERTY_SETTER" ; k = "PROPERTY_DELETER").
+
// ── A USER-WRITTEN DATA DESCRIPTOR IS THE SAME PROTOCOL (issue #326) ─────────
// `@property` IS a data descriptor; the decorator is sugar over `__get__`/`__set__`. So
// the clause above reads one spelling of the protocol and a class-level
diff --git a/graph/python/engine/resolution/builtin-types.dl b/graph/python/engine/resolution/builtin-types.dl
index c18052d5..457f3453 100644
--- a/graph/python/engine/resolution/builtin-types.dl
+++ b/graph/python/engine/resolution/builtin-types.dl
@@ -199,14 +199,12 @@ expr_builtin_type("client", aw, b) :-
// keyed on expr_attr_of_self is self-only by construction and needs the general form
// beside it.
expr_builtin_type(p, e, b) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
annotation_builtin(p, f, "FIELD", b).
expr_builtin_type(p, e, b) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj),
expr_type(p, obj, ot),
type_attr_field(p, ot, n, f),
field_write_value(p, f, val), expr_builtin_type(p, val, b).
@@ -474,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/builtins.dl b/graph/python/engine/resolution/builtins.dl
index ca1e14d1..e08902e1 100644
--- a/graph/python/engine/resolution/builtins.dl
+++ b/graph/python/engine/resolution/builtins.dl
@@ -182,6 +182,18 @@ py_copy_module("copy").
py_copy_function("copy").
py_copy_function("deepcopy").
+// ── py_returns_arg(DottedName, Position) — a library call that hands back an argument ──
+// Each returns, unchanged, the argument at Position (documented behaviour, not inference):
+// functools.update_wrapper(wrapper, wrapped) -> wrapper (copies __name__/__doc__ onto it)
+// typing.cast(T, value) -> value (a no-op at run time)
+// A decorator written `return update_wrapper(wrapper, f)`, or the typed spelling
+// `return t.cast(F, update_wrapper(wrapper, f))`, is therefore the same shape as one
+// returning `wrapper` under @functools.wraps. Without these every method it decorates was
+// decorator_replaced_target, and no call through the attribute reached anything
+// (call-edge-generation/call_chain.dl). Neither module is staged in a client-only run.
+py_returns_arg("functools.update_wrapper", "0").
+py_returns_arg("typing.cast", "1").
+
// ── py_builtin_dynamic(Name) — the escape hatches ────────────────────────────
// These do not merely lack Python source; they make the PROGRAM unanalysable at that
// point. A call to getattr/eval/exec means the engine cannot know what runs, and the
@@ -358,3 +370,79 @@ class_definition_keyword("metaclass").
// (expression-resolution/expr-type.dl, clause a'), where the convention is the only
// signal there is: a lambda has no annotation and no `def` to classify.
instance_receiver_name("self").
+
+// ── builtin_protocol_slot(Builtin, Dunder) / builtin_protocol_fallback ───────
+// The one-dunder builtins: `repr(x)` runs type(x).__repr__, and so on. str() falls back to
+// __repr__ and bool() to __len__ when the class has no slot of its own. Read by
+// call-edge-generation/call_chain.dl's BUILTIN PROTOCOL edges.
+builtin_protocol_slot("repr", "__repr__").
+builtin_protocol_slot("str", "__str__").
+builtin_protocol_slot("len", "__len__").
+builtin_protocol_slot("hash", "__hash__").
+builtin_protocol_slot("bool", "__bool__").
+builtin_protocol_slot("iter", "__iter__").
+builtin_protocol_slot("next", "__next__").
+builtin_protocol_slot("abs", "__abs__").
+builtin_protocol_slot("format", "__format__").
+builtin_protocol_slot("reversed", "__reversed__").
+builtin_protocol_fallback("str", "__str__", "__repr__").
+builtin_protocol_fallback("bool", "__bool__", "__len__").
+
+// ── operator_protocol_slot(Operator, Dunder, ReflectedDunder) ────────────────
+// `a + b` runs type(a).__add__, and type(b).__radd__ when that is missing or returns
+// NotImplemented; a comparison runs the left operand's slot and the right operand's mirror
+// (`a < b` -> b.__gt__, `a == b` -> b.__eq__). The language reference's data model, 3.3.8
+// (numeric) and 3.3.1 (rich comparison). Read by call-edge-generation/call_chain.dl's
+// OPERATOR PROTOCOL edges.
+operator_protocol_slot("+", "__add__", "__radd__").
+operator_protocol_slot("-", "__sub__", "__rsub__").
+operator_protocol_slot("*", "__mul__", "__rmul__").
+operator_protocol_slot("@", "__matmul__", "__rmatmul__").
+operator_protocol_slot("/", "__truediv__", "__rtruediv__").
+operator_protocol_slot("//", "__floordiv__", "__rfloordiv__").
+operator_protocol_slot("%", "__mod__", "__rmod__").
+operator_protocol_slot("**", "__pow__", "__rpow__").
+operator_protocol_slot("<<", "__lshift__", "__rlshift__").
+operator_protocol_slot(">>", "__rshift__", "__rrshift__").
+operator_protocol_slot("&", "__and__", "__rand__").
+operator_protocol_slot("|", "__or__", "__ror__").
+operator_protocol_slot("^", "__xor__", "__rxor__").
+operator_protocol_slot("==", "__eq__", "__eq__").
+operator_protocol_slot("!=", "__ne__", "__ne__").
+operator_protocol_slot("<", "__lt__", "__gt__").
+operator_protocol_slot("<=", "__le__", "__ge__").
+operator_protocol_slot(">", "__gt__", "__lt__").
+operator_protocol_slot(">=", "__ge__", "__le__").
+// `a != b` on a class with no __ne__ of its own runs object.__ne__, which calls __eq__.
+operator_protocol_fallback("__ne__", "__eq__").
+// ── membership_operator(Operator) — `x in c` runs type(c).__contains__ ───────
+// and, for a class with no __contains__, iterates it (__iter__). Data model 3.3.7.
+membership_operator("in").
+membership_operator("not in").
+membership_protocol_slot("__contains__").
+membership_protocol_fallback("__contains__", "__iter__").
+// ── unary_protocol_slot(Operator, Dunder) ────────────────────────────────────
+unary_protocol_slot("-", "__neg__").
+unary_protocol_slot("+", "__pos__").
+unary_protocol_slot("~", "__invert__").
+// ── truth testing — `if x:`, `while x:`, `not x`, `x and y`, `assert x` ───────
+// runs type(x).__bool__, or __len__ when the class has no __bool__ (data model 3.3.1,
+// object.__bool__). truth_negation_operator is the one unary operator that truth-tests its
+// operand; truth_test_root the statement contexts whose expression is truth-tested.
+truth_negation_operator("not").
+truth_protocol_fallback("__bool__", "__len__").
+truth_test_root("IF_CONDITION").
+truth_test_root("WHILE_CONDITION").
+truth_test_root("ASSERT_CONDITION").
+truth_test_role("CONDITION").
+truth_test_role("COMPREHENSION_CONDITION").
+
+// ── dict_view_name / dict_view_slot / dict_items_view — a mapping's views ────
+// `keys()` yields the key (generic position 0), `values()` the value (position 1), and
+// `items()` a (key, value) pair. Read by resolution/iteration.dl.
+dict_view_name("keys").
+dict_view_name("values").
+dict_view_name("items").
+dict_view_slot("keys", "0").
+dict_view_slot("values", "1").
+dict_items_view("items").
diff --git a/graph/python/engine/resolution/dispatch.dl b/graph/python/engine/resolution/dispatch.dl
index f1f50d21..ed56e8cc 100644
--- a/graph/python/engine/resolution/dispatch.dl
+++ b/graph/python/engine/resolution/dispatch.dl
@@ -142,7 +142,31 @@ call_candidate_count(site, n) :-
// ── call_dispatch_over_cap(SiteHash) ─────────────────────────────────────────
call_dispatch_over_cap(site) :-
- call_candidate_count(site, n), dispatch_capped_at(cap), n > cap.
+ call_candidate_count(site, n), dispatch_capped_at(cap), n > cap,
+ !call_computed_name_family(site).
+
+// ── call_computed_name_family(SiteHash) — a fan the PROGRAM wrote down ───────
+// The cap guards against a receiver type inferred too wide, where most of the fan is the
+// engine's imprecision. A callee looked up by a name with a written prefix --
+// `getattr(self, f"visit_{...}")`, directly, through the local it was stored in, or
+// through a method that returns it -- is the program's own dispatch table: every member
+// named with the prefix is a target by construction, and a visitor with sixty visit_*
+// methods is sixty targets, not sixty guesses. Refusing it dropped the visitor's whole
+// body from every test that drives it.
+method_returns_computed_name(m) :-
+ method_return_value_expr("client", m, e), getattr_computed_name(e, _, _).
+computed_name_value(e) :- getattr_computed_name(e, _, _).
+computed_name_value(e) :-
+ call_of_expr(e, s), expr_call_candidate(s, m), method_returns_computed_name(m).
+call_computed_name_family(site) :-
+ call_callee_is_value(site), call_callee_expr(site, callee), computed_name_value(callee).
+call_computed_name_family(site) :-
+ call_callee_is_value(site), call_callee_expr(site, callee),
+ expr_binding("client", b, ctx, callee), ctx != "STORE",
+ binding_lookup("client", b, b2),
+ expr_binding("client", b2, "STORE", tgt),
+ assign_pair("client", tgt, val),
+ computed_name_value(val).
// ── expr_resolves_to_method(SiteHash, MethodHash) ────────────────────────────
// THE DISAMBIGUATED CALLEE SET the edge layer consumes. Same relation name as Java's,
diff --git a/graph/python/engine/resolution/iteration.dl b/graph/python/engine/resolution/iteration.dl
index 4f32f14b..76d50692 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".
@@ -93,6 +98,18 @@ with_target_declared(p, tgt, ct) :-
method_decl(p, _, _, _, ret, m),
self_return_name(ret).
+// The same, WRITTEN rather than declared: an __enter__ with no return annotation whose
+// return value is `self` (`def __enter__(self): return self`). The protocol hands the
+// target what __enter__ returned, and that is the context manager itself.
+with_target_declared(p, tgt, ct) :-
+ with_pair(p, tgt, cm),
+ expr_root_context(p, crc, cm), context_manager_enter_of(crc, en),
+ expr_type(p, cm, ct),
+ mro_lookup(p, ct, en, m),
+ !method_declared_return_type(p, m, _),
+ method_return_value_expr(p, m, e),
+ expr_node(p, "SELF_REFERENCE", _, _, e).
+
// AN __enter__ RETURN IS A DECLARED TYPE, not an exact one, so it goes out through
// declared_dispatch and is registered with call_recv_declared in dispatch.dl. A declared
// type escaping that gate has produced a confident-wrong answer four times in this engine.
@@ -125,14 +142,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).
@@ -278,6 +293,79 @@ binding_element_type(p, b, t) :-
expr_binding(p, b, _, tgt),
element_type_of(p, src, t).
+// ── A MAPPING'S VIEWS: `.values()`, `.keys()`, `.items()` ─────────────────────
+// self.dump_fields: dict[str, Field] = {}
+// for name, field_obj in self.dump_fields.items(): field_obj.serialize(...)
+// Every clause above types the container itself; a view is a CALL on it, and none of them
+// looked through one, so the commonest way to walk a mapping typed nothing -- and an
+// `items()` loop binds a TUPLE target, which no clause bound at all. A mapping annotation
+// names its key at generic position 0 and its value at position 1, so the slots are
+// read positionally rather than through type_ref_element (which merges both, the safe
+// direction for iterating the mapping itself, and wrong for a view that yields one side).
+// view_slot names which side each view yields; `items()` yields (key, value) pairs.
+// dict_view_slot / dict_items_view are CPython names, catalogued in resolution/builtins.dl.
+
+// the annotation a mapping-valued expression carries: a field (through self or through
+// any typed object), a parameter, or a callee's declared return
+mapping_annotation_of(src, r) :-
+ expr_attr_of_self("client", src, n, owner),
+ type_attr_field("client", owner, n, f),
+ type_ref_owner("client", f, "FIELD", r),
+ type_ref("client", k, "FIELD_TYPE", _, _, r), annotation_container_kind(k).
+mapping_annotation_of(src, r) :-
+ attr_access("client", src, n, obj),
+ expr_type("client", obj, ot),
+ type_attr_field("client", ot, n, f),
+ type_ref_owner("client", f, "FIELD", r),
+ type_ref("client", k, "FIELD_TYPE", _, _, r), annotation_container_kind(k).
+mapping_annotation_of(src, r) :-
+ expr_names_param("client", src, ph),
+ type_ref_owner("client", ph, "METHOD_PARAM", r),
+ type_ref("client", k, "METHOD_PARAM", _, _, r), annotation_container_kind(k).
+mapping_annotation_of(src, r) :-
+ call_of_expr(src, site), expr_call_candidate(site, m),
+ type_ref_owner("client", m, "METHOD", r),
+ type_ref("client", k, "METHOD_RETURN", _, _, r), annotation_container_kind(k).
+// a single-write local holding one of those
+mapping_annotation_of(src, r) :-
+ expr_binding("client", b, ctx, src), ctx != "STORE",
+ binding_lookup("client", b, b2),
+ binding_rebinding("client", "1", _, _, b2),
+ expr_binding("client", b2, "STORE", tgt),
+ assign_pair("client", tgt, val),
+ mapping_annotation_of(val, r).
+
+// the type at one generic position of an annotation (resolved, or by name in its module)
+type_ref_slot(r, pos, t) :-
+ type_ref_nesting("client", r, pos, "1", child),
+ type_ref("client", _, "GENERIC_ARGUMENT", _, _, child),
+ type_ref_resolved("client", t, child).
+type_ref_slot(r, pos, t) :-
+ type_ref_nesting("client", r, pos, "1", child),
+ type_ref("client", _, "GENERIC_ARGUMENT", tn, _, child), tn != "",
+ type_ref_owner("client", owner, ok, r),
+ annotation_owner_module("client", owner, ok, mod),
+ type_name_in_module("client", mod, tn, t).
+
+// `m.keys()` / `m.values()` / `m.items()` written with no argument
+dict_view_call(src, recv, view) :-
+ call_of_expr(src, site), call_name(site, view), dict_view_name(view),
+ call_receiver_object(site, recv), !call_arg(site, _, _).
+// the element a `keys()` / `values()` view yields
+element_type_of("client", src, t) :-
+ dict_view_call(src, recv, view), dict_view_slot(view, pos),
+ mapping_annotation_of(recv, r), r != "", type_ref_slot(r, pos, t).
+// an `items()` pair's two slots, and the names a TUPLE loop target binds to them
+dict_item_slot_type(src, pos, t) :-
+ dict_items_view(view), dict_view_call(src, recv, view), dict_view_slot(_, pos),
+ mapping_annotation_of(recv, r), type_ref_slot(r, pos, t).
+binding_element_type("client", b, t) :-
+ iter_pair("client", tgt, src),
+ expr_node("client", "TUPLE", _, _, tgt),
+ expr_parent("client", tgt, "ELEMENT", pos, el),
+ expr_binding("client", b, _, el),
+ dict_item_slot_type(src, pos, t).
+
// ── iteration_untyped(Prov, TargetExprHash, IterableExprHash) ────────────────
// A loop whose container the engine could not type. Emitted so the cost is countable
// rather than absorbed into a general "receiver untyped".
diff --git a/graph/python/engine/resolution/name-resolution.dl b/graph/python/engine/resolution/name-resolution.dl
index 33df19ae..b9345eb1 100644
--- a/graph/python/engine/resolution/name-resolution.dl
+++ b/graph/python/engine/resolution/name-resolution.dl
@@ -161,13 +161,20 @@ assign_pair(p, te, ve) :-
// the two-member set is the correct answer. A block that contains exactly one of the
// two is what proves the separation, and py_block — emitted by the parser and read by
// no rule before this — is the only place that is written down.
+// RESTRICTED TO THE PAIRS ITS ONE CONSUMER ASKS ABOUT. def_rebound_by_later_def reads
+// this only for two defs of the SAME BINDING; materialized over every same-file pair it
+// held 1.88M rows (230s) on a 526k-LOC subject, of which the consumer could ever touch a
+// few hundred. The binding join also keys the method pairs, so the block scan runs per
+// rebinding candidate instead of per file-wide cross product. Queried results unchanged.
def_separated_by_block(p, m1, m2) :-
+ method_declaring_binding(p, b, m1), method_declaring_binding(p, b, m2),
method_file(p, f, m1), method_file(p, f, m2),
method_start_line(p, l1s, m1), method_start_line(p, l2s, m2),
block_span(p, f, bs, be, _),
l1 = to_number(l1s), l2 = to_number(l2s), bstart = to_number(bs), bend = to_number(be),
l1 >= bstart, l1 <= bend, l2 > bend.
def_separated_by_block(p, m1, m2) :-
+ method_declaring_binding(p, b, m1), method_declaring_binding(p, b, m2),
method_file(p, f, m1), method_file(p, f, m2),
method_start_line(p, l1s, m1), method_start_line(p, l2s, m2),
block_span(p, f, bs, be, _),
@@ -1001,18 +1008,41 @@ computed_name_prefix(nameArg, pre) :-
expr_operator("client", op, nameArg), py_str_concat_operator(op),
expr_parent("client", nameArg, "OPERAND_LEFT", _, l),
expr_node("client", "LITERAL", _, pre, l), pre != "".
+// The same prefix written as an f-string, `getattr(self, f"visit_{type(node).__name__}")`:
+// the parser hands over an f-string's constant lead (the text before its first `{`) as its
+// literal value, and every string the f-string can produce starts with it.
+computed_name_prefix(nameArg, pre) :-
+ expr_node("client", "FSTRING", _, pre, nameArg), pre != "".
getattr_computed_name(e, obj, pre) :-
call_of_expr(e, site),
call_name(site, "getattr"),
call_arg(site, "0", obj),
call_arg(site, "1", nameArg),
computed_name_prefix(nameArg, pre).
-expr_denotes_method("client", e, m) :-
+// The receiver's type, and where the receiver is `self`, every CONSTRUCTED subclass of it too:
+// a visitor base names `visit_*` members it does not define itself, and self is whichever
+// subclass was built -- the same self-dispatch bound (RTA) dispatch.dl puts on `self.m()`.
+getattr_computed_type(e, pre, t) :-
getattr_computed_name(e, obj, pre),
- expr_type("client", obj, t),
+ expr_type("client", obj, t).
+getattr_computed_type(e, pre, s) :-
+ getattr_computed_name(e, obj, pre),
+ expr_node("client", "SELF_REFERENCE", _, _, obj),
+ expr_type("client", obj, k),
+ type_subclass("client", k, s),
+ type_instantiated_reachable("client", s).
+getattr_computed_member(e, m) :-
+ getattr_computed_type(e, pre, t),
mro_lookup("client", t, n, m),
strlen(n) > strlen(pre),
substr(n, 0, strlen(pre)) = pre.
+// a member a decorator replaced is the callable the decorator returned (a wrapper)
+getattr_computed_member(e, m) :-
+ getattr_computed_type(e, pre, t),
+ mro_lookup_decorated("client", t, n, m),
+ strlen(n) > strlen(pre),
+ substr(n, 0, strlen(pre)) = pre.
+expr_denotes_method("client", e, m) :- getattr_computed_member(e, m).
// ── getattr(obj, name, default) IS AT LEAST the default ──────────────────────
// h = getattr(record, "highlighter", self.highlighter)
diff --git a/graph/python/engine/resolution/value-flow.dl b/graph/python/engine/resolution/value-flow.dl
index 230c1f1b..6fd224af 100644
--- a/graph/python/engine/resolution/value-flow.dl
+++ b/graph/python/engine/resolution/value-flow.dl
@@ -97,6 +97,52 @@ param_arg_type(ph, t) :-
param_decl("client", kw, _, _, m, ph),
expr_type("client", a, t).
+// ── a parameter the TEST RUNNER fills ─────────────────────────────────────────
+// pytest calls `test_commit(session=)`: the call is the runner's,
+// so no call site spells it, but it is an argument reaching a parameter all the same, and
+// the parameter is typed exactly as one passed at a call site would be. Which fixture
+// serves the parameter is framework-behavior/dispatch.dl's py_fixs_injection (2a), the
+// runner's own lookup (same class, same module, nearest conftest, a pytest_plugins
+// module), so this is as deterministic as a name the LEGB walk resolves.
+// MEASURED across ten public suites: 1,529 method calls on such parameters were
+// ambiguous_unknown, and everything derived from them (`tx = session.begin(); tx.commit()`)
+// with them; a suite whose fixtures carry annotations resolved its own.
+param_arg_type(ph, t) :-
+ py_fixs_injection(req, fix, pn),
+ param_decl("client", pn, _, _, req, ph),
+ py_fixture_value_type(fix, t).
+
+// ── a parameter's DEFAULT VALUE reaches it ────────────────────────────────────
+// `def getter(val, field_obj=field_obj): return field_obj.deserialize(val)` -- the closure
+// idiom that pins a loop variable -- is called with one argument, so the only value
+// field_obj ever holds is its default, and no call site passes one. The default is an
+// argument the def statement writes once, evaluated where the def runs: one more value
+// reaching the parameter, alongside whatever the call sites pass.
+param_arg_type(ph, t) :- param_default_expr("client", e, ph), expr_type("client", e, t).
+param_arg_class(ph, t) :- param_default_expr("client", e, ph), expr_type_class_object("client", e, t).
+param_arg_method(ph, m) :- param_default_expr("client", e, ph), expr_denotes_method("client", e, m).
+
+// ── a parameter the runner fills from a PARAMETRIZE row, or from FIXTURE PARAMS ──
+// `@pytest.mark.parametrize("cls", [String, Integer])` calls the test once per value, so
+// each value reaches `cls` as an argument would: an instance types it, a class object
+// makes it that class, a function makes it callable as that function. Inside a fixture
+// declared `params=[...]`, `request.param` is each of those values in turn.
+// framework-behavior/dispatch.dl reads the rows (py_pz_param_value, py_pz_request_param).
+param_arg_type(ph, t) :- py_pz_param_value(ph, v), expr_type("client", v, t).
+param_arg_class(ph, t) :- py_pz_param_value(ph, v), expr_type_class_object("client", v, t).
+param_arg_method(ph, m) :- py_pz_param_value(ph, v), expr_denotes_method("client", v, m).
+expr_type("client", e, t) :- py_pz_request_param(e, v), expr_type("client", v, t).
+expr_type_class_object("client", e, t) :- py_pz_request_param(e, v), expr_type_class_object("client", v, t).
+expr_denotes_method("client", e, m) :- py_pz_request_param(e, v), expr_denotes_method("client", v, m).
+// a fixture that hands back a CLASS (`return request.param` over classes) makes the
+// requesting parameter that class, as a returned instance makes it an instance
+py_fixture_value_class(fix, t) :- py_fixture_decl(fix, _),
+ method_return_value_expr("client", fix, e), expr_type_class_object("client", e, t).
+param_arg_class(ph, t) :-
+ py_fixs_injection(req, fix, pn),
+ param_decl("client", pn, _, _, req, ph),
+ py_fixture_value_class(fix, t).
+
// ── param_arg_method(ParamHash, MethodHash) — a CALLABLE reaching a parameter ──
// `Delegator(target)` puts the FUNCTION `target` into the parameter `fn`. Java would
// need a functional interface for this; in Python it is an ordinary assignment.
@@ -230,9 +276,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 +287,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).
@@ -260,9 +304,7 @@ method_dispatch_candidate(base, m, "value") :-
// Read as a value -- `k = self._row_class` -- the attribute denotes the class, so a
// call on it or through it resolves like any other class object.
expr_type_class_object(p, e, c) :-
- expr_node(p, "ATTRIBUTE_ACCESS", _, n, e), n != "",
- !expr_name_context(p, "STORE", e),
- expr_parent(p, e, "ATTRIBUTE_OBJECT", _, obj),
+ attr_access(p, e, n, obj), !expr_name_context(p, "STORE", e),
expr_type(p, obj, t),
type_attr_class_value(p, t, n, c).
diff --git a/graph/python/libsum/libsum.py b/graph/python/libsum/libsum.py
new file mode 100644
index 00000000..304a8cf5
--- /dev/null
+++ b/graph/python/libsum/libsum.py
@@ -0,0 +1,1127 @@
+#!/usr/bin/env python3
+"""libsum.py -- what an installed Python library does with the objects a client hands it.
+
+ libsum.py --src --out [--site [,...]] [--cache ]
+
+THE GAP THIS CLOSES. The engine reads a dependency's signatures, never its bodies, so a hop the
+LIBRARY makes back into client code is invisible: a test client's `get()` calls the `open()` a
+client subclass overrides, a CLI runner calls `main()` on the command it was handed, a test client
+built around an application calls `app(environ, start_response)` on it. Every one of those is a
+library frame between two client frames, and the call graph stops at the first.
+
+WHAT A SUMMARY SAYS. One row per hand-back a library callable is known to make, rooted at one of
+its parameters (or at `self`, the object it was called on):
+
+ py-lib-callback.csv callee root pos path member via type
+ callee the library callable, canonical dotted name: `pkg.mod.func`, `pkg.mod.Class.method`
+ (one row per method a class HAS, its inherited ones included), `pkg.mod.Class` for
+ constructing it
+ root the parameter the object arrives in, or `self`
+ pos where the CLIENT writes that argument: the index among the positional arguments of
+ the call as written (an instance method's `self` is not written), -1 for `self`
+ path the attribute chain the library follows from that object first ("" for the object
+ itself, `application` for `self.application`), at most two long
+ member the method the library invokes on what it reached: a name it calls, or the dunder a
+ syntax runs (`__call__` for `x()`, `__enter__` for `with x`, `__iter__` for `for`...)
+ via the library function in which that invocation is written
+ type the class the LIBRARY declares that object to be (a parameter annotation, a field's), "" when it
+ declares none: when the client's own object cannot be told, a client subclass of it is the answer
+ py-lib-field-init.csv class field param pos
+ constructing `class` stores the argument `param` (written at `pos`) on the new object as
+ `field`, so a later `self.` hand-back reaches what the client passed
+ py-lib-ancestor.csv class ancestor
+ a library class's library ancestors, for the classes the client can name
+ py-lib-alias.csv alias canonical
+ a name a package re-exports (`clikit.Command` is `clikit.core.Command`)
+ py-lib-decorates.csv decorator form class field
+ applying the library callable as a decorator -- `@decorator` (DIRECT) or `@decorator(...)` (FACTORY) --
+ to a function f yields a new `class` whose `field` holds f; class "" when f itself comes back
+
+HOW, AND WHY NOT IN THE SOLVE. Library source is just Python, so the facts are DERIVED from it by a
+small flow-insensitive pass over each function body (which parameter-rooted value meets which call or
+protocol syntax), composed to a fixpoint across the library's own calls: `Client.get` calls
+`self.open`, `open` calls `run_app(self.application, ...)`, `run_app` calls `app(...)`, so
+`Client.get` hands back `__call__` on `self.application`. Nothing is solved over library bodies at
+index time: the pass reads only the packages the client imports (and what they import), is cached by
+package and version, and writes three small tables. A library written in C has no body to read and
+yields nothing; that is a declared blind spot, not a guess.
+
+WHERE THE LIBRARY IS. The project's own virtual environment (`.venv`, `venv`, `env` in the repository,
+else $VIRTUAL_ENV), its stdlib through pyvenv.cfg, or AXIOMCODE_PY_SITE (a comma list of directories)
+when set. No environment found means no summaries: the engine then behaves exactly as before.
+"""
+import argparse, ast, csv, hashlib, json, os, re, sys
+
+VERSION = '1'
+MAXPATH = 2
+DUNDER_BUILTIN = {'len': '__len__', 'str': '__str__', 'repr': '__repr__', 'iter': '__iter__', 'next': '__next__',
+ 'hash': '__hash__', 'bool': '__bool__', 'format': '__format__', 'reversed': '__reversed__',
+ 'abs': '__abs__', 'aiter': '__aiter__', 'anext': '__anext__'}
+SKIP_DIRS = {'__pycache__', 'tests', 'test', 'testing_support', 'docs', 'examples', 'benchmarks'}
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# where the library is
+# ─────────────────────────────────────────────────────────────────────────────
+def find_sites(src):
+ if os.environ.get('AXIOMCODE_PY_SITE'):
+ return [d for d in os.environ['AXIOMCODE_PY_SITE'].split(',') if os.path.isdir(d)], None
+ envs = [os.path.join(src, n) for n in ('.venv', 'venv', 'env', '.env')]
+ if os.environ.get('VIRTUAL_ENV'):
+ envs.append(os.environ['VIRTUAL_ENV'])
+ for e in envs:
+ cfg = os.path.join(e, 'pyvenv.cfg')
+ if not os.path.isfile(cfg):
+ continue
+ sites = []
+ for root in (os.path.join(e, 'lib'), os.path.join(e, 'Lib')):
+ if not os.path.isdir(root):
+ continue
+ if os.path.isdir(os.path.join(root, 'site-packages')):
+ sites.append(os.path.join(root, 'site-packages'))
+ for d in sorted(os.listdir(root)):
+ sp = os.path.join(root, d, 'site-packages')
+ if d.startswith('python') and os.path.isdir(sp):
+ sites.append(sp)
+ stdlib = None
+ try:
+ kv = dict(l.split('=', 1) for l in open(cfg, encoding='utf-8') if '=' in l)
+ kv = {k.strip(): v.strip() for k, v in kv.items()}
+ home, ver = kv.get('home', ''), (kv.get('version_info') or kv.get('version') or '')
+ mm = '.'.join(ver.split('.')[:2])
+ for cand in (os.path.join(os.path.dirname(home), 'lib', f'python{mm}'), os.path.join(home, 'Lib'),
+ os.path.join(os.path.dirname(home), 'Lib')):
+ if mm and os.path.isfile(os.path.join(cand, 'contextlib.py')):
+ stdlib = cand; break
+ except Exception:
+ pass
+ if sites:
+ return sites, stdlib
+ return [], None
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# which modules: the client's imports, then what those import, transitively
+# ─────────────────────────────────────────────────────────────────────────────
+IMPORT_RE = re.compile(r'^\s*(?:from\s+([A-Za-z_][\w.]*)\s+import|import\s+([A-Za-z_][\w.]*(?:\s*,\s*[A-Za-z_][\w.]*)*))', re.M)
+
+
+def client_imports(src, ir):
+ """every dotted path the client imports: `from a.b import C` gives a.b.C, `import a.b` gives a.b. From the
+ parser's import table when the IR is there, else from the source"""
+ paths = set()
+ p = os.path.join(ir, 'all-python-imports.csv') if ir else ''
+ if p and os.path.isfile(p):
+ try:
+ with open(p, newline='', encoding='utf-8') as fh:
+ r = csv.reader(fh, delimiter='\t')
+ head = next(r)
+ if 'importedPath' in head:
+ col = head.index('importedPath')
+ for row in r:
+ if len(row) > col and row[col]:
+ paths.add(row[col])
+ return paths
+ except Exception:
+ pass
+ skip = {'.venv', 'venv', 'env', '.env', '.git', 'node_modules', '.tox', '__pycache__', '.axiomcode'}
+ for d, ds, fs in os.walk(src):
+ ds[:] = [x for x in ds if x not in skip]
+ for f in fs:
+ if not f.endswith('.py'):
+ continue
+ try:
+ tree = ast.parse(open(os.path.join(d, f), encoding='utf-8', errors='replace').read())
+ except Exception:
+ continue
+ for n in ast.walk(tree):
+ if isinstance(n, ast.Import):
+ paths.update(a.name for a in n.names)
+ elif isinstance(n, ast.ImportFrom) and n.module and not n.level:
+ paths.update(f'{n.module}.{a.name}' for a in n.names if a.name != '*')
+ paths.add(n.module)
+ return paths
+
+
+class Package:
+ """one importable top-level name: a directory package or a single module file"""
+ def __init__(self, name, path, base):
+ self.name, self.path, self.base = name, path, base
+
+ def files(self):
+ if os.path.isfile(self.path):
+ yield self.name, self.path
+ return
+ for d, ds, fs in os.walk(self.path):
+ ds[:] = sorted(x for x in ds if x not in SKIP_DIRS and not x.startswith('.'))
+ for f in sorted(fs):
+ if not f.endswith('.py'):
+ continue
+ rel = os.path.relpath(os.path.join(d, f), self.base)[:-3].replace(os.sep, '.')
+ if rel.endswith('.__init__'):
+ rel = rel[:-9]
+ yield rel, os.path.join(d, f)
+
+
+def locate(name, sites, stdlib):
+ for base in list(sites) + ([stdlib] if stdlib else []):
+ p = os.path.join(base, name)
+ if os.path.isdir(p) and (os.path.isfile(os.path.join(p, '__init__.py')) or base != stdlib):
+ return Package(name, p, base)
+ if os.path.isfile(p + '.py'):
+ return Package(name, p + '.py', base)
+ return None
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# one module: its namespace, its classes, and the events in each function body
+# ─────────────────────────────────────────────────────────────────────────────
+class Fn:
+ __slots__ = ('q', 'params', 'pos', 'kind', 'events', 'cls', 'stores', 'ptypes', 'rets', 'local')
+
+ def __init__(self, q, cls):
+ self.q, self.cls, self.ptypes, self.rets, self.local = q, cls, {}, [], False
+ self.params, self.pos, self.kind = [], {}, 'function'
+ self.events = [] # ('use', val, member) | ('call', callee, selfval, {param: [vals]}, [posvals], {kw: [vals]})
+ self.stores = [] # (field, val) written on self
+
+
+class Cls:
+ __slots__ = ('q', 'bases', 'methods', 'mod', 'ftypes')
+
+ def __init__(self, q, mod):
+ self.q, self.mod, self.bases, self.methods, self.ftypes = q, mod, [], {}, {}
+
+
+def resolve_rel(mod, is_pkg, level, name):
+ parts = mod.split('.')
+ if not is_pkg:
+ parts = parts[:-1]
+ if level > 1:
+ parts = parts[:len(parts) - (level - 1)]
+ return '.'.join([p for p in parts if p] + ([name] if name else []))
+
+
+class Module:
+ def __init__(self, name, path, is_pkg):
+ self.name, self.path, self.is_pkg = name, path, is_pkg
+ self.ns = {} # local name -> qualified name (a def, a class, an imported thing)
+ self.fns, self.classes = {}, {}
+ self.imports = set()
+ self.stars, self.all_names = [], None # `from M import *` sources; a literal __all__, when declared
+ self.call_alias = {} # `Name = wrap(pkg.Class, ...)`: the dotted first argument, an alias when it is a class
+
+ def load(self):
+ try:
+ tree = ast.parse(open(self.path, encoding='utf-8', errors='replace').read())
+ except Exception:
+ return False
+ self.tree = tree
+ for st in tree.body:
+ self._ns_stmt(st)
+ return True
+
+ def _ns_stmt(self, st):
+ if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
+ self.ns[st.name] = f'{self.name}.{st.name}'
+ elif isinstance(st, ast.ImportFrom):
+ base = resolve_rel(self.name, self.is_pkg, st.level, st.module or '') if st.level else (st.module or '')
+ if base:
+ self.imports.add(base)
+ for a in st.names:
+ if a.name != '*':
+ self.ns[a.asname or a.name] = f'{base}.{a.name}' if base else a.name
+ elif base:
+ self.stars.append(base)
+ elif isinstance(st, ast.Import):
+ for a in st.names:
+ self.imports.add(a.name)
+ if a.asname:
+ self.ns[a.asname] = a.name
+ else:
+ self.ns.setdefault(a.name.split('.')[0], a.name.split('.')[0])
+ elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name) \
+ and st.targets[0].id == '__all__' and isinstance(st.value, (ast.List, ast.Tuple)):
+ self.all_names = [e.value for e in st.value.elts if isinstance(e, ast.Constant) and isinstance(e.value, str)]
+ elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name) \
+ and isinstance(st.value, ast.Call) and st.value.args and dotted(st.value.args[0]):
+ v = dotted(st.value.args[0])
+ if v.split('.')[0] in self.ns:
+ self.call_alias.setdefault(st.targets[0].id, self.ns[v.split('.')[0]] + v[len(v.split('.')[0]):])
+ elif isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name):
+ v = dotted(st.value)
+ if v and v.split('.')[0] in self.ns:
+ self.ns.setdefault(st.targets[0].id, self.ns[v.split('.')[0]] + v[len(v.split('.')[0]):])
+ elif isinstance(st, (ast.If, ast.Try)):
+ for b in [st.body, getattr(st, 'orelse', []), getattr(st, 'finalbody', [])] + [h.body for h in getattr(st, 'handlers', [])]:
+ for s in b:
+ self._ns_stmt(s)
+
+ def qual(self, n):
+ return self.ns.get(n)
+
+ def analyse(self):
+ for st in self.tree.body:
+ if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ self._fn(st, None)
+ elif isinstance(st, ast.ClassDef):
+ c = Cls(f'{self.name}.{st.name}', self)
+ c.bases = [b for b in (self.qual_expr(x) for x in st.bases) if b]
+ self.classes[c.q] = c
+ for s in st.body:
+ if isinstance(s, ast.AnnAssign) and isinstance(s.target, ast.Name):
+ t = self.ann_qual(s.annotation)
+ if t:
+ c.ftypes[s.target.id] = t
+ for s in st.body:
+ if isinstance(s, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ f = self._fn(s, c)
+ c.methods[s.name] = f.q
+
+ def ann_qual(self, ann):
+ """the class an annotation names: `T`, `pkg.T`, `"T"`, `Optional[T]`, `T | None`"""
+ if isinstance(ann, ast.Constant) and isinstance(ann.value, str):
+ try:
+ ann = ast.parse(ann.value, mode='eval').body
+ except SyntaxError:
+ return None
+ if isinstance(ann, ast.BinOp):
+ return self.ann_qual(ann.left)
+ if isinstance(ann, ast.Subscript) and (dotted(ann.value) or '').split('.')[-1] == 'Optional':
+ return self.ann_qual(ann.slice)
+ return self.qual_expr(ann) if ann is not None else None
+
+ def qual_expr(self, e):
+ d = dotted(e.value if isinstance(e, ast.Subscript) else e)
+ if not d:
+ return None
+ head, _, rest = d.partition('.')
+ q = self.qual(head)
+ return (q + ('.' + rest if rest else '')) if q else None
+
+ def _fn(self, node, cls):
+ q = f'{cls.q}.{node.name}' if cls else f'{self.name}.{node.name}'
+ f = self.signature(node, q, cls)
+ self.fns[q] = f
+ Body(self, f, f.params[0] if f.kind in ('method', 'classmethod', 'property') and f.params else None, cls).run(node)
+ return f
+
+ def signature(self, node, q, cls):
+ f = Fn(q, cls.q if cls else None)
+ decos = {dotted(d.func if isinstance(d, ast.Call) else d) for d in node.decorator_list}
+ a = node.args
+ names = [x.arg for x in a.posonlyargs + a.args]
+ if cls is not None:
+ if 'staticmethod' in decos:
+ f.kind = 'static'
+ elif 'classmethod' in decos:
+ f.kind = 'classmethod'
+ elif 'property' in decos or any(d and d.endswith(('.setter', '.getter', 'cached_property')) for d in decos):
+ f.kind = 'property'
+ else:
+ f.kind = 'method'
+ f.params = names + [x.arg for x in a.kwonlyargs]
+ skip = 1 if f.kind in ('method', 'classmethod', 'property') and names else 0
+ for i, n in enumerate(names):
+ f.pos[n] = i - skip if i >= skip else -1
+ for x in a.kwonlyargs:
+ f.pos[x.arg] = -2 # keyword only: matched by name
+ for x in a.posonlyargs + a.args + a.kwonlyargs:
+ if x.annotation is not None:
+ t = self.ann_qual(x.annotation)
+ if t:
+ f.ptypes[x.arg] = t
+ return f
+
+
+def dotted(e):
+ if isinstance(e, ast.Name):
+ return e.id
+ if isinstance(e, ast.Attribute):
+ d = dotted(e.value)
+ return f'{d}.{e.attr}' if d else None
+ return None
+
+
+class Body:
+ """a flow-insensitive pass over one function body (nested defs and lambdas included: a closure the
+ library builds around a parameter is how a decorator or a callback wrapper hands it back)"""
+ def __init__(self, mod, fn, selfname, cls):
+ self.mod, self.fn, self.selfname, self.cls = mod, fn, selfname, cls
+ self.env = {}
+ self.params = set(fn.params)
+
+ def run(self, node):
+ for _ in range(2): # twice: a local assigned below its first use
+ self.fn.events = []; self.fn.stores = []; self.fn.rets = []
+ for st in node.body:
+ self.stmt(st)
+ return self
+
+ # values: ('p', root, path) | ('ref', qname) | ('new', class, argvals) | ('self', ...) is ('p', 'self', ())
+ def val(self, e):
+ if isinstance(e, ast.Name):
+ if e.id in self.env:
+ return self.env[e.id]
+ if e.id in self.params:
+ return [('p', 'self' if e.id == self.selfname else e.id, ())]
+ q = self.mod.qual(e.id)
+ return [('ref', q)] if q else []
+ if isinstance(e, ast.Attribute):
+ out = []
+ for v in self.val(e.value):
+ if v[0] == 'p' and len(v[2]) < MAXPATH:
+ out.append(('p', v[1], v[2] + (e.attr,)))
+ elif v[0] == 'ref':
+ out.append(('ref', f'{v[1]}.{e.attr}'))
+ elif v[0] == 'new':
+ out.append(('field', v, e.attr))
+ return out
+ if isinstance(e, ast.Call):
+ return self.call(e)
+ if isinstance(e, (ast.IfExp,)):
+ self.val(e.test)
+ return self.val(e.body) + self.val(e.orelse)
+ if isinstance(e, ast.BoolOp):
+ out = []
+ for x in e.values:
+ out += self.val(x)
+ return out
+ if isinstance(e, ast.NamedExpr):
+ v = self.val(e.value)
+ if isinstance(e.target, ast.Name):
+ self.env[e.target.id] = v
+ return v
+ if isinstance(e, ast.Await):
+ return self.val(e.value)
+ if isinstance(e, ast.Subscript):
+ for v in self.val(e.value):
+ self.use(v, '__getitem__')
+ self.val(e.slice)
+ return []
+ if isinstance(e, ast.Compare):
+ self.val(e.left)
+ for op, c in zip(e.ops, e.comparators):
+ cv = self.val(c)
+ if isinstance(op, (ast.In, ast.NotIn)):
+ for v in cv:
+ self.use(v, '__contains__')
+ return []
+ if isinstance(e, ast.JoinedStr):
+ for x in e.values:
+ if isinstance(x, ast.FormattedValue):
+ m = {115: '__str__', 114: '__repr__', 97: '__repr__'}.get(x.conversion, '__format__')
+ for v in self.val(x.value):
+ self.use(v, m)
+ return []
+ if isinstance(e, ast.Lambda):
+ self.nested(e.body if isinstance(e.body, list) else [ast.Expr(e.body)], e.args)
+ return []
+ if isinstance(e, (ast.ListComp, ast.SetComp, ast.GeneratorExp, ast.DictComp)):
+ for g in e.generators:
+ for v in self.val(g.iter):
+ self.use(v, '__iter__')
+ for c in g.ifs:
+ self.val(c)
+ for x in ([e.key, e.value] if isinstance(e, ast.DictComp) else [e.elt]):
+ self.val(x)
+ return []
+ for ch in ast.iter_child_nodes(e):
+ if isinstance(ch, ast.expr):
+ self.val(ch)
+ return []
+
+ def use(self, v, member):
+ if member == '__call__' and v == ('p', 'self', ()) and self.fn.kind == 'classmethod':
+ member = '__init__' # cls(...) constructs the class the method was called on
+ if v[0] == 'p' or v[0] == 'field':
+ # what the library itself declares the object to be, where it says: a parameter's annotation, a field's
+ hint = None
+ if v[0] == 'p':
+ base = self.cls.q if v[1] == 'self' and self.cls else self.fn.ptypes.get(v[1])
+ if base and not v[2]:
+ hint = ('t', base)
+ elif base and len(v[2]) == 1:
+ hint = ('f', base, v[2][0])
+ self.fn.events.append(('use', v, member, hint))
+
+ def call(self, e):
+ f = e.func
+ posv = [self.val(a) if not isinstance(a, ast.Starred) else None for a in e.args]
+ kwv = {k.arg: self.val(k.value) for k in e.keywords if k.arg}
+ for k in e.keywords:
+ if not k.arg:
+ self.val(k.value)
+ if isinstance(f, ast.Name) and f.id in DUNDER_BUILTIN and f.id not in self.env and not self.mod.qual(f.id) and posv and posv[0]:
+ for v in posv[0]:
+ self.use(v, DUNDER_BUILTIN[f.id])
+ return []
+ if isinstance(f, ast.Name) and f.id == 'super' and not e.args:
+ return [('super',)]
+ fd = dotted(f) or ''
+ if fd.split('.')[-1] == 'cast' and len(posv) == 2 and posv[1]:
+ return posv[1] # typing.cast(T, x) is x
+ if fd == 'type' and len(posv) == 1 and posv[0]:
+ return posv[0] # type(x).__enter__ is looked up on x's class: x's member
+ if isinstance(f, ast.Attribute):
+ out = []
+ for rv in self.val(f.value):
+ if rv[0] in ('p', 'field'):
+ self.use(rv, f.attr)
+ self.fn.events.append(('call', ('method', rv, f.attr), posv, kwv))
+ elif rv[0] == 'super':
+ self.fn.events.append(('call', ('super', self.cls.q if self.cls else None, f.attr), posv, kwv))
+ elif rv[0] == 'new':
+ self.fn.events.append(('call', ('method', rv, f.attr), posv, kwv))
+ elif rv[0] == 'ref':
+ self.fn.events.append(('call', ('ref', f'{rv[1]}.{f.attr}'), posv, kwv))
+ return out
+ out = []
+ for cv in self.val(f):
+ if cv[0] in ('p', 'field'):
+ self.use(cv, '__call__')
+ if cv[0] == 'p' and cv[2]:
+ # a member read first and called later (`_enter = type(cm).__enter__; _enter(cm)`)
+ self.use(('p', cv[1], cv[2][:-1]), cv[2][-1])
+ elif cv[0] == 'ref':
+ self.fn.events.append(('call', ('ref', cv[1]), posv, kwv))
+ out.append(('new', cv[1], self.args(posv, kwv)))
+ elif cv[0] == 'closure':
+ self.fn.events.append(('call', ('ref', cv[1]), posv, kwv))
+ out.append(('res', cv[1], self.args(posv, kwv)))
+ elif cv[0] in ('new', 'res', 'res2'):
+ out.append(('res2', cv, self.args(posv, kwv))) # calling what a call returned: `command(...)(f)`
+ return out
+
+ @staticmethod
+ def args(posv, kwv):
+ return (tuple(tuple(p) if p is not None else None for p in posv), tuple(sorted((k, tuple(v)) for k, v in kwv.items())))
+
+ def nested(self, body, args):
+ saved = set(self.params)
+ for a in args.posonlyargs + args.args + args.kwonlyargs + [x for x in (args.vararg, args.kwarg) if x]:
+ self.params.discard(a.arg); self.env.pop(a.arg, None)
+ for st in body:
+ self.stmt(st)
+ self.params = saved
+
+ def assign(self, t, vals):
+ if isinstance(t, ast.Name):
+ # flow-insensitive: a parameter rebound keeps what it arrived as (`callback = other.callback`)
+ base = self.env.get(t.id) or ([('p', 'self' if t.id == self.selfname else t.id, ())] if t.id in self.params else [])
+ self.env[t.id] = list(dict.fromkeys(base + vals))
+ elif isinstance(t, ast.Attribute):
+ for ov in self.val(t.value):
+ if ov == ('p', 'self', ()):
+ for v in vals:
+ self.fn.stores.append((t.attr, v))
+ if self.cls and v[0] == 'p' and not v[2] and v[1] in self.fn.ptypes:
+ self.cls.ftypes.setdefault(t.attr, self.fn.ptypes[v[1]])
+ elif isinstance(t, (ast.Tuple, ast.List)):
+ for x in t.elts:
+ self.assign(x, [])
+ elif isinstance(t, ast.Subscript):
+ for v in self.val(t.value):
+ self.use(v, '__setitem__')
+
+ def stmt(self, st):
+ if isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ self.nested(st.body, st.args)
+ # and as a callable of its own, so what it RETURNS is known: a decorator factory returns this closure,
+ # and the closure returns the object it wrapped the decorated function in
+ sq = f'{self.fn.q}..{st.name}'
+ sub = self.mod.signature(st, sq, None)
+ sub.local = True
+ self.mod.fns[sq] = sub
+ b = Body(self.mod, sub, None, None)
+ b.env = {k: list(v) for k, v in self.env.items() if k not in set(sub.params)}
+ b.run(st)
+ self.env[st.name] = [('closure', sq)]
+ return
+ if isinstance(st, ast.Return):
+ if st.value is not None:
+ self.fn.rets.extend(self.val(st.value))
+ return
+ if isinstance(st, ast.ClassDef):
+ return
+ if isinstance(st, ast.Assign):
+ v = self.val(st.value)
+ for t in st.targets:
+ self.assign(t, v)
+ return
+ if isinstance(st, ast.AnnAssign) and isinstance(st.target, ast.Attribute) and self.cls \
+ and isinstance(st.target.value, ast.Name) and st.target.value.id == self.selfname:
+ t = self.mod.ann_qual(st.annotation)
+ if t:
+ self.cls.ftypes[st.target.attr] = t
+ if isinstance(st, (ast.AnnAssign, ast.AugAssign)):
+ if st.value is not None:
+ v = self.val(st.value)
+ self.assign(st.target, v if isinstance(st, ast.AnnAssign) else [])
+ return
+ if isinstance(st, (ast.For, ast.AsyncFor)):
+ for v in self.val(st.iter):
+ self.use(v, '__aiter__' if isinstance(st, ast.AsyncFor) else '__iter__')
+ self.assign(st.target, [])
+ if isinstance(st, (ast.With, ast.AsyncWith)):
+ a = isinstance(st, ast.AsyncWith)
+ for it in st.items:
+ vs = self.val(it.context_expr)
+ for v in vs:
+ self.use(v, '__aenter__' if a else '__enter__'); self.use(v, '__aexit__' if a else '__exit__')
+ if it.optional_vars is not None:
+ self.assign(it.optional_vars, [])
+ if isinstance(st, ast.Delete):
+ for t in st.targets:
+ if isinstance(t, ast.Subscript):
+ for v in self.val(t.value):
+ self.use(v, '__delitem__')
+ return
+ for fld, ch in ast.iter_fields(st):
+ if fld in ('target', 'targets', 'iter', 'items') and isinstance(st, (ast.For, ast.AsyncFor, ast.With, ast.AsyncWith)):
+ continue
+ for c in (ch if isinstance(ch, list) else [ch]):
+ if isinstance(c, ast.stmt):
+ self.stmt(c)
+ elif isinstance(c, ast.expr):
+ self.val(c)
+ elif isinstance(c, ast.excepthandler):
+ for s in c.body:
+ self.stmt(s)
+ elif isinstance(c, ast.match_case) if hasattr(ast, 'match_case') else False:
+ for s in c.body:
+ self.stmt(s)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# the program: every analysed module, then the fixpoint across the library's own calls
+# ─────────────────────────────────────────────────────────────────────────────
+class Program:
+ def __init__(self):
+ self.mods, self.fns, self.classes = {}, {}, {}
+
+ def add(self, m):
+ self.mods[m.name] = m
+ self.fns.update(m.fns); self.classes.update(m.classes)
+
+ def canon(self, q, seen=None):
+ """follow a module re-export to the declaration: clikit.Command -> clikit.core.Command"""
+ if q in self.fns or q in self.classes:
+ return q
+ seen = seen or set()
+ if q in seen or not q:
+ return None
+ seen.add(q)
+ parts = q.split('.')
+ for i in range(len(parts) - 1, 0, -1):
+ m = self.mods.get('.'.join(parts[:i]))
+ if m is None:
+ continue
+ nxt = m.qual(parts[i])
+ if nxt and nxt != '.'.join(parts[:i + 1]):
+ return self.canon('.'.join([nxt] + parts[i + 1:]), seen)
+ if not nxt and parts[i] in m.call_alias:
+ # a generic alias built around a class (`Mapping = _alias(collections.abc.Mapping, 2)`) is that class;
+ # a wrapper around anything else (`f = partial(g, x)`) is not followed
+ c = self.canon(m.call_alias[parts[i]], seen)
+ if c in self.classes:
+ return self.canon('.'.join([c] + parts[i + 1:]), seen)
+ return None
+ if i + 1 < len(parts): # Class.method
+ c = self.canon('.'.join(parts[:i + 1]), seen)
+ if c in self.classes:
+ r = self.lookup(c, parts[i + 1])
+ return r if len(parts) == i + 2 else None
+ return None
+ return None
+
+ def type_of(self, hint):
+ """the canonical library class a use hint names: ('t', T) the object's own declared type, ('f', B, f) the
+ declared type of field f on B (annotated in the class, or assigned from an annotated parameter)"""
+ if not hint:
+ return ''
+ if hint[0] == 't':
+ c = self.canon(hint[1])
+ return c if c in self.classes else ''
+ b = self.canon(hint[1])
+ if b not in self.classes:
+ return ''
+ for k in self.mro(b):
+ t = self.classes[k].ftypes.get(hint[2])
+ if t:
+ c = self.canon(t) or self.canon(f'{self.classes[k].mod.name}.{t}')
+ return c if c in self.classes else ''
+ return ''
+
+ def mro(self, c, seen=None):
+ seen = seen if seen is not None else set()
+ if c in seen:
+ return []
+ seen.add(c)
+ out = [c]
+ k = self.classes.get(c)
+ for b in (k.bases if k else []):
+ bc = self.canon(b)
+ if bc in self.classes:
+ for x in self.mro(bc, seen):
+ if x not in out:
+ out.append(x)
+ return out
+
+ def lookup(self, c, name):
+ for k in self.mro(c):
+ m = self.classes[k].methods.get(name)
+ if m:
+ return m
+ return None
+
+ def solve(self):
+ S = {q: set() for q in self.fns} # (root, path, member, via)
+ ST = {c: set() for c in self.classes} # class -> (field, init param)
+ for f in self.fns.values():
+ for ev in f.events:
+ if ev[0] == 'use' and ev[1][0] == 'p' and (ev[1][1] == 'self' or ev[1][1] in f.pos):
+ S[f.q].add((ev[1][1], ev[1][2], ev[2], f.q, self.type_of(ev[3])))
+ if f.cls and f.q.endswith('.__init__'):
+ for fld, v in f.stores:
+ if v[0] == 'p' and v[2] == () and v[1] in f.pos:
+ ST[f.cls].add((fld, v[1]))
+
+ def init_of(c):
+ return self.lookup(c, '__init__')
+
+ def stores(c):
+ out = set()
+ for k in self.mro(c):
+ out |= ST.get(k, set())
+ return out
+
+ def bind(callee, posv, kwv):
+ """callee parameter -> the caller's values"""
+ g = self.fns.get(callee)
+ if not g:
+ return {}
+ out = {}
+ byp = {i: n for n, i in g.pos.items() if i >= 0}
+ for i, vs in enumerate(posv):
+ if vs is not None and i in byp:
+ out.setdefault(byp[i], []).extend(vs)
+ for k, vs in kwv.items():
+ if k in g.pos:
+ out.setdefault(k, []).extend(vs)
+ return out
+
+ def lift(v, path, depth=0):
+ """a value reached in the caller, followed down `path`: param-rooted values only"""
+ if v[0] == 'p':
+ p = v[2] + tuple(path)
+ return [(v[1], p)] if len(p) <= MAXPATH else []
+ if v[0] == 'field' and depth < 3:
+ return [x for w in field_vals(v[1], v[2]) for x in lift(w, path, depth + 1)]
+ if v[0] == 'new' and path and depth < 3:
+ return [x for w in field_vals(v, path[0]) for x in lift(w, path[1:], depth + 1)]
+ return []
+
+ def field_vals(newv, fld):
+ c = self.canon(newv[1])
+ if c not in self.classes:
+ return []
+ ini = init_of(c)
+ if not ini:
+ return []
+ posv = [list(x) if x is not None else None for x in newv[2][0]]
+ kwv = {k: list(v) for k, v in newv[2][1]}
+ b = bind(ini, posv, kwv)
+ return [w for f, pn in stores(c) if f == fld for w in b.get(pn, [])]
+
+ changed, rounds = True, 0
+ while changed and rounds < 12:
+ changed, rounds = False, rounds + 1
+ for f in self.fns.values():
+ acc = S[f.q]; n0 = len(acc)
+ for ev in f.events:
+ if ev[0] == 'use' and ev[1][0] == 'field':
+ for root, path in lift(ev[1], ()):
+ if root == 'self' or root in f.pos:
+ acc.add((root, path, ev[2], f.q, ''))
+ continue
+ if ev[0] != 'call':
+ continue
+ tgt, posv, kwv = ev[1], ev[2], ev[3]
+ targets = [] # (callee, selfvals)
+ if tgt[0] == 'ref':
+ q = self.canon(tgt[1])
+ if q in self.fns:
+ targets.append((q, []))
+ elif q in self.classes and init_of(q):
+ targets.append((init_of(q), []))
+ elif tgt[0] == 'method':
+ rv, name = tgt[1], tgt[2]
+ if rv == ('p', 'self', ()) and f.cls:
+ m = self.lookup(f.cls, name)
+ if m:
+ targets.append((m, [rv]))
+ elif rv[0] == 'p' and rv[2] == () and rv[1] in f.ptypes:
+ c = self.canon(f.ptypes[rv[1]])
+ m = self.lookup(c, name) if c in self.classes else None
+ if m:
+ targets.append((m, [rv]))
+ elif rv[0] == 'p' and len(rv[2]) == 1:
+ # `self.loader.load(...)`: the field's declared type says whose `load` runs
+ base = f.cls if rv[1] == 'self' else f.ptypes.get(rv[1])
+ c = self.type_of(('f', base, rv[2][0])) if base else ''
+ m = self.lookup(c, name) if c else None
+ if m:
+ targets.append((m, [rv]))
+ elif rv[0] == 'new':
+ c = self.canon(rv[1])
+ if c in self.classes and self.lookup(c, name):
+ targets.append((self.lookup(c, name), [rv]))
+ elif tgt[0] == 'super' and tgt[1]:
+ for k in self.mro(tgt[1])[1:]:
+ m = self.classes[k].methods.get(tgt[2])
+ if m:
+ targets.append((m, [('p', 'self', ())])); break
+ for callee, selfv in targets:
+ b = bind(callee, posv, kwv)
+ for (r, p, mem, via, ty) in list(S.get(callee, ())):
+ src = selfv if r == 'self' else b.get(r, [])
+ for v in src:
+ for root, path in lift(v, p):
+ if root == 'self' or root in f.pos:
+ acc.add((root, path, mem, via, ty))
+ if callee.endswith('.__init__'):
+ g = self.fns[callee]
+ # super().__init__(x) / Base.__init__(self, x): what the base stores, this class stores
+ if f.cls and f.q.endswith('.__init__') and (selfv == [('p', 'self', ())] or tgt[0] == 'super'):
+ for fld, pn in stores(g.cls):
+ for v in b.get(pn, []):
+ if v[0] == 'p' and v[2] == () and v[1] in f.pos and (fld, v[1]) not in ST[f.cls]:
+ ST[f.cls].add((fld, v[1])); changed = True
+ if len(acc) != n0:
+ changed = True
+ self.S, self.ST = S, ST
+ return rounds
+
+ def returns(self):
+ """R[f]: what f's return value is, as far as a decorator needs it --
+ ('obj', C, field, param) a new C whose `field` holds f's parameter `param`
+ ('closure', D) the nested function D (a decorator factory's decorator)
+ ('param', param) f's own parameter, unchanged (an identity / registering decorator)"""
+ R = {q: set() for q in self.fns}
+
+ def bindv(g, args):
+ posv = [list(x) if x is not None else None for x in args[0]]
+ return self._bind(g, posv, {k: list(v) for k, v in args[1]})
+
+ def own(f, w):
+ return w[0] == 'p' and w[2] == () and w[1] in f.pos and w[1] != 'self'
+
+ def apply(f, g, args, out):
+ b = bindv(g, args)
+ for d in list(R.get(g, ())):
+ if d[0] == 'closure':
+ out.add(d)
+ elif d[0] in ('obj', 'param'):
+ for w in b.get(d[-1], []):
+ if own(f, w):
+ out.add(d[:-1] + (w[1],))
+ elif w[0] == 'closure' and d[0] == 'param':
+ out.add(w)
+
+ def resolve(f, v, out, depth=0):
+ if depth > 4:
+ return
+ if v[0] == 'closure':
+ out.add(v)
+ elif own(f, v):
+ out.add(('param', v[1]))
+ elif v[0] == 'new':
+ c = self.canon(v[1])
+ if c in self.classes:
+ ini = self.lookup(c, '__init__')
+ if ini:
+ b = bindv(ini, v[2])
+ for fld, pn in self._stores(c):
+ for w in b.get(pn, []):
+ if own(f, w):
+ out.add(('obj', c, fld, w[1]))
+ elif c in self.fns:
+ apply(f, c, v[2], out)
+ elif v[0] == 'res':
+ apply(f, v[1], v[2], out)
+ elif v[0] == 'res2':
+ inner = set()
+ resolve(f, v[1], inner, depth + 1)
+ for d in inner:
+ if d[0] == 'closure':
+ apply(f, d[1], v[2], out)
+
+ for _ in range(8):
+ changed = False
+ for f in self.fns.values():
+ acc = R[f.q]; n0 = len(acc)
+ for v in f.rets:
+ resolve(f, v, acc)
+ changed |= len(acc) != n0
+ if not changed:
+ break
+ self.R = R
+
+ def _bind(self, callee, posv, kwv):
+ g = self.fns.get(callee)
+ if not g:
+ return {}
+ out = {}
+ byp = {i: n for n, i in g.pos.items() if i >= 0}
+ for i, vs in enumerate(posv):
+ if vs is not None and i in byp:
+ out.setdefault(byp[i], []).extend(vs)
+ for k, vs in kwv.items():
+ if k in g.pos:
+ out.setdefault(k, []).extend(vs)
+ return out
+
+ def _stores(self, c):
+ out = set()
+ for k in self.mro(c):
+ out |= self.ST.get(k, set())
+ return out
+
+ def decorates(self, g):
+ """what applying the callable g as a decorator to a function f yields: (form, class, field) -- DIRECT for
+ `@g`, FACTORY for `@g(...)`; class '' when f itself comes back (a registering decorator)"""
+ f = self.fns[g]
+ first = lambda h: next((n for n, i in self.fns[h].pos.items() if i == 0), None)
+ out = set()
+ p0 = first(g)
+ for d in self.R.get(g, ()):
+ if d[0] == 'obj' and d[3] == p0:
+ out.add(('DIRECT', d[1], d[2]))
+ elif d[0] == 'param' and d[1] == p0:
+ out.add(('DIRECT', '', ''))
+ elif d[0] == 'closure':
+ q0 = first(d[1])
+ for e in self.R.get(d[1], ()):
+ if e[0] == 'obj' and e[3] == q0:
+ out.add(('FACTORY', e[1], e[2]))
+ elif e[0] == 'param' and e[1] == q0:
+ out.add(('FACTORY', '', ''))
+ return out
+
+ def named(self, imports, lib_pkgs):
+ """the library classes and functions the client can name: what it imports, what a module it imports
+ (and that module's direct submodules) declares, and the class a decorator it imports wraps a function
+ in. Summaries are emitted for these only -- the solve reads what the client can reach, not the venv."""
+ out = set()
+ def add(q):
+ c = self.canon(q)
+ if c and c.split('.')[0] in lib_pkgs and '' not in c:
+ out.add(c)
+ for p in imports:
+ add(p)
+ if p in self.mods:
+ for m in [p] + [x for x in self.mods if x.startswith(p + '.') and '.' not in x[len(p) + 1:]]:
+ for local in self.mods[m].ns:
+ add(f'{m}.{local}')
+ for q in list(out):
+ if q in self.fns:
+ for _, c, _ in self.decorates(q):
+ if c:
+ out.add(c)
+ elif q in self.classes:
+ for n in {n for k in self.mro(q) for n in self.classes[k].methods}:
+ m = self.lookup(q, n)
+ if m:
+ for _, c, _ in self.decorates(m):
+ if c:
+ out.add(c)
+ return out
+
+ def rows(self, keep):
+ """flattened rows: every method a class HAS (inherited ones included) and every constructor"""
+ cb, fi, al, de, an = set(), set(), set(), set(), set()
+
+ def emit(callee_name, fq, kind):
+ g = self.fns[fq]
+ for (r, p, mem, via, ty) in self.S.get(fq, ()):
+ if r == 'self':
+ pos = -1
+ else:
+ pos = g.pos.get(r, -3)
+ if pos == -3 or (pos == -1 and kind != 'function'):
+ continue
+ cb.add((callee_name, r, pos, '.'.join(p), mem, via, ty))
+ for q, g in self.fns.items():
+ if g.cls is None and not g.local and keep(q):
+ emit(q, q, 'function')
+ for form, c, fld in self.decorates(q):
+ de.add((q, form, c, fld))
+ for c in self.classes:
+ if not keep(c):
+ continue
+ names = set()
+ for k in self.mro(c):
+ names |= set(self.classes[k].methods)
+ for n in names:
+ m = self.lookup(c, n)
+ if m and self.fns[m].kind != 'property':
+ emit(f'{c}.{n}', m, 'method' if self.fns[m].kind != 'static' else 'function')
+ for form, k, fld in self.decorates(m):
+ de.add((f'{c}.{n}', form, k, fld))
+ for a in self.mro(c)[1:]:
+ an.add((c, a))
+ ini = self.lookup(c, '__init__')
+ if ini:
+ emit(c, ini, 'ctor')
+ g = self.fns[ini]
+ for fld, pn in set().union(*[self.ST.get(k, set()) for k in self.mro(c)]):
+ p = g.pos.get(pn, -3)
+ if p != -3:
+ fi.add((c, fld, pn, p))
+ if p == 0:
+ de.add((c, 'DIRECT', c, fld)) # `@C def f`: C(f) holds f
+ for mname, m in self.mods.items():
+ for local, q in list(m.ns.items()) + [(k, v) for k, v in m.call_alias.items() if k not in m.ns]:
+ a = f'{mname}.{local}'
+ if a == q:
+ continue
+ c = self.canon(a)
+ if c and c != a and keep(c):
+ al.add((a, c))
+ return cb, fi, al, de, an
+
+
+def write(path, head, rows):
+ tmp = path + '.tmp'
+ with open(tmp, 'w', newline='', encoding='utf-8') as fh:
+ w = csv.writer(fh, delimiter='\t', lineterminator='\n')
+ w.writerow(head)
+ for r in sorted(rows, key=lambda x: tuple(str(c) for c in x)):
+ w.writerow(r)
+ os.replace(tmp, path)
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--src', required=True); ap.add_argument('--out', required=True)
+ ap.add_argument('--ir', default=None); ap.add_argument('--stats', action='store_true')
+ A = ap.parse_args()
+ os.makedirs(A.out, exist_ok=True)
+ heads = (('py-lib-callback.csv', ('callee', 'root', 'pos', 'path', 'member', 'via', 'type')),
+ ('py-lib-field-init.csv', ('class', 'field', 'param', 'pos')),
+ ('py-lib-alias.csv', ('alias', 'canonical')),
+ ('py-lib-decorates.csv', ('decorator', 'form', 'class', 'field')),
+ ('py-lib-ancestor.csv', ('class', 'ancestor')))
+ sites, stdlib = find_sites(os.path.abspath(A.src))
+ if not sites:
+ for f, h in heads:
+ write(os.path.join(A.out, f), h, [])
+ print('▶ library callbacks: no Python environment found in the repository (.venv, venv, $VIRTUAL_ENV, '
+ 'AXIOMCODE_PY_SITE); none summarised')
+ return
+ imports = client_imports(A.src, A.ir or A.out)
+ # CACHED BY WHAT DECIDES THE ANSWER: this file, the client's import paths, and the installed distributions
+ # (name and version from each *.dist-info) of every environment read. An edit that adds no import -- every
+ # refresh but a few -- reuses the tables instead of reading the library again.
+ h = hashlib.sha256()
+ h.update(open(os.path.abspath(__file__), 'rb').read())
+ h.update('\n'.join(sorted(imports)).encode())
+ for d in sites + ([stdlib] if stdlib else []):
+ h.update(d.encode())
+ try:
+ h.update('\n'.join(sorted(x for x in os.listdir(d) if x.endswith(('.dist-info', '.egg-info', '.pth')))).encode())
+ except OSError:
+ pass
+ cache = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.join(os.path.expanduser('~'), '.cache'),
+ 'axiomcode', 'libsum', h.hexdigest()[:32])
+ if all(os.path.isfile(os.path.join(cache, f)) for f, _ in heads):
+ import shutil
+ for f, _ in heads:
+ shutil.copyfile(os.path.join(cache, f), os.path.join(A.out, f))
+ try:
+ os.utime(cache)
+ print(open(os.path.join(cache, 'summary.txt'), encoding='utf-8').read().strip() + ' (cached)')
+ except OSError:
+ print('▶ library callbacks: reused the cached summaries')
+ return
+ roots = {p.split('.')[0] for p in imports}
+ prog, todo, seen, client_pkgs = Program(), sorted(roots), set(), set()
+ # the client's own package installed in editable mode is not a library: its top-level names in the source tree
+ for n in os.listdir(A.src):
+ if os.path.isfile(os.path.join(A.src, n, '__init__.py')) or n.endswith('.py'):
+ client_pkgs.add(n[:-3] if n.endswith('.py') else n)
+ for sub in ('src', 'lib'):
+ d = os.path.join(A.src, sub)
+ if os.path.isdir(d):
+ for n in os.listdir(d):
+ if os.path.isfile(os.path.join(d, n, '__init__.py')):
+ client_pkgs.add(n)
+ nfiles, loaded = 0, {}
+ while todo:
+ r = todo.pop()
+ if r in seen or r in client_pkgs:
+ continue
+ seen.add(r)
+ pkg = locate(r, sites, stdlib)
+ if not pkg:
+ continue
+ for modname, path in pkg.files():
+ m = Module(modname, path, path.endswith('__init__.py'))
+ if not m.load():
+ continue
+ nfiles += 1
+ loaded[m.name] = m
+ for imp in m.imports:
+ t = imp.split('.')[0]
+ if t and t not in seen:
+ todo.append(t)
+ # `from M import *` re-exports M's public names (its __all__ when it declares one), so a name the client imports
+ # through the star module (`collections.abc.MutableMapping`) is the declaration in M. Expanded before any body is
+ # analysed, to a fixpoint for a star of a star.
+ for _ in range(4):
+ grew = False
+ for m in loaded.values():
+ for b in m.stars:
+ bm = loaded.get(b)
+ if bm is None:
+ continue
+ names = bm.all_names if bm.all_names is not None else [n for n in bm.ns if not n.startswith('_')]
+ for n in names:
+ if n in bm.ns and n not in m.ns:
+ m.ns[n] = bm.ns[n]; grew = True
+ if not grew:
+ break
+ for m in loaded.values():
+ m.analyse()
+ del m.tree
+ prog.add(m)
+ rounds = prog.solve()
+ prog.returns()
+ lib_pkgs = {p for p in seen if p not in client_pkgs}
+ named = prog.named(imports, lib_pkgs)
+ keep = lambda q: q in named
+ cb, fi, al, de, an = prog.rows(keep)
+ for (f, h), rows in zip(heads, (cb, fi, al, de, an)):
+ write(os.path.join(A.out, f), h, rows)
+ line = (f'▶ library callbacks: {len(cb)} hand-backs, {len(fi)} constructor stores, {len(al)} re-exports, '
+ f'{len(de)} decorators, from {nfiles} library modules ({len(lib_pkgs)} packages, {rounds} rounds)')
+ print(line)
+ try:
+ import shutil
+ tmp = cache + f'.tmp{os.getpid()}'
+ os.makedirs(tmp, exist_ok=True)
+ for f, _ in heads:
+ shutil.copyfile(os.path.join(A.out, f), os.path.join(tmp, f))
+ open(os.path.join(tmp, 'summary.txt'), 'w', encoding='utf-8').write(line + '\n')
+ os.replace(tmp, cache)
+ root = os.path.dirname(cache) # keep the 64 most recently used
+ old = sorted((os.path.getmtime(os.path.join(root, x)), x) for x in os.listdir(root) if '.tmp' not in x)
+ for _, x in old[:-64]:
+ shutil.rmtree(os.path.join(root, x), ignore_errors=True)
+ except OSError:
+ pass
+
+
+if __name__ == '__main__':
+ main()
diff --git a/graph/python/souffle/decls_all.dl b/graph/python/souffle/decls_all.dl
index 9aa06420..08cc0035 100644
--- a/graph/python/souffle/decls_all.dl
+++ b/graph/python/souffle/decls_all.dl
@@ -52,6 +52,11 @@
.decl py_fixture_value_method(v:symbol)
.decl py_parametrize_deco(v:symbol)
.decl py_parametrize_argnames_kw(v:symbol)
+.decl py_parametrize_argvalues_kw(v:symbol)
+.decl py_fixture_params_kw(v:symbol)
+.decl py_param_row_wrapper(v:symbol)
+.decl py_fixture_request_param(v:symbol)
+.decl py_request_param_attr(v:symbol)
.decl py_parametrize_indirect_kw(v:symbol)
.decl py_parametrize_indirect_all(v:symbol)
.decl py_argnames_sep(v:symbol)
@@ -238,6 +243,7 @@
.decl element_lib_type_of(c0:symbol,c1:symbol)
.decl binding_element_lib_type(c0:symbol,c1:symbol)
.decl param_declared_type_by_parser(c0:symbol,c1:symbol,c2:symbol)
+.decl union_operand(c0:symbol,c1:symbol,c2:symbol)
.decl type_ref_element(c0:symbol,c1:symbol,c2:symbol)
.decl annotation_owner_module(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl annotation_container_kind(c0:symbol)
@@ -260,6 +266,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)
@@ -312,6 +319,9 @@
.decl py_path_join_function(c0:symbol)
.decl py_path_join_operator(c0:symbol)
.decl py_copy_function(c0:symbol)
+.decl py_returns_arg(c0:symbol,c1:symbol)
+.decl py_passthrough_arg(c0:symbol,c1:symbol)
+.decl py_passthrough_root(c0:symbol,c1:symbol)
.decl dict_lookup_method(c0:symbol)
.decl builtin_target(c0:symbol,c1:symbol)
.decl builtin_object_init_target(c0:symbol)
@@ -402,6 +412,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)
@@ -524,10 +535,14 @@
.decl field_holds_method(c0:symbol,c1:symbol,c2:symbol)
.decl type_attr_method_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl type_attr_class_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl dict_view_name(c0:symbol)
+.decl dict_items_view(c0:symbol)
.decl computed_name_prefix(c0:symbol,c1:symbol)
.decl expr_operator(c0:symbol,c1:symbol,c2:symbol)
.decl local_holds_untyped_return(c0:symbol,c1:symbol)
.decl getattr_computed_name(c0:symbol,c1:symbol,c2:symbol)
+.decl getattr_computed_type(c0:symbol,c1:symbol,c2:symbol)
+.decl getattr_computed_member(c0:symbol,c1:symbol)
.decl method_is_async(c0:symbol,c1:symbol)
.decl await_operand(c0:symbol,c1:symbol,c2:symbol)
.decl subscript_index_is_slice(c0:symbol,c1:symbol)
@@ -592,6 +607,8 @@
.decl decorator_hits_lib(c0:symbol,c1:symbol)
.decl decorator_hits_builtin(c0:symbol,c1:symbol)
.decl property_read_edge(c0:symbol,c1:symbol,c2:symbol)
+.decl property_write_edge(c0:symbol,c1:symbol,c2:symbol)
+.decl type_property_accessor(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol)
.decl with_protocol_edge(c0:symbol,c1:symbol,c2:symbol)
.decl iteration_protocol_sync(c0:symbol)
.decl iteration_protocol_of(c0:symbol,c1:symbol)
@@ -617,6 +634,8 @@
.decl protocol_edge_wide(c0:symbol,c1:symbol,c2:number)
.decl site_has_boundary(c0:symbol)
.decl site_class(c0:symbol,c1:symbol)
+.decl attr_access(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl site_class_kind(c0:symbol)
.decl site_reason(c0:symbol,c1:symbol)
.decl site_is_decorator(c0:symbol)
.decl call_chain_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol,c6:symbol)
@@ -658,3 +677,41 @@
.decl binding_declared_call_protocol(c0:symbol,c1:symbol,c2:symbol)
.decl binding_assigned_lambda(c0:symbol,c1:symbol,c2:symbol)
.decl instance_attr_method_value(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
+.decl dict_view_slot(c0:symbol,c1:symbol)
+.decl mapping_annotation_of(c0:symbol,c1:symbol)
+.decl type_ref_slot(c0:symbol,c1:symbol,c2:symbol)
+.decl dict_view_call(c0:symbol,c1:symbol,c2:symbol)
+.decl dict_item_slot_type(c0:symbol,c1:symbol,c2:symbol)
+.decl method_returns_computed_name(c0:symbol)
+.decl computed_name_value(c0:symbol)
+.decl call_computed_name_family(c0:symbol)
+.decl py_fixture_value_class(c0:symbol,c1:symbol)
+.decl builtin_protocol_slot(c0:symbol,c1:symbol)
+.decl builtin_protocol_fallback(c0:symbol,c1:symbol,c2:symbol)
+.decl builtin_protocol_arg(c0:symbol,c1:symbol,c2:symbol)
+.decl builtin_protocol_edge(c0:symbol,c1:symbol,c2:symbol)
+.decl operator_protocol_slot(c0:symbol,c1:symbol,c2:symbol)
+.decl operator_protocol_fallback(c0:symbol,c1:symbol)
+.decl membership_operator(c0:symbol)
+.decl membership_protocol_slot(c0:symbol)
+.decl membership_protocol_fallback(c0:symbol,c1:symbol)
+.decl unary_protocol_slot(c0:symbol,c1:symbol)
+.decl truth_negation_operator(c0:symbol)
+.decl truth_protocol_fallback(c0:symbol,c1:symbol)
+.decl truth_test_root(c0:symbol)
+.decl truth_test_role(c0:symbol)
+.decl operator_protocol_edge(c0:symbol,c1:symbol,c2:symbol)
+.decl operator_type_has(c0:symbol,c1:symbol)
+.decl membership_type_has(c0:symbol,c1:symbol)
+.decl truth_tested(c0:symbol)
+.decl truth_protocol_edge(c0:symbol,c1:symbol,c2:symbol)
+.decl truth_type_has(c0:symbol,c1:symbol)
+.decl type_has_member(c0:symbol,c1:symbol)
+// staged beside the client IR by graph/python/libsum/libsum.py (framework-behavior/library-callbacks.dl)
+.decl py_lib_callback(callee:symbol, root:symbol, pos:symbol, path:symbol, member:symbol, via:symbol, type:symbol)
+.decl py_lib_ancestor(cls:symbol, ancestor:symbol)
+.decl py_lib_field_init(cls:symbol, field:symbol, param:symbol, pos:symbol)
+.decl py_lib_alias(alias:symbol, canon:symbol)
+.decl lib_callback_edge(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol)
+.decl lib_callback_site(c0:symbol,c1:symbol)
+.decl py_lib_decorates(decorator:symbol, form:symbol, cls:symbol, field:symbol)
diff --git a/graph/python/souffle/export_manifest.tsv b/graph/python/souffle/export_manifest.tsv
index 758e6d9e..19aaa3d2 100644
--- a/graph/python/souffle/export_manifest.tsv
+++ b/graph/python/souffle/export_manifest.tsv
@@ -91,3 +91,5 @@ remote_unserved remote-unserved.csv
remote_unsent remote-unsent.csv
framework_edge framework-edge.csv
framework_unjoined framework-unjoined.csv
+lib_callback_edge lib-callback-edge.csv
+lib_callback_site lib-callback-site.csv
diff --git a/graph/python/templates/client-extra.map b/graph/python/templates/client-extra.map
new file mode 100644
index 00000000..18f5076f
--- /dev/null
+++ b/graph/python/templates/client-extra.map
@@ -0,0 +1,12 @@
+# Soufflé input-relation -> CSV basename for facts staged BESIDE the client IR that are not parser facts.
+# Same format and staging as client-ir.map (absent file = empty relation, so every project compiles one program),
+# but outside the frozen parser schema: no decls_base.dl row, no lib.map counterpart (a library's own summary of
+# its dependencies is not staged), declared in decls_all.dl instead.
+#
+# The library callback summaries graph/python/libsum/libsum.py writes from the project's installed dependencies
+# (framework-behavior/library-callbacks.dl). Absent = empty = no library hand-backs, exactly as before.
+py_lib_callback py-lib-callback
+py_lib_field_init py-lib-field-init
+py_lib_alias py-lib-alias
+py_lib_decorates py-lib-decorates
+py_lib_ancestor py-lib-ancestor
diff --git a/graph/test/csharp/README.md b/graph/test/csharp/README.md
index 530ecc59..1663ccb8 100644
--- a/graph/test/csharp/README.md
+++ b/graph/test/csharp/README.md
@@ -306,7 +306,7 @@ dotnet build -c Release graph/test/csharp/ground-truth/AxiomCsOracle
dotnet build -c Release graph/test/csharp/runtime-oracle/AxiomCsInstrument
```
-`node` must be 18 or newer, and the SAME version across a baseline and a fix run: a
+`node` must be 22.13 or newer (what the repository itself requires), and the SAME version across a baseline and a fix run: a
number measured on one version and compared against another is not a measurement.
Every script exits 77 when a tool it needs is missing, so a machine without the
toolchain skips rather than reporting a false failure.
diff --git a/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js b/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js
new file mode 100644
index 00000000..e6d3397f
--- /dev/null
+++ b/graph/test/javascript/cases/28-uninstalled-dependency/src/receivers.js
@@ -0,0 +1,27 @@
+'use strict';
+// A member call whose receiver is a value of a package no IR declares: what calling it,
+// constructing from it, or chaining on it returns is the package's object, so the
+// project's own `get` is not what these call (library_receiver in the diagnostics).
+const request = require('never-staged');
+const wire = require('alpha/missing');
+class Repo { get(id) { return id; } }
+class Service {
+ constructor(repo) { this.repo = repo; }
+ load(id) { return this.repo.get(id); }
+}
+async function probe(app) {
+ await request(app).get('/x').expect(200);
+ const res = await request(app).get('/y');
+ const md = new wire.Metadata();
+ md.get('k');
+ return res.body;
+}
+// controls: none of these is a package's value
+function untyped(obj) { return obj.get(1); }
+function wrapped(repo) { return request.mocked(new Repo()).get(1); }
+function reassigned() {
+ let c = request.agent();
+ c = untyped;
+ return c.get(2);
+}
+module.exports = { Repo, Service, probe, untyped, wrapped, reassigned };
diff --git a/graph/test/javascript/expected/08-library-dependency.diag b/graph/test/javascript/expected/08-library-dependency.diag
index aaefacd3..38b34560 100644
--- a/graph/test/javascript/expected/08-library-dependency.diag
+++ b/graph/test/javascript/expected/08-library-dependency.diag
@@ -1,6 +1,9 @@
import_cause app.js:2:13 dep not_staged
import_cause app.js:3:17 dep not_staged
import_cause app.js:3:9 dep not_staged
+library_receiver app.js:5:1 c.get dep
+library_receiver app.js:6:1 new Client().request dep
+library_receiver app.js:8:1 dep.helper dep
package_entry probe6-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved app.js:4:11 FUNCTION_CALL dep callee_untyped
unresolved app.js:5:1 METHOD_CALL get receiver_untyped
diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.diag b/graph/test/javascript/expected/28-uninstalled-dependency.diag
index 2a015d3b..b3376dc9 100644
--- a/graph/test/javascript/expected/28-uninstalled-dependency.diag
+++ b/graph/test/javascript/expected/28-uninstalled-dependency.diag
@@ -4,6 +4,21 @@ import_cause app.js:6:14 alpha/missing not_staged
import_cause app.js:7:14 never-staged not_staged
import_cause app.mjs:1:10 alpha not_staged
import_cause app.mjs:2:17 alpha not_staged
+import_cause receivers.js:5:17 never-staged not_staged
+import_cause receivers.js:6:14 alpha/missing not_staged
+library_receiver app.js:10:3 alpha.createClient alpha
+library_receiver app.js:10:3 alpha.createClient().run alpha
+library_receiver app.js:13:3 gone.anything alpha/missing
+library_receiver app.js:14:3 nope.thing never-staged
+library_receiver app.mjs:3:1 createClient().run alpha
+library_receiver app.mjs:4:1 m.createClient alpha
+library_receiver app.mjs:4:1 m.createClient().run alpha
+library_receiver receivers.js:13:9 request(app).get never-staged
+library_receiver receivers.js:13:9 request(app).get('/x').expect never-staged
+library_receiver receivers.js:14:21 request(app).get never-staged
+library_receiver receivers.js:16:3 md.get alpha/missing
+library_receiver receivers.js:21:33 request.mocked never-staged
+library_receiver receivers.js:23:11 request.agent never-staged
package_entry case26-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved app.js:10:3 METHOD_CALL createClient receiver_untyped
unresolved app.js:10:3 METHOD_CALL run receiver_untyped
@@ -16,3 +31,16 @@ unresolved app.mjs:3:1 FUNCTION_CALL createClient callee_untyped
unresolved app.mjs:3:1 METHOD_CALL run receiver_untyped
unresolved app.mjs:4:1 METHOD_CALL createClient receiver_untyped
unresolved app.mjs:4:1 METHOD_CALL run receiver_untyped
+unresolved receivers.js:10:21 METHOD_CALL get receiver_untyped
+unresolved receivers.js:13:9 FUNCTION_CALL request callee_untyped
+unresolved receivers.js:13:9 METHOD_CALL expect receiver_untyped
+unresolved receivers.js:13:9 METHOD_CALL get receiver_untyped
+unresolved receivers.js:14:21 FUNCTION_CALL request callee_untyped
+unresolved receivers.js:14:21 METHOD_CALL get receiver_untyped
+unresolved receivers.js:15:14 CONSTRUCTOR_CALL Metadata receiver_untyped
+unresolved receivers.js:16:3 METHOD_CALL get receiver_untyped
+unresolved receivers.js:20:32 METHOD_CALL get receiver_untyped
+unresolved receivers.js:21:33 METHOD_CALL get receiver_untyped
+unresolved receivers.js:21:33 METHOD_CALL mocked receiver_untyped
+unresolved receivers.js:23:11 METHOD_CALL agent receiver_untyped
+unresolved receivers.js:25:10 METHOD_CALL get member_absent
diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.edges b/graph/test/javascript/expected/28-uninstalled-dependency.edges
index cb072a1a..11563c90 100644
--- a/graph/test/javascript/expected/28-uninstalled-dependency.edges
+++ b/graph/test/javascript/expected/28-uninstalled-dependency.edges
@@ -11,3 +11,17 @@ app.mjs:3:1 FUNCTION_CALL createClient -> ambiguous_unknown -
app.mjs:3:1 METHOD_CALL createClient().run -> ambiguous_unknown -
app.mjs:4:1 METHOD_CALL m.createClient -> ambiguous_unknown -
app.mjs:4:1 METHOD_CALL m.createClient().run -> ambiguous_unknown -
+receivers.js:10:21 METHOD_CALL this.repo.get -> ambiguous_unknown -
+receivers.js:13:9 FUNCTION_CALL request -> ambiguous_unknown -
+receivers.js:13:9 METHOD_CALL request(app).get -> ambiguous_unknown -
+receivers.js:13:9 METHOD_CALL request(app).get('/x').expect -> ambiguous_unknown -
+receivers.js:14:21 FUNCTION_CALL request -> ambiguous_unknown -
+receivers.js:14:21 METHOD_CALL request(app).get -> ambiguous_unknown -
+receivers.js:15:14 CONSTRUCTOR_CALL wire.Metadata -> ambiguous_unknown -
+receivers.js:16:3 METHOD_CALL md.get -> ambiguous_unknown -
+receivers.js:20:32 METHOD_CALL obj.get -> ambiguous_unknown -
+receivers.js:21:33 METHOD_CALL request.mocked -> ambiguous_unknown -
+receivers.js:21:33 METHOD_CALL request.mocked(new Repo()).get -> ambiguous_unknown -
+receivers.js:21:48 CONSTRUCTOR_CALL Repo -> implicit_constructor -
+receivers.js:23:11 METHOD_CALL request.agent -> ambiguous_unknown -
+receivers.js:25:10 METHOD_CALL c.get -> ambiguous_unknown -
diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag b/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag
index 1e4b56db..cb5c2a22 100644
--- a/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag
+++ b/graph/test/javascript/expected/28-uninstalled-dependency.lib.diag
@@ -1,5 +1,15 @@
import_cause app.js:6:14 alpha/missing staged_unreached
import_cause app.js:7:14 never-staged not_staged
+import_cause receivers.js:5:17 never-staged not_staged
+import_cause receivers.js:6:14 alpha/missing staged_unreached
+library_receiver app.js:13:3 gone.anything alpha/missing
+library_receiver app.js:14:3 nope.thing never-staged
+library_receiver receivers.js:13:9 request(app).get never-staged
+library_receiver receivers.js:13:9 request(app).get('/x').expect never-staged
+library_receiver receivers.js:14:21 request(app).get never-staged
+library_receiver receivers.js:16:3 md.get alpha/missing
+library_receiver receivers.js:21:33 request.mocked never-staged
+library_receiver receivers.js:23:11 request.agent never-staged
package_entry alpha . [] MAIN lib/index.js RESOLVED -> lib:lib/index.js
package_entry alpha . [import] EXPORTS lib/index.mjs RESOLVED -> lib:lib/index.mjs
package_entry alpha . [require] EXPORTS lib/index.js RESOLVED -> lib:lib/index.js
@@ -9,3 +19,16 @@ package_entry case26-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved app.js:13:3 METHOD_CALL anything receiver_untyped
unresolved app.js:14:3 METHOD_CALL thing receiver_untyped
unresolved app.mjs:2:17 DYNAMIC_IMPORT_CALL dynamic
+unresolved receivers.js:10:21 METHOD_CALL get receiver_untyped
+unresolved receivers.js:13:9 FUNCTION_CALL request callee_untyped
+unresolved receivers.js:13:9 METHOD_CALL expect receiver_untyped
+unresolved receivers.js:13:9 METHOD_CALL get receiver_untyped
+unresolved receivers.js:14:21 FUNCTION_CALL request callee_untyped
+unresolved receivers.js:14:21 METHOD_CALL get receiver_untyped
+unresolved receivers.js:15:14 CONSTRUCTOR_CALL Metadata receiver_untyped
+unresolved receivers.js:16:3 METHOD_CALL get receiver_untyped
+unresolved receivers.js:20:32 METHOD_CALL get receiver_untyped
+unresolved receivers.js:21:33 METHOD_CALL get receiver_untyped
+unresolved receivers.js:21:33 METHOD_CALL mocked receiver_untyped
+unresolved receivers.js:23:11 METHOD_CALL agent receiver_untyped
+unresolved receivers.js:25:10 METHOD_CALL get member_absent
diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges b/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges
index 6dfe1d1b..952c1e2c 100644
--- a/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges
+++ b/graph/test/javascript/expected/28-uninstalled-dependency.lib.edges
@@ -11,3 +11,17 @@ app.mjs:3:1 FUNCTION_CALL createClient -> boundary_lib lib:lib/index.mjs:2:1
app.mjs:3:1 METHOD_CALL createClient().run -> boundary_lib lib:lib/index.mjs:1:21 run
app.mjs:4:1 METHOD_CALL m.createClient -> boundary_lib lib:lib/index.mjs:2:1 createClient
app.mjs:4:1 METHOD_CALL m.createClient().run -> boundary_lib lib:lib/index.mjs:1:21 run
+receivers.js:10:21 METHOD_CALL this.repo.get -> ambiguous_unknown -
+receivers.js:13:9 FUNCTION_CALL request -> ambiguous_unknown -
+receivers.js:13:9 METHOD_CALL request(app).get -> ambiguous_unknown -
+receivers.js:13:9 METHOD_CALL request(app).get('/x').expect -> ambiguous_unknown -
+receivers.js:14:21 FUNCTION_CALL request -> ambiguous_unknown -
+receivers.js:14:21 METHOD_CALL request(app).get -> ambiguous_unknown -
+receivers.js:15:14 CONSTRUCTOR_CALL wire.Metadata -> ambiguous_unknown -
+receivers.js:16:3 METHOD_CALL md.get -> ambiguous_unknown -
+receivers.js:20:32 METHOD_CALL obj.get -> ambiguous_unknown -
+receivers.js:21:33 METHOD_CALL request.mocked -> ambiguous_unknown -
+receivers.js:21:33 METHOD_CALL request.mocked(new Repo()).get -> ambiguous_unknown -
+receivers.js:21:48 CONSTRUCTOR_CALL Repo -> implicit_constructor -
+receivers.js:23:11 METHOD_CALL request.agent -> ambiguous_unknown -
+receivers.js:25:10 METHOD_CALL c.get -> ambiguous_unknown -
diff --git a/graph/test/javascript/expected/28-uninstalled-dependency.oracle b/graph/test/javascript/expected/28-uninstalled-dependency.oracle
index 6d77f120..d56dda00 100644
--- a/graph/test/javascript/expected/28-uninstalled-dependency.oracle
+++ b/graph/test/javascript/expected/28-uninstalled-dependency.oracle
@@ -1,3 +1,4 @@
app.js:11:3 CONSTRUCTOR_CALL Mine SYNTHESIZED_OK
app.js:16:1 FUNCTION_CALL main EXACT app.js:9:1
+receivers.js:21:48 CONSTRUCTOR_CALL Repo SYNTHESIZED_OK
# defects: 0
diff --git a/graph/test/javascript/expected/30-subpath-pattern-export.diag b/graph/test/javascript/expected/30-subpath-pattern-export.diag
index ce53dcce..9a41da01 100644
--- a/graph/test/javascript/expected/30-subpath-pattern-export.diag
+++ b/graph/test/javascript/expected/30-subpath-pattern-export.diag
@@ -6,6 +6,9 @@ import_cause app.js:7:18 kit/internal/secret not_staged
import_cause app.js:8:16 kit/feat/deep/none not_staged
import_cause app.mjs:1:10 kit/mjs/gamma not_staged
import_cause app.mjs:2:10 kit/feat/alpha not_staged
+library_receiver app.js:9:19 createApp().start kit
+library_receiver app.js:9:66 internal.secret kit/internal/secret
+library_receiver app.js:9:85 nested.x kit/feat/deep/none
package_entry case30-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved app.js:9:19 FUNCTION_CALL createApp callee_untyped
unresolved app.js:9:19 METHOD_CALL start receiver_untyped
diff --git a/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag b/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag
index 3f4e3746..4844004a 100644
--- a/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag
+++ b/graph/test/javascript/expected/30-subpath-pattern-export.lib.diag
@@ -1,5 +1,7 @@
import_cause app.js:7:18 kit/internal/secret staged_unreached
import_cause app.js:8:16 kit/feat/deep/none staged_unreached
+library_receiver app.js:9:66 internal.secret kit/internal/secret
+library_receiver app.js:9:85 nested.x kit/feat/deep/none
package_entry case30-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
package_entry kit . [default] EXPORTS src/index.js RESOLVED -> lib:src/index.js
package_entry kit . [node.require] EXPORTS src/index.js RESOLVED -> lib:src/index.js
diff --git a/graph/test/javascript/expected/34-options-object-callbacks.diag b/graph/test/javascript/expected/34-options-object-callbacks.diag
index 2260affa..1d54eea4 100644
--- a/graph/test/javascript/expected/34-options-object-callbacks.diag
+++ b/graph/test/javascript/expected/34-options-object-callbacks.diag
@@ -3,6 +3,8 @@ import_cause main.js:2:14 walker not_staged
import_cause main.js:37:136 events builtin
import_cause main.js:3:15 https builtin
import_cause main.js:4:9 stream builtin
+library_receiver main.js:43:25 walk.register walker
+library_receiver main.js:43:48 walk.register walker
package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved main.js:10:28 FUNCTION_CALL walk callee_untyped
unresolved main.js:11:89 FUNCTION_CALL walk callee_untyped
diff --git a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag
index ed5868cc..e30f6720 100644
--- a/graph/test/javascript/expected/34-options-object-callbacks.lib.diag
+++ b/graph/test/javascript/expected/34-options-object-callbacks.lib.diag
@@ -2,6 +2,8 @@ import_cause main.js:17:16 assert builtin
import_cause main.js:37:136 events builtin
import_cause main.js:3:15 https builtin
import_cause main.js:4:9 stream builtin
+library_receiver main.js:43:25 walk.register walker
+library_receiver main.js:43:48 walk.register walker
package_entry options-callbacks . [] DEFAULT_INDEX index.js MISSING_FILE -> -
package_entry walker . [] MAIN index.js RESOLVED -> lib:index.js
unresolved main.js:12:86 METHOD_CALL destroy no_target
diff --git a/graph/test/javascript/expected/48-umd-library.diag b/graph/test/javascript/expected/48-umd-library.diag
index 7f81443e..dd637fad 100644
--- a/graph/test/javascript/expected/48-umd-library.diag
+++ b/graph/test/javascript/expected/48-umd-library.diag
@@ -1,5 +1,7 @@
import_cause app.js:3:13 umdlib not_staged
import_cause app.js:4:9 umdlib not_staged
+library_receiver app.js:5:1 umd.format umdlib
+library_receiver app.js:7:1 new umd.Formatter().render umdlib
package_entry case48-app . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved app.js:5:1 METHOD_CALL format receiver_untyped
unresolved app.js:6:1 FUNCTION_CALL format callee_untyped
diff --git a/graph/test/javascript/expected/60-ts-emit-helpers.diag b/graph/test/javascript/expected/60-ts-emit-helpers.diag
index 9cb642a2..f4182214 100644
--- a/graph/test/javascript/expected/60-ts-emit-helpers.diag
+++ b/graph/test/javascript/expected/60-ts-emit-helpers.diag
@@ -1,4 +1,5 @@
import_cause main.js:5:9 tspkg not_staged
+library_receiver main.js:7:26 new Client().send tspkg
package_entry 60-ts-emit-helpers . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved main.js:7:26 CONSTRUCTOR_CALL Client callee_untyped
unresolved main.js:7:26 METHOD_CALL send receiver_untyped
diff --git a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag
index ce31a3fe..cc4ccc71 100644
--- a/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag
+++ b/graph/test/javascript/expected/63-jsx-wrapper-lazy.diag
@@ -6,6 +6,7 @@ import_cause app.jsx:3:10 thing-kit not_staged
import_cause defmemo.jsx:1:10 react not_staged
import_cause parts.jsx:1:10 react not_staged
import_cause parts.jsx:1:16 react not_staged
+library_receiver app.jsx:11:16 React.memo react
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved app.jsx:10:18 FUNCTION_CALL observer callee_untyped
unresolved app.jsx:11:16 METHOD_CALL memo receiver_untyped
diff --git a/graph/test/javascript/expected/64-memoize-wrapper-result.edges b/graph/test/javascript/expected/64-memoize-wrapper-result.edges
index c3d46e25..46a76a11 100644
--- a/graph/test/javascript/expected/64-memoize-wrapper-result.edges
+++ b/graph/test/javascript/expected/64-memoize-wrapper-result.edges
@@ -1,6 +1,8 @@
app.js:11:3 FUNCTION_CALL getAlpha -> known_edge memoize.js:5:10
+app.js:11:3 FUNCTION_CALL getAlpha -> multi_inferred app.js:7:26
app.js:11:3 METHOD_CALL getAlpha().getHooks -> known_edge alpha.js:2:3 getHooks
app.js:12:3 FUNCTION_CALL getBeta -> known_edge memoize.js:5:10
+app.js:12:3 FUNCTION_CALL getBeta -> multi_inferred app.js:8:25
app.js:12:3 METHOD_CALL getBeta().getHooks -> known_edge beta.js:2:3 getHooks
app.js:16:20 FUNCTION_CALL once -> callback_registered app.js:16:25
app.js:16:20 FUNCTION_CALL once -> known_edge once.js:2:1 once
@@ -15,12 +17,16 @@ app.js:19:19 FUNCTION_CALL lazy -> callback_registered app.js:19:24
app.js:19:19 FUNCTION_CALL lazy -> known_edge once.js:14:1 lazy
app.js:19:30 CONSTRUCTOR_CALL AlphaPlugin -> implicit_constructor -
app.js:22:3 FUNCTION_CALL firstAlpha -> known_edge once.js:5:10
+app.js:22:3 FUNCTION_CALL firstAlpha -> multi_inferred app.js:16:25
app.js:22:3 METHOD_CALL firstAlpha().run -> known_edge alpha.js:3:3 run
app.js:23:3 FUNCTION_CALL makeBeta -> known_edge once.js:15:10
+app.js:23:3 FUNCTION_CALL makeBeta -> multi_inferred app.js:17:23
app.js:23:3 METHOD_CALL makeBeta().run -> known_edge beta.js:3:3 run
app.js:24:3 FUNCTION_CALL firstBeta -> known_edge once.js:5:10
+app.js:24:3 FUNCTION_CALL firstBeta -> multi_inferred app.js:18:24
app.js:24:3 METHOD_CALL firstBeta().run -> known_edge beta.js:3:3 run
app.js:25:3 FUNCTION_CALL makeAlpha -> known_edge once.js:15:10
+app.js:25:3 FUNCTION_CALL makeAlpha -> multi_inferred app.js:19:24
app.js:25:3 METHOD_CALL makeAlpha().run -> known_edge alpha.js:3:3 run
app.js:29:18 FUNCTION_CALL constant -> known_edge once.js:18:1 constant
app.js:29:27 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor -
@@ -57,10 +63,14 @@ declared.js:27:16 FUNCTION_CALL pickShared -> callback_registered declared.js
declared.js:27:16 FUNCTION_CALL pickShared -> known_edge declared.js:20:1 pickShared
declared.js:27:33 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor -
declared.js:30:3 FUNCTION_CALL declAlpha -> known_edge declared.js:9:3 inner
+declared.js:30:3 FUNCTION_CALL declAlpha -> multi_inferred declared.js:22:28
declared.js:30:3 METHOD_CALL declAlpha().run -> known_edge alpha.js:3:3 run
declared.js:31:3 FUNCTION_CALL declBeta -> known_edge declared.js:9:3 inner
+declared.js:31:3 FUNCTION_CALL declBeta -> multi_inferred declared.js:23:27
declared.js:31:3 METHOD_CALL declBeta().run -> known_edge beta.js:3:3 run
declared.js:32:3 FUNCTION_CALL declOfGetter -> known_edge declared.js:9:3 inner
+declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred declared.js:24:26
+declared.js:32:3 FUNCTION_CALL declOfGetter -> multi_inferred memoize.js:5:10
declared.js:32:3 METHOD_CALL declOfGetter().run -> known_edge alpha.js:3:3 run
declared.js:33:3 FUNCTION_CALL tappedDecl -> known_edge declared.js:15:3 inner
declared.js:33:3 METHOD_CALL tappedDecl().markOnly -> known_edge declared.js:13:14 markOnly
@@ -202,24 +212,33 @@ shapes.js:45:27 FUNCTION_CALL memoize -> callback_registered shapes.js:45:35
shapes.js:45:27 FUNCTION_CALL memoize -> known_edge memoize.js:2:17
shapes.js:45:41 CONSTRUCTOR_CALL BetaPlugin -> implicit_constructor -
shapes.js:48:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10
+shapes.js:48:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28
shapes.js:48:3 METHOD_CALL getFb().run -> known_edge alpha.js:3:3 run
shapes.js:49:3 FUNCTION_CALL getFb -> known_edge shapes.js:15:10
+shapes.js:49:3 FUNCTION_CALL getFb -> multi_inferred shapes.js:36:28
shapes.js:49:3 METHOD_CALL getFb().fallbackOnly -> known_edge shapes.js:8:18 fallbackOnly
shapes.js:50:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10
+shapes.js:50:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22
shapes.js:50:3 METHOD_CALL getOr().run -> ambient_terminal -
shapes.js:50:3 METHOD_CALL getOr().run -> multi_inferred beta.js:3:3 run
shapes.js:51:3 FUNCTION_CALL getOr -> known_edge shapes.js:22:10
+shapes.js:51:3 FUNCTION_CALL getOr -> multi_inferred shapes.js:38:22
shapes.js:51:3 METHOD_CALL getOr().mockOnly -> ambient_terminal -
shapes.js:51:3 METHOD_CALL getOr().mockOnly -> multi_inferred shapes.js:9:14 mockOnly
shapes.js:52:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10
+shapes.js:52:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26
shapes.js:52:3 METHOD_CALL getD().run -> known_edge alpha.js:3:3 run
shapes.js:53:3 FUNCTION_CALL getD -> known_edge shapes.js:29:10
+shapes.js:53:3 FUNCTION_CALL getD -> multi_inferred shapes.js:39:26
shapes.js:53:3 METHOD_CALL getD().defaultsOnly -> known_edge shapes.js:25:20 defaultsOnly
shapes.js:54:3 FUNCTION_CALL getDefault -> known_edge shapes.js:33:44
shapes.js:54:3 METHOD_CALL getDefault().defaultOnly -> known_edge shapes.js:10:19 defaultOnly
shapes.js:55:3 FUNCTION_CALL getSpread -> known_edge shapes.js:33:44
shapes.js:55:3 METHOD_CALL getSpread().run -> ambiguous_unknown -
shapes.js:56:3 FUNCTION_CALL warm -> known_edge shapes.js:34:32
+shapes.js:56:3 FUNCTION_CALL warm -> multi_inferred shapes.js:43:20
shapes.js:56:3 METHOD_CALL warm().run -> known_edge alpha.js:3:3 run
shapes.js:57:3 FUNCTION_CALL getNested -> known_edge memoize.js:5:10
+shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred memoize.js:5:10
+shapes.js:57:3 FUNCTION_CALL getNested -> multi_inferred shapes.js:45:35
shapes.js:57:3 METHOD_CALL getNested().run -> known_edge beta.js:3:3 run
diff --git a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges
index d56c695d..8373c4e3 100644
--- a/graph/test/javascript/expected/65-wrapper-param-reassigned.edges
+++ b/graph/test/javascript/expected/65-wrapper-param-reassigned.edges
@@ -2,51 +2,66 @@ app.js:10:23 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:10:3 FUNCTION_CALL lazyOrDefault -> callback_registered app.js:10:17
app.js:10:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault
app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> known_edge wrap.js:19:10
+app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred app.js:10:17
+app.js:10:3 FUNCTION_CALL lazyOrDefault(() => new A()) -> multi_inferred wrap.js:18:15
app.js:10:3 METHOD_CALL lazyOrDefault(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly
app.js:11:3 FUNCTION_CALL lazyNullish -> known_edge wrap.js:22:1 lazyNullish
app.js:11:3 FUNCTION_CALL lazyNullish() -> known_edge wrap.js:24:10
+app.js:11:3 FUNCTION_CALL lazyNullish() -> multi_inferred wrap.js:23:10
app.js:11:3 METHOD_CALL lazyNullish()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly
app.js:15:15 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:15:3 FUNCTION_CALL plain -> callback_registered app.js:15:9
app.js:15:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain
app.js:15:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10
+app.js:15:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:15:9
app.js:15:3 METHOD_CALL plain(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly
app.js:16:15 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:16:3 FUNCTION_CALL plain -> callback_registered app.js:16:9
app.js:16:3 FUNCTION_CALL plain -> known_edge wrap.js:27:1 plain
app.js:16:3 FUNCTION_CALL plain(() => new A()) -> known_edge wrap.js:28:10
+app.js:16:3 FUNCTION_CALL plain(() => new A()) -> multi_inferred app.js:16:9
app.js:16:3 METHOD_CALL plain(() => new A())().replacedOnly -> ambiguous_unknown -
app.js:17:22 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:17:3 FUNCTION_CALL otherWritten -> callback_registered app.js:17:16
app.js:17:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten
app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10
+app.js:17:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:17:16
app.js:17:3 METHOD_CALL otherWritten(() => new A(), null)().aOnly -> known_edge wrap.js:2:11 aOnly
app.js:18:22 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:18:3 FUNCTION_CALL otherWritten -> callback_registered app.js:18:16
app.js:18:3 FUNCTION_CALL otherWritten -> known_edge wrap.js:31:1 otherWritten
app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> known_edge wrap.js:33:10
+app.js:18:3 FUNCTION_CALL otherWritten(() => new A(), null) -> multi_inferred app.js:18:16
app.js:18:3 METHOD_CALL otherWritten(() => new A(), null)().replacedOnly -> ambiguous_unknown -
app.js:4:14 FUNCTION_CALL swapped -> callback_registered app.js:4:22
app.js:4:14 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped
app.js:4:28 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:5:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10
+app.js:5:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8
app.js:5:3 METHOD_CALL sw().replacedOnly -> known_edge wrap.js:3:18 replacedOnly
app.js:6:3 FUNCTION_CALL sw -> known_edge wrap.js:9:10
+app.js:6:3 FUNCTION_CALL sw -> multi_inferred wrap.js:8:8
app.js:6:3 METHOD_CALL sw().aOnly -> ambiguous_unknown -
app.js:7:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard
app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> known_edge wrap.js:14:10
+app.js:7:3 FUNCTION_CALL lazyGuard(undefined) -> multi_inferred wrap.js:13:38
app.js:7:3 METHOD_CALL lazyGuard(undefined)().replacedOnly -> known_edge wrap.js:3:18 replacedOnly
app.js:8:19 CONSTRUCTOR_CALL A -> implicit_constructor -
app.js:8:3 FUNCTION_CALL lazyGuard -> callback_registered app.js:8:13
app.js:8:3 FUNCTION_CALL lazyGuard -> known_edge wrap.js:12:1 lazyGuard
app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> known_edge wrap.js:14:10
+app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred app.js:8:13
+app.js:8:3 FUNCTION_CALL lazyGuard(() => new A()) -> multi_inferred wrap.js:13:38
app.js:8:3 METHOD_CALL lazyGuard(() => new A())().aOnly -> known_edge wrap.js:2:11 aOnly
app.js:9:3 FUNCTION_CALL lazyOrDefault -> known_edge wrap.js:17:1 lazyOrDefault
app.js:9:3 FUNCTION_CALL lazyOrDefault() -> known_edge wrap.js:19:10
+app.js:9:3 FUNCTION_CALL lazyOrDefault() -> multi_inferred wrap.js:18:15
app.js:9:3 METHOD_CALL lazyOrDefault()().dfltOnly -> known_edge wrap.js:4:14 dfltOnly
many.js:11:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10
+many.js:11:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8
many.js:11:3 METHOD_CALL s21().replacedOnly -> known_edge wrap.js:3:18 replacedOnly
many.js:12:3 FUNCTION_CALL s21 -> known_edge wrap.js:9:10
+many.js:12:3 FUNCTION_CALL s21 -> multi_inferred wrap.js:8:8
many.js:12:3 METHOD_CALL s21().aOnly -> ambiguous_unknown -
many.js:3:13 FUNCTION_CALL swapped -> callback_registered many.js:3:21
many.js:3:13 FUNCTION_CALL swapped -> known_edge wrap.js:7:1 swapped
diff --git a/graph/test/javascript/expected/70-single-file-component-scripts.diag b/graph/test/javascript/expected/70-single-file-component-scripts.diag
index f01b3a10..55bf1e5c 100644
--- a/graph/test/javascript/expected/70-single-file-component-scripts.diag
+++ b/graph/test/javascript/expected/70-single-file-component-scripts.diag
@@ -1,6 +1,7 @@
import_cause components/List.svelte:2:12 svelte not_staged
import_cause components/Price.vue:4:10 vue not_staged
import_cause main.js:2:10 vue not_staged
+library_receiver main.js:5:1 createApp(Price).mount vue
package_entry single-file-component-scripts . [] DEFAULT_INDEX index.js MISSING_FILE -> -
unresolved components/Legacy.vue:7:26 METHOD_CALL $emit member_absent
unresolved components/List.svelte:5:3 FUNCTION_CALL onMount callee_untyped
diff --git a/graph/test/javascript/expected/74-orm-model-factory.diag b/graph/test/javascript/expected/74-orm-model-factory.diag
index f191f565..f55347e0 100644
--- a/graph/test/javascript/expected/74-orm-model-factory.diag
+++ b/graph/test/javascript/expected/74-orm-model-factory.diag
@@ -1,5 +1,9 @@
import_cause models.js:21:17 other-orm not_staged
import_cause models.js:7:18 mongoose not_staged
+library_receiver models.js:12:1 docSchema.static mongoose
+library_receiver models.js:13:1 docSchema.static mongoose
+library_receiver models.js:15:1 docSchema.method mongoose
+library_receiver models.js:28:10 mongoose.model mongoose
package_entry @axiomcode/code-graph . [] MAIN dist/reason.js NOT_STAGED -> -
unresolved models.js:10:19 CONSTRUCTOR_CALL Schema callee_untyped
unresolved models.js:11:51 METHOD_CALL find receiver_untyped
diff --git a/graph/test/javascript/tools/normalize_diagnostics.py b/graph/test/javascript/tools/normalize_diagnostics.py
index 242a8bfd..d7cdfdd3 100644
--- a/graph/test/javascript/tools/normalize_diagnostics.py
+++ b/graph/test/javascript/tools/normalize_diagnostics.py
@@ -89,6 +89,10 @@ def raw(path, width):
for f in raw(os.path.join(out, 'unresolved-value-callee.csv'), 2):
pos, _, text = site.get(f[0], ('?', '?', '?'))
lines.add('value_callee %s %s %s' % (pos, text, f[1]))
+# library_receiver(CallExpr, Specifier): the receiver is a package's value, so a same-named project method is not the callee
+for f in raw(os.path.join(out, 'library-receiver.csv'), 2):
+ pos, _, text = site.get(f[0], ('?', '?', '?'))
+ lines.add('library_receiver %s %s %s' % (pos, text, f[1]))
# package_entry(Prov, Package, Subpath, Condition, Source, Target, Outcome, ModuleHash)
for f in raw(os.path.join(out, 'package-entry.csv'), 8):
lines.add('package_entry %s %s [%s] %s %s %s -> %s' % (
diff --git a/graph/test/python/cases/43-module-singleton-import/src/api.py b/graph/test/python/cases/43-module-singleton-import/src/api.py
new file mode 100644
index 00000000..a169f808
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/api.py
@@ -0,0 +1,7 @@
+"""Imports the singleton whose name collides with its module's last segment."""
+
+from order_service import order_service
+
+
+def cancel_endpoint(oid):
+ return order_service.cancel(oid)
diff --git a/graph/test/python/cases/43-module-singleton-import/src/order_service.py b/graph/test/python/cases/43-module-singleton-import/src/order_service.py
new file mode 100644
index 00000000..a3ba1433
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/order_service.py
@@ -0,0 +1,20 @@
+"""A module-level singleton named after its own module — the ordinary idiom.
+
+`from order_service import order_service` must bind the VALUE, not suffix-match
+back to this module and report a module import (#1140).
+"""
+
+
+class OrderService:
+ def cancel(self, oid):
+ return oid
+
+ def refund(self, oid):
+ return oid
+
+
+order_service = OrderService()
+
+
+def same_module_caller(oid):
+ return order_service.cancel(oid)
diff --git a/graph/test/python/cases/43-module-singleton-import/src/pkg/__init__.py b/graph/test/python/cases/43-module-singleton-import/src/pkg/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py b/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py
new file mode 100644
index 00000000..a02ce3d5
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/pkg/tools.py
@@ -0,0 +1,5 @@
+"""Control: a GENUINE submodule import must keep resolving as a module."""
+
+
+def helper():
+ return 1
diff --git a/graph/test/python/cases/43-module-singleton-import/src/publisher.py b/graph/test/python/cases/43-module-singleton-import/src/publisher.py
new file mode 100644
index 00000000..4466e43e
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/publisher.py
@@ -0,0 +1,5 @@
+from signals import order_placed
+
+
+def publish(sender):
+ return order_placed.send(sender)
diff --git a/graph/test/python/cases/43-module-singleton-import/src/signals.py b/graph/test/python/cases/43-module-singleton-import/src/signals.py
new file mode 100644
index 00000000..b5bdb0e8
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/signals.py
@@ -0,0 +1,9 @@
+"""A module-level value whose name does NOT collide with the module name."""
+
+
+class Signal:
+ def send(self, sender):
+ return sender
+
+
+order_placed = Signal()
diff --git a/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py b/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py
new file mode 100644
index 00000000..027d3e32
--- /dev/null
+++ b/graph/test/python/cases/43-module-singleton-import/src/use_pkg.py
@@ -0,0 +1,5 @@
+from pkg import tools
+
+
+def run():
+ return tools.helper()
diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.edges b/graph/test/python/expected/21-url-and-signal-dispatch.edges
index 63aeda15..9d07c714 100644
--- a/graph/test/python/expected/21-url-and-signal-dispatch.edges
+++ b/graph/test/python/expected/21-url-and-signal-dispatch.edges
@@ -1,13 +1,13 @@
ambiguous_unknown DECORATOR_APPLICATION handlers. -> -
ambiguous_unknown DECORATOR_APPLICATION main. -> -
ambiguous_unknown METHOD_CALL main.not_a_signal_send -> -
-ambiguous_unknown METHOD_CALL ship.ship_order -> -
boundary_lib SIMPLE_CALL main. -> builtin:object.__init__
boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__
known_edge DECORATOR_CALL handlers. -> handlers.receiver
known_edge DECORATOR_CALL main. -> main.receiver
known_edge METHOD_CALL main. -> main.Signal.connect
known_edge METHOD_CALL main.place_order -> main.Signal.send
+known_edge METHOD_CALL ship.ship_order -> signals.Signal.send
known_edge SIMPLE_CALL handlers.on_never_shipped -> handlers._record
known_edge SIMPLE_CALL handlers.on_shipped -> handlers._record
known_edge SIMPLE_CALL main. -> main.path
diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.tiers b/graph/test/python/expected/21-url-and-signal-dispatch.tiers
index dff4f56f..56e70052 100644
--- a/graph/test/python/expected/21-url-and-signal-dispatch.tiers
+++ b/graph/test/python/expected/21-url-and-signal-dispatch.tiers
@@ -1,9 +1,9 @@
distinct call sites emitted: 32
--- by tier: edge ROWS, and the distinct SITES they cover ---
- 6 rows 6 sites ambiguous_unknown
+ 5 rows 5 sites ambiguous_unknown
4 rows 4 sites boundary_lib
- 22 rows 22 sites known_edge
+ 23 rows 23 sites known_edge
--- edge rows by call kind ---
4 DECORATOR_APPLICATION
@@ -13,14 +13,13 @@ distinct call sites emitted: 32
--- unresolved reasons ---
4 decorator_factory_result_untyped
- 1 untyped_receiver:local_untyped
1 untyped_receiver:parameter
--- the engine's own conservation ledger ---
32 _total_sites
- 6 ambiguous_unknown
+ 5 ambiguous_unknown
4 boundary_lib
- 22 known_edge
+ 23 known_edge
--- reconciling rows against the conserved site count ---
edge rows 32
diff --git a/graph/test/python/expected/33-url-view-forms.edges b/graph/test/python/expected/33-url-view-forms.edges
index f5bb9f3f..c0500600 100644
--- a/graph/test/python/expected/33-url-view-forms.edges
+++ b/graph/test/python/expected/33-url-view-forms.edges
@@ -1,9 +1,11 @@
ambiguous_unknown DECORATOR_APPLICATION shop.decorators.audited -> -
ambiguous_unknown DECORATOR_BARE shop.views. -> -
ambiguous_unknown METHOD_CALL shop.urls. -> -
-ambiguous_unknown SIMPLE_CALL catalog.urls. -> -
-ambiguous_unknown SIMPLE_CALL shop.urls. -> -
boundary_lib DECORATOR_CALL shop.decorators.audited -> external:functools.wraps
+boundary_lib SIMPLE_CALL catalog.urls. -> external:path
+boundary_lib SIMPLE_CALL shop.urls. -> external:include
+boundary_lib SIMPLE_CALL shop.urls. -> external:path
+boundary_lib SIMPLE_CALL shop.urls. -> external:re_path
known_edge DECORATOR_BARE shop.views. -> shop.decorators.audited
known_edge SIMPLE_CALL shop.decorators.audited..wrapper -> shop.views.order_history
known_edge SIMPLE_CALL shop.views.BaseWidgetView.get -> shop.views._render
diff --git a/graph/test/python/expected/33-url-view-forms.tiers b/graph/test/python/expected/33-url-view-forms.tiers
index 2713fd47..f05de8d3 100644
--- a/graph/test/python/expected/33-url-view-forms.tiers
+++ b/graph/test/python/expected/33-url-view-forms.tiers
@@ -1,8 +1,8 @@
distinct call sites emitted: 51
--- by tier: edge ROWS, and the distinct SITES they cover ---
- 32 rows 32 sites ambiguous_unknown
- 1 rows 1 sites boundary_lib
+ 6 rows 6 sites ambiguous_unknown
+ 27 rows 27 sites boundary_lib
18 rows 18 sites known_edge
--- edge rows by call kind ---
@@ -14,13 +14,13 @@ distinct call sites emitted: 51
--- unresolved reasons ---
1 decorator_factory_result_untyped
- 27 no_rule
+ 1 no_rule
4 unmodelled_decorator
--- the engine's own conservation ledger ---
51 _total_sites
- 32 ambiguous_unknown
- 1 boundary_lib
+ 6 ambiguous_unknown
+ 27 boundary_lib
18 known_edge
--- reconciling rows against the conserved site count ---
diff --git a/graph/test/python/expected/37-model-signals.edges b/graph/test/python/expected/37-model-signals.edges
index a50f46f3..67531829 100644
--- a/graph/test/python/expected/37-model-signals.edges
+++ b/graph/test/python/expected/37-model-signals.edges
@@ -1,6 +1,4 @@
ambiguous_unknown DECORATOR_APPLICATION models. -> -
-ambiguous_unknown METHOD_CALL models.Invoice. -> -
-ambiguous_unknown METHOD_CALL models.Order. -> -
ambiguous_unknown METHOD_CALL views.create_order -> -
ambiguous_unknown METHOD_CALL views.drop_invoice -> -
ambiguous_unknown METHOD_CALL views.drop_order -> -
@@ -9,4 +7,6 @@ ambiguous_unknown METHOD_CALL views.save_draft -> -
ambiguous_unknown METHOD_CALL views.touch_invoice -> -
boundary_lib DECORATOR_CALL models. -> external:receiver
boundary_lib METHOD_CALL models. -> external:post_save.connect
+boundary_lib METHOD_CALL models.Invoice. -> external:models.IntegerField
+boundary_lib METHOD_CALL models.Order. -> external:models.CharField
known_edge METHOD_CALL views.save_cache -> views.Cache.save
diff --git a/graph/test/python/expected/37-model-signals.tiers b/graph/test/python/expected/37-model-signals.tiers
index a7e08a5c..e87fd3fa 100644
--- a/graph/test/python/expected/37-model-signals.tiers
+++ b/graph/test/python/expected/37-model-signals.tiers
@@ -1,8 +1,8 @@
distinct call sites emitted: 14
--- by tier: edge ROWS, and the distinct SITES they cover ---
- 10 rows 10 sites ambiguous_unknown
- 3 rows 3 sites boundary_lib
+ 8 rows 8 sites ambiguous_unknown
+ 5 rows 5 sites boundary_lib
1 rows 1 sites known_edge
--- edge rows by call kind ---
@@ -14,13 +14,12 @@ distinct call sites emitted: 14
2 decorator_factory_result_untyped
4 escape_hatch
1 untyped_receiver:class_attribute_absent
- 2 untyped_receiver:local_untyped
1 untyped_receiver:parameter
--- the engine's own conservation ledger ---
14 _total_sites
- 10 ambiguous_unknown
- 3 boundary_lib
+ 8 ambiguous_unknown
+ 5 boundary_lib
1 known_edge
--- reconciling rows against the conserved site count ---
diff --git a/graph/test/python/expected/38-app-wiring.edges b/graph/test/python/expected/38-app-wiring.edges
index 89dad32f..39d93d28 100644
--- a/graph/test/python/expected/38-app-wiring.edges
+++ b/graph/test/python/expected/38-app-wiring.edges
@@ -3,8 +3,6 @@ ambiguous_unknown DECORATOR_ATTRIBUTE main. -> -
ambiguous_unknown DECORATOR_BARE main. -> -
ambiguous_unknown SIMPLE_CALL main.Timing.dispatch -> -
ambiguous_unknown SIMPLE_CALL main.add_header -> -
-ambiguous_unknown SIMPLE_CALL shop.jobs.nightly -> -
-ambiguous_unknown SIMPLE_CALL shop.jobs.unknown -> -
boundary_lib DECORATOR_CALL main. -> external:APIRouter.api_route
boundary_lib DECORATOR_CALL main. -> external:FastAPI.exception_handler
boundary_lib DECORATOR_CALL main. -> external:FastAPI.middleware
@@ -17,6 +15,8 @@ boundary_lib SIMPLE_CALL main. -> external:FastAPI
boundary_lib SIMPLE_CALL main. -> external:Route
boundary_lib SIMPLE_CALL main. -> external:Starlette
boundary_lib SIMPLE_CALL main. -> external:WebSocketRoute
+boundary_lib SIMPLE_CALL shop.jobs.nightly -> external:call_command
+boundary_lib SIMPLE_CALL shop.jobs.unknown -> external:call_command
known_edge METHOD_CALL main. -> main.Registry.add_route
known_edge SIMPLE_CALL main. -> main.configure
known_edge SIMPLE_CALL shop.management.commands.close_orders.Command.handle -> shop.orders.close_all
diff --git a/graph/test/python/expected/38-app-wiring.tiers b/graph/test/python/expected/38-app-wiring.tiers
index 4df06d8a..8496a4b9 100644
--- a/graph/test/python/expected/38-app-wiring.tiers
+++ b/graph/test/python/expected/38-app-wiring.tiers
@@ -1,8 +1,8 @@
distinct call sites emitted: 26
--- by tier: edge ROWS, and the distinct SITES they cover ---
- 10 rows 10 sites ambiguous_unknown
- 13 rows 13 sites boundary_lib
+ 8 rows 8 sites ambiguous_unknown
+ 15 rows 15 sites boundary_lib
3 rows 3 sites known_edge
--- edge rows by call kind ---
@@ -16,13 +16,12 @@ distinct call sites emitted: 26
--- unresolved reasons ---
2 callee_is_parameter
4 decorator_factory_result_untyped
- 2 no_rule
2 unmodelled_decorator
--- the engine's own conservation ledger ---
26 _total_sites
- 10 ambiguous_unknown
- 13 boundary_lib
+ 8 ambiguous_unknown
+ 15 boundary_lib
3 known_edge
--- reconciling rows against the conserved site count ---
diff --git a/graph/test/python/expected/42-signal-forms.edges b/graph/test/python/expected/42-signal-forms.edges
index c42f783d..456f0118 100644
--- a/graph/test/python/expected/42-signal-forms.edges
+++ b/graph/test/python/expected/42-signal-forms.edges
@@ -1,11 +1,11 @@
ambiguous_unknown DECORATOR_APPLICATION handlers. -> -
ambiguous_unknown METHOD_CALL handlers. -> -
ambiguous_unknown METHOD_CALL orders.close -> -
-ambiguous_unknown METHOD_CALL orders.pay -> -
-ambiguous_unknown METHOD_CALL orders.place -> -
ambiguous_unknown METHOD_CALL orders.ship -> -
-ambiguous_unknown METHOD_CALL orders.void -> -
boundary_lib DECORATOR_CALL handlers. -> external:receiver
boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__
known_edge DECORATOR_CALL handlers. -> handlers.remember
known_edge METHOD_CALL orders.not_a_signal -> orders.Outbox.asend
+known_edge METHOD_CALL orders.pay -> signals.Signal.asend
+known_edge METHOD_CALL orders.place -> signals.Signal.send
+known_edge METHOD_CALL orders.void -> signals.Signal.send
diff --git a/graph/test/python/expected/42-signal-forms.tiers b/graph/test/python/expected/42-signal-forms.tiers
index e28d1d9d..e780b18a 100644
--- a/graph/test/python/expected/42-signal-forms.tiers
+++ b/graph/test/python/expected/42-signal-forms.tiers
@@ -1,9 +1,9 @@
distinct call sites emitted: 22
--- by tier: edge ROWS, and the distinct SITES they cover ---
- 11 rows 11 sites ambiguous_unknown
+ 8 rows 8 sites ambiguous_unknown
9 rows 9 sites boundary_lib
- 2 rows 2 sites known_edge
+ 5 rows 5 sites known_edge
--- edge rows by call kind ---
5 DECORATOR_APPLICATION
@@ -14,13 +14,12 @@ distinct call sites emitted: 22
--- unresolved reasons ---
5 decorator_factory_result_untyped
3 untyped_receiver:attribute_object_untyped
- 3 untyped_receiver:local_untyped
--- the engine's own conservation ledger ---
22 _total_sites
- 11 ambiguous_unknown
+ 8 ambiguous_unknown
9 boundary_lib
- 2 known_edge
+ 5 known_edge
--- reconciling rows against the conserved site count ---
edge rows 22
diff --git a/graph/test/python/expected/43-module-singleton-import.edges b/graph/test/python/expected/43-module-singleton-import.edges
new file mode 100644
index 00000000..c35a14c7
--- /dev/null
+++ b/graph/test/python/expected/43-module-singleton-import.edges
@@ -0,0 +1,6 @@
+boundary_lib SIMPLE_CALL order_service. -> builtin:object.__init__
+boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__
+known_edge METHOD_CALL api.cancel_endpoint -> order_service.OrderService.cancel
+known_edge METHOD_CALL order_service.same_module_caller -> order_service.OrderService.cancel
+known_edge METHOD_CALL publisher.publish -> signals.Signal.send
+known_edge METHOD_CALL use_pkg.run -> pkg.tools.helper
diff --git a/graph/test/python/expected/43-module-singleton-import.entries b/graph/test/python/expected/43-module-singleton-import.entries
new file mode 100644
index 00000000..276e39fa
--- /dev/null
+++ b/graph/test/python/expected/43-module-singleton-import.entries
@@ -0,0 +1 @@
+── entry_point (0) ──
diff --git a/graph/test/python/expected/43-module-singleton-import.framework b/graph/test/python/expected/43-module-singleton-import.framework
new file mode 100644
index 00000000..29577b75
--- /dev/null
+++ b/graph/test/python/expected/43-module-singleton-import.framework
@@ -0,0 +1,6 @@
+── framework_edge (0) ──
+── framework_unjoined (1) ──
+ 1 signal_dispatch no_receiver
+── remote_edge (0) ──
+── remote_unserved (0) ──
+── remote_unsent (0) ──
diff --git a/graph/test/python/expected/43-module-singleton-import.tiers b/graph/test/python/expected/43-module-singleton-import.tiers
new file mode 100644
index 00000000..3f658c74
--- /dev/null
+++ b/graph/test/python/expected/43-module-singleton-import.tiers
@@ -0,0 +1,23 @@
+distinct call sites emitted: 6
+
+--- by tier: edge ROWS, and the distinct SITES they cover ---
+ 2 rows 2 sites boundary_lib
+ 4 rows 4 sites known_edge
+
+--- edge rows by call kind ---
+ 4 METHOD_CALL
+ 2 SIMPLE_CALL
+
+--- unresolved reasons ---
+ (none — every site resolved)
+
+--- the engine's own conservation ledger ---
+ 6 _total_sites
+ 2 boundary_lib
+ 4 known_edge
+
+--- reconciling rows against the conserved site count ---
+ edge rows 6
+ minus extra rows from multi-target sites 0
+ = tier/site pairs 6
+ engine's conserved site total 6
diff --git a/graph/test/python/tools/literal_gate.py b/graph/test/python/tools/literal_gate.py
index db44b130..0e7af6f7 100644
--- a/graph/test/python/tools/literal_gate.py
+++ b/graph/test/python/tools/literal_gate.py
@@ -83,7 +83,12 @@
# unresolved_value_callee's second column is `binding`: WHAT the callee of an
# unresolved call is ("parameter", "loop_variable", "field", ...), the same words
# the JavaScript rules write. Written into the output and never joined on.
- "unresolved_value_callee(")
+ "unresolved_value_callee(",
+ # lib_callback_edge's How column (receiver / receiver_field / argument / argument_field) says
+ # where on the call the handed-back object arrived. Written into the output by
+ # framework-behavior/library-callbacks.dl and never joined on; every name that DOES decide a
+ # library hand-back comes from the tables graph/python/libsum derives from the library's source.
+ "lib_callback_edge(", "lcb_hit(")
GROUND_FACT = re.compile(r'^[a-z_]+\((?:\s*"[^"]*"\s*,?)+\)\.\s*$')
diff --git a/graph/test/python/tools/protocol-fan-test.sh b/graph/test/python/tools/protocol-fan-test.sh
index 009607d6..b6c9b283 100755
--- a/graph/test/python/tools/protocol-fan-test.sh
+++ b/graph/test/python/tools/protocol-fan-test.sh
@@ -64,7 +64,7 @@ PYEOF
node "$PARSER" "$W/src" protofan false "$W/ir" > "$W/parse.log" 2>&1 || {
echo " FAIL protocol-fan: the parser failed on the fixture"; sed 's/^/ /' "$W/parse.log" | tail -3
echo "protocol-fan: FAILED (1 check)"; exit 1; }
-bash "$ROOT/graph/pipeline/run-souffle.sh" --debug --language python --client-ir "$W/ir" \
+bash "$ROOT/graph/pipeline/run-souffle.sh" --debug --language python --dispatch-cap 20 --client-ir "$W/ir" \
--library "$EMPTY_LIB" --intermediate "$W/int" --output "$W/out" > "$W/solve.log" 2>&1 || {
echo " FAIL protocol-fan: the solve failed"; tail -3 "$W/solve.log" | sed 's/^/ /'
echo "protocol-fan: FAILED (1 check)"; exit 1; }
diff --git a/graph/test/python/torture/client/f43_def_rebind.py b/graph/test/python/torture/client/f43_def_rebind.py
index 107989af..3a454e7e 100644
--- a/graph/test/python/torture/client/f43_def_rebind.py
+++ b/graph/test/python/torture/client/f43_def_rebind.py
@@ -89,10 +89,10 @@ def call_branched() -> str:
def read_property() -> int:
"""The getter survives — the control that keeps this rule off property pairs.
- EXPECT: miss — `h.value = 5` invokes the SETTER, and the engine emits a
- PROPERTY_READ edge for a property read and nothing at all for a property WRITE.
- That is a pre-existing gap this fixture happens to expose, not something #383
- changed; the getter edge on the next line is what this family is asserting.
+ `h.value = 5` invokes the SETTER, and the engine now emits a PROPERTY_WRITE
+ edge for it beside the PROPERTY_READ for the read; both halves of the pair
+ are live, which is exactly what this control exists to keep true. The getter
+ edge on the next line is what this family is asserting.
"""
h = Holder()
h.value = 5
diff --git a/graph/test/python/torture/expected/coverage.txt b/graph/test/python/torture/expected/coverage.txt
index 06b88e32..daf7e130 100644
--- a/graph/test/python/torture/expected/coverage.txt
+++ b/graph/test/python/torture/expected/coverage.txt
@@ -1,4 +1,4 @@
-=== per-family coverage (tier-4, 471 scored sites) ===
+=== per-family coverage (tier-4, 474 scored sites) ===
f01 inheritance & MRO links= 10 found= 10 (100.0%) missed= 0 wide= 0 WRONG= 0
f02 callables & closures links= 10 found= 9 (90.0%) missed= 1 wide= 0 WRONG= 0
f03 generics links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0
@@ -38,16 +38,16 @@
f40 data descriptor links= 14 found= 14 (100.0%) missed= 0 wide= 0 WRONG= 0
f41 class attribute absent links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0
f42 f42 links= 6 found= 6 (100.0%) missed= 0 wide= 0 WRONG= 0
- f43 f43 links= 7 found= 7 (100.0%) missed= 0 wide= 1 WRONG= 0
+ f43 f43 links= 10 found= 10 (100.0%) missed= 0 wide= 1 WRONG= 0
nestmod.py nestmod.py links= 1 found= 1 (100.0%) missed= 0 wide= 0 WRONG= 0
pkgmod relative imports (subpackage) links= 3 found= 3 (100.0%) missed= 0 wide= 0 WRONG= 0
- TOTAL links=434 found=428 (98.6%) missed= 6 wide=37 WRONG= 0
- recall 428/434 = 98.6% of the links that actually ran
+ TOTAL links=437 found=431 (98.6%) missed= 6 wide=37 WRONG= 0
+ recall 431/437 = 98.6% of the links that actually ran
wide 37 a member of a SOUND SET that did not run on this pass
WRONG 0 a single target asserted as certain that never ran
- EXPECTED-MISS cases: {'FOUND': 20, 'MISSED': 24} (a CONCRETE here means a known blind spot closed)
+ EXPECTED-MISS cases: {'FOUND': 18, 'MISSED': 23} (a CONCRETE here means a known blind spot closed)
--- non-concrete sites ---
f02_callables.py:31 MISSED true=[('lib', 'callables.py', 22)] engine=[]
diff --git a/graph/test/python/torture/expected/torture.edges b/graph/test/python/torture/expected/torture.edges
index 4d85cc1a..557d3208 100644
--- a/graph/test/python/torture/expected/torture.edges
+++ b/graph/test/python/torture/expected/torture.edges
@@ -635,6 +635,7 @@ known_edge PROPERTY_READ f40_data_descriptor.via_data_descriptor -> f40_data_des
known_edge PROPERTY_READ f40_data_descriptor.via_delete_descriptor -> f40_data_descriptor.Deletable.__get__
known_edge PROPERTY_READ f40_data_descriptor.via_property -> f40_data_descriptor.Prop.slot
known_edge PROPERTY_READ f43_def_rebind.read_property -> f43_def_rebind.Holder.value
+known_edge PROPERTY_WRITE f43_def_rebind.read_property -> f43_def_rebind.Holder.value
known_edge SELF_CALL f02_callables.HoldsCallables.run -> f02_callables.LocalCallable.__call__
known_edge SELF_CALL f28_await.Registry.into_a_field -> f28_await.Registry.build
known_edge SELF_CALL f28_await.Registry.via_self_receiver -> f28_await.Registry.build
diff --git a/graph/test/python/torture/expected/torture.oracle b/graph/test/python/torture/expected/torture.oracle
index 95fd8d90..4267008a 100644
--- a/graph/test/python/torture/expected/torture.oracle
+++ b/graph/test/python/torture/expected/torture.oracle
@@ -1 +1 @@
-oracle=634 engine=624 agree=587 missing=47 extra=37
+oracle=634 engine=625 agree=588 missing=46 extra=37
diff --git a/graph/test/tools/engine-package-test.sh b/graph/test/tools/engine-package-test.sh
index eec6de84..eaa49139 100755
--- a/graph/test/tools/engine-package-test.sh
+++ b/graph/test/tools/engine-package-test.sh
@@ -11,6 +11,11 @@
# ─────────────────────────────────────────────────────────────────────────────
set -u
ROOT="$(d="$(cd "$(dirname "$0")" && pwd)"; while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="$(dirname "$d")"; done; echo "$d")" # the repository root, found by its marker
+# This test counts compiles and asserts cache-entry names, and the parallel flavor is a per-machine
+# question answered by probing c++ — under the stub c++ the probe and a later real-c++ run can answer
+# differently, so prepare and the run after it would disagree on the -par cache name. The flavor is
+# pinned serial: what is under test is packaging and caching, not the solve.
+export AXIOM_SOLVE_PARALLEL=0
fail=0; bad(){ echo " ✗ $*"; fail=$((fail+1)); }
[ -x "$ROOT/node_modules/.bin/tsx" ] || { echo "engine-package: SKIP (no node_modules/.bin/tsx — run npm install)"; exit 0; }
W="$(mktemp -d)"; SHADOW=""; trap 'rm -rf "$W" ${SHADOW:+"$SHADOW"}' EXIT
@@ -68,7 +73,64 @@ fake="$W/fake-engine"
{ echo '#!/usr/bin/env bash'
echo 'while [ $# -gt 0 ]; do case "$1" in -D) D="$2"; shift 2;; -F) shift 2;; *) shift;; esac; done'
cut -f2 "$ROOT/graph/$lang/souffle/export_manifest.tsv" | sed 's|^|: > "$D/|; s|$|"|'; } > "$fake"
-mkdir -p "$W/stub" "$W/inc/souffle"; : > "$W/inc/souffle/CompiledSouffle.h"; : > "$W/cc-calls"
+mkdir -p "$W/stub" "$W/inc/souffle/utility" "$W/inc/souffle/datastructure"; : > "$W/inc/souffle/CompiledSouffle.h"; : > "$W/cc-calls"
+# the stub include dir must carry what souffle_overlay patches (the seqlock fix seds these
+# write-entry RMWs, the publication fix anchors on BTree.h's two link stores, and the overlay
+# refuses an include dir without either), as a real install's headers do
+printf '%s\n' 'version.fetch_or(0x1, std::memory_order_acquire);' \
+ 'version.fetch_or(0x1, std::memory_order_acquire);' \
+ 'version.fetch_or(0x1, std::memory_order_acquire);' > "$W/inc/souffle/utility/ParallelUtil.h"
+cat > "$W/inc/souffle/datastructure/BTree.h" <<'BTREE_STUB'
+ // move child pointers
+ if (this->inner) {
+ // move pointers to sibling
+ auto* other = static_cast(sibling);
+ for (unsigned i = split_point + 1, j = 0; i <= maxKeys; ++i, ++j) {
+ other->children[j] = getChildren()[i];
+ other->children[j]->parent = other;
+ other->children[j]->position = static_cast(j);
+ }
+ }
+
+ // update number of elements
+ this->numElements = split_point;
+ sibling->numElements = maxKeys - split_point - 1;
+ // link this and the sibling node to new root
+ this->parent = new_root;
+ // switch root node
+ *root = new_root;
+ keys[pos] = key;
+ getChildren()[pos + 1] = newNode;
+ newNode->parent = this;
+ newNode->position = static_cast(pos) + 1;
+ // get next pointer
+ auto next = cur->getChild(idx);
+
+ // get lease on next level
+ auto next_lease = next->lock.start_read();
+BTREE_STUB
+cat > "$W/inc/souffle/datastructure/BTreeDelete.h" <<'BTDEL_STUB'
+ // get next pointer
+ auto next = cur->getChild(idx);
+
+ // get lease on next level
+ auto next_lease = next->lock.start_read();
+BTDEL_STUB
+mkdir -p "$W/inc/souffle/utility"
+printf '%s\n' '#if _WIN64' '#define __builtin_popcountll __popcnt64' '#else' '#define __builtin_popcountll __popcnt' '#endif' >> "$W/inc/souffle/utility/MiscUtil.h"
+printf '%s\n' '#ifdef _WIN32' '#include ' > "$W/inc/souffle/datastructure/PiggyList.h"
+cat > "$W/inc/souffle/datastructure/ConcurrentFlyweight.h" <<'FLY_STUB'
+ const Key& fetch(const lane_id H, const index_type Idx) const {
+ const auto Lane = Lanes.guard(H);
+ assert(Idx < SlotCount.load(std::memory_order_relaxed));
+ return Slots[Idx]->first;
+ }
+FLY_STUB
+cat > "$W/inc/souffle/datastructure/RecordTableImpl.h" <<'REC_STUB'
+ const RamDomain* unpack(RamDomain Index) const override {
+ return fetch(Index).data();
+ }
+REC_STUB
{ echo '#!/usr/bin/env bash'
echo "[ \"\$1\" = --version ] && { echo 'Version: $SOUFFLE_VERSION'; exit 0; }"
echo 'while [ $# -gt 0 ]; do case "$1" in -g) : > "$2"; echo "// c++" > "$2"; shift 2;; *) shift;; esac; done'; } > "$W/stub/souffle"
@@ -83,6 +145,15 @@ if prep; then
[ -x "$W/cache/souffle-engine-$lang-$id" ] || bad "prepare left no binary under the run's cache name (souffle-engine-$lang-${id:0:12}…)"
[ "$(calls)" = 1 ] || bad "prepare compiled $(calls) time(s), expected 1"
grep -q "engine ready" "$W/log" || bad "prepare did not report the engine ready"
+ h="$W/cache/include-seqlock-fix-4/souffle/utility/ParallelUtil.h"
+ grep -q 'memory_order_seq_cst' "$h" 2>/dev/null && ! grep -q 'fetch_or(0x1, std::memory_order_acquire)' "$h" \
+ || bad "prepare did not leave the patched overlay header (seqlock fix) in the cache"
+ b="$W/cache/include-seqlock-fix-4/souffle/datastructure/BTree.h"
+ [ "$(grep -c 'seqlock-fix-3' "$b" 2>/dev/null)" = "4" ] \
+ || bad "prepare did not leave the four publication fences (seqlock-fix-3) in the overlay BTree.h"
+ f="$W/cache/include-seqlock-fix-4/souffle/datastructure/ConcurrentFlyweight.h"
+ [ "$(grep -c 'seqlock-fix-4' "$f" 2>/dev/null)" = "1" ] \
+ || bad "prepare did not leave the speculative-fetch guard (seqlock-fix-4) in the overlay flyweight"
else bad "prepare failed:"; tail -8 "$W/log" | sed 's/^/ /'; fi
prep || bad "a second prepare failed"
grep -q "reusing cached binary" "$W/log" && [ "$(calls)" = 1 ] || bad "a second prepare compiled again ($(calls) compiles)"
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts
new file mode 100644
index 00000000..f78a28dc
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/cfg.ts
@@ -0,0 +1,21 @@
+// A module-scope const read through member access, as a call argument, in a template and
+// under a type query: every use of CFG / S / TOKENS below binds to THIS file's declaration.
+export const CFG = { name: 'a', port: 1 } as const;
+export const S = mk({});
+export const TOKENS = { Store: 'store' } as const;
+export let counter = 0;
+// a const that holds a function is a function: its uses are calls, not reads (control)
+export const make = (n: number) => n + 1;
+
+export function mk(o: object): object {
+ return o;
+}
+
+export function localUse(): string {
+ return CFG.name;
+}
+
+export function bump(): void {
+ counter += 1;
+ counter = 0;
+}
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts
new file mode 100644
index 00000000..348509d6
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/src/index.ts
@@ -0,0 +1 @@
+export * from './cfg';
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts b/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts
new file mode 100644
index 00000000..97b17b45
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/a/test/t.ts
@@ -0,0 +1,47 @@
+import { CFG, S, TOKENS, make } from '../src';
+
+function use(x: unknown): void {}
+
+export function viaMember(): void {
+ use(CFG.name);
+}
+
+export function asArgument(): void {
+ use(S);
+}
+
+export function inTemplate(): string {
+ return `${CFG.name}-${TOKENS.Store}`;
+}
+
+export function callsTheFunction(): number {
+ return make(1);
+}
+
+export function shadowed(): string {
+ // a local of the same name: the read is the local's, not the module's (control)
+ const CFG = { name: 'local' };
+ return CFG.name;
+}
+
+// a read in a type: at module level it is the module's, in a signature the function's
+type T = typeof S;
+
+export function annotated(x: typeof CFG): string {
+ return x.name;
+}
+
+// a parameter of the same name: `typeof TOKENS` here is the parameter's type (control)
+export function shadowedInType(TOKENS: number): typeof TOKENS {
+ return TOKENS;
+}
+
+function Route(x: unknown): MethodDecorator { return () => {}; }
+function Body(x: unknown): ParameterDecorator { return () => {}; }
+class Pipe { constructor(readonly s: unknown) {} }
+
+export class Handler {
+ // a read in a decorator is the decorated method's: on the method, and on its parameter
+ @Route(TOKENS.Store)
+ handle(@Body(new Pipe(S)) body: unknown): void {}
+}
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts b/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts
new file mode 100644
index 00000000..74ad08dd
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/b/src/settings.ts
@@ -0,0 +1,3 @@
+// The same names, declared again in a sibling package: reads here bind here (control).
+export const CFG = { name: 'b', port: 2 } as const;
+export const S = {};
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts b/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts
new file mode 100644
index 00000000..90a49c65
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/b/test/sibling.ts
@@ -0,0 +1,5 @@
+import { CFG, S } from '../src/settings';
+
+export function siblingRead(): unknown {
+ return [CFG.port, S];
+}
diff --git a/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json b/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json
new file mode 100644
index 00000000..65ed3564
--- /dev/null
+++ b/graph/test/typescript/cases/86-module-variable-reads/src/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "experimentalDecorators": true,
+ "noEmit": true
+ }
+}
diff --git a/graph/test/typescript/expected/17-constrained-generics.fields b/graph/test/typescript/expected/17-constrained-generics.fields
index 51e56615..f0dbf2a4 100644
--- a/graph/test/typescript/expected/17-constrained-generics.fields
+++ b/graph/test/typescript/expected/17-constrained-generics.fields
@@ -1,3 +1,7 @@
ambiguous_unknown read consumer#ownConstraint(T) -> -
ambiguous_unknown read consumer#ownConstraintNamed(T) -> -
ambiguous_unknown read curried-local#widest(unknown) -> -
+known_edge read consumer#dataFirst() -> consumer#rows
+known_edge read consumer#localDataFirst() -> consumer#rows
+known_edge read consumer#localWidest() -> consumer#rows
+known_edge read consumer#widestFirst() -> consumer#rows
diff --git a/graph/test/typescript/expected/17-constrained-generics.fields-oracle b/graph/test/typescript/expected/17-constrained-generics.fields-oracle
index 320aafb9..d6d71ba2 100644
--- a/graph/test/typescript/expected/17-constrained-generics.fields-oracle
+++ b/graph/test/typescript/expected/17-constrained-generics.fields-oracle
@@ -1,7 +1,7 @@
17-constrained-generics [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 3 resolved 0 (0.0%)
- tiers ambiguous_unknown=3
- access read=3
- not scored: 3 rows whose target is not a client declaration
+ sites 7 resolved 4 (57.1%)
+ tiers ambiguous_unknown=3 known_edge=4
+ access read=7
+ not scored: 7 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/36-function-value-containers.fields b/graph/test/typescript/expected/36-function-value-containers.fields
index e69de29b..741ac6a1 100644
--- a/graph/test/typescript/expected/36-function-value-containers.fields
+++ b/graph/test/typescript/expected/36-function-value-containers.fields
@@ -0,0 +1 @@
+known_edge read r229#callObjectLiteral(number) -> r229#handlers
diff --git a/graph/test/typescript/expected/36-function-value-containers.fields-oracle b/graph/test/typescript/expected/36-function-value-containers.fields-oracle
index 6fb4ee24..152dfcc5 100644
--- a/graph/test/typescript/expected/36-function-value-containers.fields-oracle
+++ b/graph/test/typescript/expected/36-function-value-containers.fields-oracle
@@ -1,9 +1,9 @@
36-function-value-containers [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 2 resolved 2 (100.0%)
+ tiers known_edge=2
+ access read=2
+ not scored: 2 rows whose target is not a client declaration
MISSING r229#callClassMembers(number) READ Holder#viaField
MISSING r229#callObjectLiteral(number) READ r229#viaArrow
diff --git a/graph/test/typescript/expected/49-indexed-access-return.fields b/graph/test/typescript/expected/49-indexed-access-return.fields
index 64f7525f..3f375c0b 100644
--- a/graph/test/typescript/expected/49-indexed-access-return.fields
+++ b/graph/test/typescript/expected/49-indexed-access-return.fields
@@ -1,2 +1,5 @@
known_edge read main#getDog() -> Zoo#dog
+known_edge read main#getDog() -> main#zoo
known_edge read main#getDogPlain() -> Zoo#dog
+known_edge read main#getDogPlain() -> main#zoo
+known_edge read main#pick(T) -> main#zoo
diff --git a/graph/test/typescript/expected/49-indexed-access-return.fields-oracle b/graph/test/typescript/expected/49-indexed-access-return.fields-oracle
index 2ff6d466..198a9f5b 100644
--- a/graph/test/typescript/expected/49-indexed-access-return.fields-oracle
+++ b/graph/test/typescript/expected/49-indexed-access-return.fields-oracle
@@ -1,7 +1,7 @@
49-indexed-access-return [fields]
precision 1.0000 (2 correct, 0 wrong)
recall 1.0000 (2 of 2 the compiler resolved)
- sites 2 resolved 2 (100.0%)
- tiers known_edge=2
- access read=2
- not scored: 0 rows whose target is not a client declaration
+ sites 5 resolved 5 (100.0%)
+ tiers known_edge=5
+ access read=5
+ not scored: 3 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/62-field-access-and-type-use.fields b/graph/test/typescript/expected/62-field-access-and-type-use.fields
index 32b614fb..5659500c 100644
--- a/graph/test/typescript/expected/62-field-access-and-type-use.fields
+++ b/graph/test/typescript/expected/62-field-access-and-type-use.fields
@@ -8,6 +8,7 @@ known_edge read Box#run() -> Box#label
known_edge read Box#staticRead() -> Box#static total
known_edge read Box#viaInterface(Shaped) -> Shaped#width
known_edge read Narrow#own() -> Narrow#width
+known_edge read members#Shaped2() -> members#Marker2
known_edge readwrite Box#compound(number) -> Box#width
known_edge readwrite Box#decrement() -> Box#hidden
known_edge readwrite Box#increment() -> Box#hidden
diff --git a/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle b/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle
index 5964a26b..e23a07d7 100644
--- a/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle
+++ b/graph/test/typescript/expected/62-field-access-and-type-use.fields-oracle
@@ -1,7 +1,7 @@
62-field-access-and-type-use [fields]
precision 1.0000 (20 correct, 0 wrong)
recall 1.0000 (20 of 20 the compiler resolved)
- sites 19 resolved 18 (94.7%)
- tiers ambiguous_unknown=1 known_edge=18
- access read=11 readwrite=3 write=5
- not scored: 1 rows whose target is not a client declaration
+ sites 20 resolved 19 (95.0%)
+ tiers ambiguous_unknown=1 known_edge=19
+ access read=12 readwrite=3 write=5
+ not scored: 2 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/63-annotated-callable-member.fields b/graph/test/typescript/expected/63-annotated-callable-member.fields
index e69de29b..fe88d80b 100644
--- a/graph/test/typescript/expected/63-annotated-callable-member.fields
+++ b/graph/test/typescript/expected/63-annotated-callable-member.fields
@@ -0,0 +1 @@
+known_edge read members#callVarAmbient() -> members#varAmbient
diff --git a/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle b/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle
index 8e35ea27..146bd47f 100644
--- a/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle
+++ b/graph/test/typescript/expected/63-annotated-callable-member.fields-oracle
@@ -1,9 +1,9 @@
63-annotated-callable-member [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 1 resolved 1 (100.0%)
+ tiers known_edge=1
+ access read=1
+ not scored: 1 rows whose target is not a client declaration
MISSING members#callPropInline(PropInline) READ PropInline#run
MISSING members#callPropNamed(PropNamed) READ PropNamed#run
diff --git a/graph/test/typescript/expected/64-array-dispatch.fields b/graph/test/typescript/expected/64-array-dispatch.fields
index b1252eaf..12ff9d9f 100644
--- a/graph/test/typescript/expected/64-array-dispatch.fields
+++ b/graph/test/typescript/expected/64-array-dispatch.fields
@@ -1 +1,4 @@
+known_edge read pipeline#runByName(Node) -> pipeline#byName
+known_edge read pipeline#runIndexed(Node) -> pipeline#steps
+known_edge read pipeline#runIterated(Node) -> pipeline#steps
known_edge read steps#measure(Node) -> { width: number }#width
diff --git a/graph/test/typescript/expected/64-array-dispatch.fields-oracle b/graph/test/typescript/expected/64-array-dispatch.fields-oracle
index cedeaacf..86bafa38 100644
--- a/graph/test/typescript/expected/64-array-dispatch.fields-oracle
+++ b/graph/test/typescript/expected/64-array-dispatch.fields-oracle
@@ -1,10 +1,10 @@
64-array-dispatch [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
- sites 1 resolved 1 (100.0%)
- tiers known_edge=1
- access read=1
- not scored: 0 rows whose target is not a client declaration
+ sites 4 resolved 4 (100.0%)
+ tiers known_edge=4
+ access read=4
+ not scored: 3 rows whose target is not a client declaration
WRONG steps#measure(Node) READ { width: number }#width
MISSING pipeline#runByName(Node) READ pipeline#square
MISSING steps#measure(Node) READ steps#width
diff --git a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields
index e69de29b..5310006c 100644
--- a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields
+++ b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields
@@ -0,0 +1,3 @@
+known_edge read fewer#callAmbient() -> fewer#ambient
+known_edge read fewer#callSiblingsOne() -> fewer#siblings
+known_edge read fewer#callSiblingsTwo() -> fewer#siblings
diff --git a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle
index 0d95ec28..a5e21475 100644
--- a/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle
+++ b/graph/test/typescript/expected/65-annotated-callable-fewer-parameters.fields-oracle
@@ -1,8 +1,8 @@
65-annotated-callable-fewer-parameters [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 3 resolved 3 (100.0%)
+ tiers known_edge=3
+ access read=3
+ not scored: 3 rows whose target is not a client declaration
MISSING fewer#callHolderRun(Holder) READ Holder#run
diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.fields b/graph/test/typescript/expected/69-named-and-inline-handlers.fields
index e69de29b..ff8c71ee 100644
--- a/graph/test/typescript/expected/69-named-and-inline-handlers.fields
+++ b/graph/test/typescript/expected/69-named-and-inline-handlers.fields
@@ -0,0 +1,3 @@
+known_edge read routes#() -> routes#app
+known_edge read routes#() -> routes#cache
+known_edge read routes#() -> routes#headers
diff --git a/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle b/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle
index ba7a4326..f63426fd 100644
--- a/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle
+++ b/graph/test/typescript/expected/69-named-and-inline-handlers.fields-oracle
@@ -1,7 +1,7 @@
69-named-and-inline-handlers [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 4 resolved 4 (100.0%)
+ tiers known_edge=4
+ access read=4
+ not scored: 4 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields
index 97e25270..c12de5c7 100644
--- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields
+++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields
@@ -1 +1,2 @@
known_edge read fetcher#get(string,Options) -> Options#fetch
+known_edge read pool#main() -> pool#fixed
diff --git a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle
index 91b3cf30..7a227b92 100644
--- a/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle
+++ b/graph/test/typescript/expected/72-object-literal-and-expression-callees.fields-oracle
@@ -1,8 +1,8 @@
72-object-literal-and-expression-callees [fields]
precision 1.0000 (1 correct, 0 wrong)
recall 0.5000 (1 of 2 the compiler resolved)
- sites 1 resolved 1 (100.0%)
- tiers known_edge=1
- access read=1
- not scored: 0 rows whose target is not a client declaration
+ sites 2 resolved 2 (100.0%)
+ tiers known_edge=2
+ access read=2
+ not scored: 1 rows whose target is not a client declaration
MISSING registry#price(number) READ Registry#static format
diff --git a/graph/test/typescript/expected/74-jsx-component-forms.edges b/graph/test/typescript/expected/74-jsx-component-forms.edges
index 8a9a7700..1215ed2b 100644
--- a/graph/test/typescript/expected/74-jsx-component-forms.edges
+++ b/graph/test/typescript/expected/74-jsx-component-forms.edges
@@ -10,6 +10,7 @@ callback_registered FUNCTION_CALL app#() @L4 -> app#()
callback_registered FUNCTION_CALL parts#() @L33 -> parts#UserCard({ id: string })
callback_registered FUNCTION_CALL parts#() @L41 -> parts#FieldImpl({ label: string },unknown)
callback_registered FUNCTION_CALL parts#() @L73 -> parts#buildDefault()
+callback_registered JSX_COMPONENT_CALL app#App() @L13 -> Panel#render()
intrinsic_terminal DYNAMIC_IMPORT_CALL app#() @L4 -> -
known_edge FUNCTION_CALL app#useFactory() @L23 -> parts#()@L22
known_edge FUNCTION_CALL parts#FieldImpl({ label: string },unknown) @L39 -> parts#trackClick(string)
diff --git a/graph/test/typescript/expected/74-jsx-component-forms.fields b/graph/test/typescript/expected/74-jsx-component-forms.fields
index 1aa6da98..6dcde21d 100644
--- a/graph/test/typescript/expected/74-jsx-component-forms.fields
+++ b/graph/test/typescript/expected/74-jsx-component-forms.fields
@@ -1,6 +1,11 @@
ambiguous_unknown read parts#Badge(?) -> -
known_edge read Panel#render() -> Panel#props
known_edge read Panel#render() -> { title: string }#title
+known_edge read app#App() -> app#Settings
+known_edge read app#App() -> parts#Field
+known_edge read app#App() -> parts#UserCard
+known_edge read app#App() -> parts#ui
+known_edge read app#useFactory() -> parts#factory
known_edge read parts#Avatar({ src: string }) -> { src: string }#src
known_edge read parts#Chip({ text: string }) -> { text: string }#text
known_edge read parts#FieldImpl({ label: string },unknown) -> { label: string }#label
diff --git a/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle b/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle
index da9a8ee2..e2cf577f 100644
--- a/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle
+++ b/graph/test/typescript/expected/74-jsx-component-forms.fields-oracle
@@ -1,10 +1,10 @@
74-jsx-component-forms [fields]
precision 0.2857 (2 correct, 5 wrong)
recall 0.2500 (2 of 8 the compiler resolved)
- sites 8 resolved 7 (87.5%)
- tiers ambiguous_unknown=1 known_edge=7
- access read=7 write=1
- not scored: 1 rows whose target is not a client declaration
+ sites 13 resolved 12 (92.3%)
+ tiers ambiguous_unknown=1 known_edge=12
+ access read=12 write=1
+ not scored: 6 rows whose target is not a client declaration
WRONG Panel#render() READ { title: string }#title
WRONG parts#Avatar({ src: string }) READ { src: string }#src
WRONG parts#Chip({ text: string }) READ { text: string }#text
diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields
index 3da15a75..82a233a4 100644
--- a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields
+++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields
@@ -2,4 +2,14 @@ ambiguous_unknown read app#(?) -> -
ambiguous_unknown read app#(?) -> -
ambiguous_unknown read app#(?) -> -
ambiguous_unknown read app#() -> -
+known_edge read app#Page() -> app#DynAwait
+known_edge read app#Page() -> app#DynDefault
+known_edge read app#Page() -> app#DynNamed
+known_edge read app#Page() -> app#LazyBlock
+known_edge read app#Page() -> app#LazyDefault
+known_edge read app#Page() -> app#LazyNamed
+known_edge read app#Page() -> app#Made
+known_edge read app#Page() -> app#Memoed
+known_edge read app#Page() -> app#Observed
+known_edge read app#Page() -> app#Thing
known_edge read ui/Button#Button({ label: string }) -> { label: string }#label
diff --git a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle
index 3617bfdb..aaddbd64 100644
--- a/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle
+++ b/graph/test/typescript/expected/75-jsx-wrapper-guards.fields-oracle
@@ -1,9 +1,9 @@
75-jsx-wrapper-guards [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
- sites 5 resolved 1 (20.0%)
- tiers ambiguous_unknown=4 known_edge=1
- access read=5
- not scored: 4 rows whose target is not a client declaration
+ sites 15 resolved 11 (73.3%)
+ tiers ambiguous_unknown=4 known_edge=11
+ access read=15
+ not scored: 14 rows whose target is not a client declaration
WRONG ui/Button#Button({ label: string }) READ { label: string }#label
MISSING ui/Button#Button({ label: string }) READ ui/Button#label
diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields
index ce8e4343..da638d2c 100644
--- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields
+++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields
@@ -1,2 +1,6 @@
ambiguous_unknown read app#(?) -> -
ambiguous_unknown read app#useName() -> -
+known_edge read app#App() -> app#LazyDefault
+known_edge read app#App() -> app#LazyNamedObj
+known_edge read app#App() -> app#MemoPicked
+known_edge read app#App() -> parts#MemoFwd
diff --git a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle
index c4ca47b6..371b7582 100644
--- a/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle
+++ b/graph/test/typescript/expected/76-jsx-default-nested-wrappers.fields-oracle
@@ -1,7 +1,7 @@
76-jsx-default-nested-wrappers [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 2 resolved 0 (0.0%)
- tiers ambiguous_unknown=2
- access read=2
- not scored: 2 rows whose target is not a client declaration
+ sites 6 resolved 4 (66.7%)
+ tiers ambiguous_unknown=2 known_edge=4
+ access read=6
+ not scored: 6 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/77-package-published-entries.fields b/graph/test/typescript/expected/77-package-published-entries.fields
index 0233e076..ccc83f66 100644
--- a/graph/test/typescript/expected/77-package-published-entries.fields
+++ b/graph/test/typescript/expected/77-package-published-entries.fields
@@ -1,2 +1,3 @@
ambiguous_unknown read index#useStore(number) -> -
ambiguous_unknown read plugins/logger#() -> -
+known_edge read vanilla#() -> vanilla#DEFAULT_INITIAL
diff --git a/graph/test/typescript/expected/77-package-published-entries.fields-oracle b/graph/test/typescript/expected/77-package-published-entries.fields-oracle
index 85d59b99..72c9bcde 100644
--- a/graph/test/typescript/expected/77-package-published-entries.fields-oracle
+++ b/graph/test/typescript/expected/77-package-published-entries.fields-oracle
@@ -1,8 +1,8 @@
77-package-published-entries [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
- sites 2 resolved 0 (0.0%)
- tiers ambiguous_unknown=2
- access read=2
- not scored: 2 rows whose target is not a client declaration
+ sites 3 resolved 1 (33.3%)
+ tiers ambiguous_unknown=2 known_edge=1
+ access read=3
+ not scored: 3 rows whose target is not a client declaration
MISSING index#useStore(number) READ vanilla#value
diff --git a/graph/test/typescript/expected/78-callable-collections.fields b/graph/test/typescript/expected/78-callable-collections.fields
index b5e742ff..a43f3a76 100644
--- a/graph/test/typescript/expected/78-callable-collections.fields
+++ b/graph/test/typescript/expected/78-callable-collections.fields
@@ -1,2 +1,17 @@
ambiguous_unknown read pipeline#countIdle() -> -
+known_edge read pipeline#() -> pipeline#registry
+known_edge read pipeline#() -> pipeline#steps
+known_edge read pipeline#countIdle() -> pipeline#idle
+known_edge read pipeline#keysOnly() -> pipeline#table
+known_edge read pipeline#runByKey(Node,'grow' | 'shrink') -> pipeline#table
+known_edge read pipeline#runByName(Node) -> pipeline#table
+known_edge read pipeline#runForEach(Node) -> pipeline#steps
+known_edge read pipeline#runFrozen(Node) -> pipeline#frozen
+known_edge read pipeline#runImported(Node) -> steps#exportedSteps
+known_edge read pipeline#runIndexed(Node,number) -> pipeline#steps
+known_edge read pipeline#runIterated(Node) -> pipeline#steps
+known_edge read pipeline#runReduce(Node) -> pipeline#frozen
+known_edge read pipeline#runRegistry(Node) -> pipeline#extended
+known_edge read pipeline#runRegistry(Node) -> pipeline#registry
+known_edge read pipeline#runValues(Node) -> pipeline#table
known_edge read steps#measure(Node) -> { width: number }#width
diff --git a/graph/test/typescript/expected/78-callable-collections.fields-oracle b/graph/test/typescript/expected/78-callable-collections.fields-oracle
index 8fc5cf9a..0fe6772f 100644
--- a/graph/test/typescript/expected/78-callable-collections.fields-oracle
+++ b/graph/test/typescript/expected/78-callable-collections.fields-oracle
@@ -1,10 +1,10 @@
78-callable-collections [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 2 the compiler resolved)
- sites 2 resolved 1 (50.0%)
- tiers ambiguous_unknown=1 known_edge=1
- access read=2
- not scored: 1 rows whose target is not a client declaration
+ sites 18 resolved 17 (94.4%)
+ tiers ambiguous_unknown=1 known_edge=17
+ access read=18
+ not scored: 17 rows whose target is not a client declaration
WRONG steps#measure(Node) READ { width: number }#width
MISSING pipeline#runByName(Node) READ pipeline#shrink
MISSING steps#measure(Node) READ steps#width
diff --git a/graph/test/typescript/expected/78-cross-process-destinations.fields b/graph/test/typescript/expected/78-cross-process-destinations.fields
index 4fea0459..1d6b842c 100644
--- a/graph/test/typescript/expected/78-cross-process-destinations.fields
+++ b/graph/test/typescript/expected/78-cross-process-destinations.fields
@@ -1 +1,14 @@
-ambiguous_unknown read client/client#dailyReport(string) -> -
+known_edge read client/axios#() -> axios.d#axios
+known_edge read client/client#() -> axios.d#axios
+known_edge read client/client#audit() -> axios.d#axios
+known_edge read client/client#cached() -> client#cache
+known_edge read client/client#dailyReport(string) -> axios.d#axios
+known_edge read client/client#dailyReport(string) -> config#config
+known_edge read client/client#dailyReport(string) -> shared/config#reportsUrl
+known_edge read client/client#invoice(string) -> client#billing
+known_edge read client/client#listAll() -> paths#ORDERS
+known_edge read client/client#placeOrder(unknown) -> axios.d#axios
+known_edge read client/client#placeOrder(unknown) -> paths#ORDERS
+known_edge read server/orders#() -> orders#app
+known_edge read server/orders#() -> orders#router
+known_edge read server/orders#() -> paths#ORDERS
diff --git a/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle b/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle
index bd6298b8..2244ae08 100644
--- a/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle
+++ b/graph/test/typescript/expected/78-cross-process-destinations.fields-oracle
@@ -1,8 +1,7 @@
78-cross-process-destinations [fields]
- precision 0.0000 (0 correct, 0 wrong)
- recall 0.0000 (0 of 1 the compiler resolved)
- sites 1 resolved 0 (0.0%)
- tiers ambiguous_unknown=1
- access read=1
- not scored: 1 rows whose target is not a client declaration
- MISSING client/client#dailyReport(string) READ shared/config#reportsUrl
+ precision 1.0000 (1 correct, 0 wrong)
+ recall 1.0000 (1 of 1 the compiler resolved)
+ sites 18 resolved 18 (100.0%)
+ tiers known_edge=18
+ access read=18
+ not scored: 17 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields
index e69de29b..ca48bcea 100644
--- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields
+++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields
@@ -0,0 +1,12 @@
+known_edge read app#(?) -> rate#RATE
+known_edge read app#() -> rate#RATE
+known_edge read app#(?) -> rate#RATE
+known_edge read app#boot() -> app#host
+known_edge read app#boot() -> plugin#plugin
+known_edge read app#bootBare() -> app#host
+known_edge read app#bootSettings() -> app#host
+known_edge read app#bootSettings() -> plugin#settings
+known_edge read app#record(number) -> app#seen
+known_edge read store#known() -> store#subs
+known_edge read store#store() -> store#handlers
+known_edge read store#store() -> store#subs
diff --git a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle
index be4f8be6..8c2412a1 100644
--- a/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle
+++ b/graph/test/typescript/expected/78-hof-callback-at-library-boundary.fields-oracle
@@ -1,7 +1,7 @@
78-hof-callback-at-library-boundary [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 12 resolved 12 (100.0%)
+ tiers known_edge=12
+ access read=12
+ not scored: 12 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/79-package-published-members.fields b/graph/test/typescript/expected/79-package-published-members.fields
index e69de29b..ec69ab45 100644
--- a/graph/test/typescript/expected/79-package-published-members.fields
+++ b/graph/test/typescript/expected/79-package-published-members.fields
@@ -0,0 +1,3 @@
+known_edge read src/bound#() -> bound#engine
+known_edge read src/index.test#() -> api#api
+known_edge read src/index.test#() -> bound#produce
diff --git a/graph/test/typescript/expected/79-package-published-members.fields-oracle b/graph/test/typescript/expected/79-package-published-members.fields-oracle
index bf1e0d9b..f5234231 100644
--- a/graph/test/typescript/expected/79-package-published-members.fields-oracle
+++ b/graph/test/typescript/expected/79-package-published-members.fields-oracle
@@ -1,7 +1,7 @@
79-package-published-members [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 5 resolved 5 (100.0%)
+ tiers known_edge=5
+ access read=5
+ not scored: 5 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/80-generic-return-receiver.fields b/graph/test/typescript/expected/80-generic-return-receiver.fields
index b7d27851..e6f9deac 100644
--- a/graph/test/typescript/expected/80-generic-return-receiver.fields
+++ b/graph/test/typescript/expected/80-generic-return-receiver.fields
@@ -1,2 +1,10 @@
known_edge read Circ#area() -> Circ#r
known_edge read Sq#area() -> Sq#s
+known_edge read use#() -> use#WARRIOR
+known_edge read use#() -> use#container
+known_edge read use#ctlAnyIndex(string) -> use#anyBag
+known_edge read use#indexValue(string) -> use#shapes
+known_edge read use#recordArea(string,number) -> use#makers
+known_edge read use#run() -> use#WARRIOR
+known_edge read use#run() -> use#container
+known_edge read use#totalArea(string,number) -> use#factories
diff --git a/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle b/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle
index f770da18..09fc58b0 100644
--- a/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle
+++ b/graph/test/typescript/expected/80-generic-return-receiver.fields-oracle
@@ -1,7 +1,7 @@
80-generic-return-receiver [fields]
precision 1.0000 (2 correct, 0 wrong)
recall 1.0000 (2 of 2 the compiler resolved)
- sites 4 resolved 4 (100.0%)
- tiers known_edge=4
- access read=4
- not scored: 0 rows whose target is not a client declaration
+ sites 12 resolved 12 (100.0%)
+ tiers known_edge=12
+ access read=12
+ not scored: 8 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/80-object-literal-member-receivers.fields b/graph/test/typescript/expected/80-object-literal-member-receivers.fields
index 4b531045..493c648a 100644
--- a/graph/test/typescript/expected/80-object-literal-member-receivers.fields
+++ b/graph/test/typescript/expected/80-object-literal-member-receivers.fields
@@ -1,3 +1,9 @@
-ambiguous_unknown read local#drive() -> -
-ambiguous_unknown read local#trimName() -> -
-ambiguous_unknown read use#callNested() -> -
+known_edge read local#drive() -> local#handlers
+known_edge read local#drive() -> local#jobs
+known_edge read local#trimName() -> local#cfg
+known_edge read local#trimName() -> local#name
+known_edge read use#callGreeter() -> api#greeter
+known_edge read use#callNested() -> api#api
+known_edge read use#callNested() -> api#users
+known_edge read use#callTop() -> api#api
+known_edge read use#keepUnrelated() -> use#unrelated
diff --git a/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle b/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle
index 66475c28..b41e6134 100644
--- a/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle
+++ b/graph/test/typescript/expected/80-object-literal-member-receivers.fields-oracle
@@ -1,16 +1,13 @@
80-object-literal-member-receivers [fields]
- precision 0.0000 (0 correct, 0 wrong)
- recall 0.0000 (0 of 9 the compiler resolved)
- sites 6 resolved 0 (0.0%)
- tiers ambiguous_unknown=6
- access read=6
- not scored: 6 rows whose target is not a client declaration
- MISSING local#drive() READ local#jobs
+ precision 1.0000 (3 correct, 0 wrong)
+ recall 0.3333 (3 of 9 the compiler resolved)
+ sites 17 resolved 17 (100.0%)
+ tiers known_edge=17
+ access read=17
+ not scored: 11 rows whose target is not a client declaration
MISSING local#drive() READ local#run
- MISSING local#trimName() READ local#name
MISSING use#callNested() READ api#byName
MISSING use#callNested() READ api#get
- MISSING use#callNested() READ api#users
MISSING use#callStore() READ api#reset
MISSING use#callTop() READ api#find
MISSING use#callTop() READ api#remove
diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges
index 332f0630..15c58497 100644
--- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges
+++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.edges
@@ -14,6 +14,7 @@ ambiguous_unknown FUNCTION_CALL parts#render() @L35 -> -
ambiguous_unknown FUNCTION_CALL reg#() @L18 -> -
ambiguous_unknown JSX_COMPONENT_CALL app#() @L20 -> -
callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?)
+callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing()
known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper()
known_edge FUNCTION_CALL button#() @L10 -> button#leafButton()
known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard()
diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields
index e69de29b..d5c739df 100644
--- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields
+++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields
@@ -0,0 +1,9 @@
+known_edge read app#() -> parts#Child
+known_edge read app#() -> parts#FnForm
+known_edge read app#() -> parts#NuxtCard
+known_edge read app#() -> parts#Panel
+known_edge read app#() -> parts#Store
+known_edge read app#() -> reg#Button
+known_edge read app#() -> reg#Local
+known_edge read card#render() -> parts#Child
+known_edge read reg#() -> reg#_Local
diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle
index 7aa79d05..cc14c676 100644
--- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle
+++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.fields-oracle
@@ -1,7 +1,7 @@
80-vue-definecomponent-jsx [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 9 resolved 9 (100.0%)
+ tiers known_edge=9
+ access read=9
+ not scored: 9 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges
index 40aed3c8..92c54f4d 100644
--- a/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges
+++ b/graph/test/typescript/expected/80-vue-definecomponent-jsx.lib.edges
@@ -14,6 +14,7 @@ boundary_lib FUNCTION_CALL parts#() @L42 -> vue#defineNuxtComponent
boundary_lib FUNCTION_CALL parts#() @L49 -> vue#makeStore(ComponentOptions)
boundary_lib FUNCTION_CALL reg#() @L18 -> vue#defineComponent(ComponentOptions)
callback_registered FUNCTION_CALL parts#() @L40 -> parts#(?)
+callback_registered JSX_COMPONENT_CALL app#() @L? -> app#onPing()
known_edge FUNCTION_CALL app#onPing() @L12 -> app#helper()
known_edge FUNCTION_CALL button#() @L10 -> button#leafButton()
known_edge FUNCTION_CALL card#render() @L12 -> card#leafCard()
diff --git a/graph/test/typescript/expected/80-vue-sfc.fields b/graph/test/typescript/expected/80-vue-sfc.fields
index 2c586263..7fae20e7 100644
--- a/graph/test/typescript/expected/80-vue-sfc.fields
+++ b/graph/test/typescript/expected/80-vue-sfc.fields
@@ -1 +1,3 @@
+known_edge read Comp.vue#() -> Comp#total
known_edge read child#Child({ msg: string }) -> { msg: string }#msg
+known_edge read shims-vue#() -> shims-vue.d#component
diff --git a/graph/test/typescript/expected/80-vue-sfc.fields-oracle b/graph/test/typescript/expected/80-vue-sfc.fields-oracle
index 706b5d61..4288e38b 100644
--- a/graph/test/typescript/expected/80-vue-sfc.fields-oracle
+++ b/graph/test/typescript/expected/80-vue-sfc.fields-oracle
@@ -1,9 +1,9 @@
80-vue-sfc [fields]
precision 0.0000 (0 correct, 1 wrong)
recall 0.0000 (0 of 1 the compiler resolved)
- sites 1 resolved 1 (100.0%)
- tiers known_edge=1
- access read=1
- not scored: 0 rows whose target is not a client declaration
+ sites 4 resolved 4 (100.0%)
+ tiers known_edge=4
+ access read=4
+ not scored: 3 rows whose target is not a client declaration
WRONG child#Child({ msg: string }) READ { msg: string }#msg
MISSING child#Child({ msg: string }) READ child#msg
diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields b/graph/test/typescript/expected/81-vue-component-tag.fields
index e69de29b..d2a02654 100644
--- a/graph/test/typescript/expected/81-vue-component-tag.fields
+++ b/graph/test/typescript/expected/81-vue-component-tag.fields
@@ -0,0 +1 @@
+known_edge read shims-vue#() -> shims-vue.d#component
diff --git a/graph/test/typescript/expected/81-vue-component-tag.fields-oracle b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle
index 7a65896b..21219347 100644
--- a/graph/test/typescript/expected/81-vue-component-tag.fields-oracle
+++ b/graph/test/typescript/expected/81-vue-component-tag.fields-oracle
@@ -1,7 +1,7 @@
81-vue-component-tag [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 1 resolved 1 (100.0%)
+ tiers known_edge=1
+ access read=1
+ not scored: 1 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.fields b/graph/test/typescript/expected/83-closed-world-dispatch.fields
index e69de29b..aa725fb8 100644
--- a/graph/test/typescript/expected/83-closed-world-dispatch.fields
+++ b/graph/test/typescript/expected/83-closed-world-dispatch.fields
@@ -0,0 +1,2 @@
+known_edge read handlers#main() -> handlers#registry
+known_edge read handlers#register(HandlerClass) -> handlers#registry
diff --git a/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle b/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle
index 3355e740..966f7948 100644
--- a/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle
+++ b/graph/test/typescript/expected/83-closed-world-dispatch.fields-oracle
@@ -1,7 +1,7 @@
83-closed-world-dispatch [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
- sites 0 resolved 0
- tiers
- access
- not scored: 0 rows whose target is not a client declaration
+ sites 2 resolved 2 (100.0%)
+ tiers known_edge=2
+ access read=2
+ not scored: 2 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields b/graph/test/typescript/expected/85-workspace-package-import.fields
index 565a7ee6..be580e37 100644
--- a/graph/test/typescript/expected/85-workspace-package-import.fields
+++ b/graph/test/typescript/expected/85-workspace-package-import.fields
@@ -1 +1,3 @@
known_edge read S#run() -> S#bus
+known_edge read S#run() -> index#K
+known_edge read S#run() -> zod#z
diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields-oracle b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle
index 44b1f6d7..346e6652 100644
--- a/graph/test/typescript/expected/85-workspace-package-import.fields-oracle
+++ b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle
@@ -1,7 +1,7 @@
85-workspace-package-import [fields]
precision 1.0000 (1 correct, 0 wrong)
recall 1.0000 (1 of 1 the compiler resolved)
- sites 1 resolved 1 (100.0%)
- tiers known_edge=1
- access read=1
- not scored: 0 rows whose target is not a client declaration
+ sites 3 resolved 3 (100.0%)
+ tiers known_edge=3
+ access read=3
+ not scored: 2 rows whose target is not a client declaration
diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.fields b/graph/test/typescript/expected/86-keyed-callback-registry.fields
index 02ad1620..7d5b88cf 100644
--- a/graph/test/typescript/expected/86-keyed-callback-registry.fields
+++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields
@@ -1,12 +1,26 @@
ambiguous_unknown read Bus#count(string) -> -
-ambiguous_unknown read Indexer#start() -> -
-ambiguous_unknown read app#() -> -
-ambiguous_unknown read app#create() -> -
-ambiguous_unknown read app#size() -> -
known_edge read Bus#count(string) -> Bus#handlers
known_edge read Bus#dispatch(string,unknown) -> Bus#handlers
+known_edge read Bus#dispatch(string,unknown) -> bus#ANY
known_edge read Bus#subscribe(string,Handler) -> Bus#handlers
known_edge read Indexer#start() -> Indexer#b
+known_edge read Indexer#start() -> topics#TOPIC
+known_edge read Indexer#start() -> topics#created
+known_edge read Indexer#start() -> topics#removed
known_edge read MiniBus#fire(string,unknown) -> MiniBus#h
known_edge read MiniBus#on(string,(p: unknown) =) -> MiniBus#h
+known_edge read app#() -> app#bus
+known_edge read app#() -> app#mini
+known_edge read app#() -> bus#ANY
+known_edge read app#() -> topics#TOPIC
+known_edge read app#() -> topics#created
+known_edge read app#create() -> app#bus
+known_edge read app#create() -> topics#TOPIC
+known_edge read app#create() -> topics#created
+known_edge read app#go() -> app#mini
+known_edge read app#relay({ type: string }) -> app#bus
known_edge read app#relay({ type: string }) -> { type: string }#type
+known_edge read app#remove() -> app#bus
+known_edge read app#size() -> app#bus
+known_edge read app#size() -> topics#TOPIC
+known_edge read app#size() -> topics#created
diff --git a/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle b/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle
index 9148ff26..f6c67a1e 100644
--- a/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle
+++ b/graph/test/typescript/expected/86-keyed-callback-registry.fields-oracle
@@ -1,14 +1,9 @@
86-keyed-callback-registry [fields]
- precision 0.8571 (6 correct, 1 wrong)
- recall 0.5000 (6 of 12 the compiler resolved)
- sites 17 resolved 11 (64.7%)
- tiers ambiguous_unknown=6 known_edge=11
- access read=17
- not scored: 6 rows whose target is not a client declaration
+ precision 0.9167 (11 correct, 1 wrong)
+ recall 0.9167 (11 of 12 the compiler resolved)
+ sites 36 resolved 35 (97.2%)
+ tiers ambiguous_unknown=1 known_edge=35
+ access read=36
+ not scored: 20 rows whose target is not a client declaration
WRONG app#relay({ type: string }) READ { type: string }#type
- MISSING Indexer#start() READ topics#created
- MISSING Indexer#start() READ topics#removed
- MISSING app#() READ topics#created
- MISSING app#create() READ topics#created
MISSING app#relay({ type: string }) READ app#type
- MISSING app#size() READ topics#created
diff --git a/graph/test/typescript/expected/86-module-variable-reads.edges b/graph/test/typescript/expected/86-module-variable-reads.edges
new file mode 100644
index 00000000..c94f0255
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.edges
@@ -0,0 +1,7 @@
+known_edge CONSTRUCTOR_CALL Handler#handle(unknown) @L46 -> Pipe#(unknown)
+known_edge DECORATOR_CALL Handler#handle(unknown) @L45 -> a/test/t#Route(unknown)
+known_edge DECORATOR_CALL Handler#handle(unknown) @L46 -> a/test/t#Body(unknown)
+known_edge FUNCTION_CALL a/src/cfg#() @L4 -> a/src/cfg#mk(object)
+known_edge FUNCTION_CALL a/test/t#asArgument() @L10 -> a/test/t#use(unknown)
+known_edge FUNCTION_CALL a/test/t#callsTheFunction() @L18 -> a/src/cfg#make(number)
+known_edge FUNCTION_CALL a/test/t#viaMember() @L6 -> a/test/t#use(unknown)
diff --git a/graph/test/typescript/expected/86-module-variable-reads.entries b/graph/test/typescript/expected/86-module-variable-reads.entries
new file mode 100644
index 00000000..9b291f2d
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.entries
@@ -0,0 +1,15 @@
+── entry_point (14) ──
+ exported_from_entry_module a/src/cfg#bump cfg.ts:18
+ exported_from_entry_module a/src/cfg#localUse cfg.ts:14
+ exported_from_entry_module a/src/cfg#mk cfg.ts:10
+ exported_from_entry_module a/test/t#annotated t.ts:30
+ exported_from_entry_module a/test/t#asArgument t.ts:9
+ exported_from_entry_module a/test/t#callsTheFunction t.ts:17
+ exported_from_entry_module a/test/t#inTemplate t.ts:13
+ exported_from_entry_module a/test/t#shadowed t.ts:21
+ exported_from_entry_module a/test/t#shadowedInType t.ts:35
+ exported_from_entry_module a/test/t#viaMember t.ts:5
+ exported_from_entry_module b/test/sibling#siblingRead sibling.ts:3
+ unimported_module a/src/cfg# cfg.ts:1
+ unimported_module a/test/t# t.ts:1
+ unimported_module b/test/sibling# sibling.ts:1
diff --git a/graph/test/typescript/expected/86-module-variable-reads.fields b/graph/test/typescript/expected/86-module-variable-reads.fields
new file mode 100644
index 00000000..3ea4db04
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.fields
@@ -0,0 +1,21 @@
+ambiguous_unknown read a/test/t#annotated(typeof CFG) -> -
+ambiguous_unknown read a/test/t#shadowed() -> -
+known_edge read Handler#handle(unknown) -> a/src/cfg#Store
+known_edge read Handler#handle(unknown) -> cfg#S
+known_edge read Handler#handle(unknown) -> cfg#TOKENS
+known_edge read a/src/cfg#localUse() -> a/src/cfg#name
+known_edge read a/src/cfg#localUse() -> cfg#CFG
+known_edge read a/test/t#() -> cfg#S
+known_edge read a/test/t#annotated(typeof CFG) -> cfg#CFG
+known_edge read a/test/t#asArgument() -> cfg#S
+known_edge read a/test/t#inTemplate() -> a/src/cfg#Store
+known_edge read a/test/t#inTemplate() -> a/src/cfg#name
+known_edge read a/test/t#inTemplate() -> cfg#CFG
+known_edge read a/test/t#inTemplate() -> cfg#TOKENS
+known_edge read a/test/t#viaMember() -> a/src/cfg#name
+known_edge read a/test/t#viaMember() -> cfg#CFG
+known_edge read b/test/sibling#siblingRead() -> b/src/settings#port
+known_edge read b/test/sibling#siblingRead() -> settings#CFG
+known_edge read b/test/sibling#siblingRead() -> settings#S
+known_edge readwrite a/src/cfg#bump() -> cfg#counter
+known_edge write a/src/cfg#bump() -> cfg#counter
diff --git a/graph/test/typescript/expected/86-module-variable-reads.fields-oracle b/graph/test/typescript/expected/86-module-variable-reads.fields-oracle
new file mode 100644
index 00000000..67805e29
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.fields-oracle
@@ -0,0 +1,9 @@
+86-module-variable-reads [fields]
+ precision 1.0000 (6 correct, 0 wrong)
+ recall 0.7500 (6 of 8 the compiler resolved)
+ sites 21 resolved 19 (90.5%)
+ tiers ambiguous_unknown=2 known_edge=19
+ access read=19 readwrite=1 write=1
+ not scored: 15 rows whose target is not a client declaration
+ MISSING a/test/t#annotated(typeof CFG) READ a/src/cfg#name
+ MISSING a/test/t#shadowed() READ a/test/t#name
diff --git a/graph/test/typescript/expected/86-module-variable-reads.oracle b/graph/test/typescript/expected/86-module-variable-reads.oracle
new file mode 100644
index 00000000..fa4000b6
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.oracle
@@ -0,0 +1 @@
+oracle=7 engine=7 agree=7 missing=0 (known 0, NEW 0) extra=0
diff --git a/graph/test/typescript/expected/86-module-variable-reads.type-use b/graph/test/typescript/expected/86-module-variable-reads.type-use
new file mode 100644
index 00000000..a403269b
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.type-use
@@ -0,0 +1,5 @@
+ambiguous_unknown AS_TARGET 0 a/src/cfg [EXPRESSION] -> -
+ambiguous_unknown AS_TARGET 0 b/src/settings [EXPRESSION] -> -
+ambiguous_unknown DECORATOR_TYPE 0 Handler [DECORATOR] -> -
+ambiguous_unknown METHOD_RETURN 0 a/test/t [METHOD] -> -
+known_edge OBJECT_CREATION_TYPE 0 Handler [EXPRESSION] -> Pipe
diff --git a/graph/test/typescript/expected/86-module-variable-reads.types-oracle b/graph/test/typescript/expected/86-module-variable-reads.types-oracle
new file mode 100644
index 00000000..e0d90aa4
--- /dev/null
+++ b/graph/test/typescript/expected/86-module-variable-reads.types-oracle
@@ -0,0 +1,7 @@
+86-module-variable-reads [types]
+ precision 1.0000 (1 correct, 0 wrong)
+ recall 1.0000 (1 of 1 the compiler resolved)
+ sites 8 resolved 1 (12.5%)
+ tiers ambiguous_unknown=7 known_edge=1
+ contexts AS_TARGET=3 DECORATOR_TYPE=2 METHOD_RETURN=2 OBJECT_CREATION_TYPE=1
+ not scored: 7 rows whose target is not a client declaration
diff --git a/graph/test/typescript/tools/normalize_members.py b/graph/test/typescript/tools/normalize_members.py
index 3a47baf3..b6698c7c 100644
--- a/graph/test/typescript/tools/normalize_members.py
+++ b/graph/test/typescript/tools/normalize_members.py
@@ -25,8 +25,17 @@ def field_names(ir, prefix='typescript'):
out = {}
types = {t['tsTypeUniqueHash']: (t.get('name') or t.get('qualifiedName') or '?')
for t in rows(f'{ir}/all-{prefix}-types.csv')}
+ mods = {m['tsModuleUniqueHash']: m['qualifiedName'] or m['filePath']
+ for m in rows(f'{ir}/all-{prefix}-modules.csv')}
for r in rows(f'{ir}/all-{prefix}-fields.csv'):
- owner = types.get(r.get('tsTypeLinkHash')) or r.get('ownerTypeName') or '?'
+ owner = types.get(r.get('tsTypeLinkHash'))
+ # A key of a module-level `const X = { a: … }` is owned by the variable X (or by the
+ # key holding a nested literal), which is no type. The compiler side names the nearest
+ # enclosing named declaration and the module otherwise, so `X.a` is `module#a` there;
+ # naming it `X#a` here scored every correct read as one wrong plus one missing.
+ if owner is None and r.get('memberKind') == 'OBJECT_LITERAL_PROPERTY':
+ owner = mods.get(r.get('tsModuleLinkHash'))
+ owner = owner or r.get('ownerTypeName') or '?'
# `static ` prefixes the NAME, as normalize_edges.Names does for a static method and
# as the compiler side's labelOf does. Without it a static member is named one way on
# each side and every access to it scores as both a miss and a false positive.
@@ -35,6 +44,14 @@ def field_names(ir, prefix='typescript'):
return out
+def module_var_names(ir, prefix='typescript'):
+ """variable hash -> (module label, name), for a module-scope variable read by name: its owner is its
+ module, labelled by the file's stem as the module initializer is (`members#()`)."""
+ return {r['tsVariableUniqueHash']: (os.path.splitext(os.path.basename(r.get('filePath') or '?'))[0], r['name'])
+ for r in rows(f'{ir}/all-{prefix}-variables.csv')
+ if r.get('scopeKind') in ('MODULE_SCOPE', 'GLOBAL_SCOPE', 'AMBIENT_SCOPE', 'NAMESPACE_SCOPE')}
+
+
def type_names(ir, prefix='typescript'):
return {t['tsTypeUniqueHash']: (t.get('name') or t.get('qualifiedName') or '?')
for t in rows(f'{ir}/all-{prefix}-types.csv')}
@@ -53,6 +70,9 @@ def main():
ir, out = args[0], args[1]
n = Names(ir, lib_ir)
fields = field_names(ir)
+ # the compiler oracle scores member accesses only: a module variable's read is golden-checked, not oracle-paired
+ mvars = module_var_names(ir)
+ fields.update({k: v for k, v in mvars.items() if k not in fields})
types = type_names(ir)
if lib_ir and os.path.isdir(lib_ir):
fields.update({k: v for k, v in field_names(lib_ir).items() if k not in fields})
@@ -72,7 +92,7 @@ def main():
f"{fields[field][0]}#{fields[field][1]}" if field in fields
else f"")
if pairs:
- if field == '-' or field not in fields:
+ if field == '-' or field not in fields or field in mvars:
continue
for d in (['READ'] if access == 'read' else ['WRITE'] if access == 'write'
else ['READ', 'WRITE']):
diff --git a/graph/typescript/engine/call-edge-generation/call_chain.dl b/graph/typescript/engine/call-edge-generation/call_chain.dl
index 56518799..890a08ad 100644
--- a/graph/typescript/engine/call-edge-generation/call_chain.dl
+++ b/graph/typescript/engine/call-edge-generation/call_chain.dl
@@ -134,6 +134,14 @@ call_chain_edge(ce, caller, "-", m, "client", "callback_registered", kind) :-
caller != m,
invocation_site(ce, kind).
+// …and a function written inside a JSX `{…}` (resolution/value-flow.dl, jsx_handed_value). The `{…}` is
+// the site: an intrinsic tag has no call site of its own, and the expression positions the edge.
+call_chain_edge(r, caller, "-", m, "client", "callback_registered", "JSX_COMPONENT_CALL") :-
+ jsx_handed_value(r, m),
+ method_prov(m, "client"),
+ jsx_root_caller(r, caller),
+ caller != m.
+
// ── call_runs_edge(FromMethod, ToMethod, Prov) ──────────────────────────────
// The chain a consumer should WALK: overload signatures re-pointed to the body that
// actually executes (resolution/overload-sets.dl). Kept separate from
diff --git a/graph/typescript/engine/call-edge-generation/calls.dl b/graph/typescript/engine/call-edge-generation/calls.dl
index 959c3c48..b4b87c8f 100644
--- a/graph/typescript/engine/call-edge-generation/calls.dl
+++ b/graph/typescript/engine/call-edge-generation/calls.dl
@@ -126,3 +126,67 @@ unresolved_covered(ce) :- call_site("client", ck, _, "NONE", _, ce, _),
unresolved_receiver(ce, cn, rk, "other_form") :- call_site("client", _, cn, rk, _, ce, _),
!call_resolved(ce),
!unresolved_covered(ce).
+
+// ── library_receiver(CallExpr, Specifier) ───────────────────────────────────
+// An unresolved member call whose receiver is a PACKAGE'S value: a binding imported
+// from a package no staged IR declares, a member of one, what calling or constructing
+// one returns, and a const holding any of those — `request(app).get('/x').expect(200)`,
+// `new grpc.Metadata().get(k)`. The receiver's origin is known, so the site is not an
+// untyped `x.get()`: a project method of the same name is not what it calls. It stays
+// unresolved (the package is not staged), and consumers list it apart from the name
+// matches on unknown receivers rather than among them. Read off the source, as the
+// JavaScript rule is. A call handed a value of a project type is left out: a wrapper,
+// a proxy or a mock helper may return the very object it was given.
+package_import(imp, spec) :- import_specifier("client", spec, imp), !import_resolved_module("client", _, imp),
+ !import_is_type_only("client", imp), import_resolution_kind("client", rk, imp),
+ rk != "RELATIVE_FILE", rk != "PATHS_ALIAS", !match("[.#~/].*", spec), !match("@/.*", spec).
+library_value(e, spec) :- expr_referenced("client", "IMPORT_BINDING", imp, e), package_import(imp, spec).
+library_value(e, spec) :- expr_kind("client", k, _, e), (k = "PROPERTY_ACCESS" ; k = "ELEMENT_ACCESS"),
+ expr_child("client", e, role, _, r), (role = "RECEIVER" ; role = "QUALIFIER"), library_value(r, spec).
+library_value(e, spec) :- expr_kind("client", k, _, e), (k = "CALL_EXPRESSION" ; k = "NEW_EXPRESSION"),
+ expr_child("client", e, "METHOD_NAME", _, c), library_value(c, spec), !call_passes_project_instance(e).
+library_value(e, spec) :- expr_kind("client", k, _, e),
+ (k = "AWAIT_EXPRESSION" ; k = "NON_NULL_EXPRESSION" ; k = "AS_EXPRESSION" ; k = "SATISFIES_EXPRESSION"),
+ expr_child("client", e, _, _, c), library_value(c, spec).
+library_value(e, spec) :- expr_referenced("client", "VARIABLE", v, e), var_initializer("client", _, i, v),
+ !var_reassigned(v), library_value(i, spec).
+call_passes_project_instance(e) :- expr_child("client", e, "ARGUMENT", _, a), expr_type(a, "client", _).
+// …and the runtime's own objects: `JSON.parse(s)`, `Promise.resolve(v).then(f)`, `Object.keys(o).forEach(g)`,
+// `document.createElement('a')`, `console.error(e)`. With no standard library staged the name resolves to nothing
+// in the run (AMBIENT_GLOBAL), so every such call was an untyped `x.parse()` to the consumers, and a project method
+// that happens to be named parse, resolve, all, keys, error or getMetadata was given every one of them as a by-name
+// caller — and every test around them. The receiver is the platform's, never the project's: a program that declares
+// the name itself binds it as its own variable, not as an ambient global, and stays out. Only the objects whose
+// members are called through the name; `window`, `globalThis` and `self` are left out, since a program may hang its
+// own functions on them.
+library_value(e, n) :- expr_referenced("client", "AMBIENT_GLOBAL", _, e), expr_name("client", n, e),
+ builtin_global_object(n).
+builtin_global_object("JSON").
+builtin_global_object("Math").
+builtin_global_object("Object").
+builtin_global_object("Reflect").
+builtin_global_object("Promise").
+builtin_global_object("Array").
+builtin_global_object("Number").
+builtin_global_object("String").
+builtin_global_object("Boolean").
+builtin_global_object("BigInt").
+builtin_global_object("Symbol").
+builtin_global_object("Date").
+builtin_global_object("RegExp").
+builtin_global_object("Error").
+builtin_global_object("Intl").
+builtin_global_object("Atomics").
+builtin_global_object("ArrayBuffer").
+builtin_global_object("console").
+builtin_global_object("process").
+builtin_global_object("Buffer").
+builtin_global_object("document").
+builtin_global_object("navigator").
+builtin_global_object("localStorage").
+builtin_global_object("sessionStorage").
+builtin_global_object("performance").
+builtin_global_object("crypto").
+builtin_global_object("URL").
+library_receiver(ce, spec) :- call_site("client", _, _, rk, recv, ce, _), receiver_kind_is_value(rk),
+ !call_resolved(ce), library_value(recv, spec).
diff --git a/graph/typescript/engine/call-edge-generation/field_access.dl b/graph/typescript/engine/call-edge-generation/field_access.dl
index d7d0057f..39396ab7 100644
--- a/graph/typescript/engine/call-edge-generation/field_access.dl
+++ b/graph/typescript/engine/call-edge-generation/field_access.dl
@@ -86,18 +86,43 @@ field_site_excluded(e, "method_value") :- ts_field_site_is_method(e),
!expr_kind("client", "PROPERTY_ACCESS", "METHOD_NAME", e).
// ── RESOLUTION — the three receiver-typing paths expr-type.dl already has ───
-ts_field_access_target(e, f) :- property_access_name(e, n),
+ts_field_access_typed(e, f) :- property_access_name(e, n),
expr_child("client", e, "RECEIVER", _, q),
expr_type(q, _, qt),
field_in_scope(qt, n, "false", f).
-ts_field_access_target(e, f) :- property_access_name(e, n),
+ts_field_access_typed(e, f) :- property_access_name(e, n),
expr_child("client", e, "RECEIVER", _, q),
expr_static_type(q, _, qt),
field_in_scope(qt, n, "true", f).
-ts_field_access_target(e, f) :- property_access_name(e, n),
+ts_field_access_typed(e, f) :- property_access_name(e, n),
expr_child("client", e, "RECEIVER", _, q),
expr_shape(q, qs),
field_in_scope(qs, n, _, f).
+ts_field_access_target(e, f) :- ts_field_access_typed(e, f).
+ts_field_access_is_typed(e) :- ts_field_access_typed(e, _).
+
+// ── A KEY OF A MODULE-LEVEL CONST TABLE — `TOPICS.orderEvents` ──────────────
+// `export const TOPICS = { orderEvents: '…' } as const`: the parser declares each key
+// as an OBJECT_LITERAL_PROPERTY owned by the VARIABLE (a nested literal's keys by the
+// property holding it). The owner is no type, so none of the three joins above can
+// reach it; the receiver names the owner instead — the variable itself, an import of
+// it, a namespace import's export of it, or a key of an outer table. Only where the
+// typed lookup found nothing: `{ … } satisfies Config` is read as Config's member.
+ts_objlit_owner(q, v) :- expr_referenced("client", "VARIABLE", v, q).
+ts_objlit_owner(q, v) :- expr_referenced("client", "IMPORT_BINDING", ih, q),
+ import_binds(ih, _, "VARIABLE", v).
+ts_objlit_owner(q, v) :- property_access_name(q, n),
+ expr_child("client", q, "RECEIVER", _, r),
+ expr_namespace(r, _, m),
+ module_export(_, m, n, "VARIABLE", v).
+ts_objlit_owner(q, pf) :- ts_field_access_target(q, pf),
+ field_kind("client", "OBJECT_LITERAL_PROPERTY", _, _, pf).
+ts_field_access_target(e, f) :- property_access_name(e, n),
+ expr_child("client", e, "RECEIVER", _, q),
+ ts_objlit_owner(q, o),
+ field_decl("client", n, _, o, f),
+ field_kind("client", "OBJECT_LITERAL_PROPERTY", _, _, f),
+ !ts_field_access_is_typed(e).
// ── ACCESS DIRECTION ────────────────────────────────────────────────────────
// property_written and property_write_only are accessors.dl's, and are reused rather
@@ -124,12 +149,39 @@ ts_field_access_class(e, "ambiguous_unknown") :- ts_field_access_count(e, 0).
// The enclosing function of the ACCESS, by expr_enclosing_method, exactly as accessors.dl
// attributes an accessor invocation: there is no call site to attribute through. Top-level
// code is the module initializer, which that relation already supplies.
+// A read inside a METHOD'S DECORATOR is that method's: `@EventPattern(TOPICS.orderEvents)`
+// binds the handler to the topic, and the module initializer that evaluates the
+// decorator is no user of it. A decorator's expressions carry no owner hash, so the
+// subtree is walked down from the decorator's own expression.
+ts_deco_expr_method(e, m) :- decorator_expr("client", e, d), decorator_target("client", _, m, d), m != "".
+// ...and so is a read in a PARAMETER'S decorator: `constructor(@inject(TOKENS.Store) s)`
+// binds the constructor to the token. The decorator's owner is the parameter.
+ts_deco_expr_method(e, m) :- decorator_expr("client", e, d),
+ ts_decorator(_, _, "PARAMETER_DECLARATION", p, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, d),
+ param_decl("client", _, _, _, m, p), m != "".
+ts_deco_expr_method(c, m) :- ts_deco_expr_method(p, m), expr_child("client", p, _, _, c).
+ts_field_access_in_decorator(e) :- ts_deco_expr_method(e, _).
+// A read in a parameter's DEFAULT is its method's too: `constructor(topic = TOPICS.payments)`
+// is evaluated on every call that omits the argument, and the extractor places it in the
+// module initializer. Only there: a function written as the default keeps its own reads.
+ts_param_default_expr(e, m) :- ts_method_parameter(_, _, m, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, e, _, _, _, _),
+ e != "", m != "".
+ts_param_default_expr(c, m) :- ts_param_default_expr(p, m), expr_child("client", p, _, _, c).
+ts_field_access_in_function(e) :- expr_enclosing_method(e, m), m != "", !module_init("client", m, _).
+ts_param_default_method(e, m) :- ts_param_default_expr(e, m),
+ !ts_field_access_in_function(e), !ts_field_access_in_decorator(e).
ts_field_access_named(e) :- expr_enclosing_method(e, m), m != "".
-ts_field_access_from(e, m) :- ts_field_access_site(e), expr_enclosing_method(e, m), m != "".
-// ...and the MODULE itself for the residue expr_enclosing_method cannot place, which is
-// the marker call_sites.caller_id already uses in TypeScript rather than a blank. Never
-// NULL, so a site can always be attributed to the code that contains it.
-ts_field_access_from(e, mod) :- ts_field_access_site(e), !ts_field_access_named(e),
+ts_field_access_placed(e) :- ts_field_access_named(e).
+ts_field_access_placed(e) :- ts_field_access_in_decorator(e).
+ts_field_access_placed(e) :- ts_param_default_method(e, _).
+ts_field_access_from(e, m) :- ts_field_access_site(e), ts_deco_expr_method(e, m).
+ts_field_access_from(e, m) :- ts_field_access_site(e), ts_param_default_method(e, m).
+ts_field_access_from(e, m) :- ts_field_access_site(e), expr_enclosing_method(e, m), m != "",
+ !ts_field_access_in_decorator(e), !ts_param_default_method(e, _).
+// ...and the MODULE itself for the residue none of those can place, which is the marker
+// call_sites.caller_id already uses in TypeScript rather than a blank. Never NULL, so a
+// site can always be attributed to the code that contains it.
+ts_field_access_from(e, mod) :- ts_field_access_site(e), !ts_field_access_placed(e),
expr_module("client", mod, e), mod != "".
// ── THE EXPORTED ROW — field_access(Site, Caller, Field, Prov, Tier, Access) ─
@@ -150,3 +202,88 @@ ts_field_access_has_row(e) :- ts_field_access_target(e, f), ts_field_prov(_, f).
field_access(e, caller, "-", "-", "ambiguous_unknown", acc) :- ts_field_access_site(e),
!ts_field_access_has_row(e),
ts_field_access_from(e, caller), ts_field_access_kind(e, acc).
+
+// ── A MODULE-SCOPE VARIABLE, READ BY NAME ───────────────────────────────────
+// `SERVICE.name`, `f(EnvSchema)`, `TOKENS.IntentStore`: the receiver or argument is an
+// IDENTIFIER_REFERENCE that the binder already tied to one declaration -- a module-scope
+// variable of its own file (VARIABLE), or an import whose binding import_binds follows,
+// through every re-export, to the exported variable (IMPORT_BINDING). Nothing recorded
+// that tie as a data edge, so "who reads this exported const" was answered by name, and
+// the name matched every same-named const in every sibling package.
+//
+// The variable is the field of the row: a module is the owner of its variables the way a
+// class is the owner of its fields, and consumers read one relation for both. A variable
+// that holds a function is a function (`const f = () => ...`): its uses are calls, which
+// call_edges already carries, so it is left out here.
+ts_module_var(v) :- var_decl("client", _, _, sk, _, _, v), scope_is_module_level(sk),
+ !var_is_function(v, _).
+ts_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, e),
+ expr_referenced("client", "VARIABLE", v, e), ts_module_var(v).
+ts_var_read_target(e, v) :- expr_kind("client", "IDENTIFIER_REFERENCE", _, e),
+ expr_referenced("client", "IMPORT_BINDING", ih, e),
+ import_binds(ih, "client", "VARIABLE", v), ts_module_var(v).
+// the direction, by the same law property_written applies to a member
+ts_var_written(e) :- ts_var_read_target(e, _), expr_child("client", asg, "LEFT_OPERAND", _, e),
+ expr_kind("client", ak, _, asg), assignment_kind(ak).
+ts_var_written(e) :- ts_var_read_target(e, _), expr_child("client", u, "UNARY_OPERAND", _, e),
+ expr_kind("client", "UNARY_EXPRESSION", _, u), expr_operator("client", op, u), update_operator(op).
+ts_var_write_only(e) :- ts_var_read_target(e, _), expr_child("client", asg, "LEFT_OPERAND", _, e),
+ expr_kind("client", "ASSIGNMENT_EXPRESSION", _, asg).
+ts_var_read_kind(e, "write") :- ts_var_written(e), ts_var_write_only(e).
+ts_var_read_kind(e, "readwrite") :- ts_var_written(e), !ts_var_write_only(e).
+ts_var_read_kind(e, "read") :- ts_var_read_target(e, _), !ts_var_written(e).
+// the caller, as for a property access: the enclosing function, else the module.
+// A DECORATOR on a method or on one of its parameters is that method's: `place(@Payload(new
+// Pipe(CommandSchema)) c)` reads CommandSchema for place(). The parser gives an expression
+// inside a decorator no owner hash, and expr_enclosing_method routes it to the module
+// initializer, so the read is found by walking down from the decorator's own expression.
+ts_decorated_method(dx, m) :- decorator_expr("client", dx, d),
+ annotation_on("client", _, _, "METHOD_DECLARATION", m, d), m != "".
+ts_decorated_method(dx, m) :- decorator_expr("client", dx, d),
+ annotation_on("client", _, _, "PARAMETER_DECLARATION", p, d), param_decl("client", _, _, _, m, p), m != "".
+ts_under_decorator(dx, dx) :- ts_decorated_method(dx, _).
+ts_under_decorator(dx, c) :- ts_under_decorator(dx, p), expr_child("client", p, _, _, c).
+ts_var_read_decorated(e, m) :- ts_var_read_target(e, _), expr_owner("client", "DECORATOR", _, _, e),
+ ts_under_decorator(dx, e), ts_decorated_method(dx, m).
+ts_var_read_named(e) :- ts_var_read_decorated(e, _).
+ts_var_read_named(e) :- ts_var_read_target(e, _), expr_enclosing_method(e, m), m != "".
+ts_var_read_from(e, m) :- ts_var_read_decorated(e, m).
+ts_var_read_from(e, m) :- ts_var_read_target(e, _), !ts_var_read_decorated(e, _),
+ expr_enclosing_method(e, m), m != "".
+ts_var_read_from(e, mod) :- ts_var_read_target(e, _), !ts_var_read_named(e),
+ expr_module("client", mod, e), mod != "".
+// one declaration, bound by the binder: known_edge
+field_access(e, caller, v, "client", "known_edge", acc) :- ts_var_read_target(e, v),
+ ts_var_read_from(e, caller), ts_var_read_kind(e, acc).
+
+// ── THE SAME VARIABLE, READ IN A TYPE: `typeof X` ───────────────────────────
+// `type OrderDto = z.infer` depends on OrderSchema exactly as
+// `f(OrderSchema)` does, but the name sits in a TYPE_QUERY type reference, not in an
+// expression, so the binder's expression tie above never sees it. The name is bound
+// through the module's own scope (name_binds_any: a declaration of this file, or a named
+// import followed to the exported variable). Only the bare form `typeof X`: in
+// `typeof A.b` the variable read is A, and the reference names b. A parameter or a local
+// of the same name in the enclosing callable shadows the module's (control).
+// The site is the type reference; the bundle positions it from the type-reference table.
+ts_type_query_site(ref, tn, mod) :- type_ref("client", "TYPE_QUERY", _, tn, ctn, _, _, ref),
+ tn != "", ctn = cat("typeof ", tn), type_ref_module("client", mod, ref).
+ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, m, "METHOD", ref).
+ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, p, "METHOD_PARAM", ref),
+ param_decl("client", _, _, _, m, p).
+ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, lv, "VARIABLE", ref),
+ var_decl("client", _, _, _, m, _, lv), m != "".
+ts_type_query_method(ref, m) :- ts_type_query_site(ref, _, _), type_ref("client", _, _, _, _, x, "EXPRESSION", ref),
+ expr_enclosing_method(x, m), m != "".
+ts_type_query_shadowed(ref) :- ts_type_query_site(ref, tn, _), ts_type_query_method(ref, m),
+ param_decl("client", tn, _, _, m, _).
+ts_type_query_shadowed(ref) :- ts_type_query_site(ref, tn, _), ts_type_query_method(ref, m),
+ var_decl("client", tn, _, _, m, _, _).
+ts_type_query_read(ref, v) :- ts_type_query_site(ref, tn, mod), !ts_type_query_shadowed(ref),
+ name_binds_any(mod, tn, "client", "VARIABLE", v), ts_module_var(v).
+ts_type_query_has_method(ref) :- ts_type_query_method(ref, _).
+field_access(ref, m, v, "client", "known_edge", "read") :- ts_type_query_read(ref, v),
+ ts_type_query_method(ref, m).
+// a type alias, an interface, a class member's annotation: the module's initializer, as for
+// any module-level read
+field_access(ref, init, v, "client", "known_edge", "read") :- ts_type_query_read(ref, v),
+ !ts_type_query_has_method(ref), ts_type_query_site(ref, _, mod), module_init("client", init, mod).
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/contextual-params.dl b/graph/typescript/engine/resolution/contextual-params.dl
index c9f675e1..99b05b5a 100644
--- a/graph/typescript/engine/resolution/contextual-params.dl
+++ b/graph/typescript/engine/resolution/contextual-params.dl
@@ -278,6 +278,20 @@ builtin_elem_ref(parent, x) :- ref_is_readonly_operator(parent),
builtin_elem_ref(child, x).
builtin_elem_ref(ref, x) :- ref_via_alias(ref, rhs),
builtin_elem_ref(rhs, x).
+// A tuple's elements are its members: `[Module, ...Module[]]`.
+builtin_elem_ref(ref, child) :- type_ref(rp, "TUPLE", _, _, _, _, _, ref),
+ type_ref_parent(rp, ref, _, child).
+// A TYPE VARIABLE constrained to a list holds that list's elements: `(ms: Ms)`,
+// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The
+// constraint is what every element is at least, as for a `` receiver. Both routes
+// ref_type_target takes: the exact link to the declaration, and the name in scope where it is absent.
+builtin_elem_ref(ref, x) :- type_ref_type_param(rp, tp, ref),
+ type_param_constraint(rp, cref, tp),
+ builtin_elem_ref(cref, x).
+builtin_elem_ref(ref, x) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref),
+ !type_ref_type_param(_, _, ref),
+ type_var_constraint_in_scope(owner, tn, cref),
+ builtin_elem_ref(cref, x).
builtin_elem_ref(ref, a) :- ref_via_alias(ref, rhs),
builtin_elem_ref(rhs, x),
type_ref(_, "TYPE_VARIABLE", _, n, _, _, _, x),
@@ -339,6 +353,13 @@ lambda_param_ref(lp, c) :- call_site("client", _, "then", _, recv, ce, _),
!param_type_ref(_, _, lp).
lambda_param_type(lp, prov, t) :- lambda_param_ref(lp, c),
ref_type_target(c, prov, t).
+// …and its SHAPE, where the element is an object type written as a type alias or a literal rather
+// than a class or an interface: `type Plugin = { init(): void }`, then `plugins.map((p) => p.init())`.
+// The element has no type declaration to dispatch on, only members, so the type clause above gave
+// the parameter nothing and every call on it was unresolved; an annotated parameter of the same type
+// resolved, through param_shape_target. The element reference is the same one, read as a shape.
+lambda_param_shape(lp, sh) :- lambda_param_ref(lp, c),
+ ref_shape_target(c, sh).
// The parameter's reference is its declared reference, as an annotation's would be.
expr_decl_ref(e, c) :- expr_referenced("client", "PARAMETER", p, e),
lambda_param_ref(p, c).
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/type-resolution.dl b/graph/typescript/engine/resolution/type-resolution.dl
index 2c8e5ef2..0d3cff9f 100644
--- a/graph/typescript/engine/resolution/type-resolution.dl
+++ b/graph/typescript/engine/resolution/type-resolution.dl
@@ -587,6 +587,19 @@ ref_element_target(ref, prov, t) :- type_ref(rp, k, _, _, _, _, _, ref),
ref_kind_is_array(k),
type_ref_parent(rp, ref, _, child),
ref_type_target(child, prov, t).
+// …and of a TYPE VARIABLE constrained to an array or a tuple: `(ms: Ms)`,
+// `(...modules: Ms)`, then `modules.map((m) => m.init())`. The
+// constraint is what every element is at least, exactly as it is for a `` receiver
+// (ref_type_target above), so the callback's parameter is typed by the constraint's element. Without it
+// the element of a generic list had no type, and every call on it was unresolved. Same two routes as
+// ref_type_target: the exact link to the declaration, and the name in scope where the link is absent.
+ref_element_target(ref, prov, t) :- type_ref_type_param(rp, tp, ref),
+ type_param_constraint(rp, cref, tp),
+ ref_element_target(cref, prov, t).
+ref_element_target(ref, prov, t) :- type_ref(_, "TYPE_VARIABLE", _, tn, _, owner, _, ref),
+ !type_ref_type_param(_, _, ref),
+ type_var_constraint_in_scope(owner, tn, cref),
+ ref_element_target(cref, prov, t).
// ── ref_element_shape(Ref, Shape) — an element that is ITSELF a function type ──
// `fns: readonly ((data: T) => boolean)[]` then `fns.every((fn) => fn(1))`. The element
diff --git a/graph/typescript/engine/resolution/value-flow.dl b/graph/typescript/engine/resolution/value-flow.dl
index 496892d3..284c6e88 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),
@@ -261,6 +257,106 @@ handed_function(ce, m) :- hof_boundary_site(ce), !collection_store_site(ce), exp
value_branch(a, x),
method_value(x, m).
+// …AND A PROXY HANDLER'S TRAPS. `new Proxy(target, handler)` and `Proxy.revocable(target, handler)` hand the runtime
+// an object whose `get` / `set` / `has` … the runtime calls on every touch of the proxy. No call site names a trap, so
+// what the traps call was reached by nothing: a change to the draft logic of a Proxy-based library selected none of the
+// tests that create drafts. The creating site hands each trap the handler defines, the way `xs.map(cb)` hands `cb`.
+// The handler is followed through the holders it is read from (a const, a reassigned `let`, a parameter it was passed
+// as: holder_value), to an object literal's methods or the methods of the class a `new C()` builds, its bases included.
+proxy_site(ce) :- call_site("client", "CONSTRUCTOR_CALL", "Proxy", _, _, ce, _), !type_decl("client", "Proxy", _, _, _, _, _).
+proxy_site(ce) :- call_site("client", _, "revocable", "IDENTIFIER", recv, ce, _), expr_name("client", "Proxy", recv),
+ !type_decl("client", "Proxy", _, _, _, _, _).
+proxy_handler_value(ce, a) :- proxy_site(ce), expr_child("client", ce, "ARGUMENT", "1", a).
+proxy_handler_value(ce, v) :- proxy_handler_value(ce, e), expr_referenced("client", k, h, e), holder_ref_kind(k),
+ holder_value(h, v).
+proxy_handler_value(ce, x) :- proxy_handler_value(ce, e), value_branch_of(e, x).
+handed_function(ce, m) :- proxy_handler_value(ce, o), objlit_member_fn(o, name, m), proxy_trap_name(name).
+handed_function(ce, m) :- proxy_handler_value(ce, n), expr_kind("client", "NEW_EXPRESSION", _, n), expr_type(n, _, t),
+ proxy_handler_class(t, c), method_owner("client", c, m), method_decl("client", name, _, _, m), proxy_trap_name(name).
+proxy_handler_class(t, t) :- type_decl("client", _, _, _, _, _, t).
+proxy_handler_class(t, a) :- type_decl("client", _, _, _, _, _, t), type_ancestor(t, a).
+// a conditional or a cast between the holder and the site: `isArray ? arrayTraps : objectTraps`, `h as ProxyHandler`
+value_branch_of(e, x) :- expr_kind("client", "TERNARY_EXPRESSION", _, e), expr_child("client", e, r, _, x), ternary_value_role(r).
+value_branch_of(e, x) :- expr_kind("client", k, _, e), expr_kind_is_transparent(k), expr_child("client", e, _, _, x).
+ternary_value_role("TERNARY_THEN").
+ternary_value_role("TERNARY_ELSE").
+proxy_trap_name("get").
+proxy_trap_name("set").
+proxy_trap_name("has").
+proxy_trap_name("deleteProperty").
+proxy_trap_name("ownKeys").
+proxy_trap_name("getOwnPropertyDescriptor").
+proxy_trap_name("defineProperty").
+proxy_trap_name("getPrototypeOf").
+proxy_trap_name("setPrototypeOf").
+proxy_trap_name("isExtensible").
+proxy_trap_name("preventExtensions").
+proxy_trap_name("apply").
+proxy_trap_name("construct").
+
+// …AND A FUNCTION WRITTEN AS A JSX ATTRIBUTE OR CHILD. ``,
+// ``, ``: the element is handed the function and
+// the renderer runs it, on an event or while it renders. The parser writes each `{…}` as an expression
+// ROOT of its own, linked to no element (an intrinsic tag has no call site at all), so no rule above saw
+// it: a handler read off a field or named by reference was reached by nothing, and its whole body was
+// cut off from the tests that render the component and fire the event. The `{…}` itself is the site that
+// hands the function over, from the function it is written in, the way `xs.map(cb)` hands over `cb`;
+// what the value is read through is followed exactly as for an argument (value_branch, holders, a field
+// holding an arrow, a method read as a value, `bind`). A call there (`onClick={make()}`) hands over what
+// the call returns, not its callee, and stays out.
+jsx_value_root(r) :- expr_root_context("client", "JSX_EMBEDDED_EXPRESSION", r), expr_kind("client", _, "ROOT", r).
+value_branch(r, r) :- jsx_value_root(r).
+jsx_handed_value(r, m) :- jsx_value_root(r), value_branch(r, x),
+ !expr_kind("client", "CALL_EXPRESSION", _, x),
+ expr_callable(x, m).
+jsx_handed_value(r, m) :- jsx_value_root(r), value_branch(r, x),
+ expr_referenced("client", k, h, x), holder_ref_kind(k),
+ holder_holds_function(h, m).
+jsx_handed_value(r, m) :- jsx_value_root(r), value_branch(r, x),
+ ts_field_access_target(x, f),
+ holder_holds_function(f, m).
+jsx_handed_value(r, m) :- jsx_value_root(r), value_branch(r, x),
+ method_value(x, m).
+// the function the `{…}` is written in: its block's method, or the module initializer at top level
+jsx_root_caller(r, c) :- jsx_handed_value(r, _), expr_owner("client", "BLOCK", b, _, r), block_owner("client", c, _, b), c != "".
+jsx_root_caller(r, c) :- jsx_handed_value(r, _), expr_owner("client", "METHOD", c, _, r), c != "".
+jsx_root_caller(r, c) :- jsx_handed_value(r, _), expr_owner("client", "MODULE_INIT", _, _, r),
+ expr_module("client", mod, r), module_init("client", c, mod).
+
+// …AND A CLASS COMPONENT'S RENDER AND LIFECYCLE. ` ` where App is a class builds an instance
+// (callee-resolution rule 13 resolves the tag to the constructor), and from then on the renderer
+// calls the instance's render and its lifecycle methods; nothing in the program does. Without these
+// the whole component below its constructor was unreachable from every test that mounts it. Only a
+// class that has a render method — its own or inherited — is a component.
+jsx_class_component_site(ce, t) :- call_site("client", "JSX_COMPONENT_CALL", cn, _, _, ce, cs),
+ cn != "",
+ call_module(cs, mod),
+ !callee_names_local(ce),
+ name_binds(mod, cn, _, "TYPE", t),
+ method_in_scope(t, "render", "false", _).
+jsx_lifecycle_name("render").
+jsx_lifecycle_name("componentDidMount").
+jsx_lifecycle_name("componentDidUpdate").
+jsx_lifecycle_name("componentWillUnmount").
+jsx_lifecycle_name("shouldComponentUpdate").
+jsx_lifecycle_name("getSnapshotBeforeUpdate").
+jsx_lifecycle_name("componentDidCatch").
+jsx_lifecycle_name("componentWillMount").
+jsx_lifecycle_name("componentWillReceiveProps").
+jsx_lifecycle_name("componentWillUpdate").
+jsx_lifecycle_name("UNSAFE_componentWillMount").
+jsx_lifecycle_name("UNSAFE_componentWillReceiveProps").
+jsx_lifecycle_name("UNSAFE_componentWillUpdate").
+handed_function(ce, m) :- jsx_class_component_site(ce, t),
+ jsx_lifecycle_name(n),
+ method_in_scope(t, n, "false", m).
+// the static ones the renderer calls on the class itself
+jsx_static_lifecycle_name("getDerivedStateFromProps").
+jsx_static_lifecycle_name("getDerivedStateFromError").
+handed_function(ce, m) :- jsx_class_component_site(ce, t),
+ jsx_static_lifecycle_name(n),
+ method_in_scope(t, n, "true", m).
+
// …AND WHAT A LIBRARY CALL WRAPPED, KEPT IN A HOLDER. `export const plugin = fp(async (app) => …)`, then
// `app.register(plugin)`: the holder keeps what a call with no client body RETURNED, so no holder rule above
// sees a function in it, and the registration reached nothing although the same literal handed to it bare
@@ -298,15 +394,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).
@@ -526,6 +618,20 @@ call_runs(ce, m) :- callee_branch(ce, x),
call_runs(ce, m) :- callee_branch(ce, x),
ts_field_access_target(x, f),
holder_holds_function(f, m).
+// A CALL THROUGH AN IMPORTED HOLDER. A package's public functions are often its default instance's methods,
+// exported as consts: `export const createDraft = immer.createDraft.bind(immer)`, `export const plain =
+// engine.plain`. The holder rules know what the const holds (method_value follows the read and `bind`), but a
+// call names the const through an import, and the two clauses above take only a local variable, a parameter
+// or a field: every caller outside the defining module — its whole test suite — ran nothing. expr_holder
+// follows the import, and a namespace member, to the exporting module's own variable, never by name.
+call_runs(ce, m) :- call_callee_expr(_, ce, x),
+ expr_referenced("client", "IMPORT_BINDING", _, x),
+ expr_holder(x, h),
+ holder_holds_function(h, m).
+call_runs(ce, m) :- call_callee_expr(_, ce, x),
+ expr_namespace_member(x),
+ expr_holder(x, h),
+ holder_holds_function(h, m).
// ============================================================================
// A FUNCTION ASSIGNED TO AN INTERFACE'S METHOD MEMBER IMPLEMENTS IT (#1283)
@@ -627,6 +733,11 @@ expr_holder(x, f) :- ts_field_access_target(x, f).
// an imported `const` is the exporting module's variable
expr_holder(x, v) :- expr_referenced("client", "IMPORT_BINDING", ih, x),
import_binds(ih, _, "VARIABLE", v).
+// …and so is one read off a namespace import, `api.createDraft`
+expr_holder(x, v) :- property_access_recv(x, n, q),
+ expr_namespace(q, _, mod),
+ module_export(_, mod, n, "VARIABLE", v).
+expr_namespace_member(x) :- property_access_recv(x, _, q), expr_namespace(q, _, _).
// ── holder_elem_fn(Holder, Fn): an element of the holder's collection is, or holds, Fn ──
holder_elem_fn(h, m) :- holder_elem_value(h, x),
diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl
index 467f2e70..500217c8 100644
--- a/graph/typescript/souffle/decls_all.dl
+++ b/graph/typescript/souffle/decls_all.dl
@@ -220,6 +220,19 @@
.decl collection_receiver(c0:symbol)
.decl keyed_collection_ref(c0:symbol)
.decl handed_function(c0:symbol,c1:symbol)
+.decl expr_namespace_member(c0:symbol)
+.decl jsx_value_root(c0:symbol)
+.decl jsx_handed_value(c0:symbol,c1:symbol)
+.decl jsx_root_caller(c0:symbol,c1:symbol)
+.decl jsx_class_component_site(c0:symbol,c1:symbol)
+.decl jsx_lifecycle_name(c0:symbol)
+.decl jsx_static_lifecycle_name(c0:symbol)
+.decl proxy_site(c0:symbol)
+.decl proxy_handler_value(c0:symbol,c1:symbol)
+.decl proxy_handler_class(c0:symbol,c1:symbol)
+.decl value_branch_of(c0:symbol,c1:symbol)
+.decl ternary_value_role(c0:symbol)
+.decl proxy_trap_name(c0:symbol)
.decl holder_wraps_handed_function(c0:symbol,c1:symbol) // (holder, fn): the holder keeps what a library call returned, and fn was handed to that call
.decl call_returns_data(c0:symbol) // the call's result is typed, and the type has no call signature
.decl call_result_callable(c0:symbol)
@@ -501,6 +514,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)
@@ -603,6 +617,11 @@
.decl unary_yields(c0:symbol,c1:symbol)
.decl universal_param_type_name(c0:symbol)
.decl unresolved_covered(c0:symbol)
+.decl package_import(c0:symbol,c1:symbol)
+.decl library_value(c0:symbol,c1:symbol)
+.decl builtin_global_object(c0:symbol)
+.decl call_passes_project_instance(c0:symbol)
+.decl library_receiver(c0:symbol,c1:symbol)
.decl unresolved_receiver(c0:symbol,c1:symbol,c2:symbol,c3:symbol)
.decl var_binding_method(c0:symbol,c1:symbol)
.decl var_binding_target(c0:symbol,c1:symbol,c2:symbol)
@@ -631,15 +650,39 @@
.decl ts_field_site_is_method(c0:symbol)
.decl field_site_excluded(c0:symbol,c1:symbol)
.decl ts_field_access_target(c0:symbol,c1:symbol)
+.decl ts_field_access_typed(c0:symbol,c1:symbol)
+.decl ts_field_access_is_typed(c0:symbol)
+.decl ts_objlit_owner(c0:symbol,c1:symbol)
.decl ts_field_access_kind(c0:symbol,c1:symbol)
.decl ts_field_access_count(c0:symbol,c1:number)
.decl ts_field_prov(c0:symbol,c1:symbol)
.decl ts_field_site_leaves_client(c0:symbol)
.decl ts_field_access_class(c0:symbol,c1:symbol)
.decl ts_field_access_named(c0:symbol)
+.decl ts_deco_expr_method(c0:symbol,c1:symbol)
+.decl ts_field_access_in_decorator(c0:symbol)
+.decl ts_param_default_method(c0:symbol,c1:symbol)
+.decl ts_param_default_expr(c0:symbol,c1:symbol)
+.decl ts_field_access_in_function(c0:symbol)
+.decl ts_field_access_placed(c0:symbol)
.decl ts_field_access_from(c0:symbol,c1:symbol)
.decl ts_field_access_has_row(c0:symbol)
.decl field_access(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol,c5:symbol)
+.decl ts_module_var(c0:symbol)
+.decl ts_var_read_target(c0:symbol,c1:symbol)
+.decl ts_var_read_from(c0:symbol,c1:symbol)
+.decl ts_var_read_named(c0:symbol)
+.decl ts_var_written(c0:symbol)
+.decl ts_var_write_only(c0:symbol)
+.decl ts_var_read_kind(c0:symbol,c1:symbol)
+.decl ts_decorated_method(c0:symbol,c1:symbol)
+.decl ts_under_decorator(c0:symbol,c1:symbol)
+.decl ts_var_read_decorated(c0:symbol,c1:symbol)
+.decl ts_type_query_site(c0:symbol,c1:symbol,c2:symbol)
+.decl ts_type_query_method(c0:symbol,c1:symbol)
+.decl ts_type_query_shadowed(c0:symbol)
+.decl ts_type_query_read(c0:symbol,c1:symbol)
+.decl ts_type_query_has_method(c0:symbol)
// ── TYPE USE (#663) — call-edge-generation/type_use.dl
.decl ts_type_use_kind_names_a_type(c0:symbol)
diff --git a/graph/typescript/souffle/export_manifest.tsv b/graph/typescript/souffle/export_manifest.tsv
index f319cddf..f9e9ca38 100644
--- a/graph/typescript/souffle/export_manifest.tsv
+++ b/graph/typescript/souffle/export_manifest.tsv
@@ -7,6 +7,7 @@ client_calls_lib client-to-lib-calls.csv
client_calls_lib_detail client-to-lib-navigation.csv
lib_boundary_reason lib-boundary-reason.csv
unresolved_receiver unresolved-receiver.csv
+library_receiver library-receiver.csv
method_has_unknown_call method-has-unknown-call.csv
entry_point entry-point.csv
entry_reachable entry-reachable.csv
diff --git a/hooks/hooks.json b/hooks/hooks.json
index 160dad33..8f6c0828 100644
--- a/hooks/hooks.json
+++ b/hooks/hooks.json
@@ -18,12 +18,6 @@
}
],
"AfterTool": [
- {
- "matcher": "read_file|grep_search|glob|run_shell_command|replace|write_file",
- "hooks": [
- { "type": "command", "command": "node \"${extensionPath}${/}plugins${/}axiomcode${/}hooks${/}run.js\" enrich.py", "timeout": 20000 }
- ]
- },
{
"matcher": "run_shell_command",
"hooks": [
diff --git a/package.json b/package.json
index 4fd812a5..448fe575 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
"schema-doc": "tsx graph/bundle/cli.ts --print-schema > graph/bundle/SCHEMA.md"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=22.13.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
@@ -55,10 +55,13 @@
"LICENSE.md"
],
"dependencies": {
+ "entities": "^6.0.0",
"sax": "^1.4.4",
"tree-sitter": "^0.21.1",
"tree-sitter-c-sharp": "0.23.1",
+ "tree-sitter-css": "0.23.0",
"tree-sitter-groovy": "^0.1.2",
+ "tree-sitter-html": "0.23.2",
"tree-sitter-java": "0.23.4",
"tree-sitter-python": "^0.21.0",
"typescript": "^5.3.3",
diff --git a/parser/README.md b/parser/README.md
index c6c5517a..69dab522 100644
--- a/parser/README.md
+++ b/parser/README.md
@@ -14,13 +14,17 @@
+
+
+
+
-Full semantic resolution for Python and Java . TypeScript and JavaScript with a binder, from the compiler's syntax layer. C# with each file read under the framework and preprocessor symbols of the project that compiles it. Build-graph and dependency resolution for Gradle . Structural extraction for XML, YAML, Properties and META-INF/services.
+Full semantic resolution for Python and Java . TypeScript and JavaScript with a binder, from the compiler's syntax layer. C# with each file read under the framework and preprocessor symbols of the project that compiles it. HTML and CSS (beta) as one front end: the element tree, every URL a page names resolved to a file, event-handler calls, template expressions, selectors and declarations. Build-graph and dependency resolution for Gradle . Structural extraction for XML, YAML, Properties and META-INF/services.
[What it is](#what-this-is) |
[The IR](#the-intermediate-representation) |
@@ -89,6 +93,23 @@ same tables and be compared.
| **YAML** | Beta | 2 | yaml | Configuration entries with anchor and alias tracking, multi document support. |
| **META-INF/services** | Stable | 2 | custom | Provider-configuration files: the service each file configures, taken from its name, and every implementation class it names, with the file and line. |
| **Gradle** | Beta | 8 | tree-sitter-groovy | Scripts and their role in the build, blocks, declarations, dependency coordinates split into group/artifact/version, version catalogs, value references with resolution, comments, and parse gaps. Groovy and Kotlin DSL. |
+| **HTML** | Beta | 9 | tree-sitter-html | Documents with their doctype and template dialects, the element tree as written (nothing a browser would imply) with XPath-like paths, attributes by kind, class tokens, every URL a page names (scripts, stylesheets, links, images, forms, frames, htmx requests) classified and resolved to a file where one exists, `` — the body is the code; `bodyStartLine`…`bodyEndLine` locate it. */
+ INLINE = 'INLINE',
+}
+
+/**
+ * How the browser treats a `
+x
${name}
+