diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6c0200346..4218dbabb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -142,6 +142,14 @@ jobs:
run: |
cargo build --release --manifest-path gems/fact-mine/Cargo.toml
cargo build --release --manifest-path gems/nil-kill/Cargo.toml
+ # The collector IS this extension -- a collect loads it and no nil-kill
+ # Ruby at all -- so without it every spec that runs one fails to require
+ # it, and the specs guarded on its presence quietly test nothing.
+ - name: Build the collector extension
+ run: |
+ cd gems/nil-kill/ext/nil_kill_trace
+ ruby extconf.rb
+ make
- run: bundle exec rspec gems/nil-kill/spec
env:
FACT_MINE_RUST_BINARY: ./gems/fact-mine/target/release/fact-mine-rust
@@ -170,16 +178,16 @@ jobs:
- name: Install Jest oracle fixture runtime
run: npm install --no-save --no-package-lock jest@30.0.5
- uses: dtolnay/rust-toolchain@stable
- - name: Build Lineage release binary
+ - name: Build Gigasail release binary
# The ingestion round-trip test below skips silently (not fails)
# when this binary is absent, so without this step CI could report
# green while that contract was never actually exercised.
- run: cargo build --release --manifest-path gems/lineage/Cargo.toml
+ run: cargo build --release --manifest-path gems/gigasail/Cargo.toml --workspace
- name: Verify runner adapters, dynamic planning, and canonical checkpoints
run: bundle exec ruby -I gems/test-miser/test gems/test-miser/test/test_miser_test.rb
- name: Verify PR step summary tallies
run: bundle exec ruby -I gems/test-miser/test gems/test-miser/test/step_summary_test.rb
- - name: Verify Lineage corpus ingestion round-trip
+ - name: Verify Gigasail corpus ingestion round-trip
run: bundle exec ruby -I gems/test-miser/test gems/test-miser/test/lineage_ingest_integration_test.rb
- name: Verify Test Miser evidence analyzers and product formats
run: |
@@ -229,7 +237,7 @@ jobs:
ruby-gems-coverage:
name: Ruby gems coverage
- needs: changes
+ needs: [changes, sarif-rust-binaries]
if: ${{ needs.changes.outputs.run_gems == 'true' }}
runs-on: ubuntu-latest
env:
@@ -250,18 +258,27 @@ jobs:
- name: Install Tree-sitter grammars
run: npm install --legacy-peer-deps
- uses: dtolnay/rust-toolchain@stable
- - name: Build Rust binaries
+ - uses: actions/download-artifact@v4
+ with:
+ name: sarif-rust-binaries
+ path: tmp/sarif-binaries
+ - name: Place the shared Rust binaries where the tests look for them
# gems/slopcop/test/cross_gem_contract_integration_test.rb and
# gems/espalier/test/architecture_tools_test.rb both exercise the
- # Lineage binary; building it here is required, not optional - the
+ # gigasail binary; having it here is required, not optional - the
# SlopCop test hard-flunks without it (this job would otherwise be
# broken), and the Espalier one silently skips without it (so its
# contract would go untested with no visible failure).
+ #
+ # They are built once by `sarif-rust-binaries` and copied to the
+ # target/release paths the tests resolve, rather than four crates
+ # being compiled again on this runner.
run: |
- cargo build --release --manifest-path gems/fact-mine/Cargo.toml
- cargo build --release --manifest-path gems/nil-kill/Cargo.toml
- cargo build --release --manifest-path gems/decomplex/Cargo.toml
- cargo build --release --manifest-path gems/lineage/Cargo.toml
+ chmod +x tmp/sarif-binaries/*
+ install -Dm755 tmp/sarif-binaries/fact-mine-rust gems/fact-mine/target/release/fact-mine-rust
+ install -Dm755 tmp/sarif-binaries/nil-kill-infer-rust gems/nil-kill/target/release/nil-kill-infer-rust
+ install -Dm755 tmp/sarif-binaries/decomplex-rust gems/decomplex/target/release/decomplex-rust
+ install -Dm755 tmp/sarif-binaries/giga gems/gigasail/target/release/giga
- name: Run Ruby gem tests with SimpleCov
run: bundle exec ruby tools/run_ruby_gem_coverage.rb
env:
@@ -356,8 +373,8 @@ jobs:
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- lineage-rust-coverage:
- name: Lineage Rust coverage
+ gigasail-rust-coverage:
+ name: Gigasail Rust coverage
needs: changes
if: ${{ needs.changes.outputs.run_gems == 'true' }}
runs-on: ubuntu-latest
@@ -365,24 +382,24 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-llvm-cov
- - name: Run cargo llvm-cov for Lineage
+ - name: Run cargo llvm-cov for Gigasail
run: |
- mkdir -p tmp/lineage-rust-coverage
+ mkdir -p tmp/gigasail-rust-coverage
cargo llvm-cov \
- --manifest-path gems/lineage/Cargo.toml \
+ --manifest-path gems/gigasail/Cargo.toml --workspace --all-targets \
--all-features \
--cobertura \
- --output-path ${{ github.workspace }}/tmp/lineage-rust-coverage/cobertura.xml
+ --output-path ${{ github.workspace }}/tmp/gigasail-rust-coverage/cobertura.xml
- uses: actions/upload-artifact@v4
with:
- name: lineage-rust-coverage
- path: tmp/lineage-rust-coverage/cobertura.xml
+ name: gigasail-rust-coverage
+ path: tmp/gigasail-rust-coverage/cobertura.xml
retention-days: 7
- uses: codecov/codecov-action@v5
with:
- files: ./tmp/lineage-rust-coverage/cobertura.xml
+ files: ./tmp/gigasail-rust-coverage/cobertura.xml
disable_search: true
- flags: lineage-rust
+ flags: gigasail-rust
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
@@ -439,7 +456,7 @@ jobs:
fact-mine-rust-coverage:
name: FactMine Rust coverage
- needs: changes
+ needs: [changes, sarif-rust-binaries]
if: ${{ needs.changes.outputs.run_gems == 'true' }}
runs-on: ubuntu-latest
steps:
@@ -461,19 +478,25 @@ jobs:
run: npm install --legacy-peer-deps
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-llvm-cov
+ - uses: actions/download-artifact@v4
+ with:
+ name: sarif-rust-binaries
+ path: tmp/sarif-binaries
- name: Run cargo llvm-cov for FactMine
run: |
mkdir -p tmp/fact-mine-rust-coverage
cargo llvm-cov clean --manifest-path gems/fact-mine/Cargo.toml
cargo llvm-cov --no-report --manifest-path gems/fact-mine/Cargo.toml --all-features
- cargo build --release --manifest-path gems/fact-mine/Cargo.toml
- cargo build --release --manifest-path gems/nil-kill/Cargo.toml
- cargo build --release --manifest-path gems/decomplex/Cargo.toml
- cargo build --release --manifest-path gems/lineage/Cargo.toml
- # The release FactMine binary is built once above. The focused Ruby
- # consumer suite reuses that same executable; LLVM coverage comes
- # from FactMine's instrumented Rust test run rather than rebuilding
- # a second binary for every cross-gem invocation.
+ # Only the instrumented build happens here -- it has to, coverage
+ # comes from it. The release binaries the consumer suite calls are
+ # the ones `sarif-rust-binaries` already built, placed where the
+ # tests look: several resolve target/release directly rather than
+ # reading the environment.
+ chmod +x tmp/sarif-binaries/*
+ install -Dm755 tmp/sarif-binaries/fact-mine-rust gems/fact-mine/target/release/fact-mine-rust
+ install -Dm755 tmp/sarif-binaries/nil-kill-infer-rust gems/nil-kill/target/release/nil-kill-infer-rust
+ install -Dm755 tmp/sarif-binaries/decomplex-rust gems/decomplex/target/release/decomplex-rust
+ install -Dm755 tmp/sarif-binaries/giga gems/gigasail/target/release/giga
FACT_MINE_RUST_BINARY=${{ github.workspace }}/gems/fact-mine/target/release/fact-mine-rust \
NIL_KILL_INFER_RUST_BINARY=${{ github.workspace }}/gems/nil-kill/target/release/nil-kill-infer-rust \
DECOMPLEX_RUST_BINARY=${{ github.workspace }}/gems/decomplex/target/release/decomplex-rust \
@@ -800,6 +823,12 @@ jobs:
with:
ruby-version: ${{ env.RUBY_VERSION }}
bundler-cache: true
+ # The corpus is traced by the collector, which is the C extension.
+ - name: Build the collector extension
+ run: |
+ cd gems/nil-kill/ext/nil_kill_trace
+ ruby extconf.rb
+ make
- run: NIL_KILL_JOBS="$(nproc)" tools/clear-nil-kill-transpile-corpus.sh
env:
NO_COLOR: "1"
@@ -1526,7 +1555,7 @@ jobs:
# any sanitizer constraint blocking.
name: SlopCop constraint coverage SARIF
runs-on: ubuntu-latest
- timeout-minutes: 25
+ timeout-minutes: 75
needs:
- changes
- zig-special-coverage
@@ -1579,7 +1608,7 @@ jobs:
args+=(--coverage="nil-kill:${path}")
done < <(find tmp/ruby-coverage-unit \( -name 'cobertura.xml' -o -name 'coverage.xml' \) -type f | sort)
fi
- timeout 20m env FACT_MINE_RUST_BINARY=tmp/sarif-binaries/fact-mine-rust bundle exec ruby gems/slopcop/exe/slopcop constraints \
+ timeout 60m env FACT_MINE_RUST_BINARY=tmp/sarif-binaries/fact-mine-rust bundle exec ruby gems/slopcop/exe/slopcop constraints \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -1601,23 +1630,32 @@ jobs:
retention-days: 7
architecture-sarif:
- # Architecture findings on changed files: dependency cycles
- # (espalier/tools/cycle_report.rb), cross-module private reach-through
- # (espalier/tools/reach_through_report.rb), and cross-module change
- # coupling (lineage/tools/change_coupling.rb). All three are advisory
- # (warning/note level); they annotate PRs and feed the Lineage ledger via
- # SARIF, they do not block merges.
+ # Cross-module change coupling (gigasail/tools/change_coupling.rb),
+ # advisory (warning/note level): it annotates PRs and feeds the Lineage
+ # ledger via SARIF, it does not block merges.
+ #
+ # cycle_report.rb and reach_through_report.rb are deliberately NOT run
+ # here. Both are WIP experiments (beb2fe8cf, "WIP anti-pattern experiment
+ # tools") and both cost a whole-corpus FactMine projection - together they
+ # were the entire runtime of this job.
+ #
+ # reach_through additionally cannot model package-scoped languages: it
+ # derives owners per file, so two files in one Go package read as a
+ # cross-module private call, which is what its 81 self-quarantined
+ # "suspect" findings are.
+ #
+ # cycle_report is the sounder of the two - its four findings here are real
+ # cycles, not artifacts - but it is off pending a measurement of whether
+ # those findings are worth acting on. If they are, it belongs inside
+ # Espalier as a library check next to privacy_analyzer.rb, not as a tool
+ # shelling out to a whole-corpus projection. The tools and their tests stay
+ # in the tree meanwhile.
name: Architecture SARIF
runs-on: ubuntu-latest
- # FactMine performs whole-corpus structural analysis. Keep one bounded
- # job-level deadline, rather than killing its Ruby parent mid-subprocess
- # and leaving Open3 reader threads to report a misleading stream-closed
- # error.
- timeout-minutes: 45
+ timeout-minutes: 15
needs:
- changes
- - sarif-rust-binaries
- if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true') && needs.sarif-rust-binaries.result == 'success' }}
+ if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true') }}
permissions:
contents: read
security-events: write
@@ -1629,33 +1667,13 @@ jobs:
with:
ruby-version: ${{ env.RUBY_VERSION }}
bundler-cache: true
- - uses: actions/download-artifact@v4
- with:
- name: sarif-rust-binaries
- path: tmp/sarif-binaries
- name: Generate architecture SARIFs
run: |
- chmod +x tmp/sarif-binaries/fact-mine-rust
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
mkdir -p tmp
- export FACT_MINE_RUST_BINARY=$PWD/tmp/sarif-binaries/fact-mine-rust
base="origin/${{ github.event.pull_request.base.ref }}"
- ruby gems/espalier/tools/cycle_report.rb . \
- --base="$base" --sarif=tmp/arch-cycles.sarif
- ruby gems/espalier/tools/reach_through_report.rb . \
- --base="$base" --sarif=tmp/arch-reach-through.sarif
- ruby gems/lineage/tools/change_coupling.rb . 8 \
+ ruby gems/gigasail/tools/change_coupling.rb . 8 \
--base="$base" --sarif=tmp/arch-change-coupling.sarif
- - name: Upload cycles SARIF
- uses: github/codeql-action/upload-sarif@v4
- with:
- sarif_file: tmp/arch-cycles.sarif
- category: architecture-cycles
- - name: Upload reach-through SARIF
- uses: github/codeql-action/upload-sarif@v4
- with:
- sarif_file: tmp/arch-reach-through.sarif
- category: architecture-reach-through
- name: Upload change-coupling SARIF
uses: github/codeql-action/upload-sarif@v4
with:
@@ -1676,21 +1694,32 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
- if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true' || needs.changes.outputs.run_zig == 'true') }}
+ # Not restricted to same-repo pull requests any more: the coverage jobs
+ # consume these binaries too, and they run on pushes. The SARIF jobs keep
+ # their own restriction, which is about where a SARIF upload may come from
+ # rather than about compiling anything.
+ if: ${{ needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true' || needs.changes.outputs.run_zig == 'true' }}
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
+ # Every release binary the run consumes is built here, once, and
+ # downloaded by the jobs that need one. Three jobs used to build the same
+ # crates from scratch on three fresh runners.
- name: Build release binaries once
run: |
cargo build --release --manifest-path gems/fact-mine/Cargo.toml
cargo build --release --manifest-path gems/decomplex/Cargo.toml
cargo build --release --manifest-path gems/sql-cov/Cargo.toml
+ cargo build --release --manifest-path gems/nil-kill/Cargo.toml
+ cargo build --release --manifest-path gems/gigasail/Cargo.toml --workspace
mkdir -p tmp/sarif-binaries
install -m 755 gems/fact-mine/target/release/fact-mine-rust tmp/sarif-binaries/fact-mine-rust
install -m 755 gems/decomplex/target/release/decomplex-rust tmp/sarif-binaries/decomplex-rust
install -m 755 gems/sql-cov/target/release/sql-cov tmp/sarif-binaries/sql-cov
+ install -m 755 gems/nil-kill/target/release/nil-kill-infer-rust tmp/sarif-binaries/nil-kill-infer-rust
+ install -m 755 gems/gigasail/target/release/giga tmp/sarif-binaries/giga
- uses: actions/upload-artifact@v4
with:
name: sarif-rust-binaries
@@ -1701,7 +1730,7 @@ jobs:
decomplex-sarif:
name: Decomplex SARIF
runs-on: ubuntu-latest
- timeout-minutes: 20
+ timeout-minutes: 50
needs: [changes, sarif-rust-binaries]
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.sarif-rust-binaries.result == 'success' }}
permissions:
@@ -1723,7 +1752,7 @@ jobs:
run: |
chmod +x tmp/sarif-binaries/decomplex-rust
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
- timeout 15m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
+ timeout 40m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -1744,7 +1773,7 @@ jobs:
boobytrap-sarif:
name: Boobytrap SARIF
runs-on: ubuntu-latest
- timeout-minutes: 25
+ timeout-minutes: 75
needs:
- changes
- ruby-gems-coverage
@@ -1756,7 +1785,7 @@ jobs:
- bc-lower-coverage-shard
- zig-coverage
- zig-mutants-coverage
- - lineage-rust-coverage
+ - gigasail-rust-coverage
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true' || needs.changes.outputs.run_zig == 'true') && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
permissions:
contents: read
@@ -1785,12 +1814,12 @@ jobs:
with:
pattern: zig-coverage-*
path: tmp/sarif-coverage
- - name: Download Lineage Rust coverage artifact
- if: ${{ needs['lineage-rust-coverage'].result == 'success' }}
+ - name: Download Gigasail Rust coverage artifact
+ if: ${{ needs['gigasail-rust-coverage'].result == 'success' }}
uses: actions/download-artifact@v4
with:
- name: lineage-rust-coverage
- path: tmp/sarif-coverage/lineage-rust-coverage
+ name: gigasail-rust-coverage
+ path: tmp/sarif-coverage/gigasail-rust-coverage
- name: Generate Boobytrap report
run: |
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
@@ -1804,7 +1833,7 @@ jobs:
! -path '*zig-coverage-vopr-*' \
| sort
)
- timeout 20m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
+ timeout 60m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -1826,7 +1855,7 @@ jobs:
slopcop-sarif:
name: SlopCop SARIF
runs-on: ubuntu-latest
- timeout-minutes: 25
+ timeout-minutes: 75
needs:
- changes
- ruby-gems-coverage
@@ -1838,7 +1867,7 @@ jobs:
- bc-lower-coverage-shard
- zig-coverage
- zig-mutants-coverage
- - lineage-rust-coverage
+ - gigasail-rust-coverage
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && (needs.changes.outputs.run_gems == 'true' || needs.changes.outputs.run_src == 'true' || needs.changes.outputs.run_zig == 'true') && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
permissions:
contents: read
@@ -1867,12 +1896,12 @@ jobs:
with:
pattern: zig-coverage-*
path: tmp/sarif-coverage
- - name: Download Lineage Rust coverage artifact
- if: ${{ needs['lineage-rust-coverage'].result == 'success' }}
+ - name: Download Gigasail Rust coverage artifact
+ if: ${{ needs['gigasail-rust-coverage'].result == 'success' }}
uses: actions/download-artifact@v4
with:
- name: lineage-rust-coverage
- path: tmp/sarif-coverage/lineage-rust-coverage
+ name: gigasail-rust-coverage
+ path: tmp/sarif-coverage/gigasail-rust-coverage
- name: Generate SlopCop report
run: |
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
@@ -1886,7 +1915,7 @@ jobs:
! -path '*zig-coverage-vopr-*' \
| sort
)
- timeout 20m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
+ timeout 60m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -1908,7 +1937,7 @@ jobs:
espalier-sarif:
name: Espalier SARIF
runs-on: ubuntu-latest
- timeout-minutes: 20
+ timeout-minutes: 50
needs: [changes, sarif-rust-binaries]
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.sarif-rust-binaries.result == 'success' }}
permissions:
@@ -1937,7 +1966,7 @@ jobs:
run: |
chmod +x tmp/sarif-binaries/fact-mine-rust
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
- timeout 15m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
+ timeout 40m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -1957,7 +1986,7 @@ jobs:
nil-kill-sarif:
name: Nil-Kill SARIF
runs-on: ubuntu-latest
- timeout-minutes: 20
+ timeout-minutes: 50
needs: [changes, sarif-rust-binaries]
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && needs.sarif-rust-binaries.result == 'success' }}
permissions:
@@ -1990,7 +2019,7 @@ jobs:
run: |
chmod +x tmp/sarif-binaries/fact-mine-rust
git fetch origin "+${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}"
- timeout 15m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
+ timeout 40m bundle exec ruby tools/generate_generalized_gem_sarif.rb \
--repo=. \
--base="origin/${{ github.event.pull_request.base.ref }}" \
--head=HEAD \
@@ -2032,7 +2061,7 @@ jobs:
timeout 10m ./tools/generate_sql_cov_sarif.rb \
--repo=. \
--out-dir=tmp/sql-cov-sarif \
- --setup=gems/lineage/sql/storage/init_schema.sql \
+ --setup=gems/gigasail/giga-core/sql/storage/init_schema.sql \
--sql-cov-bin=tmp/sarif-binaries/sql-cov
- uses: github/codeql-action/upload-sarif@v4
with:
@@ -2068,7 +2097,7 @@ jobs:
--repo=. \
--out-dir=tmp/sqlfluff-sarif \
--config=.sqlfluff \
- --sql-path=gems/lineage/sql
+ --sql-path=gems/gigasail/giga-core/sql
- uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: tmp/sqlfluff-sarif/sqlfluff.sarif
diff --git a/.github/workflows/mutants-manual.yml b/.github/workflows/mutants-manual.yml
index a1ef33099..318f7ffa8 100644
--- a/.github/workflows/mutants-manual.yml
+++ b/.github/workflows/mutants-manual.yml
@@ -175,13 +175,16 @@ jobs:
git fetch --no-tags --depth=1 "https://github.com/${GITHUB_REPOSITORY}.git" "${BASE_SHA}"
args+=(--since "${BASE_SHA}")
fi
- MUTANT_JOBS="$(nproc)" bundle exec ruby gems/lineage/tools/mutant-converters/ruby_mutant.rb "${args[@]}"
+ MUTANT_JOBS="$(nproc)" bundle exec ruby gems/gigasail/tools/mutant-converters/ruby_mutant.rb "${args[@]}"
+ # Store facts compressed (giga ingest-mutants reads .gz transparently).
+ facts="/tmp/clear-ruby-mutants-${{ matrix.shard }}/mutant-facts.json"
+ [ -f "$facts" ] && gzip -f "$facts"
- uses: actions/upload-artifact@v4
if: always()
with:
name: ruby-mutant-facts-${{ matrix.shard }}
path: |
- /tmp/clear-ruby-mutants-${{ matrix.shard }}/mutant-facts.json
+ /tmp/clear-ruby-mutants-${{ matrix.shard }}/mutant-facts.json.gz
/tmp/clear-ruby-mutants-${{ matrix.shard }}/*.log
if-no-files-found: warn
retention-days: 14
diff --git a/.gitignore b/.gitignore
index f77339bcf..94e60e7a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,11 @@
!/GEMINI.md
!/Gemfile
!/Gemfile.lock
+/.cache/
+/.ci-local/
+/kcov-bin/
+/nil-kill-golden*/
+/nil-kill-missing-evidence*/
!/gems/
!/gems/**
!/LICENSE
@@ -30,6 +35,11 @@
!/compiler/**
!/docs/
!/examples/
+/.cache/
+/.ci-local/
+/kcov-bin/
+/nil-kill-golden*/
+/nil-kill-missing-evidence*/
!/gems/
!/manifesto/
!/site/
@@ -202,3 +212,4 @@ gems/decomplex/target/
gems/fact-mine/target/
gems/hazard-contract/target/
gems/nil-kill/target/
+kcov-bin/
diff --git a/Gemfile.lock b/Gemfile.lock
index 19e47ddda..4b61b2fdf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -37,6 +37,7 @@ PATH
boobytrap (>= 0.0.1)
decomplex (= 0.0.1)
espalier (>= 0.0.1)
+ google-protobuf (>= 4.31, < 5)
parlour
rbs-trace
sorbet-runtime
@@ -120,6 +121,9 @@ GEM
path_expander (~> 2.0)
prism (~> 1.7)
sexp_processor (~> 4.8)
+ google-protobuf (4.35.1-x86_64-linux-gnu)
+ bigdecimal
+ rake (~> 13.3)
highline (3.0.1)
ice_nine (0.11.2)
io-console (0.8.2)
diff --git a/clear b/clear
index 7e206c77c..b71b4cd14 100755
--- a/clear
+++ b/clear
@@ -1401,7 +1401,11 @@ when 'test'
merged = ZigCoverageSupport.merge!('examples-benchmarks')
puts "Merged Zig coverage: #{merged}" if merged
end
- exit(failed_names.any? ? 1 : 0)
+ # A detected memory leak is a failure, not a warning: the run prints
+ # "MEMORY LEAKS: N" but must also exit non-zero, or `clear test
`
+ # reports success on a leak and any caller (CI, an agent, a human checking
+ # $?) is silently told the suite is clean.
+ exit((failed_names.any? || leak_tests.any?) ? 1 : 0)
else
source = File.expand_path(source)
gen_script = File.join(CLEAR_ROOT, 'transpile-tests', 'gen.rb')
diff --git a/codecov.yml b/codecov.yml
index bfae30e49..849fbb65d 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -54,9 +54,9 @@ flags:
- zig/
carryforward: false
joined: false
- lineage-rust:
+ gigasail-rust:
paths:
- - gems/lineage/
+ - gems/gigasail/
carryforward: false
joined: false
go:
diff --git a/compiler/spec/minivm_bc_run_env_spec.rb b/compiler/spec/minivm_bc_run_env_spec.rb
index 58f606658..b9a98037a 100644
--- a/compiler/spec/minivm_bc_run_env_spec.rb
+++ b/compiler/spec/minivm_bc_run_env_spec.rb
@@ -21,8 +21,8 @@
it "preserves RUBYOPT while nil-kill source instrumentation is active" do
ENV["NIL_KILL_TRACE"] = "1"
- ENV["RUBYOPT"] = "-r./gems/nil-kill/lib/nil_kill/runtime_trace.rb"
+ ENV["RUBYOPT"] = "-r./gems/nil-kill/ext/nil_kill_trace/nil_kill_trace.so"
- expect(Object.new.send(:clear_build_env)["RUBYOPT"]).to eq("-r./gems/nil-kill/lib/nil_kill/runtime_trace.rb")
+ expect(Object.new.send(:clear_build_env)["RUBYOPT"]).to eq("-r./gems/nil-kill/ext/nil_kill_trace/nil_kill_trace.so")
end
end
diff --git a/compiler/spec/mutant_tools_spec.rb b/compiler/spec/mutant_tools_spec.rb
index c9c74ede8..408260109 100644
--- a/compiler/spec/mutant_tools_spec.rb
+++ b/compiler/spec/mutant_tools_spec.rb
@@ -1,9 +1,9 @@
require "tmpdir"
-require_relative "../../gems/lineage/tools/mutant-converters/support" unless defined?(MutationTesting)
-require_relative "../../gems/lineage/tools/mutant-converters/ruby_mutant" unless defined?(RubySpecMutants)
-require_relative "../../gems/lineage/tools/mutant-converters/semantic_mutant" unless defined?(SemanticMutants)
-load File.expand_path("../../gems/lineage/tools/mutant-converters/zig-mutants", __dir__) unless defined?(Lineage::MutantConverters::ZigMutants)
+require_relative "../../gems/gigasail/tools/mutant-converters/support" unless defined?(MutationTesting)
+require_relative "../../gems/gigasail/tools/mutant-converters/ruby_mutant" unless defined?(RubySpecMutants)
+require_relative "../../gems/gigasail/tools/mutant-converters/semantic_mutant" unless defined?(SemanticMutants)
+load File.expand_path("../../gems/gigasail/tools/mutant-converters/zig-mutants", __dir__) unless defined?(Lineage::MutantConverters::ZigMutants)
RSpec.describe MutationTesting do
describe ".parse_mutant_summary" do
diff --git a/compiler/spec/sql_cov_sarif_generator_spec.rb b/compiler/spec/sql_cov_sarif_generator_spec.rb
index 1c632094c..95c405df8 100644
--- a/compiler/spec/sql_cov_sarif_generator_spec.rb
+++ b/compiler/spec/sql_cov_sarif_generator_spec.rb
@@ -14,7 +14,7 @@
it "uploads only native advisory hazards and reports unresolved facts" do
Dir.mktmpdir("sql-cov-sarif-spec") do |dir|
repo = File.join(dir, "repo")
- sql_dir = File.join(repo, "gems/lineage/sql/storage")
+ sql_dir = File.join(repo, "gems/gigasail/giga-core/sql/storage")
out_dir = File.join(dir, "out")
fake_bin = File.join(dir, "fake-sql-cov")
argv_log = File.join(dir, "argv.json")
@@ -55,7 +55,7 @@
generator,
"--repo=#{repo}",
"--out-dir=#{out_dir}",
- "--setup=gems/lineage/sql/storage/init_schema.sql",
+ "--setup=gems/gigasail/giga-core/sql/storage/init_schema.sql",
"--sql-cov-bin=#{fake_bin}"
)
expect(status).to be_success, stderr
@@ -68,7 +68,7 @@
expect(run.dig("properties", "scannedFiles")).to eq(1)
expect(run.dig("properties", "skippedNonQueryFiles")).to be_empty
expect(run.dig("results", 0, "locations", 0, "physicalLocation", "artifactLocation", "uri"))
- .to eq("gems/lineage/sql/storage/query.sql")
+ .to eq("gems/gigasail/giga-core/sql/storage/query.sql")
markdown = File.read(File.join(out_dir, "sql-cov.md"))
expect(markdown).to include("1 unresolved schema facts")
@@ -80,7 +80,7 @@
it "normalizes generated identifiers, skips non-query SQL, and fails on scan errors" do
Dir.mktmpdir("sql-cov-sarif-spec") do |dir|
repo = File.join(dir, "repo")
- sql_dir = File.join(repo, "gems/lineage/sql/storage")
+ sql_dir = File.join(repo, "gems/gigasail/giga-core/sql/storage")
out_dir = File.join(dir, "out")
fake_bin = File.join(dir, "fake-sql-cov")
input_log = File.join(dir, "input.sql")
@@ -107,7 +107,7 @@
generator,
"--repo=#{repo}",
"--out-dir=#{out_dir}",
- "--setup=gems/lineage/sql/storage/init_schema.sql",
+ "--setup=gems/gigasail/giga-core/sql/storage/init_schema.sql",
"--sql-cov-bin=#{fake_bin}"
)
expect(status).to be_success, stderr
@@ -116,7 +116,7 @@
run = JSON.parse(File.read(File.join(out_dir, "sql-cov.sarif"))).fetch("runs").first
expect(run.dig("properties", "scannedFiles")).to eq(1)
expect(run.dig("properties", "skippedNonQueryFiles"))
- .to eq(["gems/lineage/sql/storage/configure.sql"])
+ .to eq(["gems/gigasail/giga-core/sql/storage/configure.sql"])
File.write(fake_bin, "#!/usr/bin/env ruby\nwarn 'cannot scan'\nexit 2\n")
_stdout, stderr, status = Open3.capture3(
@@ -124,7 +124,7 @@
generator,
"--repo=#{repo}",
"--out-dir=#{out_dir}",
- "--setup=gems/lineage/sql/storage/init_schema.sql",
+ "--setup=gems/gigasail/giga-core/sql/storage/init_schema.sql",
"--sql-cov-bin=#{fake_bin}"
)
expect(status).not_to be_success
diff --git a/docs/agents/csmith.md b/docs/agents/csmith.md
index 767e61927..403471af6 100644
--- a/docs/agents/csmith.md
+++ b/docs/agents/csmith.md
@@ -1049,7 +1049,7 @@ paired with much stronger mutation evidence.
The permanent command is:
```text
-bundle exec ruby gems/lineage/tools/mutant-converters/semantic_mutant.rb \
+bundle exec ruby gems/gigasail/tools/mutant-converters/semantic_mutant.rb \
--out /tmp/clear-semantic-mutants --jobs 32 --timeout 60 --min-new-kills 1
```
@@ -1233,7 +1233,7 @@ the semantic adapter and historical template as independent coverage.
The permanent differential command is:
```text
-bundle exec ruby gems/lineage/tools/mutant-converters/semantic_mutant.rb \
+bundle exec ruby gems/gigasail/tools/mutant-converters/semantic_mutant.rb \
--out /tmp/clear-semantic-mutants-final-148 \
--jobs 32 --timeout 60 --min-new-kills 1
```
diff --git a/docs/agents/mutants.md b/docs/agents/mutants.md
index 68e01035e..c6d9aea38 100644
--- a/docs/agents/mutants.md
+++ b/docs/agents/mutants.md
@@ -3,7 +3,7 @@
This branch turns mutation testing on for the three test surfaces that carry
compiler correctness:
-- Ruby specs: `gems/lineage/tools/mutant-converters/ruby_mutant.rb`
+- Ruby specs: `gems/gigasail/tools/mutant-converters/ruby_mutant.rb`
- transpile-tests: `tools/mutants/transpile_tests.rb`
- fuzz templates: `tools/fuzz/mutants/run.rb`
@@ -42,7 +42,7 @@ cannot produce a summary, has no selected tests, drops below its baseline, or
exceeds its timeout budget. Advisory subjects still run and report coverage, but
do not block CI until they are promoted.
-The subject matrix lives in `gems/lineage/tools/mutant-converters/src_subjects.yml`.
+The subject matrix lives in `gems/gigasail/tools/mutant-converters/src_subjects.yml`.
Current matrix:
@@ -69,7 +69,7 @@ Important implementation details:
Current local validation:
```sh
-MUTANT_JOBS=32 bundle exec ruby gems/lineage/tools/mutant-converters/ruby_mutant.rb --since HEAD --out /tmp/clear-ruby-mutants-full-2
+MUTANT_JOBS=32 bundle exec ruby gems/gigasail/tools/mutant-converters/ruby_mutant.rb --since HEAD --out /tmp/clear-ruby-mutants-full-2
```
Result: exit 0. Hard-gated changed subjects passed; untouched subjects skipped;
@@ -251,5 +251,5 @@ Additional validation on this branch:
- `bundle exec prspec`: 6,167 examples, 0 failures.
- `bundle exec prspec spec/ --tag integration`: 237 examples, 0 failures.
- `bundle exec srb tc`: no errors.
-- Mutation tooling syntax check: all `gems/lineage/tools/mutant-converters/**/*.rb`,
+- Mutation tooling syntax check: all `gems/gigasail/tools/mutant-converters/**/*.rb`,
`tools/mutants/**/*.rb`, and `tools/fuzz/mutants/**/*.rb` parsed successfully.
diff --git a/docs/agents/runtime-evidence-v1-design.md b/docs/agents/runtime-evidence-v1-design.md
new file mode 100644
index 000000000..4fd416b8b
--- /dev/null
+++ b/docs/agents/runtime-evidence-v1-design.md
@@ -0,0 +1,485 @@
+# Runtime Semantic Evidence v1
+
+Status: implementation architecture. The prior ad-hoc, unused
+`fact-mine.runtime-value-evidence.v1` shape is replaced in place. There is no
+compatibility contract and no dual emission.
+
+## Decision
+
+Runtime analysis uses one versioned, language-neutral protocol with two
+top-level messages:
+
+1. FactMine emits a **Trace Plan** containing exact, typed source anchors and the
+ evidence required at each anchor.
+2. NilKill consumes that plan and emits **Runtime Semantic Evidence** containing
+ only observed runtime facts, capture status, and provenance for those exact
+ anchors.
+
+FactMine is the sole owner of source parsing, CFG/DFG propagation, semantic
+closure, SCIP generation, and Big-O completeness. NilKill is the sole owner of
+executing workloads and faithfully observing runtime values and dispatch.
+
+The canonical schema is Protobuf, like SCIP. Rust generates its binding during
+the build and Ruby checks in its generated binding. ProtoJSON encoded through
+those bindings and
+compressed as `.json.gz` remains the default human-inspectable artifact. A
+binary protobuf encoding may be added without changing protocol semantics.
+
+The protocols intentionally reuse SCIP concepts and types:
+
+- tool metadata;
+- canonical project-relative documents;
+- explicit position encoding;
+- typed, zero-based, half-open ranges;
+- canonical SCIP symbols for semantic entities;
+- document-local symbols for source anchors.
+
+They do not pretend that ordinary SCIP can represent runtime values. Runtime
+domains, execution counts, correlation, capture completeness, and run
+provenance are companion facts referencing SCIP-style symbols and occurrences.
+
+## Why the prior ad-hoc shape is not an adequate specification
+
+The repository has a nominal v1 data shape, but it is not a complete protocol:
+
+- Its only canonical definition is a Rust `serde` struct. NilKill independently
+ constructs Ruby hashes and duplicates the schema-version string.
+- Unknown fields are accepted, so a misspelled producer field can be silently
+ discarded.
+- Methods and calls are joined by path, owner, name, kind, line, selector, and
+ suffix heuristics. The evidence does not reference FactMine-generated source
+ identities.
+- No source or anchor digest proves that evidence belongs to the analyzed
+ source revision.
+- A line and selector are not a unique callsite identity.
+- Flattened `types`, `singletons`, `elements`, `targets`, result domains, and
+ truth values lose receiver-target-result correlations.
+- `target_observation_complete` has no precise world or capture semantics.
+ Current merging can turn a mixed group complete when any row has a target.
+- An absent record cannot distinguish unexecuted, not instrumented, unsupported,
+ dropped, filtered, stale, or producer failure.
+- Producer-side filtering of test code discards observations and forces
+ receiver-only reconstruction in the consumer.
+- Runtime type identities are unqualified strings rather than canonical
+ semantic entities.
+- The consumer implementation combines validation, heuristic source matching,
+ value propagation, target inference, stdlib synthesis, SCIP encoding, and
+ several dozen regression tests in one approximately 5,000-line module.
+- NilKill tests its serializer and some synthetic events; FactMine tests many
+ handwritten examples. The two projects do not run the same independent
+ conformance corpus.
+
+There are useful v1 tests, but they are regression tests for implementation
+examples, not proof that an arbitrary conforming producer will work.
+
+## Semantic authority
+
+Runtime evidence never claims all possible program behavior.
+
+`MODELED_RUNS` means:
+
+- every execution of an instrumented anchor in the listed successful runs was
+ captured unless the anchor says otherwise;
+- the value and target alternatives are complete only for those captured
+ executions;
+- unobserved inputs, unexecuted branches, and future monkey-patching remain
+ outside the modeled world.
+
+FactMine may report `complete_under_modeled_runs`, but must not silently relabel
+that as compiler-proven or universally complete. Every exported completeness
+result retains its authority.
+
+`capture_complete` is distinct from `semantic_closed`:
+
+- capture completeness is a producer attestation about the selected executions;
+- semantic closure is a FactMine proof that every alternative relevant to an
+ operation has a target and cost under the selected authority.
+
+Line coverage proves neither property.
+
+## Protocol A: Trace Plan
+
+FactMine generates the plan after parsing and normalizing source. NilKill does
+not discover callsites or reconstruct source flow.
+
+Conceptual schema:
+
+```protobuf
+message TracePlan {
+ uint32 protocol_version = 1;
+ ToolInfo producer = 2;
+ string project_root = 3;
+ bytes plan_digest = 4;
+ repeated PlannedDocument documents = 5;
+ repeated EvidenceRequest requests = 6;
+}
+
+message PlannedDocument {
+ string relative_path = 1;
+ string language = 2;
+ scip.PositionEncoding position_encoding = 3;
+ bytes content_sha256 = 4;
+}
+
+message SourceAnchor {
+ // A document-local SCIP symbol generated by FactMine.
+ string symbol = 1;
+ string relative_path = 2;
+ SourceRange range = 3;
+ AnchorKind kind = 4;
+ string enclosing_symbol = 5;
+ bytes semantic_digest = 6;
+ string display_name = 7; // informational, never a join key
+}
+
+message EvidenceRequest {
+ SourceAnchor anchor = 1;
+ repeated EvidenceKind required = 2;
+ optional SourceAnchor activation_anchor = 3;
+ optional uint32 parameter_ordinal = 4;
+}
+```
+
+Required anchor kinds include function entry, function return, call selector,
+state read/write, callback entry, collection operation, and branch predicate.
+The language adapter recognizes native syntax; the common profile pass assigns
+anchors and requirements.
+
+Anchor IDs and semantic digests have separate purposes:
+
+- the local symbol is a stable lookup key across plan/evidence;
+- the semantic digest determines whether cached evidence may be reused;
+- the document digest detects stale source;
+- only FactMine may relocate unchanged anchors into a new plan.
+
+This supplies the correctness foundation for incremental collection. NilKill
+never guesses that an old path/line remains equivalent.
+
+## Protocol B: Runtime Semantic Evidence
+
+Evidence is grouped by exact plan anchor and correlated execution alternative.
+It does not flatten independent unions.
+
+Conceptual schema:
+
+```protobuf
+message RuntimeEvidence {
+ uint32 protocol_version = 1;
+ ToolInfo producer = 2;
+ Authority authority = 3; // MODELED_RUNS
+ bytes trace_plan_digest = 4;
+ repeated EnvironmentClaim environment = 5;
+ repeated Run runs = 6;
+ repeated AnchorEvidence anchors = 7;
+ repeated CorrelationEvidence correlations = 8;
+}
+
+message AnchorEvidence {
+ string anchor_symbol = 1;
+ bytes anchor_semantic_digest = 2;
+ CaptureSummary capture = 3;
+ repeated ExecutionBucket executions = 4;
+}
+
+message CaptureSummary {
+ CaptureStatus status = 1;
+ repeated string run_ids = 2;
+ uint64 observed_executions = 3;
+ uint64 dropped_executions = 4;
+ string reason = 5;
+ repeated EvidenceKind complete_kinds = 6;
+}
+
+message CorrelationEvidence {
+ string group_id = 1;
+ repeated string candidate_anchor_symbols = 2;
+ CaptureSummary capture = 3;
+ repeated ExecutionBucket executions = 4;
+}
+
+enum CaptureStatus {
+ CAPTURE_STATUS_UNSPECIFIED = 0;
+ COMPLETE_FOR_RUNS = 1;
+ NOT_EXECUTED = 2;
+ PARTIAL = 3;
+ NOT_INSTRUMENTED = 4;
+ UNSUPPORTED = 5;
+ STALE = 6;
+ FAILED_CAPTURE = 7;
+}
+
+message ExecutionBucket {
+ uint64 count = 1;
+ ValueSet receiver = 2;
+ RuntimeTarget target = 3;
+ ValueSet result = 4;
+ optional bool boolean_result = 5;
+ Provenance provenance = 6;
+ ValueSet value = 7; // parameter, return, or state boundary
+}
+```
+
+One bucket represents one correlated
+`receiver -> actual target -> result -> predicate result` alternative. Identical
+buckets may be counted and merged. Different alternatives must never be
+cross-multiplied.
+
+Values are recursive, bounded summaries:
+
+```protobuf
+message RuntimeValue {
+ string type_symbol = 1;
+ optional string singleton_symbol = 2;
+ SourceRole source_role = 3;
+ oneof shape {
+ SequenceShape sequence = 4;
+ MappingShape mapping = 5;
+ RecordShape record = 6;
+ TupleShape tuple = 7;
+ }
+ bool truncated = 8;
+}
+
+message ValueSet {
+ repeated WeightedValue alternatives = 1;
+ bool truncated = 2;
+}
+
+message WeightedValue {
+ RuntimeValue value = 1;
+ uint64 count = 2;
+}
+```
+
+Container type and contained alternatives remain nested in the same value.
+Record members remain attached to the exact record type. Mapping summaries
+retain key/value association to the precision needed by FactMine. Depth,
+cardinality, redaction, or sampling limits set `truncated`; they never silently
+produce a closed domain.
+
+`RuntimeTarget` contains one canonical SCIP symbol when the provider can produce
+one, the exact runtime definition identity when available, package/version
+coordinates, native/workspace/dependency classification, and source role. The
+producer records test doubles and mocking targets truthfully. FactMine applies
+production-analysis policy; NilKill does not erase observations.
+
+Every requested anchor appears exactly once in the evidence, including
+`NOT_EXECUTED` anchors. Missing anchors are protocol errors, not ordinary
+unobserved calls.
+
+When a provider cannot distinguish two exact anchors—for example, identical
+selectors on one source line—it emits one `CorrelationEvidence` row containing
+the raw buckets and the sorted candidate-anchor set. It does not choose an
+anchor or duplicate the bucket into every candidate. FactMine validates that
+the candidates are compatible call anchors from the supplied plan, then may
+bind the observation only through normalized CFG/DFG or an already-proven
+static type. An unresolved group remains incomplete.
+
+## Responsibility boundary
+
+### FactMine owns
+
+- source parsing and language syntax normalization;
+- trace-plan anchors, requirements, semantic digests, and relocation;
+- protocol validation against the exact plan and source;
+- normalized CFG/DFG propagation;
+- callback, branch, iteration, state, and collection relationships;
+- joining runtime targets to project/compiler SCIP;
+- language-owned semantic normalization through an adapter;
+- stdlib/dependency cost joins;
+- closed-world checks and authority labels;
+- inferred SCIP output and Big-O analysis.
+
+### NilKill shared infrastructure owns
+
+- executing complete or incremental workloads;
+- run and shard identities;
+- ensuring every requested anchor receives a capture status;
+- lossless aggregation of identical correlated execution buckets;
+- function-level evidence ownership and replacement;
+- atomic compressed output;
+- dropped-event accounting and producer attestation.
+
+### NilKill language providers own
+
+- VM/runtime hooks and source instrumentation;
+- obtaining receiver, target, result, and predicate observations;
+- mapping native runtime entities to canonical runtime SCIP symbols;
+- bounded value/shape inspection;
+- identifying the source and package coordinates of runtime entities;
+- reporting unsupported evidence explicitly.
+
+They do not parse source for flow, infer owners through assignments, filter
+non-production evidence, infer call targets at unexecuted sites, or decide
+Big-O closure.
+
+### FactMine language adapters own
+
+A deliberately small runtime extension to the existing syntax adapter:
+
+- normalization of a provider's runtime entity identity;
+- mapping native/stdlib runtime targets to the language's canonical SCIP
+ package identity;
+- native dispatch relationships that the language runtime defines (for example
+ mixins or prototype ancestry);
+- implicit runtime operations that have no explicit source call.
+
+The shared overlay never contains `if language == ...`. New adapter methods
+must be justified by a cross-language normalized concept and exercised by the
+adapter conformance suite.
+
+## Conformance testing
+
+Correctness is established in layers. Repository completion percentages are
+benchmarks, not protocol tests.
+
+### 1. Schema and semantic validator
+
+`fact-mine runtime-evidence validate --plan PLAN --evidence EVIDENCE`
+must reject:
+
+- unknown protocol versions and fields;
+- malformed SCIP symbols or ranges;
+- non-canonical paths or inconsistent position encodings;
+- plan, document, or anchor digest mismatches;
+- missing or duplicate requested anchors;
+- counts inconsistent with capture summaries;
+- `COMPLETE_FOR_RUNS` with dropped events, truncation, failed runs, or missing
+ required fields;
+- targets or values whose source role/provenance is missing;
+- evidence referring to a different enclosing symbol or anchor kind.
+
+Checked-in valid and invalid fixtures are consumed by both generated Rust and
+Ruby bindings.
+
+### 2. FactMine consumer conformance
+
+FactMine tests use a synthetic normalized IR plus hand-authored, validator-clean
+evidence. They do not invoke NilKill. Golden cases cover:
+
+- exact callsite and definition joins;
+- parameter, return, state, and callback propagation;
+- receiver-target-result correlation;
+- multiple receiver alternatives where all targets close;
+- one unresolved alternative preventing closure;
+- collection element/key/value projections;
+- record accessors and generated declarations;
+- branch capability and truthiness refinement;
+- test replacement evidence retained but not trusted as production identity;
+- stdlib/native target normalization;
+- unexecuted, partial, truncated, stale, and unsupported evidence;
+- source movement with valid and invalid relocation;
+- incremental merge and replacement.
+
+The expected result includes exact inferred SCIP occurrences, normalized
+domains, call costs, completeness authority, and gap reasons.
+
+Property tests enforce monotonic safety:
+
+- reordering or duplicating identical buckets does not change the result;
+- removing evidence cannot improve semantic closure;
+- changing complete capture to partial cannot improve completeness;
+- adding an unresolved alternative cannot preserve a closed callsite;
+- stale or ambiguous evidence never joins;
+- a covered line alone never resolves a call;
+- no inference crosses an enclosing-function or anchor boundary.
+
+### 3. NilKill producer conformance
+
+A shared provider harness runs small real programs and compares emitted evidence
+with semantic golden expectations. Ruby is the first implementation; Python,
+JavaScript, and PHP must pass the same scenario catalog before integration.
+
+Provider fixtures cover:
+
+- instance, class/module, inherited, mixed-in, and native calls;
+- overloaded/dynamic targets at one callsite;
+- parameters, returns, state, callbacks, and nested containers;
+- generated accessors invisible to ordinary call TracePoint;
+- short-circuit and ternary branches;
+- exceptions and non-local exits;
+- test doubles and monkey patches with correct source roles;
+- anonymous classes/records;
+- dropped/truncated evidence;
+- zero executions;
+- multiple runs and incremental replacement.
+
+These tests assert evidence only. They do not accept a favorable Big-O result as
+proof that tracing was correct.
+
+### 4. End-to-end contract fixtures
+
+For each supported runtime language:
+
+```
+fixture source + fixture workload
+ -> FactMine trace plan
+ -> NilKill provider
+ -> protocol validator
+ -> FactMine runtime SCIP
+ -> exact expected SCIP + Big-O result
+```
+
+The same fixture is also run with a synthetic perfect producer. A failure then
+localizes to plan/consumer, provider, or integration rather than becoming a
+repository-level metric mystery.
+
+## Implementation layout
+
+```text
+gems/protocol/runtime-evidence/v1/
+ runtime_evidence.proto
+ conformance/
+
+gems/fact-mine/src/
+ runtime_protocol.rs # generated binding wrapper, plan, strict validator
+ runtime_evidence.rs # canonical-to-normalized overlay and SCIP export
+
+gems/nil-kill/lib/nil_kill/runtime/
+ protocol/runtime_evidence_pb.rb
+ evidence_protocol.rb # generated binding adapter only
+ value_evidence_emitter.rb
+ evidence_merger.rb
+ scip_emitter.rb
+
+gems/nil-kill/lib/nil_kill/languages/providers//
+ runtime_tracer.*
+ runtime_identity.*
+```
+
+No language-specific code belongs in a shared file.
+
+## Cutover implemented
+
+1. The v1 `.proto`, generated bindings, strict validator, and shared valid and
+ invalid corpus are the only public runtime-evidence contract.
+2. FactMine generates the exact plan and a private anchor-to-normalized-IR
+ binding table from the same source snapshot.
+3. NilKill reads the plan and writes every artifact through the generated Ruby
+ binding. Unknown or malformed fields fail before collection or merge.
+4. FactMine rebuilds the plan, requires the same plan digest, validates every
+ evidence row, and joins only through the private exact binding table.
+5. The former path/name/line ad-hoc JSON shape has no production parser or CLI
+ entry point. FactMine's private normalized facts are not a wire format.
+6. Incremental merge replaces evidence by run and anchor. Unchanged semantic
+ anchors may relocate; changed or new anchors become `STALE`.
+
+## Cutover acceptance criteria
+
+- One canonical protocol definition generates both producer and consumer types.
+- The validator proves exact plan/source compatibility before analysis.
+- Every planned anchor has explicit capture status.
+- Receiver, target, result, and predicate observations remain correlated.
+- No source path/name/line heuristic is used to join runtime evidence.
+- FactMine passes the consumer golden corpus without NilKill.
+- Ruby passes the shared producer corpus without relying on FactMine inference.
+- End-to-end fixtures pass with exact SCIP and completeness outputs.
+- All safety properties pass under randomized ordering, merging, omission, and
+ alternative expansion.
+- Incremental and full collection produce semantically identical evidence for
+ unchanged final source and workload sets.
+- SlopCop regressions are explained by authority/gap diagnostics; an older,
+ unsound percentage is not restored merely to hit a number.
+
+Only after these criteria hold should work resume on repository-specific
+covered-line gaps.
diff --git a/docs/agents/spec-mutant-burndown.md b/docs/agents/spec-mutant-burndown.md
index 24552dcf4..c53a5ed4e 100644
--- a/docs/agents/spec-mutant-burndown.md
+++ b/docs/agents/spec-mutant-burndown.md
@@ -69,7 +69,7 @@ load-bearing.
## Current Findings
-- `gems/lineage/tools/mutant-converters/ruby_mutant.rb` only accepted one `spec:` file per subject.
+- `gems/gigasail/tools/mutant-converters/ruby_mutant.rb` only accepted one `spec:` file per subject.
That hid existing load-bearing specs from mutant for broad subjects such as
`FsmTransform::Emit`.
- The runner did not accept raw class/module names such as
diff --git a/gems/boobytrap/README.md b/gems/boobytrap/README.md
index 8ab4d7553..a284957e4 100644
--- a/gems/boobytrap/README.md
+++ b/gems/boobytrap/README.md
@@ -10,7 +10,7 @@ Nil-kill, lint violations, etc. help explain what *might* be wrong when
you get there.
> [!NOTE]
-> Boobytrap uses [Lineage](../lineage/README.md) to track changes to
+> Boobytrap uses [Lineage](../gigasail/README.md) to track changes to
> lines across files over time, and to avoid penalizing non-semantic
> changes like whitespace or comments.
@@ -222,7 +222,7 @@ the heuristic is reliable here.
control-flow pressure.
- [SlopCop](../slopcop/README.md): categorizes uncovered branches and
ranks the true test gaps.
-- [Lineage](../lineage/README.md): renders history and verification
+- [Lineage](../gigasail/README.md): renders history and verification
evidence next to source.
- [Nil-kill](../nil-kill/README.md): traces nil and type pressure back
to its source.
diff --git a/gems/boobytrap/src/giga_e2e_test.go b/gems/boobytrap/src/giga_e2e_test.go
new file mode 100644
index 000000000..d45461c07
--- /dev/null
+++ b/gems/boobytrap/src/giga_e2e_test.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "os"
+ "testing"
+)
+
+// TestGigaLineageIntegrationE2E drives the real `giga summary --format json`
+// binary through loadLineage and asserts the units parse. Skips unless the
+// E2E_* env vars point at a built giga binary and a giga-built database.
+func TestGigaLineageIntegrationE2E(t *testing.T) {
+ db := os.Getenv("E2E_GIGA_DB")
+ giga := os.Getenv("E2E_GIGA_BIN")
+ repo := os.Getenv("E2E_REPO")
+ if db == "" || giga == "" || repo == "" {
+ t.Skip("set E2E_GIGA_DB, E2E_GIGA_BIN, E2E_REPO to run")
+ }
+ // The command is the bare binary; loadLineage supplies the `summary` verb
+ // and its flags itself.
+ idx := loadLineage(db, repo, nil, 10, giga)
+ if idx.Status != "ok" {
+ t.Fatalf("loadLineage status = %q, want ok", idx.Status)
+ }
+ if len(idx.Units) == 0 {
+ t.Fatal("loadLineage returned no units")
+ }
+ t.Logf("parsed %d lineage units from giga; top unit %q in %s risk=%.2f",
+ len(idx.Units), idx.Units[0].Name, idx.Units[0].File, idx.Units[0].RiskScore)
+}
diff --git a/gems/boobytrap/src/report.go b/gems/boobytrap/src/report.go
index 1c508f2b9..5e0699215 100644
--- a/gems/boobytrap/src/report.go
+++ b/gems/boobytrap/src/report.go
@@ -907,11 +907,11 @@ func loadLineage(dbPath string, repoRoot string, only []string, top int, command
parts = append(parts, args...)
cmd = exec.Command(parts[0], parts[1:]...)
} else {
- binary := filepath.Join(repoRoot, "gems", "lineage", "target", "release", "lineage")
+ binary := filepath.Join(repoRoot, "gems", "gigasail", "target", "release", "giga")
if _, err := os.Stat(binary); err == nil {
cmd = exec.Command(binary, args...)
} else {
- cmd = exec.Command("cargo", "run", "--quiet", "--manifest-path", filepath.Join(repoRoot, "gems", "lineage", "Cargo.toml"), "--")
+ cmd = exec.Command("cargo", "run", "--quiet", "--manifest-path", filepath.Join(repoRoot, "gems", "gigasail", "Cargo.toml"), "--bin", "giga", "--")
cmd.Args = append(cmd.Args, args...)
}
}
diff --git a/gems/decomplex/Cargo.lock b/gems/decomplex/Cargo.lock
index c98a0f0bc..21799f2b1 100644
--- a/gems/decomplex/Cargo.lock
+++ b/gems/decomplex/Cargo.lock
@@ -105,6 +105,12 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -127,12 +133,18 @@ version = "0.1.0"
dependencies = [
"anyhow",
"flate2",
+ "glob",
"hazard-contract",
+ "protobuf",
+ "protobuf-codegen",
+ "protobuf-json-mapping",
"regex",
+ "scip",
"serde",
"serde_json",
"serde_yaml",
"sha2",
+ "shell-words",
"streaming-iterator",
"tree-sitter",
"tree-sitter-c",
@@ -185,6 +197,12 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -199,6 +217,15 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -227,6 +254,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
[[package]]
name = "memchr"
version = "2.8.2"
@@ -243,6 +276,12 @@ dependencies = [
"simd-adler32",
]
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -252,6 +291,68 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "protobuf"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
+dependencies = [
+ "once_cell",
+ "protobuf-support",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-codegen"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace"
+dependencies = [
+ "anyhow",
+ "once_cell",
+ "protobuf",
+ "protobuf-parse",
+ "regex",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-json-mapping"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d6e4be637b310d8a5c02fa195243328e2d97fa7df1127a27281ef1187fcb1d"
+dependencies = [
+ "protobuf",
+ "protobuf-support",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-parse"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "log",
+ "protobuf",
+ "protobuf-support",
+ "tempfile",
+ "thiserror",
+ "which",
+]
+
+[[package]]
+name = "protobuf-support"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
+dependencies = [
+ "thiserror",
+]
+
[[package]]
name = "quote"
version = "1.0.45"
@@ -309,6 +410,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "scip"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26a72133c2d6fd45c9a3a343bcb3db2faa30f68f0919bfa3370ca85add5460c3"
+dependencies = [
+ "protobuf",
+]
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -377,6 +487,12 @@ dependencies = [
"digest",
]
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
[[package]]
name = "shlex"
version = "2.0.1"
@@ -418,6 +534,26 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tree-sitter"
version = "0.25.8"
@@ -612,6 +748,18 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
diff --git a/gems/decomplex/examples/oracles/structural-topology.json b/gems/decomplex/examples/oracles/structural-topology.json
index 3b21cc316..7eb9e19b9 100644
--- a/gems/decomplex/examples/oracles/structural-topology.json
+++ b/gems/decomplex/examples/oracles/structural-topology.json
@@ -25,5 +25,32 @@
],
"method_count": 5
},
- "options": {}
-}
\ No newline at end of file
+ "options": {},
+ "expected_by_language": {
+ "kotlin": {
+ "edges": [
+ {
+ "callee_name": "prepare",
+ "caller_name": "run",
+ "type": "always"
+ },
+ {
+ "callee_name": "ready",
+ "caller_name": "run",
+ "type": "conditional"
+ },
+ {
+ "callee_name": "validate",
+ "caller_name": "run",
+ "type": "conditional"
+ },
+ {
+ "callee_name": "helper",
+ "caller_name": "run",
+ "type": "iterates"
+ }
+ ],
+ "method_count": 6
+ }
+ }
+}
diff --git a/gems/decomplex/examples/oracles/temporal-ordering-pressure.json b/gems/decomplex/examples/oracles/temporal-ordering-pressure.json
index 31c8719f6..713d836d6 100644
--- a/gems/decomplex/examples/oracles/temporal-ordering-pressure.json
+++ b/gems/decomplex/examples/oracles/temporal-ordering-pressure.json
@@ -17,5 +17,24 @@
"writers": 3
}
],
- "options": {}
-}
\ No newline at end of file
+ "options": {},
+ "expected_by_language": {
+ "kotlin": [
+ {
+ "orderings": "4!",
+ "owner": "TemporalOrderExample",
+ "public_methods": 5,
+ "shared_fields": [
+ "a",
+ "b"
+ ],
+ "state_fields": [
+ "a",
+ "b"
+ ],
+ "state_methods": 4,
+ "writers": 3
+ }
+ ]
+ }
+}
diff --git a/gems/decomplex/tests/examples_oracle.rs b/gems/decomplex/tests/examples_oracle.rs
index 19ddeea6e..715dbaa2c 100644
--- a/gems/decomplex/tests/examples_oracle.rs
+++ b/gems/decomplex/tests/examples_oracle.rs
@@ -34,8 +34,18 @@ fn shared_examples_match_oracles() -> Result<()> {
}
let oracle: Value = serde_json::from_str(&fs::read_to_string(&oracle_path)?)?;
+ let language = language_for_fixture(&fixture)?;
+ let language_key = format!("{language:?}").to_lowercase();
+ // One expectation per detector, because a detector should see the same
+ // shape whatever the source language. Where a language genuinely models
+ // something differently -- Kotlin's primary constructor is a function,
+ // and other languages have no such declaration -- it says so here
+ // rather than the detector losing the language or the shared number
+ // drifting to fit one of them.
let expected = oracle
- .get("expected")
+ .get("expected_by_language")
+ .and_then(|by_language| by_language.get(&language_key))
+ .or_else(|| oracle.get("expected"))
.cloned()
.with_context(|| format!("{} missing expected", oracle_path.display()))?;
let detector_name = oracle
@@ -43,7 +53,6 @@ fn shared_examples_match_oracles() -> Result<()> {
.and_then(Value::as_str)
.with_context(|| format!("{} missing detector", oracle_path.display()))?;
let options = oracle.get("options").cloned().unwrap_or_else(|| json!({}));
- let language = language_for_fixture(&fixture)?;
if matches!(language, Language::Swift) {
continue;
}
@@ -55,7 +64,15 @@ fn shared_examples_match_oracles() -> Result<()> {
if std::env::var("UPDATE_ORACLES").is_ok() {
let mut oracle: Value = serde_json::from_str(&fs::read_to_string(&oracle_path)?)?;
- oracle["expected"] = projected_normalized;
+ if oracle
+ .get("expected_by_language")
+ .and_then(|by_language| by_language.get(&language_key))
+ .is_some()
+ {
+ oracle["expected_by_language"][&language_key] = projected_normalized;
+ } else {
+ oracle["expected"] = projected_normalized;
+ }
fs::write(&oracle_path, serde_json::to_string_pretty(&oracle)?)?;
} else if projected_normalized != expected_normalized {
failures.push(format!(
diff --git a/gems/espalier/README.md b/gems/espalier/README.md
index 84846ab6b..7cfe8d045 100644
--- a/gems/espalier/README.md
+++ b/gems/espalier/README.md
@@ -64,6 +64,25 @@ bundle exec gems/espalier/exe/espalier \
--output=/tmp/espalier-report.md
```
+Generate a source-proven standard-library complexity bundle from one manifest:
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/go-1.22.2.yml
+```
+
+The manifest owns source revision verification, source selection, and the
+language indexer's build recipe. The shared pipeline owns profiling, generic
+soundness checks, exact-symbol export, producer/consumer validation, and atomic
+publication. A new SCIP stdlib should normally require only a manifest; parser
+or runtime semantics belong in that language's FactMine module.
+
+FactMine discovers generated bundles automatically at build time. See
+`gems/fact-mine/config/stdlib_maps/support.yml` for the compatibility status of
+every maintained SCIP language. An entry remains blocked when the indexer
+cannot provide executable bodies or a source/consumer version identity; the
+pipeline does not substitute a manual override for either failure.
+
## Outputs
Espalier can output a compact architecture manifest for tools and LLMs,
@@ -133,7 +152,8 @@ combining structural facts with sibling-gem evidence:
- `--risk FILE`: Boobytrap/SlopCop churn, coverage, and risk evidence.
- `--manifest FILE`: a previously generated Espalier YAML manifest.
- `--fact-mine FILE`: a previously generated `fact-mine.json` static facts file (also honors `ENV["FACT_MINE_FACTS_FILE"]` environment variable to bypass fact extraction runs).
-- `--scip-index FILE`: import compiler-proven call identity from a `.scip` file or `scip print --json` export; repeat for multiple build roots. Binary indexes require `scip` on `PATH` (or `SCIP_BINARY`).
+- `--scip-index FILE`: import compiler-proven call identity directly from a binary `.scip` file or from a protobuf-JSON export; repeat for multiple build roots. Binary decoding is built in and does not require the `scip` CLI at analysis time.
+- `--complexity-summary FILE`: apply a reviewed Espalier complexity summary keyed by exact compiler symbols; JSON and reproducible `.json.gz` files are supported and the option may be repeated. Contradictory summaries fail before either one is applied.
Nil-kill evidence is the most important external input today because it
helps Espalier distinguish broad untyped surfaces from intentional typed
@@ -143,7 +163,7 @@ interfaces.
Espalier's Big-O bounds are static. Feed a runtime profile through Lineage
to rank them by what actually runs hot: convert profiler output with
-`gems/lineage/tools/pprof_to_hotness.rb`, ingest with `lineage
+`gems/gigasail/tools/pprof_to_hotness.rb`, ingest with `lineage
ingest-hotness`, and the Expensive Operations view sorts by Big-O first,
then measured share; critical functions get a flame icon in the file view.
Profile representative workloads, not unit tests, and build perf-profiled
diff --git a/gems/espalier/config/complexity_overrides.yml b/gems/espalier/config/complexity_overrides.yml
new file mode 100644
index 000000000..44ea1dad5
--- /dev/null
+++ b/gems/espalier/config/complexity_overrides.yml
@@ -0,0 +1,53 @@
+# Manual complexity overrides - targeted escape hatch.
+#
+# These entries are applied ONLY to functions that Espalier's structural
+# analysis reports as INCOMPLETE, and ONLY when an entry exists here. A
+# complete (fully derived) bound is never overridden. This is NOT a blanket
+# stdlib registry: every entry documents an *algorithmic guarantee that static
+# structural analysis provably cannot derive* - typically an amortized or
+# randomized bound enforced by a runtime fallback the analyzer cannot see.
+#
+# Measured scope of the irreducible set: 0.4% of Go stdlib functions, ~1.9% of
+# Ruby/Python functions (unknown-progress recursion). Overriding the public API
+# entry short-circuits its incomplete internal recursion for all callers.
+#
+# key format: "." (owner = declaring type or package leaf)
+# fields: time (required), space, note (why it is irreducible - required)
+
+go:
+ "sort.Sort":
+ time: "O(N log N)"
+ space: "O(log N)"
+ note: "introsort: pdqsort recursion + heapsort fallback at depth limit; the N-log-N guarantee is the depth-limit switch, not derivable from the recursive structure"
+ "sort.Stable":
+ time: "O(N (log N)^2)"
+ space: "O(1)"
+ note: "in-place stable block merge; symmetric-swap rotation counts are not structurally derivable"
+ "sort.Slice":
+ time: "O(N log N * C)"
+ space: "O(log N)"
+ note: "introsort over a less-callback C; same depth-limit guarantee as sort.Sort"
+ "sort.SliceStable":
+ time: "O(N (log N)^2 * C)"
+ space: "O(N)"
+ note: "stable sort over a less-callback C"
+
+ruby:
+ "Array.sort":
+ time: "O(N log N)"
+ space: "O(log N)"
+ note: "introsort in C (qsort_r with fallback); native, not analyzable from Ruby source"
+ "Array.sort_by":
+ time: "O(N log N * C)"
+ space: "O(N)"
+ note: "Schwartzian transform + native introsort over a key-callback C"
+
+python:
+ "list.sort":
+ time: "O(N log N)"
+ space: "O(N)"
+ note: "Timsort in C; adaptive merge run-detection is native, not analyzable from Python source"
+ "sorted":
+ time: "O(N log N)"
+ space: "O(N)"
+ note: "Timsort in C over an optional key-callback"
diff --git a/gems/espalier/docs/agents/interface-dispatch-design.md b/gems/espalier/docs/agents/interface-dispatch-design.md
new file mode 100644
index 000000000..04e3c13b6
--- /dev/null
+++ b/gems/espalier/docs/agents/interface-dispatch-design.md
@@ -0,0 +1,198 @@
+# Interface / virtual dispatch: a language-agnostic Big-O design
+
+## The problem
+
+A call through an abstract type has no single target:
+
+```go
+func Sort(data Interface) { // Interface = { Len; Less; Swap }
+ ... data.Less(i, j) ... data.Swap(i, j) ...
+}
+```
+
+`data.Less` can be any concrete implementation. Today the analyzer treats it as
+fully unknown, so `Sort` produces a garbage polynomial and is marked
+*incomplete* - when the honest answer is `O(N log N * C_Less)`: **known, and
+dependent on the cost of the comparison**.
+
+This is not Go-specific. Every language has the same shape under different
+syntax: Go interfaces, Rust traits, Java/C#/Kotlin interfaces and abstract
+classes, Swift protocols, TypeScript interfaces, C++ virtual classes, Python
+ABC/`Protocol`/duck typing, Ruby modules/duck typing. The solution must live in
+the language-agnostic core, with the only per-language piece being *"which
+concrete types can stand in for this abstract one."*
+
+## Core insight: an interface method call is a callback
+
+`sort.Interface` is a **bundle of callbacks** passed as one parameter. `data.Less`
+is exactly `cmp(i, j)` where `cmp` is an injected function of unknown cost. So the
+machinery already built for callbacks (#43/#44) is the foundation:
+
+- `parameterized_cost` mints a symbolic cost domain `C`.
+- `substitute` / `without_domains` substitute a concrete callback cost for `C`.
+- callback-taking functions already resolve to `O(N * C)`.
+
+The whole design is: **treat an abstract-typed parameter/receiver's method calls
+as callbacks with cost `C_{I.M}`, then resolve `C_{I.M}` to the worst concrete
+implementation.** Two views fall out, and both are wanted:
+
+1. **Definition-site (parametric).** Analyzing `Sort` alone, `Less`/`Swap` are
+ callbacks. `Sort = O(N log N * C_Less + N * C_Swap)`. This is **complete as a
+ parametric bound** - it fixes the "incomplete" bug immediately, and it is the
+ result you asked for ("known, dependent on callbacks").
+2. **Resolution (worst-case).** When we want a concrete number - at a call site
+ `Sort(myData)`, or corpus-wide - substitute `C_{I.M}` with the cost of the
+ worst concrete implementation, with provenance and an open/closed-world flag.
+
+## The normalized fact: a dispatch graph
+
+The core reasons over one new, language-neutral fact family carried on the
+profile. Adapters populate it; the algebra never inspects syntax.
+
+```
+abstract_type(I) # I dispatches at runtime; no single body per method
+requires_method(I, M) # abstract I demands method M (its callback surface)
+dispatch_impl(I, T) # concrete T is a possible runtime target for static I
+open_dispatch(I) # I is extensible outside the analyzed corpus
+```
+
+`dispatch_impl(I, T)` is the *satisfaction relation*. It is the **only**
+language-specific input, and it is pure data once emitted:
+
+| Language | Abstract type | How the adapter computes `dispatch_impl(I, T)` |
+|---|---|---|
+| Go | `interface` | structural: `methodset(T) ⊇ methods(I)` |
+| Rust | `trait` | `impl I for T` blocks |
+| Java / Kotlin | `interface`, `abstract class` | `class T implements I` / `extends` |
+| C# | `interface`, `abstract class` | `T : I` |
+| Swift | `protocol` | `T: P` incl. `extension T: P` |
+| TypeScript | `interface`, abstract | `implements`, or structural methodset |
+| C++ | class with virtuals | public inheritance |
+| Python | `ABC` / `Protocol` / duck | subclass, `ABC.register`, or Protocol methodset |
+| Ruby | `module` / duck | `include`, or responds-to the method set |
+
+`open_dispatch(I)` is likewise adapter-owned (Go: exported interface; Rust:
+public trait; Java: non-`sealed`; etc.) - it distinguishes "we have seen every
+implementation" from "external code may add more."
+
+Note we already carry `supertypes` on owners (used for Go embedding and nominal
+inheritance in `resolve_inherited_calls`). `dispatch_impl` is the inverse,
+transitive-closure relation and generalizes it; the two share extraction.
+
+## The algorithm (language-agnostic, in espalier)
+
+For a call `x.M(...)` whose static receiver type is abstract `I` (or `x` is an
+abstract/callback parameter), during the existing structural big-O fixpoint:
+
+1. **Candidate set.** `impls = { T : dispatch_impl(I, T) }` within the corpus.
+2. **Per-implementation cost.** `cost(T.M)` from the method-complexity fixpoint
+ (recursively; interface calls inside `T.M` recurse into this same rule).
+3. **Cost domain.**
+ - If `open_dispatch(I)` **or** `impls` is empty → `C_{I.M}` is a *free symbolic
+ domain* (unknown-but-bounded parameter), source-kind `interface_cost`. The
+ bound stays parametric, exactly like an unresolved callback.
+ - Else (closed set of known impls) → `C_{I.M} = max_{T ∈ impls} cost(T.M)`,
+ the **worst-case implementation**.
+4. **Substitute** `C_{I.M}` into the caller's structural bound via the existing
+ `substitute` path. `Sort → O(N log N * C_Less)`, then `C_Less → O(1)` (IntSlice)
+ or whatever the worst impl proves.
+5. **Recursion / SCC.** Interface calls can be mutually recursive (impl calls back
+ through the interface). Use a monotone fixpoint seeded at the free `C` domain
+ and widen upward; a strongly-connected interface cycle with no proven progress
+ yields `unbounded`, never a fabricated polynomial.
+
+The definition-site view is just step 3's "free domain" branch; the resolution
+view is the "closed max" branch. Same code path.
+
+## Open vs. closed world (correctness, not optimism)
+
+- **Closed** (`impls` complete, `not open_dispatch(I)`): the max is a real bound.
+ Report `O(... * C)` with `C` resolved and `bound_quality: closed_impl_max`.
+- **Open** (`open_dispatch(I)`): the true worst case is unbounded - external code
+ may implement `I` arbitrarily. Report the parametric `O(... * C)` and attach the
+ known max as an **observed lower bound**, flagged
+ `bound_quality: open_world_observed_max`. Never silently present the
+ in-corpus max as the guaranteed bound for a public interface.
+
+## Provenance: call out the worst case (required output)
+
+Every interface-dispatched term emits, alongside the bound:
+
+```
+interface: I.M
+chosen_worst: T.M (cost) # the implementation that set the bound
+distribution: [T1.M O(1), T2.M O(n), …] # so tightness of the max is visible
+world: closed | open
+```
+
+This is what surfaces "the specific worst case" in reports and lets a human see
+whether one pathological implementation is dragging an otherwise-cheap interface.
+
+## Impact analysis: how often the worst case actually matters (required output)
+
+Two levels, both mechanical once the symbolic bound carries the `C` domain:
+
+1. **Per-function significance.** After substituting the worst-case `C`, check
+ whether the interface term is the **leading** term of the symbolic expression
+ or a dominated lower-order one. `f = O(N log N * C + N)`:
+ - `C = O(1)` → leading term `N log N`; the interface is *asymptotically
+ irrelevant* - report `interface_impact: none`.
+ - `C = O(N)` → leading term `N² log N`; report `interface_impact: dominant`,
+ naming the worst impl.
+2. **Corpus frequency.** Aggregate across functions: how many have an interface
+ term that is (a) dominant, (b) present-but-dominated, (c) where the worst impl
+ diverges from the median impl (high variance = one bad actor). This answers
+ "how often does the worst case impact overall complexity" as a distribution,
+ and points at exactly which implementations to optimize.
+
+## Layering (what each component owns)
+
+| Layer | Owns | Language-specific? |
+|---|---|---|
+| **Fact-mine adapters** (`go.rs`, `ruby.rs`, …) | compute `dispatch_impl`, mark `abstract_type` / `open_dispatch` | **yes** - and *only* here |
+| **Fact-mine core** | carry the dispatch-graph facts on the profile; extend the callback-param signal to abstract-typed params | no - pure data |
+| **Espalier core** | candidate enumeration, worst-case max, symbolic substitution (reuse callback path), open/closed handling, provenance, impact analysis | no |
+
+The generic extractor never learns "interface" or "trait"; it consumes
+`abstract_type`/`dispatch_impl` edges. This satisfies the architecture invariant
+(`complexity_fact_extractor_has_no_language_iterator_lexicon`).
+
+## Edge cases
+
+- **Generics / bounded type params** (Go `[T Ordered]`, Rust `T: Trait`, Java
+ ``): the constraint *is* an abstract bound; `dispatch_impl` ranges
+ over the constraint's satisfying types. Same machinery.
+- **Multiple / embedded interfaces**: a method may be required via embedding; the
+ satisfaction relation is transitive - adapters emit the closure.
+- **No implementations in corpus**: open-world `C`; parametric bound, honest.
+- **Self-provided interfaces** (`sort.Interface` is supplied by callers, not by
+ `sort`): from `sort`'s document there are zero in-corpus impls → free `C` →
+ `Sort = O(N log N * C)`. The concrete cost only materializes at the call site
+ that passes `IntSlice` - which is the existing callback substitution.
+- **Diamond / overriding**: nearest override wins per branch (already modeled by
+ `resolve_inherited_calls`); `dispatch_impl` records the effective target.
+
+## Rollout
+
+1. **Parameterize** (biggest, cheapest win): recognize an abstract-typed
+ parameter/receiver as a callback surface; its method calls get free `C`
+ domains. This alone flips interface-heavy functions from *incomplete* to
+ *complete-parametric* (`sort`, `io`, `container/heap`, `sync` families) with no
+ satisfaction data yet.
+2. **Satisfaction extraction**: adapters emit `dispatch_impl` (start with Go
+ structural + Rust/Java/C# nominal - the highest-signal set).
+3. **Worst-case resolution + provenance**: closed-world max with the call-out.
+4. **Impact analysis**: significance flag + corpus frequency report.
+
+Phase 1 is independently valuable and reuses #43/#44 almost verbatim; phases 2-4
+add the concrete numbers and the worst-case reporting you asked for.
+
+## Success criteria
+
+- `sort.Sort` reports `O(N log N * C_Less)` and is `time_complete` (parametric).
+- A call `sort.Sort(IntSlice)` resolves to `O(N log N)` with provenance
+ `chosen_worst: IntSlice.Less O(1)`.
+- A public interface is never presented with a false closed bound; it carries the
+ open-world flag and observed max.
+- The report can answer, per function and corpus-wide, whether an interface's
+ worst case is asymptotically significant, and name the implementation.
diff --git a/gems/espalier/docs/big-o.md b/gems/espalier/docs/big-o.md
index 1ea190cb5..b63433c73 100644
--- a/gems/espalier/docs/big-o.md
+++ b/gems/espalier/docs/big-o.md
@@ -90,6 +90,23 @@ use emitted executable functions as the denominator and are rounded to one
decimal place. The categories are mutually exclusive and each row sums to its
function count.
+For an enforceable production-only measurement, run:
+
+```sh
+gems/espalier/script/check_big_o_coverage.rb \
+ --source-root /path/to/corpus \
+ --repository project \
+ --minimum 85 \
+ profile.json
+```
+
+The JSON result records the source-role and lambda policies, keeps analyzer,
+declared, modeled-world, closed-candidate, parametric, and recursive proof
+buckets separate, and exits unsuccessfully below the requested threshold. It
+also fails closed when FactMine reports an executable raw call that did not
+reach normalized call facts; otherwise an omitted call could make a function
+look complete.
+
Measured 2026-07-17 at `18a2d4cbd`. These are quality samples, not language
benchmarks: repository structure, dependency surface, callback density, and the
share of trivial accessors all affect the result.
@@ -205,3 +222,130 @@ gems/espalier/script/report_big_o_proof_metrics.rb \
The reporter and this document use the same language-neutral proof classifier.
New confidence states belong in that classifier and its tests, not in
language-specific adapters.
+
+To measure the compiler index itself, generate the same FactMine profile once
+without and once with `--scip-index`, then run:
+
+```bash
+gems/espalier/script/compare_scip_big_o.rb \
+ --source-root /path/to/corpus \
+ --repository project \
+ source-only.json indexed.json
+```
+
+The comparison uses production files only, reports exact proof buckets and call
+resolution counts, and preserves executable raw-call normalization gaps. Use
+`check_big_o_coverage.rb` for the enforcing 85% gate.
+
+Current indexed smoke corpora are pinned by source commit and indexer version:
+
+| Language | Corpus commit | Indexer | Production complete, source → SCIP | Exact project targets, source → SCIP |
+| --- | --- | --- | ---: | ---: |
+| Java | Apache Commons CLI `afb0fd148517b1bf8316ebbc44ec9ec8b201452a` | scip-java 0.12.3 | 279/524 (53.24%) → 524/524 (100.00%) | 559 → 767 |
+| C | cJSON `fb16e5cf358798aabb049655975cde8427101056` | scip-clang 0.4.0 | 41/116 (35.34%) → 102/116 (87.93%) | 185 → 188 |
+
+The production acceptance measurements below were run on 2026-07-27. Each row
+uses FactMine output from the named compiler index, counts owner-nested lambdas,
+and requires zero unnormalized raw calls inside executable functions. Rows
+below 85% remain active burn-down targets:
+
+| Language | Production corpus | Indexer | Complete bounds | Semantic calls accounted | Executable raw-call gaps |
+| --- | --- | --- | ---: | ---: | ---: |
+| Java | Apache Commons CLI `afb0fd148517b1bf8316ebbc44ec9ec8b201452a` | scip-java 0.12.3 | 524/524 (100.00%) | 1,435/1,435 (100.00%) | 0 |
+| C | cJSON `fb16e5cf358798aabb049655975cde8427101056` | scip-clang 0.4.0 | 102/116 (87.93%) | 326/326 (100.00%) | 0 |
+| Go | unslop `6b39e58b5128eb22cd8f8394dd4a64987e2b8a17` | scip-go 0.2.7 | 140/150 (93.33%) | 1,104/1,125 (98.13%) | 0 |
+| Rust | FactMine production sources in this tree | rust-analyzer 1.96.0 (ac68faa 2026-05-25) | 2,837/6,664 (42.57%) | see generated profile | 0 |
+
+These are production-scope gate results, not replacements for the older
+cross-language snapshot above. The stricter current Rust measurement supersedes
+the earlier 4,939/5,802 figure: the current tree and coverage policy count
+6,664 production functions, and the safe generated stdlib bundle does not
+change its 2,837 complete functions. That shortfall is visible rather than
+being hidden by unsafe generated claims.
+
+The Java path recognizes the `semanticdb` scheme emitted by real scip-java
+0.12.x indexes while retaining compatibility with older `scip-java`-scheme
+fixtures. This activates the reviewed generic collection, stream, and
+lambda/function-interface contracts for compiler-proven JDK symbols. Abstract
+project interface declarations are modeled as one parametric implementation
+invocation rather than as executable bodies, and enhanced-for iterable
+expressions remain in the normalized CFG. On Commons CLI all 1,435 executable
+calls are semantically accounted for and all 524 production functions have
+complete time and space bounds.
+
+The C path resolves macro definitions from compiler-indexed source/header
+locations, prices bounded expansion bodies, treats compiler-proven
+function-pointer fields parametrically, and applies reviewed C runtime costs
+under an explicit modeled-world assumption for Clang's unpackaged external
+symbols. Calling-convention and return-type macros are normalized without
+changing source offsets, so parameter-rooted structural recursion remains
+provable. On cJSON all 326 executable calls are now semantically accounted for;
+the remaining 14 incomplete functions are recursive proof obligations rather
+than missing call identity or cost.
+
+### Reusing analyzed dependency and standard-library bodies
+
+Standard-library production is manifest-driven. A manifest pins and verifies
+the source release, selects source files, declares the language-owned build and
+SCIP indexing recipe, and names the exact expected indexer build. The shared
+pipeline then profiles CFG/DFG facts, applies soundness gates, exports exact
+symbols, verifies that the bundle joins back to its producer index, checks any
+declared consumers, and atomically publishes the result:
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/go-1.22.2.yml
+```
+
+Adding another SCIP standard library should therefore normally be a manifest,
+not shared Ruby or Rust code. Language-specific behavior is confined to the
+language's syntax/normalization module and the manifest's source/index recipe.
+Everything after SCIP production is language-neutral.
+
+The shared soundness gate requires an executable source body, zero
+export-eligible methods overlapping parser call loss, no overlapping parser
+recovery, an exact compatible SCIP producer, and source-proven time and space
+bounds.
+Open implementation candidate sets are not exported. Parametric callback bounds
+are exported only when the callback is an actual declared parameter. Generated
+complete data replaces incomplete fallback data; a generated/manual complete
+disagreement is a hard failure.
+
+The v3 bundle records the SHA-256 of the complete input profile, producer
+version, verified source release, indexer version, language set, and exported
+symbol count. It may additionally require exact opaque semantic-environment
+claims and record the digest of a generated producer-to-consumer symbol bridge.
+This permits versionless or cross-language runtime symbols without adding
+language branches to the shared join. Gzip output has a zero timestamp, so
+rebuilding identical inputs is byte-for-byte reproducible. Apply it to user
+code alongside that code's index and, when required, its environment sidecar:
+
+```bash
+gems/espalier/exe/espalier \
+ --scip-index user-code.scip \
+ --semantic-environment runtime-environment.json \
+ --complexity-summary go-stdlib.go1.25.0.json.gz \
+ --format json \
+ USER_SOURCE_FILES...
+```
+
+Summary joins require the exact compiler symbol already attached by SCIP. They
+never guess from an owner or method name. Unknown schema versions, malformed
+metadata, empty bounds, and contradictory files fail closed. The v1 reader
+remains available for previously generated artifacts, but new exports are v2.
+FactMine discovers every generated bundle in
+`config/complexity_summaries/*.json.gz` at build time; adding a language does
+not require shared Rust registration code. The current set is Go 1.22.2 (322
+exact symbols), Rust 1.96.0 (1,543), JDK 21.0.12 `java.lang`/`java.util`
+(2,598), and CPython 3.11.9 selected pure-Python core (200).
+The exporter rejects apparently complete functions whose proof depends on a
+manual receiver registry, modeled-world/external-latency contract, unknown
+cardinality relation, or unresolved call-evidence gap. Bundled data is applied
+automatically only when SCIP metadata reports the exact compatible indexer
+build; this is required even when a symbol already contains a package version.
+
+The smaller regenerated bundles are intentional. The previous artifacts
+included declaration-only Go functions, open-interface candidate assumptions,
+parser-recovered Rust methods, and internal callback bounds that were not safe
+to reuse in arbitrary consumers. Those are now rejected generically rather than
+worked around per language.
diff --git a/gems/espalier/exe/espalier b/gems/espalier/exe/espalier
index eff7ef5ce..624eae7da 100755
--- a/gems/espalier/exe/espalier
+++ b/gems/espalier/exe/espalier
@@ -5,6 +5,12 @@ require "optparse"
require_relative "../lib/espalier"
require_relative "../lib/espalier/type_profile"
+if ARGV.first == "stdlib-map"
+ ARGV.shift
+ require_relative "../lib/espalier/stdlib_map"
+ exit Espalier::StdlibMap.run_cli(ARGV)
+end
+
options = {
format: :markdown,
output: nil,
@@ -15,6 +21,8 @@ options = {
exclude: [],
fact_mine: nil,
scip_indexes: [],
+ semantic_environments: [],
+ complexity_summaries: [],
vcs: nil
}
@@ -65,6 +73,14 @@ OptionParser.new do |opts|
opts.on("--scip-index FILE", "Import a .scip or SCIP JSON index (repeatable)") do |f|
options[:scip_indexes] << f
end
+
+ opts.on("--semantic-environment FILE", "Attach exact runtime/build compatibility claims (repeatable)") do |f|
+ options[:semantic_environments] << f
+ end
+
+ opts.on("--complexity-summary FILE", "Apply an exact-SCIP-symbol complexity summary (repeatable)") do |f|
+ options[:complexity_summaries] << f
+ end
end.parse!
ENV["FACT_MINE_FACTS_FILE"] = options[:fact_mine] if options[:fact_mine]
@@ -149,11 +165,30 @@ evidence = Espalier::StaticEvidence.build(
root: Dir.pwd,
vcs: options[:vcs],
scip_indexes: options[:scip_indexes],
+ semantic_environments: options[:semantic_environments],
+ complexity_summaries: options[:complexity_summaries],
include_annotations: options[:format] != :architecture
)
if options[:format] == :architecture
- output_contents = JSON.pretty_generate(Espalier::ArchitectureArtifact.build(evidence, root: Dir.pwd))
+ # Big-O complexity is computed by the aggregator (which needs the parsed
+ # modules), not the lean static-evidence pipeline the architecture artifact
+ # otherwise uses. Run it here and attach per-function Big-O to the nodes so
+ # Lineage can surface time/space complexity per function.
+ big_o_modules = Espalier::StaticEvidence.project_modules(evidence)
+ nil_kill_evidence.apply!(big_o_modules)
+ big_o_index = Espalier::ArchitectureArtifact.big_o_index(
+ Espalier::Aggregator.new(
+ decomplex_data: decomplex_data,
+ nil_kill_data: nil_kill_data,
+ risk_data: risk_data,
+ nil_kill_loops: nil_kill_evidence.loop_counts,
+ nil_kill_evidence: nil_kill_evidence
+ ).aggregate(big_o_modules)
+ )
+ output_contents = JSON.pretty_generate(
+ Espalier::ArchitectureArtifact.build(evidence, root: Dir.pwd, big_o: big_o_index)
+ )
if options[:output]
File.write(options[:output], output_contents)
else
diff --git a/gems/espalier/lib/espalier.rb b/gems/espalier/lib/espalier.rb
index 5852344e7..629d6c5da 100644
--- a/gems/espalier/lib/espalier.rb
+++ b/gems/espalier/lib/espalier.rb
@@ -13,6 +13,8 @@ module Espalier
require_relative "espalier/static_evidence"
require_relative "espalier/big_o_proof_metrics"
require_relative "espalier/big_o_gap_impact"
+require_relative "espalier/complexity_summary"
+require_relative "espalier/stdlib_map"
require_relative "espalier/privacy_analyzer"
require_relative "espalier/architecture_analyzer"
require_relative "espalier/aggregator"
diff --git a/gems/espalier/lib/espalier/aggregator.rb b/gems/espalier/lib/espalier/aggregator.rb
index 0d2fc5311..8aa5b80c3 100644
--- a/gems/espalier/lib/espalier/aggregator.rb
+++ b/gems/espalier/lib/espalier/aggregator.rb
@@ -3,11 +3,14 @@
require "set"
require_relative "big_o_analyzer"
require_relative "structural_big_o"
+require_relative "complexity_overrides"
module Espalier
# Coalescing agent that imports the static skeleton maps and merges secondary
# metadata from: decomplex (decisions/clones), nil-kill (types), and boobytrap/slopcop (risk/coverage).
class Aggregator
+ MAX_RECURSIVE_SUMMARY_STATES = 64
+
def initialize(
decomplex_data: {},
nil_kill_data: {},
@@ -147,6 +150,8 @@ def aggregate(modules)
quality[:big_o_variables] = big_o_result[:complexity_variables] unless big_o_result[:complexity_variables].empty?
quality[:big_o_complete] = big_o_result[:time_complete]
quality[:big_o_space_complete] = big_o_result[:space_complete]
+ apply_complexity_override!(quality, mod, m)
+ quality[:big_o_status] = classify_big_o_status(quality)
quality[:big_o_dynamic] = big_o_result[:is_dynamic]
quality[:complexity_trigger] = big_o_result[:trigger] if big_o_result[:trigger]
quality[:big_o_warnings] = big_o_result[:warnings] unless big_o_result[:warnings].empty?
@@ -193,18 +198,67 @@ def aggregate(modules)
private
+ # Targeted escape hatch: consult the manual-override registry ONLY when the
+ # derived bound is incomplete, and apply only when an entry exists. A fully
+ # derived (complete) bound is never overridden. Marks provenance so an
+ # override-sourced bound is distinguishable from a structurally derived one.
+ def apply_complexity_override!(quality, mod, m)
+ return if quality[:big_o_complete]
+
+ # project_modules disambiguates owners as "name@path"; the override
+ # registry keys on the bare owner (package leaf / type name).
+ owner = mod[:name].to_s.split("@", 2).first
+ entry = ComplexityOverrides.lookup(mod[:language], owner, m[:name])
+ return unless entry
+
+ quality[:big_o] = entry["time"]
+ quality[:big_o_complete] = true
+ quality[:big_o_provenance] = :manual_override
+ quality[:big_o_override_note] = entry["note"]
+ return unless entry["space"]
+
+ quality[:big_o_space] = entry["space"]
+ quality[:big_o_space_complete] = true
+ end
+
+ # Distinguishes a fully-resolved bound from one that is only "complete"
+ # parametrically - i.e. still carries an open callback/reflective parameter
+ # that a worst-case substitution would have to close. The parametric tier is
+ # what the interface worst-case pass upgrades to :complete_worst_case.
+ #
+ # A parametric contract opens a parameter on BOTH axes - time O(N*C) and
+ # auxiliary space O(S) / O(N*S) - and the space one outlives the time one,
+ # because callback substitution rewrites only the time expression. Reading
+ # the time bound alone therefore publishes a space bound whose parameter
+ # nothing can bind as if it were closed.
+ #
+ # `S` is unambiguous in a space bound today: space carries no domain-symbol
+ # table of its own, so no size domain is ever rendered as `S` there.
+ def classify_big_o_status(quality)
+ return :incomplete unless quality[:big_o_complete]
+ return :complete_override if quality[:big_o_provenance] == :manual_override
+
+ bound = quality[:big_o].to_s
+ return :parametric if bound.include?("C") || bound.include?("R")
+ return :parametric if quality[:big_o_space].to_s.include?("S")
+
+ :complete
+ end
+
def internal_edges_for(mod)
legacy_internal_edges_for(mod)
end
def legacy_internal_edges_for(mod)
- method_names = mod[:methods].map { |m| m[:name].to_s }
+ method_names = mod[:methods].map { |m| m[:name].to_s }.tally
mod[:methods].flat_map do |method|
caller = method[:name].to_s
+ next [] unless method_names[caller] == 1
+
Array(method[:delegations]).filter_map do |delegation|
callee = delegation[:message].to_s
next unless delegation[:receiver] == "self"
- next unless method_names.include?(callee)
+ next unless method_names[callee] == 1
next if caller == callee
{
@@ -288,7 +342,10 @@ def prepare_module_indexes(modules)
@big_o_nodes_cache = {}
modules.each do |mod|
methods = Array(mod[:methods])
- @module_method_names[mod.object_id] = methods.map { |method| method[:name].to_s }.to_set
+ method_name_counts = methods.map { |method| method[:name].to_s }.tally
+ @module_method_names[mod.object_id] = method_name_counts.filter_map do |name, count|
+ name if count == 1
+ end.to_set
methods.each_with_index do |method, index|
start_line = method[:line] || 0
span = method[:span]
@@ -309,11 +366,7 @@ def line_in_method_bounds?(line, start_line, end_line, end_inclusive)
end_inclusive ? line <= end_line : line < end_line
end
- def preliminary_method_complexities(modules)
- analyzer = Espalier::BigOAnalyzer.new(
- nil_kill: @nil_kill_evidence,
- declared_fields: declared_fields_for(modules)
- )
+ def initial_method_complexities(modules)
complexities = Hash.new { |h, k| h[k] = {} }
spaces = Hash.new { |h, k| h[k] = {} }
time_complete = Hash.new { |h, k| h[k] = {} }
@@ -323,31 +376,22 @@ def preliminary_method_complexities(modules)
assumptions = Hash.new { |h, k| h[k] = {} }
modules.each do |mod|
Array(mod[:methods]).each do |method|
- analyzer.instance_variable_set(:@class_name, mod[:name])
- analyzer.instance_variable_set(:@ivar_types, mod[:ivar_types] || {})
- key = "#{mod[:name]}##{method[:name]}"
- sig = @nil_kill_data[key] || method[:signature]
- result = analyzer.analyze_method(
- key,
- big_o_nodes_for(mod, method),
- local_types: local_types_for_signature(sig)
- )
method_name = method[:name].to_s
- complexities[mod[:name]][method_name] = result[:known_time_component]
- spaces[mod[:name]][method_name] = result[:known_space_component]
- time_complete[mod[:name]][method_name] = result[:time_complete]
- space_complete[mod[:name]][method_name] = result[:space_complete]
- symbolic_time[mod[:name]][method_name] = result[:symbolic_time]
- bound_qualities[mod[:name]][method_name] = result[:bound_qualities]
- assumptions[mod[:name]][method_name] = result[:complexity_assumptions]
+ complexities[mod[:name]][method_name] = "O(1)"
+ spaces[mod[:name]][method_name] = "O(1)"
+ time_complete[mod[:name]][method_name] = true
+ space_complete[mod[:name]][method_name] = true
+ symbolic_time[mod[:name]][method_name] = nil
+ bound_qualities[mod[:name]][method_name] = []
+ assumptions[mod[:name]][method_name] = []
unless method[:id].to_s.empty?
- complexities[method[:id].to_s] = result[:known_time_component]
- spaces[method[:id].to_s] = result[:known_space_component]
- time_complete[method[:id].to_s] = result[:time_complete]
- space_complete[method[:id].to_s] = result[:space_complete]
- symbolic_time[method[:id].to_s] = result[:symbolic_time]
- bound_qualities[method[:id].to_s] = result[:bound_qualities]
- assumptions[method[:id].to_s] = result[:complexity_assumptions]
+ complexities[method[:id].to_s] = "O(1)"
+ spaces[method[:id].to_s] = "O(1)"
+ time_complete[method[:id].to_s] = true
+ space_complete[method[:id].to_s] = true
+ symbolic_time[method[:id].to_s] = nil
+ bound_qualities[method[:id].to_s] = []
+ assumptions[method[:id].to_s] = []
end
end
end
@@ -356,9 +400,9 @@ def preliminary_method_complexities(modules)
def structural_method_complexities(modules)
@structural_big_o_results = {}
- return preliminary_method_complexities(modules) if modules.empty?
+ return initial_method_complexities(modules) if modules.empty?
- complexities, spaces, time_complete, space_complete, symbolic_time, bound_qualities, assumptions = preliminary_method_complexities(modules)
+ complexities, spaces, time_complete, space_complete, symbolic_time, bound_qualities, assumptions = initial_method_complexities(modules)
internal_calls = internal_calls_by_method(modules)
resolved_calls = resolved_calls_by_site(modules)
candidate_calls = candidate_calls_by_site(modules)
@@ -387,6 +431,26 @@ def structural_method_complexities(modules)
structural_big_o.instance_variable_set(:@method_symbolic_time, symbolic_time)
structural_big_o.instance_variable_set(:@method_bound_qualities, bound_qualities)
structural_big_o.instance_variable_set(:@method_assumptions, assumptions)
+ # Index each callback-taking call site to the id of the callable it passes
+ # as its callback argument - an inline lambda (found by span containment)
+ # or a named function reference (resolved by argument spelling) - so a
+ # caller can substitute that callable's cost for the callee's callback C.
+ methods_by_id = {}
+ method_candidates_by_owner_name = Hash.new { |hash, key| hash[key] = [] }
+ lambdas_by_file = Hash.new { |hash, key| hash[key] = [] }
+ modules.each do |mod|
+ Array(mod[:methods]).each do |m|
+ methods_by_id[m[:id]] = m if m[:id]
+ method_candidates_by_owner_name[[m[:raw_owner].to_s, m[:name].to_s]] << m
+ lambdas_by_file[mod[:file]] << m if m[:dispatch_kind].to_s == "lambda" && m[:span]
+ end
+ end
+ methods_by_owner_name = method_candidates_by_owner_name.filter_map do |key, candidates|
+ [key, candidates.first] if candidates.one?
+ end.to_h
+ callback_arg_by_call = callback_arguments_by_call_site(modules, methods_by_id, methods_by_owner_name, lambdas_by_file)
+ @callback_arg_by_call = callback_arg_by_call
+ structural_big_o.instance_variable_set(:@callback_arg_by_call, callback_arg_by_call)
local_analyzer = Espalier::BigOAnalyzer.new(
nil_kill: @nil_kill_evidence,
@@ -396,9 +460,10 @@ def structural_method_complexities(modules)
# Components are ordered callee-first. Acyclic components therefore run
# exactly once. Within a recursive SCC, only callers of a changed method
# are re-enqueued; unrelated methods are never rescanned.
- summary_dependency_components(modules).each do |component|
+ summary_dependency_components(modules, callback_arg_by_call).each do |component|
entries = component.fetch(:entries)
by_identity = entries.to_h { |identity, mod, method| [identity, [mod, method]] }
+ recursive_members = component.fetch(:recursive) ? by_identity.keys.to_set : Set.new
base_nodes = entries.to_h do |identity, mod, method|
[identity, big_o_nodes_for(mod, method).freeze]
end
@@ -406,16 +471,22 @@ def structural_method_complexities(modules)
queued = queue.to_set
queue_index = 0
observed_states = Hash.new { |hash, identity| hash[identity] = Set.new }
+ widened_summaries = Set.new
while queue_index < queue.length
graph_identity = queue[queue_index]
queue_index += 1
queued.delete(graph_identity)
+ next if widened_summaries.include?(graph_identity)
+
mod, method = by_identity.fetch(graph_identity)
local_analyzer.instance_variable_set(:@class_name, mod[:name])
local_analyzer.instance_variable_set(:@ivar_types, mod[:ivar_types] || {})
key = "#{mod[:name]}##{method[:name]}"
sig = @nil_kill_data[key] || method[:signature]
- nodes = base_nodes.fetch(graph_identity) +
+ nodes = substitute_callback_arguments(
+ base_nodes.fetch(graph_identity), mod, symbolic_time, complexities, time_complete,
+ excluded_callable_ids: recursive_members
+ ) +
structural_big_o.hints_for(mod[:file], method, mod[:name])
result = local_analyzer.analyze_method(key, nodes, local_types: local_types_for_signature(sig))
@structural_big_o_results[graph_identity] = result
@@ -440,6 +511,29 @@ def structural_method_complexities(modules)
result[:complexity_assumptions] != current_assumptions
next unless result_changed
+ if component.fetch(:recursive) &&
+ observed_states[graph_identity].length >= MAX_RECURSIVE_SUMMARY_STATES
+ result = widen_recursive_summary(result)
+ @structural_big_o_results[graph_identity] = result
+ structural_big_o.apply_summary_delta!(method_identity, mod[:name], method_name, {
+ time: result.fetch(:known_time_component),
+ space: result.fetch(:known_space_component),
+ time_complete: false,
+ space_complete: false,
+ symbolic_time: nil,
+ bound_qualities: result.fetch(:bound_qualities),
+ assumptions: result.fetch(:complexity_assumptions)
+ })
+ widened_summaries << graph_identity
+ component.fetch(:callers).fetch(graph_identity, Set.new).each do |caller|
+ next if queued.include?(caller)
+
+ queue << caller
+ queued << caller
+ end
+ next
+ end
+
complexity = (time_changed || symbolic_changed) ? result[:known_time_component] : current
space = space_changed ? result[:known_space_component] : current_space
expression = symbolic_changed ? result[:symbolic_time] : current_symbolic
@@ -472,18 +566,41 @@ def structural_method_complexities(modules)
[complexities, spaces, time_complete, space_complete, symbolic_time, bound_qualities, assumptions]
end
- def summary_dependency_components(modules)
+ def widen_recursive_summary(result)
+ result.merge(
+ lower_bound_complexity: "unknown",
+ space_complexity: "unknown",
+ known_time_component: "O(1)",
+ known_space_component: "O(1)",
+ symbolic_time: nil,
+ complexity_variables: [],
+ time_complete: false,
+ space_complete: false,
+ evidence_gaps: (Array(result[:evidence_gaps]) + ["unresolved_recursive_progress"]).uniq.sort,
+ warnings: (Array(result[:warnings]) +
+ ["Recursive summary did not converge within the finite complexity lattice."]).uniq
+ )
+ end
+
+ def summary_dependency_components(modules, callback_arg_by_call = {})
entries = {}
- aliases = {}
+ alias_candidates = Hash.new { |hash, key| hash[key] = Set.new }
modules.each do |mod|
Array(mod[:methods]).each do |method|
fallback = [mod[:name].to_s, method[:name].to_s]
identity = method[:id].to_s.empty? ? fallback : method[:id].to_s
entries[identity] = [mod, method]
- aliases[fallback] = identity
- aliases[method[:id].to_s] = identity unless method[:id].to_s.empty?
+ alias_candidates[fallback] << identity
+ alias_candidates[method[:id].to_s] << identity unless method[:id].to_s.empty?
end
end
+ # A lexical `(owner, short-name)` is usable only when it denotes exactly
+ # one method. Nested functions and overloads routinely share that pair;
+ # choosing the last one creates false dependency cycles and can make the
+ # symbolic fixed point grow without bound. Exact method ids remain unique.
+ aliases = alias_candidates.filter_map do |key, candidates|
+ [key, candidates.first] if candidates.one?
+ end.to_h
graph = entries.each_key.to_h { |identity| [identity, Set.new] }
entries.each do |source, (mod, method)|
@@ -496,9 +613,19 @@ def summary_dependency_components(modules)
aliases[[mod[:name].to_s, delegation[:message].to_s]]
end
graph[source] << target if target
- Array(delegation[:candidate_target_ids]).each do |candidate|
- candidate_target = aliases[candidate.to_s]
- graph[source] << candidate_target if candidate_target
+ if delegation[:consumer_closed_candidate_set] == true
+ Array(delegation[:candidate_target_ids]).each do |candidate|
+ candidate_target = aliases[candidate.to_s]
+ graph[source] << candidate_target if candidate_target
+ end
+ end
+ # A callable passed at this call site is a real summary dependency:
+ # its cost is substituted for the callee's open C, so it must be
+ # summarized first and must re-enqueue this caller when it changes.
+ span = normalized_call_span(delegation[:span])
+ Array(span && callback_arg_by_call[[mod[:file], span]]).each do |callable|
+ callable_target = aliases[callable.to_s]
+ graph[source] << callable_target if callable_target && callable_target != source
end
end
end
@@ -538,6 +665,8 @@ def summary_dependency_components(modules)
[identity, mod, method]
end,
callers: callers,
+ recursive: component_members.length > 1 ||
+ component_members.any? { |identity| graph.fetch(identity).include?(identity) },
}
reverse[component].sort.each do |caller|
remaining_dependencies[caller] -= 1
@@ -572,7 +701,9 @@ def resolved_summary_depth(modules)
delegation[:target_id].to_s
graph[source] << target if identities[target]
end
- Array(method[:delegations]).flat_map { |delegation| Array(delegation[:candidate_target_ids]) }
+ Array(method[:delegations])
+ .select { |delegation| delegation[:consumer_closed_candidate_set] == true }
+ .flat_map { |delegation| Array(delegation[:candidate_target_ids]) }
.map(&:to_s).each do |target|
graph[source] << target if identities[target]
end
@@ -611,8 +742,11 @@ def resolved_summary_depth(modules)
def internal_calls_by_method(modules)
modules.each_with_object({}) do |mod, owners|
- method_names = Array(mod[:methods]).map { |method| method[:name].to_s }.to_set
+ method_name_counts = Array(mod[:methods]).map { |method| method[:name].to_s }.tally
+ method_names = method_name_counts.filter_map { |name, count| name if count == 1 }.to_set
owners[mod[:name].to_s] = Array(mod[:methods]).each_with_object({}) do |method, callers|
+ next unless method_name_counts[method[:name].to_s] == 1
+
callers[method[:name].to_s] = Array(method[:delegations]).filter_map do |delegation|
next unless delegation[:receiver].to_s == "self"
@@ -653,10 +787,128 @@ def resolved_calls_by_site(modules)
end.compact
end
+ # A call priced parametrically from its compiler symbol carries an open C.
+ # When the callables passed at that site are analyzed, C is not open, so
+ # substitute the costliest of them. Runs per fixpoint iteration, on the
+ # current summaries, rather than on the cached base nodes.
+ def substitute_callback_arguments(
+ nodes, mod, symbolic_time, complexities, time_complete, excluded_callable_ids: Set.new
+ )
+ return nodes if @callback_arg_by_call.nil? || @callback_arg_by_call.empty?
+
+ nodes.map do |node|
+ expression = node[:symbolic_time]
+ next node if expression.nil? ||
+ Espalier::SymbolicComplexity.callback_domain_ids(expression).empty?
+
+ callable_ids = Array(@callback_arg_by_call[[mod[:file], node[:span]]]).reject do |id|
+ excluded_callable_ids.include?(id)
+ end
+ callable = worst_callable(callable_ids, symbolic_time, complexities, time_complete)
+ next node unless callable
+
+ substituted = Espalier::SymbolicComplexity.substitute_callback_cost(
+ expression, callable.fetch(:expression), callable_constant: callable.fetch(:constant)
+ )
+ next node if substituted.equal?(expression)
+
+ node.merge(
+ symbolic_time: substituted,
+ known_time_complexity: Espalier::SymbolicComplexity.render(substituted)&.first ||
+ node[:known_time_complexity]
+ )
+ end
+ end
+
+ def worst_callable(ids, symbolic_time, complexities, time_complete)
+ Espalier::SymbolicComplexity.worst_callable(
+ Array(ids).map do |id|
+ key = id.to_s
+ {
+ expression: symbolic_time[key],
+ constant: complexities[key] == "O(1)" && time_complete[key] != false
+ }
+ end
+ )
+ end
+
+ # Index every call site to the callables passed there, so a caller can
+ # substitute their cost for an open callback C. Two sources of a callable:
+ # a closure literal, which lies inside the call's own span, and a named
+ # function passed positionally into a declared callback parameter.
+ #
+ # The closure rule is deliberately independent of the callee: a stdlib
+ # higher-order call is priced parametrically from its compiler symbol and
+ # has no analyzed body to declare `callback_params`, yet the closure at its
+ # call site is exactly what closes its C.
+ def callback_arguments_by_call_site(modules, methods_by_id, methods_by_owner_name, lambdas_by_file)
+ index = Hash.new { |hash, key| hash[key] = [] }
+ delegations_by_file = Hash.new { |hash, key| hash[key] = [] }
+ modules.each do |mod|
+ Array(mod[:methods]).each do |method|
+ Array(method[:delegations]).each do |delegation|
+ span = normalized_call_span(delegation[:span])
+ next unless span
+
+ delegations_by_file[mod[:file]] << span
+ named_callback_argument(mod, method, delegation, methods_by_id, methods_by_owner_name)
+ &.then { |id| index[[mod[:file], span]] << id }
+ end
+ end
+ end
+ lambdas_by_file.each do |file, lambdas|
+ spans = delegations_by_file[file]
+ lambdas.each do |lambda_method|
+ span = normalized_call_span(lambda_method[:span])
+ # Every call whose span encloses the closure may run it: in
+ # `xs.iter().map(|x| ...).collect()` the parametric cost sits on
+ # `collect`, while the closure is lexically an argument of `map`.
+ # Charging the closure to each enclosing call is the worst-case
+ # reading, and the substitution takes the costliest callable anyway.
+ spans.select { |candidate| span_contains?(candidate, span) }
+ .each { |candidate| index[[file, candidate]] << lambda_method[:id] }
+ end
+ end
+ index.transform_values { |ids| ids.compact.uniq }
+ end
+
+ def named_callback_argument(mod, method, delegation, methods_by_id, methods_by_owner_name)
+ callee = (delegation[:target_id] && methods_by_id[delegation[:target_id]]) ||
+ (delegation[:target_method] &&
+ methods_by_owner_name[[delegation[:target_owner].to_s, delegation[:target_method].to_s]])
+ return nil unless callee
+
+ params = Array(callee[:parameters]).map(&:to_s)
+ Array(callee[:callback_params]).each do |callback_param|
+ position = params.index(callback_param.to_s)
+ next unless position
+
+ argument = Array(delegation[:arguments])[position].to_s.strip
+ function = methods_by_owner_name[[method[:raw_owner].to_s, argument]] ||
+ methods_by_owner_name[[mod[:name].to_s, argument]]
+ return function[:id] if function
+ end
+ nil
+ end
+
+ def span_contains?(outer, inner)
+ return false unless outer && inner
+
+ starts = outer[0] < inner[0] || (outer[0] == inner[0] && outer[1] <= inner[1])
+ ends = outer[2] > inner[2] || (outer[2] == inner[2] && outer[3] >= inner[3])
+ starts && ends
+ end
+
+ def span_extent(span)
+ [span[2] - span[0], span[3] - span[1]]
+ end
+
def candidate_calls_by_site(modules)
modules.each_with_object({}) do |mod, index|
Array(mod[:methods]).each do |method|
Array(method[:delegations]).each do |delegation|
+ next unless delegation[:consumer_closed_candidate_set] == true
+
candidates = Array(delegation[:candidate_target_ids]).map(&:to_s).reject(&:empty?).uniq.sort
next if candidates.empty?
@@ -669,7 +921,14 @@ def candidate_calls_by_site(modules)
keys.unshift([method[:id].to_s, delegation[:message].to_s, line])
keys.unshift([method[:id].to_s, delegation[:message].to_s, span]) if span
end
- value = { ids: candidates, reason: delegation[:candidate_reason].to_s }
+ value = {
+ ids: candidates,
+ reason: delegation[:candidate_reason].to_s,
+ qualities: Array(delegation[:complexity_bound_quality]).map(&:to_s),
+ assumptions: Array(delegation[:complexity_assumptions]).map(&:to_s),
+ external_time: delegation[:known_time_complexity],
+ external_space: delegation[:known_space_complexity]
+ }
keys.each do |key|
if index.key?(key) && index[key] != value
index[key] = nil
@@ -839,6 +1098,8 @@ def big_o_nodes_for(mod, method)
index[key] = row if !index[key] || row["power"].to_i > index[key]["power"].to_i
end
contexts_by_line = contexts.group_by { |row| [row["message"].to_s, row["line"].to_i] }
+ callback_params = Array(method[:callback_params]).map(&:to_s).to_set
+ iterations = Array(method[:complexity_facts]).flat_map { |fact| Array(fact["iterations"]) }
nodes = Array(method[:delegations]).map do |delegation|
message = delegation[:message].to_s
span = normalized_call_span(delegation[:span])
@@ -847,6 +1108,27 @@ def big_o_nodes_for(mod, method)
line_rows = contexts_by_line[[message, (delegation[:line] || method[:line] || 0).to_i]]
context = line_rows.first if line_rows&.one?
end
+ # A call to a callback parameter has a cost parametric in that callback
+ # (C), a complete algebraic atom that a loop composes to O(N*C) and a
+ # caller resolves by substituting the passed callable's cost.
+ callback_cost = if callback_params.include?(message) &&
+ !delegation[:target_method] && !delegation[:known_time_complexity]
+ # The callback runs once per iteration of its enclosing loop, so its
+ # cost is that loop's size domain times C. Match the loop by span
+ # containment and reuse its parameter size domain (which a caller can
+ # also substitute), giving O(N*C); no enclosing loop gives O(C).
+ cb_line = (delegation[:line] || method[:line] || 0).to_i
+ loop_fact = iterations.find do |it|
+ bounds = it["span"]
+ bounds && cb_line >= bounds[0].to_i && cb_line <= bounds[2].to_i
+ end
+ mult_id = loop_fact && Array(loop_fact.dig("symbolic_time", "factors")).first&.fetch("domain_id", nil)
+ mult_domains = mult_id ? [{ "id" => mult_id, "name" => Array(loop_fact["parameter_domains"]).first.to_s, "source_kind" => "parameter" }] : []
+ Espalier::SymbolicComplexity.parameterized_cost(
+ id: unknown_cost_id(delegation), name: message, source_kind: "callback_cost",
+ multiplicity_domain: mult_id, domains: mult_domains
+ )
+ end
{
type: :call,
call_id: delegation[:call_id],
@@ -858,25 +1140,27 @@ def big_o_nodes_for(mod, method)
# The canonical CallRecord is enriched after syntax normalization by
# SCIP and dependency summaries. Its exact symbol cost must outrank
# an earlier adapter-level context model for the same source span.
- known_time_complexity: delegation[:known_time_complexity] || (context && context["known_time_complexity"]),
+ known_time_complexity: (callback_cost && (Espalier::SymbolicComplexity.render(callback_cost)&.first || "O(C)")) ||
+ delegation[:known_time_complexity] || (context && context["known_time_complexity"]),
known_space_complexity: delegation[:known_space_complexity] || (context && context["known_space_complexity"]),
complexity_provenance: delegation[:complexity_provenance],
complexity_bound_quality: delegation[:complexity_bound_quality],
complexity_candidates: delegation[:complexity_candidates],
complexity_assumptions: delegation[:complexity_assumptions],
- evidence_gap: if delegation[:known_time_complexity] || delegation[:known_space_complexity]
+ evidence_gap: if delegation[:known_time_complexity] || delegation[:known_space_complexity] || callback_cost
nil
else
context && context["evidence_gap"]
end,
- symbolic_time: parametric_call_symbolic(delegation, context) ||
+ symbolic_time: callback_cost || parametric_call_symbolic(delegation, context) ||
(context && symbolic_call_complexity(context)),
collection_arguments: context && context["power"].to_i.positive? &&
(Array(context["parameter_arguments"]) & Array(context["collection_parameters"])),
internal_call: (delegation[:receiver].to_s == "self" &&
module_method_names.include?(delegation[:message].to_s)) ||
(delegation[:target_owner] && delegation[:target_method] && context) ||
- (Array(delegation[:candidate_target_ids]).any? && context)
+ (delegation[:consumer_closed_candidate_set] == true &&
+ Array(delegation[:candidate_target_ids]).any? && context)
}.compact
end
@@ -938,7 +1222,7 @@ def parametric_call_symbolic(delegation, context)
end
reflective = quality.include?("reflective")
Espalier::SymbolicComplexity.parameterized_cost(
- id: "cost:#{delegation[:call_id]}",
+ id: unknown_cost_id(delegation),
name: "#{delegation[:receiver]}.#{delegation[:message]}",
source_kind: reflective ? "reflective_target_cost" : "callback_cost",
multiplicity_domain: multiplicity_domain,
@@ -946,6 +1230,19 @@ def parametric_call_symbolic(delegation, context)
)
end
+
+ # A cost symbol names the *callee's* unknown cost, so every call to the same
+ # callee must share one symbol. Keying it on the call site instead minted a
+ # fresh C per call, producing unusable bounds (observed: 294 distinct symbols
+ # in one function) and making substitution impossible.
+ def unknown_cost_id(delegation)
+ callee = delegation[:target_id].to_s
+ callee = "#{delegation[:target_owner]}##{delegation[:target_method]}" if callee.empty? &&
+ !delegation[:target_method].to_s.empty?
+ callee = "#{delegation[:receiver]}.#{delegation[:message]}" if callee.empty?
+ "cost:#{callee}"
+ end
+
def local_types_for_signature(signature)
params_source = signature_params_source(signature.to_s)
return {} unless params_source
diff --git a/gems/espalier/lib/espalier/alias_recommendations.rb b/gems/espalier/lib/espalier/alias_recommendations.rb
index b505ac2c2..acf807809 100644
--- a/gems/espalier/lib/espalier/alias_recommendations.rb
+++ b/gems/espalier/lib/espalier/alias_recommendations.rb
@@ -20,7 +20,7 @@ def initialize(type_definitions, minimum_slots: 1)
end
def build
- slots = slot_records
+ by_path, by_owner = index_slots(slot_records)
aliases.filter_map do |definition|
target = alias_target(definition)
profile = type_profile_for_definition(definition)
@@ -29,13 +29,48 @@ def build
alias_name = qualified_alias_name(definition)
next if alias_name.empty?
- matches = slots.filter_map { |slot| alias_slot_match(slot, definition, alias_name, target, profile) }
+ matches = candidate_slots(definition, by_path, by_owner)
+ .filter_map { |slot| alias_slot_match(slot, definition, alias_name, target, profile) }
next if matches.size < @minimum_slots
recommendation(definition, alias_name, target, matches)
end.sort_by { |row| [-row["slot_count"].to_i, row["alias"].to_s, row.dig("definition", "path").to_s] }
end
+ # A slot can only match an alias in the same language whose scope covers it:
+ # the same file, or the alias owner / a nesting ancestor of it (see
+ # `alias_scope_matches_slot?`). Bucketing slots by `[language, path]` and by
+ # `[language, owner-and-every-ancestor]` lets each alias examine only that
+ # union instead of every slot, turning the O(aliases x slots) scan into a
+ # scoped lookup. The result set is identical - `alias_slot_match` still runs
+ # its full guards on the candidates.
+ def index_slots(slots)
+ by_path = Hash.new { |hash, key| hash[key] = [] }
+ by_owner = Hash.new { |hash, key| hash[key] = [] }
+ slots.each do |slot|
+ language = slot["language"].to_s
+ by_path[[language, slot["path"].to_s]] << slot
+ ancestor = slot["owner"].to_s
+ until ancestor.empty?
+ by_owner[[language, ancestor]] << slot
+ separator = ancestor.rindex("::")
+ break unless separator
+
+ ancestor = ancestor[0...separator]
+ end
+ end
+ [by_path, by_owner]
+ end
+
+ def candidate_slots(definition, by_path, by_owner)
+ language = definition["language"].to_s
+ path_slots = by_path[[language, definition["path"].to_s]]
+ owner = definition["owner"].to_s
+ return path_slots if owner.empty?
+
+ (by_owner[[language, owner]] + path_slots).uniq(&:object_id)
+ end
+
private
def aliases
diff --git a/gems/espalier/lib/espalier/architecture_artifact.rb b/gems/espalier/lib/espalier/architecture_artifact.rb
index 4299d19c3..a1a40d8cc 100644
--- a/gems/espalier/lib/espalier/architecture_artifact.rb
+++ b/gems/espalier/lib/espalier/architecture_artifact.rb
@@ -14,7 +14,7 @@ module ArchitectureArtifact
SCHEMA_VERSION = 1
- def build(evidence, root: evidence["root"], commit: nil)
+ def build(evidence, root: evidence["root"], commit: nil, big_o: {})
owners = Array(evidence["owners"])
methods = Array(evidence["methods"])
fields = Array(evidence["fields"])
@@ -26,7 +26,7 @@ def build(evidence, root: evidence["root"], commit: nil)
nodes = []
owners.each { |owner| nodes << owner_node(owner, root) if high_confidence_owner?(owner) }
- methods.each { |method| nodes << function_node(method, root, heuristic_owner_ids) }
+ methods.each { |method| nodes << function_node(method, root, heuristic_owner_ids, big_o) }
fields.each { |field| nodes << state_node(field, root) }
nodes_by_id = nodes.to_h { |node| [node["id"], node] }
@@ -62,6 +62,17 @@ def build(evidence, root: evidence["root"], commit: nil)
edges << relationship_edge(access, source, target, access["kind"], root)
end
+ # Source-level imports are not part of FactMine's call/state facts, so we
+ # scan each analyzed file once for its import/require statements and emit
+ # them as `imports` edges (target: an external node named for the module).
+ source_files(owners, methods, fields).each do |path, language|
+ extract_imports(path, language).each do |mod, line|
+ external_id = "external:import:#{Digest::SHA256.hexdigest(mod)[0, 16]}"
+ nodes_by_id[external_id] ||= import_target_node(external_id, mod, language)
+ edges << import_edge(path, mod, line, external_id, root)
+ end
+ end
+
nodes = nodes_by_id.values
edges = merge_edges(edges)
cyclic = cyclic_node_ids(edges)
@@ -117,10 +128,10 @@ def owner_node(owner, root)
)
end
- def function_node(method, root, heuristic_owner_ids = Set.new)
+ def function_node(method, root, heuristic_owner_ids = Set.new, big_o = {})
owner_id = method["owner_id"]
owner_id = nil if heuristic_owner_ids.include?(owner_id)
- base_node(method, "function", root).merge(
+ node = base_node(method, "function", root).merge(
"owner_id" => owner_id,
"metadata" => {
"visibility" => method["visibility"] || "public",
@@ -130,6 +141,75 @@ def function_node(method, root, heuristic_owner_ids = Set.new)
"confidence" => "high"
}
)
+ bo = big_o[big_o_key(method["path"], method["name"])]
+ bo ? node.merge(bo) : node
+ end
+
+ # Build a lookup of per-function Big-O (time/space + completeness) from an
+ # aggregator manifest, keyed by (file, method name). The architecture graph
+ # attaches these to function nodes so Lineage can surface complexity without
+ # re-running the analysis. Complexity lives in the aggregator pipeline, not
+ # the lean static-evidence one, so it is threaded in here.
+ def big_o_index(manifest)
+ index = {}
+ Array(manifest).each do |mod|
+ file = mod[:file] || mod["file"]
+ Array(mod[:functions] || mod["functions"]).each do |fn|
+ quality = fn[:quality_metrics] || fn["quality_metrics"] || {}
+ time, time_complete = big_o_bound(quality, :big_o, :big_o_known_component, :big_o_complete)
+ space, space_complete =
+ big_o_bound(quality, :big_o_space, :big_o_space_known_component, :big_o_space_complete)
+ next if time.nil? && space.nil?
+
+ node = {}
+ unless time.nil?
+ node["big_o_time"] = time
+ node["time_complete"] = time_complete
+ end
+ unless space.nil?
+ node["big_o_space"] = space
+ node["space_complete"] = space_complete
+ end
+ status = quality[:big_o_status] || quality["big_o_status"]
+ node["big_o_status"] = status.to_s if status
+ provenance = quality[:big_o_provenance] || quality["big_o_provenance"]
+ node["big_o_provenance"] = provenance.to_s if provenance
+ index[big_o_key(file, fn[:name] || fn["name"])] = node
+ end
+ end
+ index
+ end
+
+ # Resolve a function's bound: the complete lower-bound when the analyzer
+ # proved it, otherwise the known-but-incomplete component (a *partial* bound
+ # - most functions land here, e.g. a loop whose count the analyzer can't
+ # prove is exhaustive). Returns [bound, complete?], or [nil, false] when
+ # there is no usable bound at all.
+ def big_o_bound(quality, bound_key, known_key, complete_key)
+ bound = big_o_present(quality[bound_key] || quality[bound_key.to_s])
+ known = big_o_present(quality[known_key] || quality[known_key.to_s])
+ complete = (quality[complete_key] || quality[complete_key.to_s]) ? true : false
+ if complete && bound
+ [bound, true]
+ elsif known
+ [known, false]
+ elsif bound
+ [bound, false]
+ else
+ [nil, false]
+ end
+ end
+
+ # A bound string that actually names a bound (not nil/empty/"unknown").
+ def big_o_present(value)
+ return nil if value.nil?
+
+ str = value.to_s
+ str.empty? || str.casecmp("unknown").zero? ? nil : str
+ end
+
+ def big_o_key(path, name)
+ "#{path} #{name}"
end
def state_node(field, root)
@@ -354,6 +434,106 @@ def current_commit(root)
""
end
+ # Unique {absolute_path => language} over every record that carries a path,
+ # so we scan each analyzed source file for imports exactly once.
+ def source_files(owners, methods, fields)
+ files = {}
+ (owners + methods + fields).each do |record|
+ path = record["path"]
+ next if path.to_s.empty?
+
+ files[path] ||= record["language"]
+ end
+ files
+ end
+
+ def import_target_node(id, mod, language)
+ {
+ "id" => id, "kind" => "external", "name" => mod, "owner" => nil,
+ "language" => language, "path" => nil,
+ "start_line" => 0, "start_column" => 0, "end_line" => 0, "end_column" => 0,
+ "metadata" => { "confidence" => "high", "import" => true }
+ }
+ end
+
+ def import_edge(path, mod, line, target, root)
+ rel = relative_path(path, root)
+ {
+ "id" => "import:#{Digest::SHA256.hexdigest([rel, mod].join("\0"))[0, 16]}",
+ "source" => "file:#{rel}", "target" => target, "kind" => "imports",
+ "conditional" => false, "confidence" => "high", "weight" => 1,
+ "spans" => [{ "path" => rel, "start_line" => line, "start_column" => 0,
+ "end_line" => line, "end_column" => 0 }],
+ "metadata" => { "module" => mod }
+ }
+ end
+
+ # Language-specific import/require statements as [module, line] pairs. This
+ # is a deliberately small line scanner: the module string as written is what
+ # a reviewer wants to see, so no resolution or path normalization is done.
+ def extract_imports(path, language)
+ source = File.read(path)
+ lang = (language || File.extname(path).delete_prefix(".")).to_s.downcase
+ case lang
+ when "go" then go_imports(source)
+ when "ruby", "rb" then scan_imports(source, /^\s*require(?:_relative)?\s+['"]([^'"]+)['"]/)
+ when "python", "py" then scan_imports(source, /^\s*(?:from\s+(\S+)\s+import\b|import\s+([^\s,]+))/)
+ when "javascript", "js", "jsx", "typescript", "ts", "tsx" then js_imports(source)
+ when "rust", "rs" then scan_imports(source, /^\s*use\s+([A-Za-z_][\w:]*)/)
+ when "java", "kotlin", "kt" then scan_imports(source, /^\s*import\s+(?:static\s+)?([\w.]+)/)
+ when "c", "cpp", "cc", "h", "hpp" then scan_imports(source, /^\s*#\s*include\s+[<"]([^>"]+)[>"]/)
+ else []
+ end
+ rescue StandardError
+ []
+ end
+
+ # A single capture group per matching line; supports two alternative groups
+ # (Python `from X`/`import X`) by taking whichever captured.
+ def scan_imports(source, pattern)
+ out = []
+ source.each_line.with_index(1) do |line, number|
+ next unless (match = pattern.match(line))
+
+ mod = match[1] || match[2]
+ out << [mod, number] if mod
+ end
+ out
+ end
+
+ # Go supports both `import "x"` and a parenthesized block of quoted paths
+ # (optionally aliased). We track the block so the block members are captured.
+ def go_imports(source)
+ out = []
+ in_block = false
+ source.each_line.with_index(1) do |line, number|
+ if in_block
+ break_block = line.include?(")")
+ if (match = /["`]([^"`]+)["`]/.match(line))
+ out << [match[1], number]
+ end
+ in_block = false if break_block
+ elsif line =~ /^\s*import\s*\(/
+ in_block = true
+ elsif (match = /^\s*import\s+(?:[\w.]+\s+)?["`]([^"`]+)["`]/.match(line))
+ out << [match[1], number]
+ end
+ end
+ out
+ end
+
+ def js_imports(source)
+ out = []
+ source.each_line.with_index(1) do |line, number|
+ if (match = /^\s*import\b.*?from\s+['"]([^'"]+)['"]/.match(line)) ||
+ (match = /^\s*import\s+['"]([^'"]+)['"]/.match(line)) ||
+ (match = /require\(\s*['"]([^'"]+)['"]\s*\)/.match(line))
+ out << [match[1], number]
+ end
+ end
+ out
+ end
+
def relative_path(path, root)
return path if path.to_s.empty? || root.to_s.empty?
diff --git a/gems/espalier/lib/espalier/big_o_analyzer.rb b/gems/espalier/lib/espalier/big_o_analyzer.rb
index 1bcbc990d..c017fdd3a 100644
--- a/gems/espalier/lib/espalier/big_o_analyzer.rb
+++ b/gems/espalier/lib/espalier/big_o_analyzer.rb
@@ -29,6 +29,7 @@ def analyze_method(method_name, ast_nodes, local_types: nil)
time_complete = true
space_complete = true
symbolic_time = nil
+ symbolic_terms = []
is_dynamic = false
trigger = nil
unknown_operations = []
@@ -61,8 +62,9 @@ def analyze_method(method_name, ast_nodes, local_types: nil)
complexity_assumptions.concat(Array(node[:complexity_assumptions]))
known_complexity = node[:known_time_complexity].to_s
if node[:symbolic_time]
- symbolic_time = Espalier::SymbolicComplexity.sum(symbolic_time, node[:symbolic_time])
- known_complexity = Espalier::SymbolicComplexity.render(symbolic_time)&.first || known_complexity
+ symbolic_terms << node[:symbolic_time]
+ known_complexity =
+ Espalier::SymbolicComplexity.render(node[:symbolic_time])&.first || known_complexity
elsif node[:execution_complexity]
known_complexity = multiply_complexity(known_complexity, node[:execution_complexity])
end
@@ -107,8 +109,8 @@ def analyze_method(method_name, ast_nodes, local_types: nil)
complexity_assumptions.concat(Array(node[:complexity_assumptions]))
structural_complexity = node[:complexity].to_s
if node[:symbolic_time]
- symbolic_time = Espalier::SymbolicComplexity.sum(symbolic_time, node[:symbolic_time])
- rendered_symbolic = Espalier::SymbolicComplexity.render(symbolic_time)&.first
+ symbolic_terms << node[:symbolic_time]
+ rendered_symbolic = Espalier::SymbolicComplexity.render(node[:symbolic_time])&.first
if rendered_symbolic && complexity_rank(rendered_symbolic) >= complexity_rank(structural_complexity)
structural_complexity = rendered_symbolic
end
@@ -141,6 +143,14 @@ def analyze_method(method_name, ast_nodes, local_types: nil)
end
end
+ # Normalize the symbolic sum once. Incrementally normalizing after every
+ # node repeatedly performs the quadratic dominance comparison over an
+ # ever-growing term set; methods with hundreds of normalized call facts
+ # turn that into cubic work without changing the resulting expression.
+ symbolic_time = Espalier::SymbolicComplexity.sum(symbolic_terms)
+ rendered_symbolic = Espalier::SymbolicComplexity.render(symbolic_time)&.first
+ complexity = max_complexity(complexity, rendered_symbolic) if rendered_symbolic
+
{
method: method_name,
lower_bound_complexity: time_complete ? complexity : "unknown",
@@ -210,7 +220,7 @@ def resolve_type(receiver_name, line)
if receiver_name.include?(".")
parts = receiver_name.split(".")
current_type = resolve_simple_type(parts.first, line)
- parts[1..].each do |part|
+ Array(parts[1..]).each do |part|
return nil unless current_type
current_type = resolve_method_return_type(current_type, part)
end
@@ -225,6 +235,8 @@ def resolve_simple_type(receiver_name, line)
end
def raw_simple_type(receiver_name, line)
+ return nil if receiver_name.nil?
+
if receiver_name == "self"
return @class_name
end
diff --git a/gems/espalier/lib/espalier/big_o_gap_impact.rb b/gems/espalier/lib/espalier/big_o_gap_impact.rb
index a2d80ab98..45176faac 100644
--- a/gems/espalier/lib/espalier/big_o_gap_impact.rb
+++ b/gems/espalier/lib/espalier/big_o_gap_impact.rb
@@ -90,7 +90,14 @@ def root_categories(result, calls)
categories << "dynamic_or_reflective_dispatch" unless boundary.empty?
categories << "callback_origin_or_cost_missing" if value(call, :callback_receiver) == true
symbol = value(call, :semantic_symbol).to_s
- categories << "external_symbol_cost_missing" unless symbol.empty?
+ unless symbol.empty?
+ category = if value(call, :external_symbol_scope).to_s == "project"
+ "project_candidate_summary_missing"
+ else
+ "external_symbol_cost_missing"
+ end
+ categories << category
+ end
reason = value(call, :resolution_missing_proof).to_s
categories << "callback_origin_or_cost_missing" if reason.include?("callback")
end
diff --git a/gems/espalier/lib/espalier/big_o_proof_metrics.rb b/gems/espalier/lib/espalier/big_o_proof_metrics.rb
index f3f1f1972..d1e4d1a8d 100644
--- a/gems/espalier/lib/espalier/big_o_proof_metrics.rb
+++ b/gems/espalier/lib/espalier/big_o_proof_metrics.rb
@@ -7,6 +7,8 @@ module Espalier
# emitting O(N) is not progress unless its proof obligations are discharged
# or explicitly represented by a trusted/modelled contract.
module BigOProofMetrics
+ DEFAULT_MINIMUM_PERCENT = 85.0
+
CATEGORY_LABELS = {
known_provably: "Known provably, no assumptions",
known_likely: "Known likely/trusted/model-derived",
@@ -29,6 +31,7 @@ module BigOProofMetrics
RECURSIVE_QUALITIES = %w[
upper_bound_acyclic_project_scc
upper_bound_recursive_multiplicity
+ upper_bound_structural_descent
].freeze
MODELED_QUALITY = "upper_bound_modeled_world"
CLOSED_CANDIDATE_MAX_QUALITY = "upper_bound_closed_candidate_max"
@@ -72,6 +75,59 @@ def summarize(qualities)
}
end
+ # Build the machine-readable coverage contract used by CI and corpus
+ # measurements. +rows+ deliberately carries scope metadata alongside the
+ # quality hash so a caller cannot accidentally mix tests, benchmarks, or
+ # generated functions into the production denominator.
+ #
+ # Raw call normalization is a soundness precondition rather than another
+ # coverage percentage: an executable call that never reached normalized
+ # facts can make an otherwise "complete" function look constant. Until
+ # FactMine can account for every such call, the gate must fail closed.
+ def coverage_gate(rows, call_coverage: {}, minimum_percent: DEFAULT_MINIMUM_PERCENT,
+ include_lambdas: true)
+ minimum = Float(minimum_percent)
+ raise ArgumentError, "minimum_percent must be between 0 and 100" unless minimum.between?(0.0, 100.0)
+
+ scoped = Array(rows).select do |row|
+ fetch(row, :source_role).to_s == "production" &&
+ (include_lambdas || fetch(row, :kind).to_s != "lambda")
+ end
+ by_language = scoped.group_by { |row| fetch(row, :language).to_s }
+ .sort.to_h do |language, language_rows|
+ [language.empty? ? "unknown" : language, coverage_summary(language_rows)]
+ end
+ summary = coverage_summary(scoped)
+ raw_gaps = fetch(call_coverage, :raw_calls_not_normalized_inside_function).to_i
+ failures = []
+ failures << "no production functions were measured" if summary[:functions].zero?
+ if summary[:mapped_percent] < minimum
+ failures << format(
+ "production Big-O coverage %.2f%% is below %.2f%%",
+ summary[:mapped_percent],
+ minimum
+ )
+ end
+ if raw_gaps.positive?
+ failures << "#{raw_gaps} executable raw calls were not normalized"
+ end
+
+ {
+ schema: "espalier.big-o-coverage-gate.v1",
+ policy: {
+ source_roles: ["production"],
+ include_lambdas: include_lambdas,
+ minimum_percent: minimum,
+ raw_call_normalization: "fail_closed"
+ },
+ passed: failures.empty?,
+ failures: failures,
+ coverage: summary,
+ by_language: by_language,
+ call_soundness: call_soundness(call_coverage)
+ }
+ end
+
def complete?(quality)
fetch(quality, :big_o_complete) == true
end
@@ -92,6 +148,46 @@ def assumptions(quality)
Array(fetch(quality, :big_o_assumptions)).map(&:to_s)
end
+ def coverage_summary(rows)
+ qualities = Array(rows).map { |row| fetch(row, :quality) || row }
+ proof = summarize(qualities)
+ mapped = qualities.count { |quality| classify(quality) != :unknown }
+ total = qualities.length
+ {
+ functions: total,
+ mapped: mapped,
+ incomplete: total - mapped,
+ mapped_percent: percentage(mapped, total),
+ proof: proof
+ }
+ end
+ private_class_method :coverage_summary
+
+ def call_soundness(coverage)
+ eligible = fetch(coverage, :eligible_call_sites).to_i
+ accounted = fetch(coverage, :semantically_accounted_call_sites).to_i
+ {
+ scope: "entire_fact_mine_profile",
+ eligible_call_sites: eligible,
+ exact_project_targets: fetch(coverage, :exact_project_targets).to_i,
+ modeled_without_project_target: fetch(coverage, :modeled_without_project_target).to_i,
+ semantically_accounted_call_sites: accounted,
+ semantically_accounted_percent: percentage(accounted, eligible),
+ unresolved_call_sites: fetch(coverage, :unresolved_call_sites).to_i,
+ raw_parser_call_sites: fetch(coverage, :raw_parser_call_sites).to_i,
+ raw_calls_not_normalized: fetch(coverage, :raw_calls_not_normalized).to_i,
+ raw_calls_not_normalized_inside_function:
+ fetch(coverage, :raw_calls_not_normalized_inside_function).to_i,
+ normalized_calls_without_raw_span: fetch(coverage, :normalized_calls_without_raw_span).to_i
+ }
+ end
+ private_class_method :call_soundness
+
+ def percentage(numerator, denominator)
+ denominator.zero? ? 0.0 : (numerator * 100.0 / denominator).round(2)
+ end
+ private_class_method :percentage
+
def fetch(hash, key)
return nil unless hash.respond_to?(:key?)
return hash[key] if hash.key?(key)
diff --git a/gems/espalier/lib/espalier/complexity_overrides.rb b/gems/espalier/lib/espalier/complexity_overrides.rb
new file mode 100644
index 000000000..e0ea568f3
--- /dev/null
+++ b/gems/espalier/lib/espalier/complexity_overrides.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "yaml"
+
+module Espalier
+ # Targeted manual-override registry (see config/complexity_overrides.yml).
+ #
+ # An override is consulted ONLY for a function whose derived bound is
+ # incomplete, and returns a complexity only when an explicit entry exists.
+ # It is the escape hatch for the ~0.4-2% of functions whose true bound is an
+ # algorithmic guarantee structural analysis provably cannot derive.
+ module ComplexityOverrides
+ DEFAULT_PATH = File.expand_path("../../config/complexity_overrides.yml", __dir__)
+
+ class << self
+ def table(path = DEFAULT_PATH)
+ @tables ||= {}
+ @tables[path] ||= load_table(path)
+ end
+
+ # Returns the override entry for (language, owner, name), or nil.
+ # Entry: { "time" => ..., "space" => ..., "note" => ... }.
+ def lookup(language, owner, name, path: DEFAULT_PATH)
+ return nil unless language && name
+
+ lang = table(path)[language.to_s]
+ return nil unless lang
+
+ lang["#{owner}.#{name}"] || lang[name.to_s]
+ end
+
+ private
+
+ def load_table(path)
+ return {} unless File.exist?(path)
+
+ YAML.safe_load(File.read(path)) || {}
+ end
+ end
+ end
+end
diff --git a/gems/espalier/lib/espalier/complexity_summary.rb b/gems/espalier/lib/espalier/complexity_summary.rb
new file mode 100644
index 000000000..d9c741712
--- /dev/null
+++ b/gems/espalier/lib/espalier/complexity_summary.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module Espalier
+ # Proof boundary for reusable complexity summaries. A source profile can be
+ # complete because a reviewed/manual external model closed one of its calls;
+ # exporting that result would merely copy the registry into a generated file.
+ # Reusable summaries therefore admit only bounds proven from analyzed bodies,
+ # exact analyzed targets, CFG/DFG structure, and compiler-provided candidate
+ # sets.
+ module ComplexitySummary
+ FORBIDDEN_QUALITY_FRAGMENTS = %w[
+ declared_receiver
+ external_latency
+ modeled_world
+ unknown_cardinality
+ ].freeze
+
+ module_function
+
+ def relocate_symbol(symbol, from: nil, to: nil)
+ return symbol if from.nil? && to.nil?
+ raise ArgumentError, "symbol relocation requires both source and destination prefixes" if from.nil? || to.nil?
+ raise ArgumentError, "symbol does not start with the declared source prefix: #{symbol}" unless symbol.start_with?(from)
+
+ "#{to}#{symbol.delete_prefix(from)}"
+ end
+
+ # Apply an exact implementation-to-consumer bridge. A source body may
+ # deliberately implement more than one public declaration (for example a
+ # class method and an equivalent module function), so a bridge value is
+ # one-or-many rather than a lossy one-to-one map. Keeping this operation
+ # here makes bridges equally available to every producer language; a
+ # language-owned bridge generator need only prove its identities.
+ #
+ # Rows without a bridge target are intentionally omitted. Publishing a
+ # source identity into a consumer summary would evade the cross-indexer
+ # identity proof that the bridge is meant to establish.
+ def bridge_symbol_rows(rows, symbol_map: nil, prefix_from: nil, prefix_to: nil)
+ rows.flat_map do |symbol, row|
+ targets = if symbol_map
+ Array(symbol_map[symbol])
+ else
+ [relocate_symbol(symbol, from: prefix_from, to: prefix_to)]
+ end
+ targets.compact.map { |target| [target, row] }
+ end
+ end
+
+ def source_method_proven?(method, quality, complexity_facts)
+ return false unless method["source_export_eligible"] == true
+ return false unless source_proven?(quality, complexity_facts)
+
+ qualities = Array(quality[:big_o_bound_qualities]).map(&:to_s)
+ return false if qualities.any? { |bound| bound.include?("parametric_callback") } &&
+ Array(method["callback_params"]).empty?
+
+ true
+ end
+
+ def consumer_closed_candidate_set?(call)
+ call["consumer_closed_candidate_set"] == true
+ end
+
+ def source_proven?(quality, _complexity_facts)
+ return false unless quality
+ return false unless quality[:big_o_complete] == true
+ return false unless quality[:big_o_space_complete] == true
+
+ qualities = Array(quality[:big_o_bound_qualities]).map(&:to_s)
+ return false if qualities.any? do |bound_quality|
+ FORBIDDEN_QUALITY_FRAGMENTS.any? { |fragment| bound_quality.include?(fragment) }
+ end
+
+ # Complexity facts are captured before SCIP resolves canonical calls, so
+ # their adapter-level evidence_gap fields can be stale. The aggregate
+ # completeness result above is computed after canonical call enrichment.
+ # Parser-normalization loss is guarded independently by FactMine marking
+ # the overlapping method source_export_eligible=false.
+ true
+ end
+ end
+end
diff --git a/gems/espalier/lib/espalier/static_evidence.rb b/gems/espalier/lib/espalier/static_evidence.rb
index 47b535fcd..a3fdd20b3 100644
--- a/gems/espalier/lib/espalier/static_evidence.rb
+++ b/gems/espalier/lib/espalier/static_evidence.rb
@@ -19,13 +19,28 @@ module Espalier
# Static, language-neutral evidence for Espalier. Uses the Rust FactMine
# binary exclusively for fact extraction.
class StaticEvidence
- FACT_MINE_RUST_BINARY = ENV.fetch(
- "FACT_MINE_RUST_BINARY",
- File.join(Espalier::ROOT, "gems", "fact-mine", "target", "release", "fact-mine-rust")
- ).freeze
+ FACT_MINE_RUST_BINARY = begin
+ configured = ENV["FACT_MINE_RUST_BINARY"]
+ if configured
+ File.expand_path(configured)
+ else
+ candidates = %w[release debug].map do |profile|
+ File.join(
+ Espalier::ROOT,
+ "gems",
+ "fact-mine",
+ "target",
+ profile,
+ "fact-mine-rust"
+ )
+ end
+ candidates.select { |path| File.executable?(path) }
+ .max_by { |path| File.mtime(path) } || candidates.first
+ end
+ end.freeze
- def self.build(targets = nil, root: Espalier::ROOT, language: nil, vcs: nil, include_annotations: true, scip_indexes: [])
- new(targets, root: root, language: language, vcs: vcs, include_annotations: include_annotations, scip_indexes: scip_indexes).build
+ def self.build(targets = nil, root: Espalier::ROOT, language: nil, vcs: nil, include_annotations: true, scip_indexes: [], semantic_environments: [], complexity_summaries: [])
+ new(targets, root: root, language: language, vcs: vcs, include_annotations: include_annotations, scip_indexes: scip_indexes, semantic_environments: semantic_environments, complexity_summaries: complexity_summaries).build
end
def self.project_modules(evidence, source_roles: ["production"])
@@ -83,6 +98,10 @@ def self.project_modules(evidence, source_roles: ["production"])
end.to_set
raw_methods.each do |m|
next unless allowed_roles.include?(role_for.call(m["path"]))
+ # Macro-generated accessors remain in the FactMine profile so their
+ # declaration contract can price callers, but they are not authored
+ # production functions and must not inflate a completion denominator.
+ next if m["generated_declaration"] == true
overload_key = [m["path"].to_s, m["owner"].to_s, m["name"].to_s, m["kind"].to_s]
# TypeScript overload signatures are declarations immediately followed
# by a concrete implementation. Reporting both as executable methods
@@ -101,6 +120,7 @@ def self.project_modules(evidence, source_roles: ["production"])
signature: m["signature"],
parameters: Array(m["params"]),
visibility: (m["visibility"] || :public).to_sym,
+ callback_params: Array(m["callback_params"]).map(&:to_s),
line: m["line"]&.to_i,
span: m["span"],
file: m["path"],
@@ -166,6 +186,7 @@ def self.project_modules(evidence, source_roles: ["production"])
confidence: call["confidence"],
unresolved_reason: (target || known_time || known_space) ? nil : call["unresolved_reason"],
call_id: call["id"],
+ arguments: Array(call["arguments"]).map(&:to_s),
target_id: target && target[:id],
target_owner: target && target[:projected_owner],
target_method: target && target[:name],
@@ -173,6 +194,7 @@ def self.project_modules(evidence, source_roles: ["production"])
target_provenance: call["target_provenance"],
candidate_target_ids: Array(call["candidate_targets"]),
candidate_reason: call["candidate_reason"],
+ consumer_closed_candidate_set: call["consumer_closed_candidate_set"] == true,
complexity_provenance: call["complexity_provenance"],
complexity_bound_quality: call["complexity_bound_quality"],
complexity_candidates: Array(call["complexity_candidates"]),
@@ -355,7 +377,15 @@ def self.source_role(path)
return "example" if (parts & %w[example examples sample samples]).any?
return "test" if (parts & %w[test tests spec specs __tests__ jvmtest androidtest commontest nativetest nonwasmtest wasmtest integrationtest unittest uitest functionaltest]).any?
return "test" if parts.any? { |part| part.end_with?("test") && part.match?(/\A(?:android|common|functional|integration|jvm|native|nonwasm|unit|ui|wasm)/) }
- return "test" if basename.match?(/(?:\A|[_\.])test(?:[_\.]|\z)|(?:\A|[_\.])spec(?:[_\.]|\z)/)
+ test_named_basename = basename.match?(
+ /(?:\A|[-_\.])test(?:[-_\.]|\z)|(?:\A|[-_\.])spec(?:[-_\.]|\z)/
+ )
+ # A production library may legitimately expose a `test_*` or `spec_*`
+ # entrypoint (for example TestMiser's `lib/test_miser.rb`). Directory
+ # role is stronger evidence than that filename prefix; helper products
+ # remain covered by the dedicated test-helper rule below.
+ library_entrypoint = parts.include?("lib") && basename.match?(/\A(?:test|spec)[-_]/)
+ return "test" if test_named_basename && !library_entrypoint
# Test-helper products are executable support code, not production
# library surface. Match common portable path spellings, including the
# Swift Package Manager `ArgumentParserTestHelpers` convention.
@@ -364,14 +394,64 @@ def self.source_role(path)
"production"
end
+ # Classify callable records more precisely than their containing file.
+ # Rust commonly keeps `#[cfg(test)] mod tests` at the bottom of a
+ # production source file. rust-analyzer's SCIP symbol retains that module
+ # boundary, and synthetic closures inherit the role of the exact enclosing
+ # test callable by source span.
+ def self.method_source_roles(methods)
+ rows = Array(methods)
+ roles = rows.to_h do |method|
+ role = method["generated_declaration"] == true ? "generated" : source_role(method["path"])
+ [method.fetch("id").to_s, role]
+ end
+ test_spans_by_path = Hash.new { |hash, path| hash[path] = [] }
+
+ rows.each do |method|
+ next unless roles.fetch(method.fetch("id").to_s) == "production"
+ next unless method["language"].to_s == "rust"
+ next unless rust_test_semantic_symbol?(method["semantic_symbol"])
+
+ roles[method.fetch("id").to_s] = "test"
+ test_spans_by_path[method["path"].to_s] << Array(method["span"])
+ end
+
+ rows.each do |method|
+ id = method.fetch("id").to_s
+ next unless roles.fetch(id) == "production"
+ next unless method["language"].to_s == "rust"
+
+ span = Array(method["span"])
+ next unless span.length == 4
+ next unless test_spans_by_path[method["path"].to_s].any? { |outer| span_contains?(outer, span) }
+
+ roles[id] = "test"
+ end
+
+ roles
+ end
+
+ def self.rust_test_semantic_symbol?(symbol)
+ symbol.to_s.match?(%r{(?:\s|/)(?:tests?|test_helpers?)/})
+ end
+
+ def self.span_contains?(outer, inner)
+ return false unless outer.length == 4 && inner.length == 4
+
+ ([inner[0].to_i, inner[1].to_i] <=> [outer[0].to_i, outer[1].to_i]) >= 0 &&
+ ([inner[2].to_i, inner[3].to_i] <=> [outer[2].to_i, outer[3].to_i]) <= 0
+ end
- def initialize(targets = nil, root: Espalier::ROOT, language: nil, vcs: nil, include_annotations: true, scip_indexes: [])
+
+ def initialize(targets = nil, root: Espalier::ROOT, language: nil, vcs: nil, include_annotations: true, scip_indexes: [], semantic_environments: [], complexity_summaries: [])
@targets = Array(targets).compact
@root = root
@language = normalize_language(language)
@vcs = normalize_vcs(vcs)
@include_annotations = include_annotations
@scip_indexes = Array(scip_indexes).compact
+ @semantic_environments = Array(semantic_environments).compact
+ @complexity_summaries = Array(complexity_summaries).compact
end
def build
@@ -390,6 +470,10 @@ def build
args = [FACT_MINE_RUST_BINARY, "profile", profile, "--output", tmp.path]
args.concat(["--language", @language.to_s]) if @language
@scip_indexes.each { |index| args.concat(["--scip-index", index.to_s]) }
+ @semantic_environments.each do |environment|
+ args.concat(["--semantic-environment", environment.to_s])
+ end
+ @complexity_summaries.each { |summary| args.concat(["--complexity-summary", summary.to_s]) }
args.concat(files)
ok = system(*args)
raise "fact-mine-rust failed with exit status #{$?.exitstatus}" unless ok
diff --git a/gems/espalier/lib/espalier/stdlib_map.rb b/gems/espalier/lib/espalier/stdlib_map.rb
new file mode 100644
index 000000000..59a4ce023
--- /dev/null
+++ b/gems/espalier/lib/espalier/stdlib_map.rb
@@ -0,0 +1,901 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "json"
+require "open3"
+require "optparse"
+require "pathname"
+require "rbconfig"
+require "shellwords"
+require "tmpdir"
+require "yaml"
+require "zlib"
+
+module Espalier
+ # Manifest-driven producer for source-analyzed standard-library complexity
+ # summaries. Language-specific build and source selection live in YAML; the
+ # indexing, FactMine profiling, soundness checks, export, consumer comparison,
+ # and publication flow are shared.
+ class StdlibMap
+ SCHEMA = "fact-mine.stdlib-map.v1"
+ SUMMARY_SCHEMA = "fact-mine.external-complexity-summary.v3"
+
+ class CommandRunner
+ def run!(command, chdir:, env: {})
+ warn "$ (cd #{Shellwords.escape(chdir)} && #{display_command(command)})"
+ success = system(env, *command, chdir: chdir)
+ raise "command failed: #{display_command(command)}" unless success
+ end
+
+ def capture(command, chdir:, env: {})
+ stdout, stderr, status = Open3.capture3(env, *command, chdir: chdir)
+ [stdout, stderr, status]
+ end
+
+ private
+
+ def display_command(command)
+ shown = command.first(12).map { |argument| Shellwords.escape(argument) }
+ shown << "... (#{command.length} arguments)" if command.length > shown.length
+ shown.join(" ")
+ end
+ end
+
+ attr_reader :manifest, :manifest_path
+
+ def self.run_cli(arguments)
+ options = {
+ work_dir: nil,
+ keep_work: false,
+ index_override: nil,
+ source_root_override: nil,
+ summary_output: nil,
+ fact_mine: nil
+ }
+ parser = OptionParser.new do |opts|
+ opts.banner = "Usage: espalier stdlib-map --manifest FILE [options]"
+ opts.on("--manifest FILE", "Mapping manifest") { |value| options[:manifest] = value }
+ opts.on("--work-dir DIR", "Retain intermediate index/profile artifacts here") do |value|
+ options[:work_dir] = value
+ end
+ opts.on("--keep-work", "Keep an automatically-created work directory") do
+ options[:keep_work] = true
+ end
+ opts.on("--index FILE", "Reuse an existing SCIP index") do |value|
+ options[:index_override] = value
+ end
+ opts.on("--source-root DIR", "Reuse an existing pinned source checkout") do |value|
+ options[:source_root_override] = value
+ end
+ opts.on("--output FILE", "Override the summary output path") do |value|
+ options[:summary_output] = value
+ end
+ opts.on("--fact-mine FILE", "FactMine Rust binary") { |value| options[:fact_mine] = value }
+ end
+ parser.parse!(arguments)
+ raise ArgumentError, parser.to_s unless arguments.empty? && options[:manifest]
+
+ new(
+ options.fetch(:manifest),
+ work_dir: options.fetch(:work_dir),
+ keep_work: options.fetch(:keep_work),
+ index_override: options.fetch(:index_override),
+ source_root_override: options.fetch(:source_root_override),
+ summary_output: options.fetch(:summary_output),
+ fact_mine: options.fetch(:fact_mine)
+ ).run
+ 0
+ rescue StandardError => error
+ warn "stdlib-map failed: #{error.message}"
+ 1
+ end
+
+ def initialize(manifest_path, work_dir: nil, keep_work: false, index_override: nil,
+ source_root_override: nil,
+ summary_output: nil, fact_mine: nil, runner: CommandRunner.new)
+ @manifest_path = File.expand_path(manifest_path)
+ @manifest_dir = File.dirname(@manifest_path)
+ @workspace_root = File.expand_path("../../../..", __dir__)
+ @manifest = load_manifest
+ @keep_work = keep_work
+ @owned_work_dir = work_dir.nil?
+ @work_dir = File.expand_path(work_dir || Dir.mktmpdir("fact-mine-stdlib-map-"))
+ @index_override = index_override && File.expand_path(index_override)
+ @source_root_override = source_root_override && File.expand_path(source_root_override)
+ @summary_output_override = summary_output && File.expand_path(summary_output)
+ @fact_mine_override = fact_mine && File.expand_path(fact_mine)
+ @runner = runner
+ validate_manifest!
+ end
+
+ def run
+ FileUtils.mkdir_p(@work_dir)
+ source_root = resolve_source_root
+ source_revision = verify_source_revision(source_root)
+ prepare_source(source_root)
+ files = source_files(source_root)
+ analysis_root, files = stage_selected_source(source_root, files)
+ index = produce_index(analysis_root)
+ substitutions = {
+ "source_root" => analysis_root,
+ "work_dir" => @work_dir,
+ "index" => index,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ producer_environment = materialize_environment(
+ manifest["compatibility"],
+ "producer",
+ substitutions
+ )
+ profile = File.join(@work_dir, "stdlib.profile.json")
+ run_profile(files, index, profile, environment: producer_environment)
+ profile_data = JSON.parse(File.read(profile))
+ profile_validation = validate_profile!(profile_data, files, producer_environment)
+
+ producer_summary = File.join(@work_dir, "stdlib.producer-summary.json.gz")
+ export_summary(
+ profile,
+ producer_summary,
+ relocate: false,
+ bridge: nil,
+ compatibility: producer_environment
+ )
+ producer_summary_data = read_json(producer_summary)
+ producer_validation = validate_summary!(producer_summary_data, relocated: false)
+ producer_join_validation = verify_summary_join(profile_data, producer_summary_data)
+ symbol_bridge = materialize_symbol_bridge(
+ fetch_hash(manifest, "summary")["symbol_bridge"],
+ substitutions.merge(
+ "profile" => profile,
+ "producer_summary" => producer_summary
+ )
+ )
+
+ relocation = fetch_hash(manifest, "summary")["symbol_relocation"]
+ staged_summary = if relocation || symbol_bridge
+ path = File.join(@work_dir, "stdlib.summary.json.gz")
+ export_summary(
+ profile,
+ path,
+ relocate: !relocation.nil?,
+ bridge: symbol_bridge,
+ compatibility: producer_environment
+ )
+ path
+ else
+ producer_summary
+ end
+ summary_validation = validate_summary!(
+ read_json(staged_summary),
+ relocated: !relocation.nil?,
+ bridged: !symbol_bridge.nil?
+ )
+ consumer_results = run_consumer_checks(staged_summary, producer_environment)
+
+ output = summary_output
+ publish_summary(staged_summary, output)
+ report = {
+ "schema" => "fact-mine.stdlib-map-report.v1",
+ "manifest" => manifest_path,
+ "language" => manifest.fetch("language"),
+ "source_root" => source_root,
+ "analysis_root" => analysis_root,
+ "source_revision" => source_revision,
+ "source_files" => files.length,
+ "index" => index,
+ "profile" => profile_validation,
+ "producer_summary" => producer_validation.merge(producer_join_validation),
+ "summary" => summary_validation.merge("output" => output),
+ "consumers" => consumer_results
+ }
+ report_path = File.join(@work_dir, "stdlib-map-report.json")
+ File.write(report_path, JSON.pretty_generate(report))
+ puts JSON.pretty_generate(report)
+ report
+ ensure
+ FileUtils.remove_entry(@work_dir) if @owned_work_dir && !@keep_work && File.exist?(@work_dir)
+ end
+
+ private
+
+ def load_manifest
+ raw = YAML.safe_load(File.read(manifest_path), permitted_classes: [], aliases: false)
+ expand_environment(raw)
+ rescue Psych::Exception => error
+ raise ArgumentError, "invalid stdlib manifest #{manifest_path}: #{error.message}"
+ end
+
+ def validate_manifest!
+ raise ArgumentError, "unsupported stdlib manifest schema: #{manifest['schema'].inspect}" unless manifest["schema"] == SCHEMA
+ raise ArgumentError, "language must be present" if manifest["language"].to_s.empty?
+ source = fetch_hash(manifest, "source")
+ if source["root"].to_s.empty? && !Array(source["root_command"]).any? && !source["git"]
+ raise ArgumentError, "source.root, source.root_command, or source.git must be present"
+ end
+ includes = Array(source["include"])
+ raise ArgumentError, "source.include must contain at least one glob" if includes.empty?
+ raise ArgumentError, "source.revision must be present" if source["revision"].to_s.empty?
+ if source["git"]
+ git = fetch_hash(source, "git")
+ raise ArgumentError, "source.git.repository must be present" if git["repository"].to_s.empty?
+ raise ArgumentError, "source.git.commit must be present" if git["commit"].to_s.empty?
+ unless git["commit"].match?(/\A[0-9a-f]{40}\z/)
+ raise ArgumentError, "source.git.commit must be a full 40-character commit"
+ end
+ else
+ revision_check = fetch_hash(source, "revision_check")
+ unless Array(revision_check["command"]).any?
+ raise ArgumentError, "source.revision_check.command must be present"
+ end
+ matches = Array(revision_check["includes"])
+ exact = revision_check["equals"]
+ if matches.empty? && exact.nil?
+ raise ArgumentError, "source.revision_check requires equals or includes"
+ end
+ if !matches.empty? && !exact.nil?
+ raise ArgumentError, "source.revision_check cannot use both equals and includes"
+ end
+ end
+
+ index = fetch_hash(manifest, "index")
+ unless @index_override || index["path"] || Array(index["command"]).any?
+ raise ArgumentError, "index.path or index.command is required"
+ end
+ expected = fetch_hash(index, "expected")
+ raise ArgumentError, "index.expected.tool is required" if expected["tool"].to_s.empty?
+ raise ArgumentError, "index.expected.version is required" if expected["version"].to_s.empty?
+
+ summary = fetch_hash(manifest, "summary")
+ %w[corpus output].each do |key|
+ raise ArgumentError, "summary.#{key} is required" if summary[key].to_s.empty?
+ end
+ relocation = summary["symbol_relocation"]
+ if relocation
+ relocation = fetch_hash(summary, "symbol_relocation")
+ if relocation["from"].to_s.empty? || relocation["to"].to_s.empty?
+ raise ArgumentError, "summary.symbol_relocation requires from and to"
+ end
+ end
+ if summary["symbol_bridge"] && relocation
+ raise ArgumentError, "summary.symbol_bridge cannot be combined with symbol_relocation"
+ end
+ if summary["symbol_bridge"]
+ bridge = fetch_hash(summary, "symbol_bridge")
+ unless bridge["path"] || Array(bridge["command"]).any?
+ raise ArgumentError, "summary.symbol_bridge requires path or command"
+ end
+ end
+ consumer_indexers = Array(summary["consumer_indexers"])
+ if consumer_indexers.any? { |indexer| indexer.to_s.empty? || !indexer.to_s.include?("@") }
+ raise ArgumentError,
+ "summary.consumer_indexers must contain tool@version identities"
+ end
+ if manifest["compatibility"]
+ compatibility = fetch_hash(manifest, "compatibility")
+ modes = [
+ compatibility.key?("claims"),
+ compatibility.key?("path"),
+ Array(compatibility["command"]).any?
+ ].count(true)
+ raise ArgumentError, "compatibility requires exactly one of claims, path, or command" unless modes == 1
+ end
+ end
+
+ def source_files(root, config = fetch_hash(manifest, "source"))
+ includes = Array(config.fetch("include"))
+ excludes = Array(config["exclude"])
+ files = includes.flat_map do |pattern|
+ Dir.glob(File.join(root, pattern), File::FNM_EXTGLOB)
+ end
+ files.select! { |path| File.file?(path) }
+ files.reject! do |path|
+ relative = Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
+ excludes.any? { |pattern| File.fnmatch?(pattern, relative, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
+ end
+ files.map! { |path| File.expand_path(path) }
+ files.uniq!
+ files.sort!
+ raise ArgumentError, "source globs selected no files below #{root}" if files.empty?
+
+ files
+ end
+
+ def resolve_source_root
+ return @source_root_override if @source_root_override
+
+ source = fetch_hash(manifest, "source")
+ return materialize_git_source(source) if source["git"]
+ return expanded_path(source["root"]) if source["root"]
+
+ command = expand_command(source.fetch("root_command"), {})
+ cwd = expanded_path(source.fetch("root_working_directory", @workspace_root))
+ stdout, stderr, status = @runner.capture(command, chdir: cwd)
+ raise "source.root_command failed: #{stderr}" unless status.success?
+ root = stdout.strip
+ raise "source.root_command returned an empty path" if root.empty?
+
+ suffix = source["root_suffix"].to_s
+ File.expand_path(File.join(root, suffix))
+ end
+
+ def materialize_git_source(source)
+ config = fetch_hash(source, "git")
+ checkout = expanded_path(
+ config.fetch(
+ "directory",
+ File.join(@workspace_root, ".cache", "stdlib-sources", safe_name(source.fetch("revision")))
+ )
+ )
+ FileUtils.mkdir_p(checkout)
+ unless File.directory?(File.join(checkout, ".git"))
+ @runner.run!(["git", "init", "--quiet"], chdir: checkout)
+ @runner.run!(
+ ["git", "remote", "add", "origin", config.fetch("repository")],
+ chdir: checkout
+ )
+ end
+ remote, stderr, status = @runner.capture(
+ ["git", "remote", "get-url", "origin"],
+ chdir: checkout
+ )
+ unless status.success?
+ raise "failed to inspect git source remote: #{stderr}"
+ end
+ unless remote.strip == config.fetch("repository")
+ raise "git source remote mismatch: expected #{config.fetch('repository').inspect}, got #{remote.strip.inspect}"
+ end
+ if Array(config["sparse_paths"]).any?
+ @runner.run!(["git", "sparse-checkout", "init", "--cone"], chdir: checkout)
+ @runner.run!(
+ ["git", "sparse-checkout", "set", *Array(config["sparse_paths"]).map(&:to_s)],
+ chdir: checkout
+ )
+ end
+ commit = config.fetch("commit")
+ current, = @runner.capture(["git", "rev-parse", "HEAD"], chdir: checkout)
+ unless current.strip == commit
+ @runner.run!(["git", "fetch", "--depth", "1", "origin", commit], chdir: checkout)
+ @runner.run!(["git", "checkout", "--quiet", "--detach", "FETCH_HEAD"], chdir: checkout)
+ end
+ root = File.expand_path(File.join(checkout, source["root_suffix"].to_s))
+ raise "git source root does not exist after checkout: #{root}" unless File.directory?(root)
+
+ root
+ end
+
+ def prepare_source(source_root)
+ source = fetch_hash(manifest, "source")
+ substitutions = {
+ "source_root" => source_root,
+ "work_dir" => @work_dir,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ Array(source["prepare"]).each do |command|
+ raise ArgumentError, "each source.prepare entry must be a command array" unless command.is_a?(Array)
+
+ @runner.run!(
+ expand_command(command, substitutions),
+ chdir: expanded_path(source.fetch("prepare_working_directory", @workspace_root), substitutions)
+ )
+ end
+ end
+
+ def stage_selected_source(source_root, files)
+ source = fetch_hash(manifest, "source")
+ return [source_root, files] unless source["stage_selected_files"] == true
+
+ stage_root = File.join(@work_dir, "selected-source")
+ FileUtils.rm_rf(stage_root)
+ FileUtils.mkdir_p(stage_root)
+ stage_files = files.dup
+ Array(source["stage_include"]).each do |pattern|
+ Dir.glob(File.join(source_root, pattern), File::FNM_EXTGLOB).each do |path|
+ stage_files << File.expand_path(path) if File.file?(path)
+ end
+ end
+ stage_files.uniq.each do |path|
+ relative = Pathname.new(path).relative_path_from(Pathname.new(source_root)).to_s
+ destination = File.join(stage_root, relative)
+ FileUtils.mkdir_p(File.dirname(destination))
+ FileUtils.copy_file(path, destination)
+ end
+ staged_files = files.map do |path|
+ relative = Pathname.new(path).relative_path_from(Pathname.new(source_root)).to_s
+ File.join(stage_root, relative)
+ end
+ [stage_root, staged_files]
+ end
+
+ def verify_source_revision(source_root)
+ source = fetch_hash(manifest, "source")
+ if source["git"]
+ checkout = source_root
+ checkout = File.dirname(checkout) until File.directory?(File.join(checkout, ".git")) ||
+ File.dirname(checkout) == checkout
+ raise "git metadata not found above source root #{source_root}" unless File.directory?(File.join(checkout, ".git"))
+
+ stdout, stderr, status = @runner.capture(["git", "rev-parse", "HEAD"], chdir: checkout)
+ raise "source revision command failed: #{stderr}" unless status.success?
+ expected = fetch_hash(source, "git").fetch("commit")
+ unless stdout.strip == expected
+ raise "source revision mismatch: expected #{expected.inspect}, got #{stdout.strip.inspect}"
+ end
+ return source.fetch("revision")
+ end
+ check = fetch_hash(source, "revision_check")
+ substitutions = {
+ "source_root" => source_root,
+ "work_dir" => @work_dir,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ command = expand_command(check.fetch("command"), substitutions)
+ cwd = expanded_path(check.fetch("working_directory", source_root), substitutions)
+ env = fetch_hash(check, "environment", required: false)
+ .transform_values { |value| expand_string(value.to_s, substitutions) }
+ stdout, stderr, status = @runner.capture(command, chdir: cwd, env: env)
+ raise "source revision command failed: #{stderr}" unless status.success?
+
+ output = stdout.strip
+ exact = check["equals"]
+ includes = Array(check["includes"])
+ matches = if exact
+ output == exact.to_s
+ else
+ includes.all? { |fragment| output.include?(fragment.to_s) }
+ end
+ unless matches
+ expectation = exact ? exact.inspect : "all of #{includes.inspect}"
+ raise "source revision mismatch: expected #{expectation}, got #{output.inspect}"
+ end
+
+ source.fetch("revision")
+ end
+
+ def produce_index(source_root)
+ return checked_file(@index_override, "SCIP index") if @index_override
+
+ config = fetch_hash(manifest, "index")
+ return checked_file(expanded_path(config["path"]), "SCIP index") if config["path"]
+
+ output = File.join(@work_dir, config.fetch("output", "stdlib.scip"))
+ substitutions = {
+ "source_root" => source_root,
+ "work_dir" => @work_dir,
+ "index" => output,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ command = expand_command(config.fetch("command"), substitutions)
+ cwd = expanded_path(config.fetch("working_directory", source_root), substitutions)
+ env = fetch_hash(config, "environment", required: false)
+ .transform_values { |value| expand_string(value.to_s, substitutions) }
+ @runner.run!(command, chdir: cwd, env: env)
+ checked_file(output, "generated SCIP index")
+ end
+
+ def run_profile(files, index, output, summary: nil, environment: nil,
+ language: manifest.fetch("language"))
+ command = [
+ fact_mine_binary, "profile", "espalier",
+ "--language", language,
+ "--scip-index", index,
+ "--no-bundled-complexity-summaries",
+ "--output", output
+ ]
+ command.concat(["--semantic-environment", environment]) if environment
+ command.concat(["--complexity-summary", summary]) if summary
+ command.concat(files)
+ @runner.run!(command, chdir: @workspace_root)
+ end
+
+ def export_summary(profile, output, relocate:, bridge:, compatibility:)
+ summary = fetch_hash(manifest, "summary")
+ indexer = fetch_hash(fetch_hash(manifest, "index"), "expected")
+ command = [
+ RbConfig.ruby,
+ File.join(@workspace_root, "gems/espalier/script/export_complexity_summary.rb"),
+ "--corpus", summary.fetch("corpus"),
+ "--source-revision", fetch_hash(manifest, "source").fetch("revision"),
+ "--indexer", "#{indexer.fetch('tool')}@#{indexer.fetch('version')}"
+ ]
+ Array(summary["consumer_indexers"]).each do |consumer_indexer|
+ command.concat(["--consumer-indexer", consumer_indexer])
+ end
+ command.concat(["--compatibility", compatibility]) if compatibility
+ if relocate && (relocation = summary["symbol_relocation"])
+ command.concat(["--symbol-prefix-from", relocation.fetch("from")])
+ command.concat(["--symbol-prefix-to", relocation.fetch("to")])
+ end
+ command.concat(["--symbol-map", bridge]) if bridge
+ command.concat([profile, output])
+ @runner.run!(command, chdir: @workspace_root)
+ end
+
+ def validate_profile!(profile, files, environment = nil)
+ coverage = fetch_hash(profile, "input_coverage")
+ selected = coverage.fetch("selected_files", 0).to_i
+ parsed = coverage.fetch("parsed_files", 0).to_i
+ raise "profile selected #{selected} files, expected #{files.length}" unless selected == files.length
+ raise "profile parsed only #{parsed}/#{selected} selected files" unless parsed == selected
+ recovery_files = Array(coverage["parse_recovery_files"])
+ recoveries = Array(coverage["parse_recoveries"])
+ eligible_methods = Array(profile["methods"]).select do |method|
+ method["source_export_eligible"] == true
+ end
+ recovery_overlaps = recoveries.sum do |recovery|
+ path = recovery["path"].to_s
+ spans = Array(recovery["spans"])
+ eligible_methods.count do |method|
+ method["path"].to_s == path && spans.any? do |span|
+ spans_overlap?(Array(method["span"]), Array(span))
+ end
+ end
+ end
+ if recovery_overlaps.positive?
+ raise "parser recovery overlaps #{recovery_overlaps} source-export eligible methods"
+ end
+
+ expected = fetch_hash(fetch_hash(manifest, "index"), "expected")
+ indexes = Array(profile["semantic_indexes"])
+ unless indexes.any? { |row| row["tool"] == expected["tool"] && row["version"] == expected["version"] }
+ raise "SCIP metadata did not contain #{expected['tool']}@#{expected['version']}"
+ end
+ if environment
+ required_claims = fetch_hash(read_json(environment), "claims")
+ actual_claims = fetch_hash(profile, "semantic_environment")
+ missing = required_claims.reject { |key, value| actual_claims[key] == value }
+ unless missing.empty?
+ raise "profile semantic environment did not preserve claims: #{missing.keys.sort.join(', ')}"
+ end
+ end
+ methods = Array(profile["methods"])
+ eligible = methods.count { |method| method["source_export_eligible"] == true }
+ minimum = fetch_hash(manifest, "soundness", required: false).fetch("minimum_export_eligible_methods", 1).to_i
+ raise "only #{eligible} methods are source-export eligible; expected at least #{minimum}" if eligible < minimum
+
+ calls = fetch_hash(profile, "call_resolution_coverage", required: false)
+ raw_call_gaps = calls.fetch("raw_calls_not_normalized_inside_function", 0).to_i
+ eligible_gap_overlaps = calls
+ .fetch("source_export_eligible_methods_overlapping_raw_call_loss", 0)
+ .to_i
+ if eligible_gap_overlaps.positive?
+ raise "parser call loss overlaps #{eligible_gap_overlaps} source-export eligible methods; analyzer eligibility revocation is unsound"
+ end
+ {
+ "methods" => methods.length,
+ "source_export_eligible_methods" => eligible,
+ "raw_calls_not_normalized" => calls.fetch("raw_calls_not_normalized", 0).to_i,
+ "raw_calls_not_normalized_inside_function" => raw_call_gaps,
+ "eligible_methods_overlapping_call_loss" => eligible_gap_overlaps,
+ "parse_recovery_files" => recovery_files.length,
+ "parse_recovery_spans" => recoveries.sum { |recovery| Array(recovery["spans"]).length },
+ "eligible_methods_overlapping_parse_recovery" => recovery_overlaps,
+ "semantic_index" => "#{expected['tool']}@#{expected['version']}",
+ "semantic_environment_claims" => fetch_hash(
+ profile,
+ "semantic_environment",
+ required: false
+ ).length
+ }
+ end
+
+ def validate_summary!(summary, relocated: true, bridged: false)
+ raise "unexpected summary schema: #{summary['schema'].inspect}" unless summary["schema"] == SUMMARY_SCHEMA
+ symbols = fetch_hash(summary, "symbols")
+ source = fetch_hash(summary, "source")
+ declared = source.fetch("complete_symbol_count").to_i
+ raise "summary count #{declared} does not match #{symbols.length} symbols" unless declared == symbols.length
+
+ config = fetch_hash(manifest, "summary")
+ minimum = config.fetch("minimum_symbols", 1).to_i
+ raise "summary exported #{symbols.length} symbols; expected at least #{minimum}" if symbols.length < minimum
+ prefix = if bridged
+ nil
+ elsif !relocated && config["symbol_relocation"]
+ fetch_hash(config, "symbol_relocation").fetch("from")
+ else
+ config["expected_symbol_prefix"]
+ end
+ if prefix
+ bad = symbols.keys.find { |symbol| !symbol.start_with?(prefix) }
+ raise "summary symbol does not use expected prefix: #{bad}" if bad
+ end
+ {
+ "symbols" => symbols.length,
+ "source_proven_methods" => source.fetch("source_proven_method_count").to_i,
+ "profile_sha256" => source.fetch("profile_sha256"),
+ "indexer" => source.fetch("indexer")
+ }
+ end
+
+ def verify_summary_join(profile, summary)
+ symbols = fetch_hash(summary, "symbols")
+ joined = Array(profile["calls"]).count do |call|
+ symbols.key?(call["semantic_symbol"].to_s)
+ end
+ {"verified_join_call_sites" => joined}
+ end
+
+ def run_consumer_checks(summary, producer_environment)
+ Array(manifest["consumers"]).map do |consumer|
+ name = consumer.fetch("name")
+ root = expanded_path(consumer.fetch("source_root"))
+ files = source_files(root, consumer)
+ index = produce_consumer_index(consumer, name, root)
+ substitutions = {
+ "source_root" => root,
+ "work_dir" => @work_dir,
+ "index" => index,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ environment = if consumer.key?("compatibility")
+ materialize_environment(
+ consumer["compatibility"],
+ "consumer-#{safe_name(name)}",
+ substitutions
+ )
+ elsif producer_environment
+ raise ArgumentError,
+ "#{name} must produce its own compatibility claims"
+ else
+ nil
+ end
+ baseline = File.join(@work_dir, "consumer-#{safe_name(name)}-baseline.json")
+ generated = File.join(@work_dir, "consumer-#{safe_name(name)}-generated.json")
+ run_profile(files, index, baseline, environment: environment,
+ language: consumer.fetch("language", manifest.fetch("language")))
+ run_profile(files, index, generated, summary: summary, environment: environment,
+ language: consumer.fetch("language", manifest.fetch("language")))
+ before = coverage_report(baseline, root, consumer)
+ after = coverage_report(generated, root, consumer)
+ before_mapped = before.dig("coverage", "mapped").to_i
+ after_mapped = after.dig("coverage", "mapped").to_i
+ raise "#{name} regressed from #{before_mapped} to #{after_mapped} complete functions" if after_mapped < before_mapped
+ minimum = consumer.fetch("minimum_complete_percent", 85.0).to_f
+ percent = after.dig("coverage", "mapped_percent").to_f
+ raise "#{name} completeness #{percent}% is below #{minimum}%" if percent < minimum
+
+ {
+ "name" => name,
+ "before" => before.fetch("coverage").slice("functions", "mapped", "mapped_percent"),
+ "after" => after.fetch("coverage").slice("functions", "mapped", "mapped_percent"),
+ "complete_delta" => after_mapped - before_mapped,
+ "percentage_point_delta" =>
+ (after.dig("coverage", "mapped_percent").to_f - before.dig("coverage", "mapped_percent").to_f).round(2)
+ }
+ end
+ end
+
+ def produce_consumer_index(consumer, name, source_root)
+ config = consumer.fetch("index")
+ return checked_file(expanded_path(config), "#{name} SCIP index") if config.is_a?(String)
+ raise ArgumentError, "#{name} consumer index must be a path or mapping" unless config.is_a?(Hash)
+ return checked_file(expanded_path(config.fetch("path")), "#{name} SCIP index") if config["path"]
+
+ output = File.join(
+ @work_dir,
+ config.fetch("output", "consumer-#{safe_name(name)}.scip")
+ )
+ substitutions = {
+ "source_root" => source_root,
+ "work_dir" => @work_dir,
+ "index" => output,
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root
+ }
+ command = expand_command(config.fetch("command"), substitutions)
+ cwd = expanded_path(config.fetch("working_directory", source_root), substitutions)
+ env = fetch_hash(config, "environment", required: false)
+ .transform_values { |value| expand_string(value.to_s, substitutions) }
+ @runner.run!(command, chdir: cwd, env: env)
+ checked_file(output, "generated #{name} SCIP index")
+ end
+
+ def materialize_environment(config, label, substitutions)
+ return nil unless config
+
+ config = fetch_hash({"environment" => config}, "environment")
+ if config["path"]
+ return checked_file(
+ expanded_path(config.fetch("path"), substitutions),
+ "#{label} semantic environment"
+ )
+ end
+
+ output = File.join(@work_dir, "#{safe_name(label)}.semantic-environment.json")
+ if config["claims"]
+ claims = fetch_hash(config, "claims").to_h do |key, value|
+ [key.to_s, expand_string(value.to_s, substitutions)]
+ end
+ raise ArgumentError, "#{label} semantic environment claims cannot be empty" if claims.empty?
+ File.write(
+ output,
+ JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => claims
+ })
+ )
+ else
+ generated_substitutions = substitutions.merge("environment" => output)
+ command = expand_command(config.fetch("command"), generated_substitutions)
+ cwd = expanded_path(
+ config.fetch("working_directory", substitutions.fetch("source_root")),
+ generated_substitutions
+ )
+ env = fetch_hash(config, "environment", required: false)
+ .transform_values { |value| expand_string(value.to_s, generated_substitutions) }
+ @runner.run!(command, chdir: cwd, env: env)
+ end
+ validate_sidecar!(
+ checked_file(output, "#{label} semantic environment"),
+ "fact-mine.semantic-environment.v1",
+ "claims"
+ )
+ end
+
+ def materialize_symbol_bridge(config, substitutions)
+ return nil unless config
+
+ config = fetch_hash({"bridge" => config}, "bridge")
+ if config["path"]
+ path = expanded_path(config.fetch("path"), substitutions)
+ else
+ path = File.join(@work_dir, "stdlib.symbol-bridge.json")
+ generated_substitutions = substitutions.merge("symbol_bridge" => path)
+ command = expand_command(config.fetch("command"), generated_substitutions)
+ cwd = expanded_path(
+ config.fetch("working_directory", substitutions.fetch("source_root")),
+ generated_substitutions
+ )
+ env = fetch_hash(config, "environment", required: false)
+ .transform_values { |value| expand_string(value.to_s, generated_substitutions) }
+ @runner.run!(command, chdir: cwd, env: env)
+ end
+ validate_sidecar!(
+ checked_file(path, "stdlib symbol bridge"),
+ "fact-mine.symbol-bridge.v1",
+ "symbols"
+ )
+ end
+
+ def validate_sidecar!(path, schema, mapping_key)
+ document = JSON.parse(File.read(path))
+ raise ArgumentError, "unexpected sidecar schema in #{path}: #{document['schema'].inspect}" unless document["schema"] == schema
+
+ mapping = document[mapping_key]
+ unless mapping.is_a?(Hash) && !mapping.empty? &&
+ mapping.all? { |key, value| !key.to_s.empty? && !value.to_s.empty? }
+ raise ArgumentError, "#{path} must contain a non-empty #{mapping_key} string mapping"
+ end
+ path
+ rescue JSON::ParserError => error
+ raise ArgumentError, "invalid JSON sidecar #{path}: #{error.message}"
+ end
+
+ def coverage_report(profile, root, consumer)
+ command = [
+ RbConfig.ruby,
+ File.join(@workspace_root, "gems/espalier/script/check_big_o_coverage.rb"),
+ "--source-root", root,
+ "--minimum", "0"
+ ]
+ Array(consumer["repositories"]).each { |repository| command.concat(["--repository", repository]) }
+ command << profile
+ stdout, stderr, status = @runner.capture(command, chdir: @workspace_root)
+ raise "coverage report failed: #{stderr}" unless status.success?
+
+ JSON.parse(stdout)
+ end
+
+ def fact_mine_binary
+ candidate = @fact_mine_override || ENV["FACT_MINE_RUST"] ||
+ File.join(@workspace_root, "gems/fact-mine/target/release/fact-mine-rust")
+ checked_file(File.expand_path(candidate), "FactMine binary")
+ end
+
+ def summary_output
+ @summary_output_override || expanded_path(fetch_hash(manifest, "summary").fetch("output"))
+ end
+
+ def publish_summary(staged_summary, output)
+ directory = File.dirname(output)
+ FileUtils.mkdir_p(directory)
+ temporary = File.join(
+ directory,
+ ".#{File.basename(output)}.stdlib-map-#{Process.pid}-#{rand(1 << 32)}"
+ )
+ FileUtils.copy_file(staged_summary, temporary)
+ File.rename(temporary, output)
+ ensure
+ FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
+ end
+
+ def read_json(path)
+ if File.extname(path) == ".gz"
+ JSON.parse(Zlib::GzipReader.open(path, &:read))
+ else
+ JSON.parse(File.read(path))
+ end
+ end
+
+ def checked_file(path, label)
+ raise ArgumentError, "#{label} not found: #{path}" unless path && File.file?(path)
+ raise ArgumentError, "#{label} is empty: #{path}" unless File.size?(path)
+
+ path
+ end
+
+ def fetch_hash(parent, key, required: true)
+ value = parent[key]
+ if value.nil? && !required
+ return {}
+ end
+ raise ArgumentError, "#{key} must be a mapping" unless value.is_a?(Hash)
+
+ value
+ end
+
+ def expanded_path(value, substitutions = {})
+ path = expand_string(value.to_s, substitutions)
+ return File.expand_path(path) if Pathname.new(path).absolute?
+
+ File.expand_path(path, @manifest_dir)
+ end
+
+ def expand_command(command, substitutions)
+ Array(command).map { |token| expand_string(token.to_s, substitutions) }
+ end
+
+ def expand_string(value, substitutions = {})
+ builtins = {
+ "manifest_dir" => @manifest_dir,
+ "workspace_root" => @workspace_root,
+ "work_dir" => @work_dir
+ }.merge(substitutions)
+ value.gsub(/\{([a-z_]+)\}/) do
+ builtins.fetch(Regexp.last_match(1)) do
+ raise ArgumentError, "unknown manifest placeholder {#{Regexp.last_match(1)}}"
+ end
+ end
+ end
+
+ def expand_environment(value)
+ case value
+ when Hash
+ value.to_h { |key, child| [key.to_s, expand_environment(child)] }
+ when Array
+ value.map { |child| expand_environment(child) }
+ when String
+ value.gsub(/\$\{([A-Z][A-Z0-9_]*)\}/) do
+ ENV.fetch(Regexp.last_match(1)) do
+ raise ArgumentError, "environment variable #{Regexp.last_match(1)} is required by #{manifest_path}"
+ end
+ end
+ else
+ value
+ end
+ end
+
+ def safe_name(value)
+ value.to_s.gsub(/[^a-zA-Z0-9_.-]+/, "-")
+ end
+
+ def spans_overlap?(left, right)
+ return false unless left.length == 4 && right.length == 4
+
+ point_before_or_equal?(left[0], left[1], right[2], right[3]) &&
+ point_before_or_equal?(right[0], right[1], left[2], left[3])
+ end
+
+ def point_before_or_equal?(left_line, left_column, right_line, right_column)
+ left_line = left_line.to_i
+ right_line = right_line.to_i
+ left_line < right_line ||
+ (left_line == right_line && left_column.to_i <= right_column.to_i)
+ end
+ end
+end
diff --git a/gems/espalier/lib/espalier/structural_big_o.rb b/gems/espalier/lib/espalier/structural_big_o.rb
index 645bb2e50..f924cf8f0 100644
--- a/gems/espalier/lib/espalier/structural_big_o.rb
+++ b/gems/espalier/lib/espalier/structural_big_o.rb
@@ -100,7 +100,7 @@ def hints_for(_file, method, owner)
space: candidate_bound.fetch(:space),
is_dynamic: true,
operation: message,
- reason: "conservative upper bound over compiler-provided implementation candidates",
+ reason: "conservative upper bound over closed implementation candidates",
confidence: "partial",
time_complete: true,
space_complete: true,
@@ -112,6 +112,26 @@ def hints_for(_file, method, owner)
}
next
end
+ # FactMine already priced this call site (a builtin operator, a
+ # language intrinsic, or a stdlib-registry hit). Use that proven bound
+ # directly instead of demanding a resolved project target - otherwise
+ # a trivially O(1) operator leaves the function unknown.
+ if (known_time = context["known_time_complexity"])
+ hints << {
+ type: :structural,
+ line: line,
+ complexity: propagated_call_complexity(context, known_time),
+ space: context["known_space_complexity"] || "O(1)",
+ is_dynamic: known_time != "O(1)",
+ operation: message,
+ reason: "fact-mine modeled call cost",
+ confidence: "high",
+ time_complete: true,
+ space_complete: true,
+ fact_source: "fact_mine"
+ }
+ next
+ end
if resolved_target
callee_owner = resolved_target[0].to_s
callee = resolved_target[1].to_s
@@ -127,6 +147,7 @@ def hints_for(_file, method, owner)
state_rescan_recursion_summary(owner.to_s, caller)
end
recursive_bound = state_bound || resolved_recursive_bound(context)
+ proven = recursive_bound.fetch(:time) != "unknown"
hints << {
type: :structural,
line: line,
@@ -135,9 +156,14 @@ def hints_for(_file, method, owner)
is_dynamic: true,
operation: message,
reason: recursive_bound.fetch(:reason),
- confidence: recursive_bound[:quality] ? "partial" : "high",
- time_complete: true,
- space_complete: true,
+ confidence: if proven
+ recursive_bound[:quality] ? "partial" : "high"
+ else
+ "unknown"
+ end,
+ time_complete: proven,
+ space_complete: proven,
+ evidence_gaps: proven ? nil : ["unresolved_recursive_progress"],
complexity_bound_quality: recursive_bound[:quality],
complexity_assumptions: Array(recursive_bound[:assumption]),
fact_source: "fact_mine"
@@ -169,8 +195,9 @@ def hints_for(_file, method, owner)
confidence: mutual ? "high" : "unknown",
time_complete: !mutual.nil?,
space_complete: !mutual.nil?,
+ evidence_gaps: mutual ? nil : ["unresolved_recursive_progress"],
fact_source: "fact_mine"
- }
+ }.compact
end
next
end
@@ -183,8 +210,6 @@ def hints_for(_file, method, owner)
callee_bound_qualities = Array(summary_value(@method_bound_qualities, callee_id, callee_owner, callee))
callee_assumptions = Array(summary_value(@method_assumptions, callee_id, callee_owner, callee))
next unless callee_complexity || callee_space
- next if callee_complexity == "O(1)" && (!callee_space || callee_space == "O(1)") &&
- callee_time_complete && callee_space_complete
propagated_symbolic = propagated_call_symbolic(
callee_owner,
@@ -196,16 +221,17 @@ def hints_for(_file, method, owner)
receiver_state_dependent: receiver_state_dependent?(callee_owner, callee)
)
rendered_symbolic = Espalier::SymbolicComplexity.render(propagated_symbolic)&.first
+ propagated_complexity = rendered_symbolic || propagated_call_complexity(
+ context,
+ callee_complexity || "O(1)",
+ receiver_state_dependent: receiver_state_dependent?(callee_owner, callee)
+ )
hints << {
type: :structural,
line: context.fetch("line", method[:line]).to_i,
- complexity: rendered_symbolic || propagated_call_complexity(
- context,
- callee_complexity || "O(1)",
- receiver_state_dependent: receiver_state_dependent?(callee_owner, callee)
- ),
+ complexity: propagated_complexity,
space: callee_space,
- is_dynamic: true,
+ is_dynamic: propagated_complexity != "O(1)" || callee_space.to_s != "O(1)",
operation: context["message"],
reason: "normalized call containment and propagated callee complexity",
confidence: "high",
@@ -274,8 +300,16 @@ def candidate_upper_bound(candidate_call, context)
[propagated_call_complexity(context, time), space]
end
- source_qualities = ids.flat_map { |id| Array(@method_bound_qualities[id]) }
- source_assumptions = ids.flat_map { |id| Array(@method_assumptions[id]) }
+ if candidate_call[:external_time] && candidate_call[:external_space]
+ rows << [
+ propagated_call_complexity(context, candidate_call[:external_time]),
+ candidate_call[:external_space]
+ ]
+ end
+ source_qualities = ids.flat_map { |id| Array(@method_bound_qualities[id]) } +
+ Array(candidate_call[:qualities])
+ source_assumptions = ids.flat_map { |id| Array(@method_assumptions[id]) } +
+ Array(candidate_call[:assumptions])
closed_set_assumption = "#{candidate_call[:reason]} implementation set is closed for this analysis"
{
ids: ids.sort,
@@ -321,6 +355,15 @@ def propagated_call_symbolic(owner, callee, callee_id, caller_fact, context, cal
mapping[domain["id"]] = Array(actual) if domain && actual
end
end
+ # Substitute the callee's callback cost C with the cost of the callable
+ # passed at this call site, under the same rule the externally-parametric
+ # path uses.
+ callable = callback_argument_cost(caller_fact["path"], context["span"])
+ if callable
+ callee_symbolic = Espalier::SymbolicComplexity.substitute_callback_cost(
+ callee_symbolic, callable.fetch(:expression), callable_constant: callable.fetch(:constant)
+ )
+ end
substituted = Espalier::SymbolicComplexity.substitute(
callee_symbolic,
mapping,
@@ -351,6 +394,23 @@ def propagated_call_symbolic(owner, callee, callee_id, caller_fact, context, cal
end
end
+ # The cost of the callable passed at one call site, as the shared
+ # substitution rule wants it.
+ def callback_argument_cost(path, span)
+ ids = Array(@callback_arg_by_call && @callback_arg_by_call[[path, normalized_call_span(span)]])
+ return nil if ids.empty?
+
+ Espalier::SymbolicComplexity.worst_callable(
+ ids.map do |id|
+ key = id.to_s
+ {
+ expression: @method_symbolic_time && @method_symbolic_time[key],
+ constant: @method_complexities[key] == "O(1)" && @method_time_complete[key] != false
+ }
+ end
+ )
+ end
+
def annotate_propagated_domains(expression, callee_expression, mapping, caller_domains, owner, callee, caller_fact, context)
return expression unless expression
@@ -373,7 +433,7 @@ def annotate_propagated_domains(expression, callee_expression, mapping, caller_d
"line" => context["line"]
}.compact
end
- Espalier::SymbolicComplexity.normalize(expression.merge(domains: domains))
+ Espalier::SymbolicComplexity.with_domains(expression, domains)
end
def mutual_recursion_summary(owner, member)
@@ -666,6 +726,9 @@ def summary_hint(fact, method, owner = nil, suppress_syntactic_recursion: false)
state_replay = owner && state_replay_recursion_summary(owner, method[:name].to_s)
state_rescan = owner && state_rescan_recursion_summary(owner, method[:name].to_s)
state_recursion = state_replay || state_rescan
+ structural_recursion = !state_recursion &&
+ recursion.fetch("structural_calls", 0).to_i.positive? &&
+ recursion.fetch("unknown_progress_calls", 0).to_i.zero?
recursion_time, recursion_space, recursion_reason = if state_recursion
state_recursion.values_at(:time, :space, :reason)
else
@@ -688,14 +751,23 @@ def summary_hint(fact, method, owner = nil, suppress_syntactic_recursion: false)
is_dynamic: complexity != "O(1)",
operation: "normalized_complexity_facts",
reason: recursion_reason || iteration_reason(iterations),
- confidence: complexity == "unknown" ? "unknown" : "high",
+ confidence: if complexity == "unknown"
+ "unknown"
+ elsif structural_recursion
+ "partial"
+ else
+ "high"
+ end,
time_complete: complexity != "unknown" && (!symbolic_time || symbolic_time.fetch(:complete, true)),
space_complete: max_space_complexity(allocation_space, recursion_space) != "unknown",
evidence_gaps: evidence_gaps.uniq.sort,
symbolic_time: symbolic_time,
symbolic_space: symbolic_space,
+ complexity_bound_quality: structural_recursion ? "upper_bound_structural_descent" : nil,
+ complexity_assumptions: structural_recursion ?
+ ["the traversed input structure is finite and acyclic, so descent terminates"] : nil,
fact_source: "fact_mine"
- }
+ }.compact
end
def allocation_complexity(allocations, domains)
@@ -731,6 +803,9 @@ def recursion_complexity(recursion, parameter_count)
if visited.positive?
return ["O(N)", "O(N)", "visited-set guarded structural recursion"]
end
+ if recursion.fetch("structural_calls", 0).to_i.positive?
+ return ["O(N)", "O(N)", "recursive descent into a projection of the input"]
+ end
shrinking = recursion.fetch("shrinking_calls", 0).to_i
halving = recursion.fetch("halving_calls", 0).to_i
@@ -840,6 +915,38 @@ def resolved_recursive_bound(context)
if multiplicity == "O(1)" && progress == "shrinking"
return { time: "O(N)", space: "O(N)", reason: "exact recursive edge with normalized shrinking progress" }
end
+ # Descending into a projection of the input - `walk(node.left)` - moves
+ # strictly one level down a finite structure, so every node is reached
+ # once and the work is linear in the structure. This holds however many
+ # continuations the body has: two children still visit N nodes total.
+ # The bound is conditional on the traversed graph being acyclic, which is
+ # recorded rather than assumed silently.
+ # Flat in the structure, exactly as `partition_of` below: the enclosing
+ # loop is what enumerates the children, and the descent visits each
+ # child's subtree once, so multiplying by the loop multiplicity would
+ # count the same N nodes twice. It also keeps the bound out of the middle
+ # of the lattice, where it would widen again on every fixpoint round.
+ if progress == "structural"
+ return {
+ time: "O(N)", space: "O(N)",
+ reason: "recursive descent into a projection of the input reaches each element once",
+ quality: "upper_bound_structural_descent",
+ assumption: "the traversed input structure is finite and acyclic, so descent terminates"
+ }
+ end
+ # A recursive edge whose argument is a partition of the receiver - the
+ # loop's own iteration binding over a decomposition, i.e. a tree/graph
+ # traversal - reaches every element exactly once, so the work is linear
+ # in the structure. This proof outranks the multiplicity reading below:
+ # a traversal argument that merely happens to spell an offset
+ # (`walk(kids[i - 1])`) reads as loop-contained shrinking progress and
+ # would otherwise be priced O(N!).
+ if context["argument_cardinality_relation"] == "partition_of"
+ return {
+ time: "O(N)", space: "O(N)",
+ reason: "recursive traversal over a partition of the input reaches each element once"
+ }
+ end
if %w[halving shrinking].include?(progress)
return {
time: "O(N!)", space: "O(N)",
@@ -848,11 +955,14 @@ def resolved_recursive_bound(context)
}
end
+ # Nothing proved this edge makes progress, and one call context cannot
+ # establish a branching factor, so no bound follows - not even an
+ # exponential one. Report the missing proof the way the syntactic
+ # classifier does, so the gap is attributable instead of being published
+ # as a complete result.
{
- time: "O(2^N)", space: "O(N)",
- reason: "conservative upper bound for an exact project recursive component",
- quality: "upper_bound_acyclic_project_scc",
- assumption: "the reachable input object graph is finite and acyclic; repeated subgraphs are conservatively treated as independent recursive branches"
+ time: "unknown", space: "unknown",
+ reason: "exact recursive edge progress is unknown"
}
end
diff --git a/gems/espalier/lib/espalier/symbolic_complexity.rb b/gems/espalier/lib/espalier/symbolic_complexity.rb
index 53e8816d1..027f78a47 100644
--- a/gems/espalier/lib/espalier/symbolic_complexity.rb
+++ b/gems/espalier/lib/espalier/symbolic_complexity.rb
@@ -7,6 +7,7 @@ module SymbolicComplexity
module_function
LETTERS = %w[N M K L P Q R S T U V W X Y Z].freeze
+ RENDER_DOMAIN_LIMIT = 128
def reset_intern_pool!
@term_pool = {}
@@ -110,6 +111,23 @@ def multiply(left, right)
)
end
+ # Replace only domain metadata on an already-normalized expression. Terms
+ # are immutable and interned, so rerunning pairwise dominance elimination
+ # here is both redundant and quadratic in the number of monomials.
+ def with_domains(expression, domains)
+ return nil unless expression
+
+ canonical_domains = intern_domains(domains || {})
+ canonical = expression.merge(domains: canonical_domains).freeze
+ @expression_pool ||= {}
+ key = [
+ canonical.fetch(:terms),
+ canonical_domains,
+ canonical.fetch(:complete, true)
+ ]
+ @expression_pool[key] ||= canonical
+ end
+
def substitute(expression, mapping, caller_domains: {})
return nil unless expression
@@ -139,10 +157,84 @@ def substitute(expression, mapping, caller_domains: {})
)
end
+ # Drop the given domain ids from an expression, treating them as constant.
+ # Resolves a callback C to an O(1) callable: O(N*C) -> O(N).
+ def without_domains(expression, ids)
+ return expression if expression.nil? || Array(ids).empty?
+
+ drop = Array(ids).map(&:to_s)
+ terms = Array(expression[:terms]).map do |term|
+ {
+ factors: term[:factors].reject { |id, _| drop.include?(id.to_s) },
+ logs: term[:logs].reject { |id, _| drop.include?(id.to_s) }
+ }
+ end
+ normalize(
+ terms: terms,
+ domains: (expression[:domains] || {}).reject { |id, _| drop.include?(id.to_s) },
+ complete: expression.fetch(:complete, true)
+ )
+ end
+
+ # Ids of the open callback-cost parameters in an expression. Reflective
+ # costs are deliberately excluded: no callable is passed at those sites, so
+ # nothing can close them.
+ def callback_domain_ids(expression)
+ (expression && expression[:domains] || {}).filter_map do |id, domain|
+ next unless domain.is_a?(Hash)
+
+ id if (domain["source_kind"] || domain[:source_kind]).to_s == "callback_cost"
+ end
+ end
+
+ # Choose the substitution for a call site that several callables reach
+ # (`fold(seed, |a, b| ...)`, a nesting chain). The bound must hold for all
+ # of them, so one callable of unknown cost forces C to stay open; otherwise
+ # the costliest known cost is the substitution.
+ #
+ # Each row is `{ expression:, constant: }` - the callable's symbolic cost,
+ # and whether it is proven constant.
+ def worst_callable(rows)
+ rows = Array(rows)
+ return nil if rows.empty?
+ return { expression: nil, constant: false } if rows.any? do |row|
+ row[:expression].nil? && !row[:constant]
+ end
+
+ {
+ expression: rows.filter_map { |row| row[:expression] }.max_by { |value| degree(value) },
+ constant: true
+ }
+ end
+
+ # Replace an open callback cost C with the cost of the callable actually
+ # passed at that call site. An O(1) callable removes C (O(N*C) -> O(N)); a
+ # costlier one multiplies its own cost in (O(N*C) -> O(N*M)).
+ #
+ # A callable whose own cost is neither symbolically known nor proven
+ # constant leaves C open. Dropping it there would silently price a
+ # non-constant callback as free.
+ def substitute_callback_cost(expression, callable, callable_constant: false)
+ ids = callback_domain_ids(expression)
+ return expression if ids.empty?
+ return expression unless callable || callable_constant
+
+ reduced = without_domains(expression, ids)
+ return reduced unless callable && degree(callable).positive?
+
+ multiply(reduced, callable)
+ end
+
def render(expression)
return nil unless expression
- ids = Array(expression[:terms]).flat_map { |term| term[:factors].keys + term[:logs].keys }.uniq
+ terms = Array(expression[:terms])
+ factor_entries = terms.sum { |term| term[:factors].length + term[:logs].length }
+ if terms.length > RENDER_DOMAIN_LIMIT || factor_entries > RENDER_DOMAIN_LIMIT
+ return render_collapsed_upper_bound(expression, terms)
+ end
+
+ ids = terms.flat_map { |term| term[:factors].keys + term[:logs].keys }.uniq
ids.sort_by! do |id|
containing_terms = Array(expression[:terms]).select { |term| term[:factors].key?(id) || term[:logs].key?(id) }
exponents = containing_terms.map { |term| term[:factors].fetch(id, 0) }
@@ -196,6 +288,68 @@ def render(expression)
["O(#{bodies.join(' + ')})", variables(expression, symbols)]
end
+ # A function has a fixed number of input domains, so a sum over many
+ # independent domains is bounded by the largest domain raised to the
+ # greatest observed degree (constant coefficients disappear in Big-O).
+ # Preserve callback/reflection parameters as C/R so proof-tier
+ # classification remains parametric rather than silently closing them.
+ def render_collapsed_upper_bound(expression, terms)
+ maxima = { size: 0, size_logs: 0, callback: 0, reflection: 0 }
+ ids = { size: {}, callback: {}, reflection: {} }
+ domains = expression[:domains] || {}
+ terms.each do |term|
+ row = { size: 0, size_logs: 0, callback: 0, reflection: 0 }
+ term[:factors].each do |id, exponent|
+ domain = domains.fetch(id, {})
+ kind = (domain["source_kind"] || domain[:source_kind]).to_s
+ bucket = if kind == "reflective_target_cost"
+ :reflection
+ elsif kind.end_with?("_cost")
+ :callback
+ else
+ :size
+ end
+ row[bucket] += exponent
+ ids[bucket][id] = true
+ end
+ term[:logs].each do |id, exponent|
+ row[:size_logs] += exponent
+ ids[:size][id] = true
+ end
+ maxima.each_key { |key| maxima[key] = [maxima[key], row[key]].max }
+ end
+
+ parts = []
+ parts << power_text("N", maxima[:size]) if maxima[:size].positive?
+ parts << power_text("log N", maxima[:size_logs], grouped: true) if maxima[:size_logs].positive?
+ parts << power_text("C", maxima[:callback]) if maxima[:callback].positive?
+ parts << power_text("R", maxima[:reflection]) if maxima[:reflection].positive?
+ parts << "1" if parts.empty?
+ variables = []
+ [
+ [:size, "N", "size"],
+ [:callback, "C", "callback cost"],
+ [:reflection, "R", "reflective target cost"]
+ ].each do |bucket, symbol, label|
+ next if ids[bucket].empty?
+
+ variables << {
+ symbol: symbol,
+ domain_id: "collapsed:#{bucket}",
+ name: "maximum of #{ids[bucket].length} #{label} domains",
+ source_kind: "collapsed_upper_bound",
+ domain_count: ids[bucket].length
+ }
+ end
+ ["O(#{parts.join('*')})", variables]
+ end
+
+ def power_text(symbol, exponent, grouped: false)
+ return symbol if exponent == 1
+
+ grouped ? "(#{symbol})^#{exponent}" : "#{symbol}^#{exponent}"
+ end
+
def variables(expression, symbols = nil)
symbols ||= begin
ids = Array(expression[:terms]).flat_map { |term| term[:factors].keys + term[:logs].keys }.uniq.sort
@@ -212,7 +366,8 @@ def variables(expression, symbols = nil)
span: domain["span"] || domain[:span],
origin_owner: domain["origin_owner"] || domain[:origin_owner],
origin_function: domain["origin_function"] || domain[:origin_function],
- propagated_via: domain["propagated_via"] || domain[:propagated_via]
+ propagated_via: domain["propagated_via"] || domain[:propagated_via],
+ domain_count: domain["domain_count"] || domain[:domain_count]
}.compact
end
end
@@ -244,14 +399,20 @@ def rank_string(value)
end
def normalize(expression)
- terms = Array(expression[:terms]).map do |term|
+ raw_terms = Array(expression[:terms])
+ factor_entries = raw_terms.sum { |term| term[:factors].length + (term[:logs] || {}).length }
+ if raw_terms.length > RENDER_DOMAIN_LIMIT || factor_entries > RENDER_DOMAIN_LIMIT
+ return collapse_expression(expression, raw_terms)
+ end
+
+ terms = raw_terms.map do |term|
intern_term(
term[:factors].select { |_, exponent| exponent.to_i.positive? }.transform_values(&:to_i),
(term[:logs] || {}).select { |_, exponent| exponent.to_i.positive? }.transform_values(&:to_i)
)
end.uniq
terms.reject! do |candidate|
- terms.any? { |other| other != candidate && dominates?(other, candidate) }
+ terms.any? { |other| !other.equal?(candidate) && dominates?(other, candidate) }
end
canonical_terms = terms.sort_by { |term| [term[:factors].to_a, term[:logs].to_a] }.freeze
canonical_domains = intern_domains(expression[:domains] || {})
@@ -261,6 +422,61 @@ def normalize(expression)
@expression_pool[key] ||= canonical
end
+ def collapse_expression(expression, terms)
+ source_domains = expression[:domains] || {}
+ maxima = { size: 0, size_logs: 0, callback: 0, reflection: 0 }
+ ids = { size: {}, callback: {}, reflection: {} }
+ terms.each do |term|
+ row = { size: 0, size_logs: 0, callback: 0, reflection: 0 }
+ term[:factors].each do |id, exponent|
+ domain = source_domains.fetch(id, {})
+ kind = (domain["source_kind"] || domain[:source_kind]).to_s
+ bucket = if kind == "reflective_target_cost"
+ :reflection
+ elsif kind.end_with?("_cost")
+ :callback
+ else
+ :size
+ end
+ row[bucket] += exponent.to_i
+ ids[bucket][id] = true
+ end
+ (term[:logs] || {}).each do |id, exponent|
+ row[:size_logs] += exponent.to_i
+ ids[:size][id] = true
+ end
+ maxima.each_key { |key| maxima[key] = [maxima[key], row[key]].max }
+ end
+
+ factors = {}
+ factors["collapsed:size"] = maxima[:size] if maxima[:size].positive?
+ factors["collapsed:callback"] = maxima[:callback] if maxima[:callback].positive?
+ factors["collapsed:reflection"] = maxima[:reflection] if maxima[:reflection].positive?
+ logs = {}
+ logs["collapsed:size"] = maxima[:size_logs] if maxima[:size_logs].positive?
+ canonical_terms = [intern_term(factors, logs)].freeze
+ domains = {}
+ [
+ [:size, "size", "collapsed_upper_bound"],
+ [:callback, "callback cost", "callback_cost"],
+ [:reflection, "reflective target cost", "reflective_target_cost"]
+ ].each do |bucket, label, source_kind|
+ next if ids[bucket].empty?
+
+ domains["collapsed:#{bucket}"] = {
+ "id" => "collapsed:#{bucket}",
+ "name" => "maximum of #{ids[bucket].length} #{label} domains",
+ "source_kind" => source_kind,
+ "domain_count" => ids[bucket].length
+ }
+ end
+ canonical_domains = intern_domains(domains)
+ canonical = expression.merge(terms: canonical_terms, domains: canonical_domains).freeze
+ @expression_pool ||= {}
+ key = [canonical_terms, canonical_domains, canonical.fetch(:complete, true)]
+ @expression_pool[key] ||= canonical
+ end
+
def intern_term(factors, logs)
@term_pool ||= {}
canonical_factors = factors.sort.to_h.freeze
@@ -272,9 +488,19 @@ def intern_term(factors, logs)
def intern_domains(domains)
@domain_pool ||= {}
canonical = domains.sort_by { |id, _| id.to_s }.to_h do |id, domain|
- [id.to_s.freeze, deep_freeze_copy(domain)]
- end.freeze
- @domain_pool[canonical] ||= canonical
+ [id.to_s.freeze, intern_domain_value(domain)]
+ end
+ @domain_pool[canonical] ||= canonical.freeze
+ end
+
+ # Deep-freeze a single domain once and reuse it across every expression that
+ # references the same domain. `normalize` used to re-`deep_freeze_copy` every
+ # domain on every call, which dominated GC; the per-domain pool collapses that
+ # to one copy per distinct domain (facts are read-only, so keying on the raw
+ # domain hash is safe).
+ def intern_domain_value(domain)
+ @domain_value_pool ||= {}
+ @domain_value_pool[domain] ||= deep_freeze_copy(domain)
end
def deep_freeze_copy(value)
@@ -284,7 +510,9 @@ def deep_freeze_copy(value)
when Array
value.map { |child| deep_freeze_copy(child) }.freeze
when String
- value.dup.freeze
+ # `-string` returns a frozen, de-duplicated copy (shared fstring), which
+ # is cheaper and lower-churn than dup+freeze for the repeated domain text.
+ -value
else
value.freeze
end
diff --git a/gems/espalier/script/check_big_o_coverage.rb b/gems/espalier/script/check_big_o_coverage.rb
new file mode 100755
index 000000000..96530d83a
--- /dev/null
+++ b/gems/espalier/script/check_big_o_coverage.rb
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Enforce production function-level Big-O coverage for a FactMine profile.
+# The report separates proof tiers and fails closed when FactMine reports raw
+# executable calls that did not reach normalized call facts.
+
+require "json"
+require "optparse"
+require "pathname"
+
+$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
+require "espalier"
+
+options = {
+ source_root: nil,
+ repositories: [],
+ minimum: Espalier::BigOProofMetrics::DEFAULT_MINIMUM_PERCENT,
+ include_lambdas: true
+}
+OptionParser.new do |parser|
+ parser.banner = "Usage: check_big_o_coverage.rb --source-root PATH [options] PROFILE.json"
+ parser.on("--source-root PATH", "Root used to select and relativize profile paths") do |path|
+ options[:source_root] = File.expand_path(path)
+ end
+ parser.on("--repository NAME", "Limit scope to a direct child of SOURCE_ROOT (repeatable)") do |name|
+ options[:repositories] << name
+ end
+ parser.on("--minimum PERCENT", Float, "Required mapped production functions (default: 85)") do |value|
+ options[:minimum] = value
+ end
+ parser.on("--exclude-lambdas", "Exclude lambda/closure methods from the denominator") do
+ options[:include_lambdas] = false
+ end
+end.parse!
+
+abort "expected one FactMine profile" unless ARGV.length == 1
+abort "--source-root is required" unless options[:source_root]
+
+profile = JSON.parse(File.read(ARGV.fetch(0)))
+source_root = options.fetch(:source_root)
+absolute_profile_path = lambda do |path|
+ path = path.to_s
+ Pathname.new(path).absolute? ? path : File.expand_path(path, source_root)
+end
+prefixes =
+ if options[:repositories].empty?
+ ["#{source_root}/"]
+ else
+ options[:repositories].map { |repository| "#{File.join(source_root, repository)}/" }
+ end
+in_scope = lambda do |row|
+ prefixes.any? { |prefix| absolute_profile_path.call(row.fetch("path", "")).start_with?(prefix) }
+end
+select_path = ->(rows) { Array(rows).select(&in_scope) }
+methods = select_path.call(profile["methods"])
+method_roles = Espalier::StaticEvidence.method_source_roles(methods)
+production_methods = methods.select do |method|
+ method_roles.fetch(method.fetch("id").to_s) == "production"
+end
+method_ids = production_methods.to_h { |method| [method.fetch("id"), true] }
+paths = methods.map { |method| method.fetch("path") }.uniq
+evidence = {
+ "root" => source_root,
+ "input_coverage" => profile["input_coverage"],
+ "files" => paths.map do |path|
+ { "path" => path, "source_role" => Espalier::StaticEvidence.source_role(path) }
+ end,
+ "owners" => select_path.call(profile["owners"]),
+ "methods" => production_methods,
+ "fields" => select_path.call(profile["fields"]),
+ "facts" => {
+ "calls" => Array(profile["calls"]).select { |call| method_ids[call["source"]] },
+ "complexity_facts" => select_path.call(profile["complexity_facts"]),
+ "state_accesses" => select_path.call(profile["state_accesses"]),
+ "struct_declarations" => select_path.call(profile["struct_declarations"]),
+ "state_protocol_records" => select_path.call(profile["state_protocol_records"]),
+ "state_param_origin_records" => select_path.call(profile["state_param_origin_records"])
+ }
+}
+
+modules = Espalier::StaticEvidence.project_modules(evidence, source_roles: ["production"])
+manifest = Espalier::Aggregator.new.aggregate(modules)
+method_by_id = methods.to_h { |method| [method.fetch("id").to_s, method] }
+rows = manifest.flat_map do |mod|
+ Array(mod[:functions]).filter_map do |function|
+ method = method_by_id[function[:id].to_s]
+ next unless method
+
+ {
+ source_role: method_roles.fetch(method.fetch("id").to_s),
+ language: method["language"] || mod[:language],
+ kind: method["kind"],
+ quality: function.fetch(:quality_metrics, {})
+ }
+ end
+end
+
+report = Espalier::BigOProofMetrics.coverage_gate(
+ rows,
+ call_coverage: profile["call_resolution_coverage"] || {},
+ minimum_percent: options.fetch(:minimum),
+ include_lambdas: options.fetch(:include_lambdas)
+)
+report[:scope] = {
+ source_root: source_root,
+ repositories: options[:repositories].sort,
+ profile: File.expand_path(ARGV.fetch(0))
+}
+puts JSON.pretty_generate(report)
+exit(report[:passed] ? 0 : 1)
diff --git a/gems/espalier/script/compare_scip_big_o.rb b/gems/espalier/script/compare_scip_big_o.rb
index 4aab81902..23a58bc2a 100755
--- a/gems/espalier/script/compare_scip_big_o.rb
+++ b/gems/espalier/script/compare_scip_big_o.rb
@@ -6,6 +6,7 @@
require "json"
require "optparse"
+require "pathname"
require "set"
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
@@ -23,14 +24,34 @@
scip_profile = JSON.parse(File.read(ARGV[1]))
source_root = options[:source_root]
-def evidence_for(profile, prefix)
- methods = Array(profile["methods"]).select { |row| row.fetch("path").start_with?(prefix) }
+def absolute_profile_path(path, source_root)
+ text = path.to_s
+ return text unless source_root && !Pathname.new(text).absolute?
+
+ File.expand_path(text, source_root)
+end
+
+def under_profile_prefix?(path, prefix, source_root)
+ absolute_profile_path(path, source_root).start_with?(prefix)
+end
+
+def evidence_for(profile, prefix, source_root)
+ methods = Array(profile["methods"]).select do |row|
+ under_profile_prefix?(row.fetch("path"), prefix, source_root)
+ end
method_ids = methods.to_h { |row| [row.fetch("id"), true] }
paths = methods.map { |row| row.fetch("path") }.uniq
- select_path = ->(rows) { Array(rows).select { |row| row.fetch("path", "").start_with?(prefix) } }
+ select_path = lambda do |rows|
+ Array(rows).select do |row|
+ under_profile_prefix?(row.fetch("path", ""), prefix, source_root)
+ end
+ end
{
- "root" => prefix,
- "files" => paths.map { |path| { "path" => path, "source_role" => "production" } },
+ "root" => source_root,
+ "input_coverage" => profile["input_coverage"],
+ "files" => paths.map do |path|
+ { "path" => path, "source_role" => Espalier::StaticEvidence.source_role(path) }
+ end,
"owners" => select_path.call(profile["owners"]),
"methods" => methods,
"fields" => select_path.call(profile["fields"]),
@@ -45,20 +66,31 @@ def evidence_for(profile, prefix)
}
end
-def function_rows(profile, prefixes)
+def function_rows(profile, prefixes, source_root)
+ methods = Array(profile["methods"]).to_h { |method| [method.fetch("id").to_s, method] }
prefixes.each_with_object({}) do |(repository, prefix), rows|
- evidence = evidence_for(profile, prefix)
- modules = Espalier::StaticEvidence.project_modules(evidence)
+ evidence = evidence_for(profile, prefix, source_root)
+ modules = Espalier::StaticEvidence.project_modules(evidence, source_roles: ["production"])
Espalier::Aggregator.new.aggregate(modules).each do |mod|
Array(mod[:functions]).each do |function|
+ method = methods[function[:id].to_s]
+ next unless method
+
quality = function.fetch(:quality_metrics, {})
- key = [mod[:file], function[:span], mod[:module], function[:name]]
+ # A reopenable Ruby module can span several files. `mod[:file]` is an
+ # aggregate container choice and may change with profile ordering;
+ # method identity must instead use the FactMine declaration path.
+ key = [method.fetch("path"), function[:span], mod[:module], function[:name]]
rows[key] = {
repository: repository,
file: mod[:file],
owner: mod[:module],
name: function[:name],
span: function[:span],
+ language: method["language"],
+ kind: method["kind"],
+ source_role: Espalier::StaticEvidence.source_role(method["path"]),
+ quality: quality,
time_complete: quality[:big_o_complete],
space_complete: quality[:big_o_space_complete],
big_o: quality[:big_o],
@@ -75,22 +107,42 @@ def function_rows(profile, prefixes)
def counts(rows)
bound_quality_counts = rows.flat_map { |row| row[:bound_qualities] }.tally.sort.to_h
+ known = rows.count { |row| row[:time_complete] }
{
functions: rows.length,
- time_known: rows.count { |row| row[:time_complete] },
- time_unknown: rows.count { |row| !row[:time_complete] },
+ time_known: known,
+ time_known_percent: rows.empty? ? 0.0 : (known * 100.0 / rows.length).round(2),
+ time_unknown: rows.length - known,
space_known: rows.count { |row| row[:space_complete] },
space_unknown: rows.count { |row| !row[:space_complete] },
- bound_quality_counts: bound_quality_counts
+ bound_quality_counts: bound_quality_counts,
+ proof: Espalier::BigOProofMetrics.summarize(rows.map { |row| row[:quality] })
}
end
+def call_resolution(coverage)
+ coverage ||= {}
+ %w[
+ eligible_call_sites
+ exact_project_targets
+ modeled_without_project_target
+ semantically_accounted_call_sites
+ semantically_accounted_call_percent
+ unresolved_call_sites
+ raw_parser_call_sites
+ raw_calls_not_normalized
+ raw_calls_not_normalized_inside_function
+ normalized_calls_without_raw_span
+ ].to_h { |key| [key, coverage.fetch(key, 0)] }
+end
+
paths = Array(baseline_profile["methods"]).map { |method| method.fetch("path") }
repositories = options[:repositories]
if repositories.empty?
abort "--source-root is required when repositories are not explicit" unless source_root
repositories = paths.filter_map do |path|
- path.delete_prefix("#{source_root}/").split("/", 2).first if path.start_with?("#{source_root}/")
+ absolute = absolute_profile_path(path, source_root)
+ absolute.delete_prefix("#{source_root}/").split("/", 2).first if absolute.start_with?("#{source_root}/")
end.uniq.sort
end
prefixes = repositories.to_h do |repository|
@@ -98,8 +150,8 @@ def counts(rows)
[repository, prefix]
end
-baseline = function_rows(baseline_profile, prefixes)
-enhanced = function_rows(scip_profile, prefixes)
+baseline = function_rows(baseline_profile, prefixes, source_root)
+enhanced = function_rows(scip_profile, prefixes, source_root)
abort "profile function sets differ" unless baseline.keys.to_set == enhanced.keys.to_set
changed = baseline.keys.filter_map do |key|
@@ -109,8 +161,18 @@ def counts(rows)
end
summary = {
+ schema: "espalier.scip-big-o-comparison.v1",
+ policy: {
+ source_roles: ["production"],
+ include_lambdas: true,
+ raw_call_normalization: "reported_fail_closed_by_coverage_gate"
+ },
baseline: counts(baseline.values),
scip: counts(enhanced.values),
+ call_resolution: {
+ baseline: call_resolution(baseline_profile["call_resolution_coverage"]),
+ scip: call_resolution(scip_profile["call_resolution_coverage"])
+ },
by_repository: repositories.to_h do |repository|
before = baseline.values.select { |row| row[:repository] == repository }
after = enhanced.values.select { |row| row[:repository] == repository }
diff --git a/gems/espalier/script/diagnose_big_o_gaps.rb b/gems/espalier/script/diagnose_big_o_gaps.rb
index af480ea88..a475d252e 100755
--- a/gems/espalier/script/diagnose_big_o_gaps.rb
+++ b/gems/espalier/script/diagnose_big_o_gaps.rb
@@ -8,6 +8,7 @@
require "json"
require "optparse"
+require "pathname"
require "set"
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
@@ -33,9 +34,20 @@
end.uniq.sort
end
prefixes = repositories.map { |repository| File.join(source_root, repository, "") }
-in_scope = ->(row) { prefixes.any? { |prefix| row.fetch("path", "").start_with?(prefix) } }
+absolute_profile_path = lambda do |path|
+ path = path.to_s
+ Pathname.new(path).absolute? ? path : File.expand_path(path, source_root)
+end
+in_scope = lambda do |row|
+ absolute = absolute_profile_path.call(row.fetch("path", ""))
+ prefixes.any? { |prefix| absolute.start_with?(prefix) }
+end
-methods = Array(profile["methods"]).select(&in_scope)
+scoped_methods = Array(profile["methods"]).select(&in_scope)
+method_roles = Espalier::StaticEvidence.method_source_roles(scoped_methods)
+methods = scoped_methods.select do |method|
+ method_roles.fetch(method.fetch("id").to_s) == "production"
+end
method_ids = methods.to_h { |method| [method.fetch("id"), method] }
calls = Array(profile["calls"]).select { |call| method_ids.key?(call["source"]) }
facts = Array(profile["complexity_facts"]).select(&in_scope)
@@ -43,7 +55,9 @@
select_path = ->(rows) { Array(rows).select(&in_scope) }
evidence = {
"root" => source_root,
- "files" => paths.map { |path| { "path" => path, "source_role" => "production" } },
+ "files" => paths.map do |path|
+ { "path" => path, "source_role" => Espalier::StaticEvidence.source_role(path) }
+ end,
"owners" => select_path.call(profile["owners"]),
"methods" => methods,
"fields" => select_path.call(profile["fields"]),
@@ -90,6 +104,7 @@
call["complexity_missing_kind"] || case call["external_symbol_scope"]
when "stdlib" then "stdlib_cost_model_missing"
when "dependency" then "dependency_cost_model_missing"
+ when "project" then "project_candidate_summary_missing"
else "external_cost_model_missing"
end
end
@@ -101,8 +116,10 @@
"modeled_nonproject_call"
elsif !call["semantic_symbol"].to_s.empty?
external_category.call(call)
- else
+ elsif call["runtime_evidence_observed"] == true
"semantic_identity_missing"
+ else
+ "runtime_callsite_unobserved"
end
end
@@ -138,7 +155,8 @@
next if call["known_time_complexity"] || call["known_space_complexity"]
category = if call["semantic_symbol"].to_s.empty?
- "semantic_identity_missing"
+ call["runtime_evidence_observed"] == true ?
+ "semantic_identity_missing" : "runtime_callsite_unobserved"
else
external_category.call(call)
end
@@ -183,6 +201,7 @@
end
priority = %w[
+ runtime_callsite_unobserved
semantic_identity_missing
callback_cost_missing
reflective_target_cost_missing
@@ -225,11 +244,22 @@
functions: symbol_calls.map { |call| call["source"] }.uniq.length
}
end.sort_by { |row| [-row[:functions], -row[:calls], row[:symbol].to_s] }.first(15)
+ call_examples = rows.first(options[:examples]).map do |call|
+ {
+ path: call["path"],
+ line: call["line"],
+ receiver: call["receiver"],
+ message: call["message"],
+ semantic_symbol: call["semantic_symbol"],
+ unresolved_reason: call["unresolved_reason"]
+ }
+ end
[category, {
affected_incomplete_functions: affected.length,
direct_incomplete_functions: direct.length,
direct_call_sites: rows.length,
top_symbols: top_symbols,
+ call_examples: call_examples,
examples: (direct.empty? ? affected : direct).first(options[:examples]).map(&location)
}]
end
@@ -284,7 +314,11 @@
report = {
schema: "espalier.big-o-gap-diagnostics.v1",
- scope: { source_root: source_root, repositories: repositories },
+ scope: {
+ source_root: source_root,
+ repositories: repositories,
+ source_roles: ["production"]
+ },
summary: {
functions: results.length,
complete_time_bounds: results.count { |_, result| result[:complete] },
diff --git a/gems/espalier/script/export_complexity_summary.rb b/gems/espalier/script/export_complexity_summary.rb
index 1bd544fa8..b158f2402 100644
--- a/gems/espalier/script/export_complexity_summary.rb
+++ b/gems/espalier/script/export_complexity_summary.rb
@@ -6,13 +6,85 @@
# excluded/generated/dependency methods into the product's reported corpus.
require "json"
+require "digest"
+require "optparse"
+require "zlib"
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "espalier"
-abort "usage: export_complexity_summary.rb PROFILE.json [OUTPUT.json]" unless (1..2).cover?(ARGV.length)
+metadata = {
+ producer_version: Espalier.const_defined?(:VERSION) ? Espalier::VERSION : "unknown",
+ corpus: nil,
+ source_revision: nil,
+ indexer: nil,
+ consumer_indexers: [],
+ symbol_prefix_from: nil,
+ symbol_prefix_to: nil,
+ compatibility: nil,
+ symbol_map: nil
+}
+OptionParser.new do |opts|
+ opts.banner = "usage: export_complexity_summary.rb [options] PROFILE.json [OUTPUT.json[.gz]]"
+ opts.on("--corpus ID", "Stable corpus identity (for example go-stdlib)") { |value| metadata[:corpus] = value }
+ opts.on("--source-revision REV", "Source commit or release") { |value| metadata[:source_revision] = value }
+ opts.on("--indexer ID", "SCIP indexer and version") { |value| metadata[:indexer] = value }
+ opts.on("--consumer-indexer ID", "Compatible consumer SCIP indexer and version (repeatable)") do |value|
+ metadata[:consumer_indexers] << value
+ end
+ opts.on("--producer-version VERSION", "Override the Espalier producer version") { |value| metadata[:producer_version] = value }
+ opts.on("--symbol-prefix-from PREFIX", "Relocate producer symbols from this exact prefix") do |value|
+ metadata[:symbol_prefix_from] = value
+ end
+ opts.on("--symbol-prefix-to PREFIX", "Relocate producer symbols to this exact prefix") do |value|
+ metadata[:symbol_prefix_to] = value
+ end
+ opts.on("--compatibility FILE", "Semantic-environment sidecar required by consumers") do |value|
+ metadata[:compatibility] = value
+ end
+ opts.on("--symbol-map FILE", "Exact producer-to-consumer symbol bridge") do |value|
+ metadata[:symbol_map] = value
+ end
+end.parse!
+abort "usage: export_complexity_summary.rb [options] PROFILE.json [OUTPUT.json[.gz]]" unless (1..2).cover?(ARGV.length)
+if metadata[:symbol_prefix_from].nil? != metadata[:symbol_prefix_to].nil?
+ abort "--symbol-prefix-from and --symbol-prefix-to must be supplied together"
+end
+abort "--symbol-map cannot be combined with prefix relocation" if metadata[:symbol_map] && metadata[:symbol_prefix_from]
-profile = JSON.parse(File.read(ARGV.fetch(0)))
+compatibility_claims = {}
+if metadata[:compatibility]
+ environment = JSON.parse(File.read(metadata[:compatibility]))
+ unless environment["schema"] == "fact-mine.semantic-environment.v1"
+ abort "unsupported semantic environment schema: #{environment['schema'].inspect}"
+ end
+ compatibility_claims = environment.fetch("claims")
+ unless compatibility_claims.is_a?(Hash) &&
+ compatibility_claims.all? { |key, value| !key.to_s.empty? && !value.to_s.empty? }
+ abort "semantic environment claims must be a mapping of non-empty strings"
+ end
+end
+
+symbol_map = nil
+symbol_map_sha256 = nil
+if metadata[:symbol_map]
+ symbol_map_bytes = File.binread(metadata[:symbol_map])
+ bridge = JSON.parse(symbol_map_bytes)
+ unless bridge["schema"] == "fact-mine.symbol-bridge.v1"
+ abort "unsupported symbol bridge schema: #{bridge['schema'].inspect}"
+ end
+ symbol_map = bridge.fetch("symbols")
+ unless symbol_map.is_a?(Hash) && symbol_map.all? { |key, value|
+ targets = value.is_a?(Array) ? value : [value]
+ !key.to_s.empty? && !targets.empty? && targets.all? { |target| !target.to_s.empty? }
+ }
+ abort "symbol bridge symbols must map non-empty producer symbols to one or more non-empty consumer symbols"
+ end
+ symbol_map_sha256 = "sha256:#{Digest::SHA256.hexdigest(symbol_map_bytes)}"
+end
+
+profile_bytes = File.binread(ARGV.fetch(0))
+profile = JSON.parse(profile_bytes)
paths = Array(profile["methods"]).map { |method| method.fetch("path") }.uniq
evidence = {
"root" => "/",
@@ -36,11 +108,22 @@
results = Espalier::Aggregator.new.aggregate(Espalier::StaticEvidence.project_modules(evidence))
.flat_map { |mod| Array(mod[:functions]) }
.to_h { |function| [function[:id].to_s, function.fetch(:quality_metrics, {})] }
+facts_by_location = Array(profile["complexity_facts"]).group_by do |fact|
+ [fact["path"].to_s, fact["line"].to_i, fact["function"].to_s]
+end
+source_proven_ids = Array(profile["methods"]).filter_map do |method|
+ quality = results[method["id"].to_s]
+ facts = facts_by_location.fetch(
+ [method["path"].to_s, method["line"].to_i, method["name"].to_s],
+ []
+ )
+ method["id"].to_s if Espalier::ComplexitySummary.source_method_proven?(method, quality, facts)
+end.to_h { |id| [id, true] }
symbols = Array(profile["methods"]).filter_map do |method|
symbol = method["semantic_symbol"].to_s
quality = results[method["id"].to_s]
- next if symbol.empty? || !quality || quality[:big_o_complete] != true || quality[:big_o_space_complete] != true
+ next if symbol.empty? || !source_proven_ids[method["id"].to_s]
source_qualities = Array(quality[:big_o_bound_qualities]).map(&:to_s).reject(&:empty?)
source_assumptions = Array(quality[:big_o_assumptions]).map(&:to_s).reject(&:empty?)
@@ -61,20 +144,19 @@
}]
end
-# A compiler can attach an interface/trait declaration symbol to a call while
-# separately providing the closed implementation set visible in this index.
-# When every candidate has a complete analyzed bound, publish the conservative
-# maximum under that declaration symbol. This is language-neutral and retains
-# the closed-world assumption explicitly.
+# A compiler can attach a declaration symbol to a call while separately
+# providing candidate implementations visible in this index. Visibility in a
+# producer index is not proof that downstream consumers cannot add another
+# implementation. Publish a conservative candidate maximum only when the
+# profile carries a separate, explicit consumer-closure proof.
candidate_symbols = Array(profile["calls"]).filter_map do |call|
symbol = call["semantic_symbol"].to_s
candidate_ids = Array(call["candidate_targets"]).map(&:to_s).reject(&:empty?).uniq.sort
next if symbol.empty? || candidate_ids.empty?
+ next unless Espalier::ComplexitySummary.consumer_closed_candidate_set?(call)
candidate_qualities = candidate_ids.map { |id| results[id] }
- next if candidate_qualities.any? do |quality|
- !quality || quality[:big_o_complete] != true || quality[:big_o_space_complete] != true
- end
+ next unless candidate_ids.all? { |id| source_proven_ids[id] }
worst_time = candidate_qualities.map { |quality| quality[:big_o] }
.max_by { |value| Espalier::SymbolicComplexity.rank_string(value) }
@@ -93,6 +175,12 @@
}]
end
symbols.concat(candidate_symbols)
+symbols = Espalier::ComplexitySummary.bridge_symbol_rows(
+ symbols,
+ symbol_map: symbol_map,
+ prefix_from: metadata[:symbol_prefix_from],
+ prefix_to: metadata[:symbol_prefix_to]
+)
# A compiler symbol should identify one declaration. Omit conflicting symbols
# instead of selecting by order if an index violates that contract; one
@@ -108,7 +196,33 @@
end
output = {
- "schema" => "fact-mine.external-complexity-summary.v1",
+ "schema" => "fact-mine.external-complexity-summary.v3",
+ "producer" => {
+ "name" => "espalier",
+ "version" => metadata[:producer_version].to_s
+ },
+ "source" => {
+ "profile_sha256" => "sha256:#{Digest::SHA256.hexdigest(profile_bytes)}",
+ "method_count" => Array(profile["methods"]).length,
+ "complete_symbol_count" => grouped.length,
+ "source_proven_method_count" => source_proven_ids.length,
+ "proof_policy" => "analyzed_bodies_exact_targets_cfg_dfg_v1",
+ "corpus" => metadata[:corpus],
+ "source_revision" => metadata[:source_revision],
+ "indexer" => metadata[:indexer],
+ "consumer_indexers" => metadata[:consumer_indexers].uniq.sort,
+ "symbol_relocation" => if metadata[:symbol_prefix_from] || metadata[:symbol_prefix_to]
+ {
+ "from" => metadata[:symbol_prefix_from],
+ "to" => metadata[:symbol_prefix_to]
+ }
+ end,
+ "symbol_bridge_sha256" => symbol_map_sha256,
+ "languages" => Array(profile["methods"]).map { |method| method["language"].to_s }.reject(&:empty?).uniq.sort
+ }.compact,
+ "compatibility" => {
+ "claims" => compatibility_claims
+ },
"symbols" => grouped.to_h do |symbol, rows|
merged = rows.first.last.dup
merged["candidates"] = rows.flat_map { |row| Array(row.last["candidates"]) }.uniq.sort
@@ -118,7 +232,15 @@
}
rendered = JSON.pretty_generate(output)
if ARGV[1]
- File.write(ARGV[1], rendered)
+ if File.extname(ARGV[1]) == ".gz"
+ Zlib::GzipWriter.open(ARGV[1]) do |gzip|
+ gzip.mtime = 0
+ gzip.orig_name = ""
+ gzip.write(rendered)
+ end
+ else
+ File.write(ARGV[1], rendered)
+ end
else
puts rendered
end
diff --git a/gems/espalier/script/interface_worst_case.rb b/gems/espalier/script/interface_worst_case.rb
new file mode 100644
index 000000000..ab40bd0a5
--- /dev/null
+++ b/gems/espalier/script/interface_worst_case.rb
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Interface-dispatch worst-case analysis (design phases 3 + 4).
+#
+# Consumes phase-2 satisfaction edges (`dispatch_impls`) from a FactMine
+# profile and per-function Big-O from an Espalier architecture report, and for
+# every abstract type's method computes:
+# phase 3 - the worst-case implementation and its cost (provenance)
+# phase 4 - whether that worst case is asymptotically significant, and how
+# often across the corpus a worst case is non-trivial / varies
+#
+# Usage: interface_worst_case.rb PROFILE.json ARCHITECTURE.json [--json]
+
+require "json"
+
+profile_path, arch_path = ARGV.reject { |a| a.start_with?("--") }
+as_json = ARGV.include?("--json")
+abort "usage: interface_worst_case.rb PROFILE.json ARCHITECTURE.json [--json]" unless profile_path && arch_path
+
+profile = JSON.parse(File.read(profile_path))
+arch = JSON.parse(File.read(arch_path))
+
+# Per-(owner, method) Big-O from the architecture report.
+cost = Hash.new { |h, k| h[k] = {} }
+walk = lambda do |node, &blk|
+ blk.call(node)
+ children = node.is_a?(Hash) ? node.values : node.is_a?(Array) ? node : []
+ children.each { |c| walk.call(c, &blk) }
+end
+walk.call(arch) do |node|
+ next unless node.is_a?(Hash) && node.key?("time_complete")
+ cost[node["owner"].to_s][node["name"].to_s] = {
+ time: node["big_o_time"], complete: node["time_complete"]
+ }
+end
+
+# Interface -> [implementers]; interface -> [required methods].
+impls = Hash.new { |h, k| h[k] = [] }
+Array(profile["dispatch_impls"]).each { |e| impls[e["interface"]] << e }
+requirements = {}
+Array(profile["owners"]).each do |o|
+ requirements[o["name"]] = Array(o["requirements"]) unless Array(o["requirements"]).empty?
+end
+
+# Coarse worst-case ordering over Big-O classes. A parametric cost (contains an
+# unresolved callback C or reflective R) ranks above any concrete class of the
+# same N-structure: worst-case, an unbounded parameter can be anything.
+parametric = lambda { |c| c.to_s.include?("C") || c.to_s.include?("R") }
+rank = lambda do |c|
+ s = c.to_s
+ base = if s.include?("^") then 5
+ elsif s.include?("N log N") || s.include?("N*log") then 4
+ elsif s.include?("N") then 3
+ elsif s.include?("log") then 1
+ else 0
+ end
+ parametric.call(c) ? base + 10 : base
+end
+
+results = []
+impls.each do |iface, edges|
+ methods = requirements[iface] || []
+ # Fall back to the union of methods the implementers define, if no explicit
+ # requirement list (nominal languages).
+ methods = edges.flat_map { |e| cost[e["implementer"]].keys }.uniq if methods.empty?
+ methods.each do |m|
+ priced = edges.filter_map do |e|
+ c = cost[e["implementer"]][m]
+ c && c[:complete] ? [e["implementer"], c[:time]] : nil
+ end
+ next if priced.empty?
+ worst = priced.max_by { |(_, t)| rank.call(t) }
+ variance = priced.map { |(_, t)| t }.uniq.size
+ # A caller's `O(N * C)` bound for this method resolves to a concrete
+ # "complete worst case" only when the worst implementation is itself
+ # concrete. If the worst impl is still parametric (its own C/R), the
+ # substitution stays parametric and the caller bound cannot be closed.
+ status = parametric.call(worst[1]) ? "worst_case_parametric" : "complete_worst_case"
+ results << {
+ interface: iface, method: m,
+ worst_impl: worst[0], worst_cost: worst[1],
+ implementers_priced: priced.size, distinct_costs: variance,
+ significant: rank.call(worst[1]) > 0, # non-O(1) -> may dominate a caller
+ status: status,
+ distribution: priced.sort_by { |(_, t)| -rank.call(t) }.first(4)
+ }
+ end
+end
+
+results.sort_by! { |r| [-rank.call(r[:worst_cost]), r[:interface], r[:method]] }
+
+if as_json
+ puts JSON.pretty_generate(results)
+else
+ significant = results.count { |r| r[:significant] }
+ varying = results.count { |r| r[:distinct_costs] > 1 }
+ resolvable = results.count { |r| r[:status] == "complete_worst_case" }
+ puts "interface methods analyzed: #{results.size}"
+ puts " non-trivial worst case (may dominate a caller): #{significant}"
+ puts " worst case varies across implementations: #{varying}"
+ puts " complete worst case (C resolves to a concrete bound): #{resolvable}"
+ puts " worst-case parametric (C stays open): #{results.size - resolvable}"
+ puts
+ puts "phase 3 - worst-case implementation per interface method:"
+ results.first(25).each do |r|
+ flag = r[:distinct_costs] > 1 ? " [varies]" : ""
+ puts format(" %-22s %-10s worst=%-9s via %s%s",
+ "#{r[:interface]}.#{r[:method]}", r[:worst_cost], r[:worst_cost], r[:worst_impl], flag)
+ end
+end
diff --git a/gems/espalier/script/report_big_o_proof_metrics.rb b/gems/espalier/script/report_big_o_proof_metrics.rb
index 5970ba1e4..e4c72c965 100644
--- a/gems/espalier/script/report_big_o_proof_metrics.rb
+++ b/gems/espalier/script/report_big_o_proof_metrics.rb
@@ -6,6 +6,7 @@
require "json"
require "optparse"
+require "pathname"
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "espalier"
@@ -24,19 +25,33 @@
profile = JSON.parse(File.read(ARGV.fetch(0)))
source_root = options.fetch(:source_root)
repositories = options.fetch(:repositories)
+absolute_profile_path = lambda do |path|
+ path = path.to_s
+ Pathname.new(path).absolute? ? path : File.expand_path(path, source_root)
+end
if repositories.empty?
repositories = Array(profile["methods"]).filter_map do |method|
- path = method.fetch("path")
+ path = absolute_profile_path.call(method.fetch("path"))
path.delete_prefix("#{source_root}/").split("/", 2).first if path.start_with?("#{source_root}/")
end.uniq.sort
end
focus = options[:focus] || repositories.find { |name| name.downcase.delete("-") == "javapoet" } || repositories.first
-def evidence_for(profile, prefix)
- methods = Array(profile["methods"]).select { |row| row.fetch("path").start_with?(prefix) }
+def evidence_for(profile, prefix, source_root)
+ absolute_profile_path = lambda do |path|
+ path = path.to_s
+ Pathname.new(path).absolute? ? path : File.expand_path(path, source_root)
+ end
+ methods = Array(profile["methods"]).select do |row|
+ absolute_profile_path.call(row.fetch("path")).start_with?(prefix)
+ end
method_ids = methods.to_h { |row| [row.fetch("id"), true] }
paths = methods.map { |row| row.fetch("path") }.uniq
- select_path = ->(rows) { Array(rows).select { |row| row.fetch("path", "").start_with?(prefix) } }
+ select_path = lambda do |rows|
+ Array(rows).select do |row|
+ absolute_profile_path.call(row.fetch("path", "")).start_with?(prefix)
+ end
+ end
{
"root" => prefix,
"files" => paths.map { |path| { "path" => path, "source_role" => "production" } },
@@ -56,7 +71,7 @@ def evidence_for(profile, prefix)
qualities_by_repository = repositories.to_h do |repository|
prefix = File.join(source_root, repository, "")
- modules = Espalier::StaticEvidence.project_modules(evidence_for(profile, prefix))
+ modules = Espalier::StaticEvidence.project_modules(evidence_for(profile, prefix, source_root))
qualities = Espalier::Aggregator.new.aggregate(modules).flat_map do |mod|
Array(mod[:functions]).map { |function| function.fetch(:quality_metrics, {}) }
end
diff --git a/gems/espalier/test/aggregator_test.rb b/gems/espalier/test/aggregator_test.rb
index e4b2041f0..9d693040d 100644
--- a/gems/espalier/test/aggregator_test.rb
+++ b/gems/espalier/test/aggregator_test.rb
@@ -388,6 +388,403 @@ def test_big_o_uses_fact_mine_normalized_call_costs
refute fn[:quality_metrics].key?(:big_o_unknowns)
end
+ def incomplete_recursive_module(language, owner, fn_name)
+ [
+ {
+ # project_modules disambiguates owners as "name@path"; the override
+ # must key on the bare leaf.
+ type: :class, name: "#{owner}@src/#{owner}.x", file: "src/#{owner}.x", states: Set.new,
+ language: language,
+ methods: [
+ {
+ name: fn_name, signature: "func #{fn_name}()",
+ parameters: ["data"], visibility: :public, line: 20, span: [20, 0, 40, 1],
+ effects: { reads: Set.new, writes: Set.new },
+ complexity_facts: [{
+ "line" => 20, "parameters" => ["data"], "collection_parameters" => ["data"],
+ "iterations" => [], "allocations" => [], "size_domains" => [],
+ "recursion" => { "calls" => 2, "unknown_progress_calls" => 2 },
+ "call_contexts" => []
+ }]
+ }
+ ]
+ }
+ ]
+ end
+
+ def test_manual_override_completes_only_incomplete_registered_functions
+ modules = incomplete_recursive_module(:go, "sort", "Sort")
+ fn = Espalier::Aggregator.new.aggregate(modules).first[:functions].first
+
+ assert_equal "O(N log N)", fn[:quality_metrics][:big_o]
+ assert_equal true, fn[:quality_metrics][:big_o_complete]
+ assert_equal :manual_override, fn[:quality_metrics][:big_o_provenance]
+ assert_equal :complete_override, fn[:quality_metrics][:big_o_status]
+ assert_equal "O(log N)", fn[:quality_metrics][:big_o_space]
+ end
+
+ def test_manual_override_ignores_unregistered_functions
+ # Same incomplete shape, but no registry entry for sort.Frobnicate.
+ modules = incomplete_recursive_module(:go, "sort", "Frobnicate")
+ fn = Espalier::Aggregator.new.aggregate(modules).first[:functions].first
+
+ refute_equal true, fn[:quality_metrics][:big_o_complete]
+ refute_equal :manual_override, fn[:quality_metrics][:big_o_provenance]
+ assert_equal :incomplete, fn[:quality_metrics][:big_o_status]
+ end
+
+ def test_manual_override_never_replaces_a_complete_bound
+ # A registered name (list.sort / python) but a COMPLETE derived bound:
+ # the override must not fire.
+ modules = [
+ {
+ type: :class, name: "list", file: "x.py", states: Set.new, language: :python,
+ methods: [{
+ name: "sort", signature: "def sort(self)", parameters: [], visibility: :public,
+ line: 1, span: [1, 0, 2, 1], effects: { reads: Set.new, writes: Set.new },
+ complexity_facts: [{
+ "line" => 1, "parameters" => [], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "size_domains" => [],
+ "recursion" => { "calls" => 0 }, "call_contexts" => []
+ }]
+ }]
+ }
+ ]
+ fn = Espalier::Aggregator.new.aggregate(modules).first[:functions].first
+
+ assert_equal true, fn[:quality_metrics][:big_o_complete]
+ refute_equal :manual_override, fn[:quality_metrics][:big_o_provenance]
+ refute_equal "O(N log N)", fn[:quality_metrics][:big_o]
+ assert_equal :complete, fn[:quality_metrics][:big_o_status]
+ end
+
+ def test_big_o_status_distinguishes_parametric_from_complete
+ # The core "complete" vs "complete worst case" distinction. A bound carrying
+ # an open callback/reflective parameter (C/R) is complete only parametrically
+ # - it is the tier a worst-case substitution upgrades to :complete_worst_case
+ # and must NOT read as a plain (closed) :complete bound.
+ agg = Espalier::Aggregator.new
+ classify = ->(q) { agg.send(:classify_big_o_status, q) }
+
+ assert_equal :complete, classify.call(big_o: "O(N)", big_o_complete: true)
+ assert_equal :parametric, classify.call(big_o: "O(N * C)", big_o_complete: true)
+ assert_equal :parametric, classify.call(big_o: "O(R)", big_o_complete: true)
+ assert_equal :incomplete, classify.call(big_o: "unknown", big_o_complete: false)
+ assert_equal :complete_override,
+ classify.call(big_o: "O(N log N)", big_o_complete: true,
+ big_o_provenance: :manual_override)
+
+ # The open parameter can sit on the space axis alone: a parametric contract
+ # prices auxiliary space as O(S) / O(N*S), and substitution rewrites only
+ # the time expression. Reading only the time bound published those as closed.
+ assert_equal :parametric,
+ classify.call(big_o: "O(N^2)", big_o_complete: true,
+ big_o_space: "O(S)", big_o_space_complete: true)
+ assert_equal :parametric,
+ classify.call(big_o: "O(N)", big_o_complete: true,
+ big_o_space: "O(N*S)", big_o_space_complete: true)
+ assert_equal :complete,
+ classify.call(big_o: "O(N)", big_o_complete: true, big_o_space: "O(N)")
+ end
+
+ def test_open_space_parameter_is_parametric_through_the_aggregation_pipeline
+ # The classifier is only correct if it runs against a quality record that
+ # already carries the space bound. A reflective contract closes no space,
+ # so the reported tier must be parametric even though the time bound is a
+ # closed O(N).
+ modules = [{
+ type: :class, name: "demo", file: "demo.go", states: Set.new, language: :go,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 1], parameters: ["xs"],
+ visibility: :public, effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "c1", receiver: "fmt", message: "Sprintf", line: 2,
+ span: [2, 4, 2, 40], known_time_complexity: "O(N)",
+ known_space_complexity: "O(S)"
+ }],
+ complexity_facts: [{
+ "line" => 1, "parameters" => ["xs"], "collection_parameters" => ["xs"],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [], "call_contexts" => [{
+ "line" => 2, "span" => [2, 4, 2, 40], "message" => "Sprintf",
+ "execution_multiplicity" => "O(1)", "power" => 0
+ }]
+ }]
+ }]
+ }]
+
+ quality = Espalier::Aggregator.new.aggregate(modules)
+ .first[:functions].find { |fn| fn[:name] == "run" }.fetch(:quality_metrics)
+
+ assert_equal "O(N)", quality[:big_o]
+ assert_equal "O(S)", quality[:big_o_space]
+ assert_equal :parametric, quality[:big_o_status]
+ end
+
+ def test_lambda_argument_closes_an_external_parametric_callback_bound
+ # A stdlib higher-order call is priced O(N*C) from its compiler symbol. The
+ # closure passed at that call site is analyzed like any other function, so C
+ # is not open - substituting it is what turns a partial bound into a closed
+ # one.
+ modules = [{
+ type: :class, name: "demo", file: "demo.rs", states: Set.new, language: :rust,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 1], parameters: ["xs"],
+ visibility: :public, effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "c1", receiver: "xs.iter()", message: "map", line: 2,
+ span: [2, 4, 2, 40], known_time_complexity: "O(N)",
+ complexity_bound_quality: "upper_bound_parametric_callback_linear"
+ }],
+ complexity_facts: [{
+ "line" => 1, "parameters" => ["xs"], "collection_parameters" => ["xs"],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }],
+ "call_contexts" => [{
+ "line" => 2, "span" => [2, 4, 2, 40], "message" => "map",
+ "execution_multiplicity" => "O(1)", "power" => 0,
+ "argument_size_domains" => [["param:xs"]],
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }]
+ }]
+ }]
+ }, {
+ id: "lambda", name: "", line: 2, span: [2, 20, 2, 34],
+ dispatch_kind: "lambda", parameters: ["x"], visibility: :private,
+ effects: { reads: Set.new, writes: Set.new }, delegations: [],
+ complexity_facts: [{
+ "line" => 2, "parameters" => ["x"], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [], "call_contexts" => []
+ }]
+ }]
+ }]
+
+ functions = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+ quality = functions.find { |fn| fn[:name] == "run" }.fetch(:quality_metrics)
+
+ assert_equal "O(N)", quality[:big_o]
+ assert_equal :complete, quality[:big_o_status]
+ end
+
+ def test_costly_lambda_argument_keeps_its_own_cost_in_the_bound
+ # Substitution is not erasure: an O(M) callable multiplies in rather than
+ # dropping out, so O(N*C) becomes O(N*M), not O(N).
+ modules = [{
+ type: :class, name: "demo", file: "demo.rs", states: Set.new, language: :rust,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 1], parameters: ["xs"],
+ visibility: :public, effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "c1", receiver: "xs.iter()", message: "map", line: 2,
+ span: [2, 4, 2, 40], known_time_complexity: "O(N)",
+ complexity_bound_quality: "upper_bound_parametric_callback_linear"
+ }],
+ complexity_facts: [{
+ "line" => 1, "parameters" => ["xs"], "collection_parameters" => ["xs"],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }],
+ "call_contexts" => [{
+ "line" => 2, "span" => [2, 4, 2, 40], "message" => "map",
+ "execution_multiplicity" => "O(1)", "power" => 0,
+ "argument_size_domains" => [["param:xs"]],
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }]
+ }]
+ }]
+ }, {
+ id: "lambda", name: "", line: 2, span: [2, 20, 2, 34],
+ dispatch_kind: "lambda", parameters: ["y"], visibility: :private,
+ effects: { reads: Set.new, writes: Set.new }, delegations: [],
+ complexity_facts: [{
+ "line" => 2, "parameters" => ["y"], "collection_parameters" => ["y"],
+ "iterations" => [{
+ "line" => 2, "span" => [2, 20, 2, 34], "power" => 1,
+ "parameter_domains" => ["y"],
+ "symbolic_time" => { "factors" => [{ "domain_id" => "param:y", "exponent" => 1 }] }
+ }],
+ "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [{ "id" => "param:y", "name" => "y", "source_kind" => "parameter" }],
+ "call_contexts" => []
+ }]
+ }]
+ }]
+
+ functions = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+ quality = functions.find { |fn| fn[:name] == "run" }.fetch(:quality_metrics)
+
+ assert_equal "O(N*M)", quality[:big_o]
+ assert_equal :complete, quality[:big_o_status]
+ end
+
+ def test_callable_of_unknown_cost_leaves_the_callback_parameter_open
+ # Substitution must never price an unproven callable as free. A closure
+ # whose own bound is not symbolically known keeps C open rather than
+ # silently dropping out of the caller's bound.
+ modules = [{
+ type: :class, name: "demo", file: "demo.rs", states: Set.new, language: :rust,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 1], parameters: ["xs"],
+ visibility: :public, effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "c1", receiver: "xs.iter()", message: "map", line: 2,
+ span: [2, 4, 2, 40], known_time_complexity: "O(N)",
+ complexity_bound_quality: "upper_bound_parametric_callback_linear"
+ }],
+ complexity_facts: [{
+ "line" => 1, "parameters" => ["xs"], "collection_parameters" => ["xs"],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }],
+ "call_contexts" => [{
+ "line" => 2, "span" => [2, 4, 2, 40], "message" => "map",
+ "execution_multiplicity" => "O(1)", "power" => 0,
+ "argument_size_domains" => [["param:xs"]],
+ "size_domains" => [{ "id" => "param:xs", "name" => "xs", "source_kind" => "parameter" }]
+ }]
+ }]
+ }, {
+ id: "lambda", name: "", line: 2, span: [2, 20, 2, 34],
+ dispatch_kind: "lambda", parameters: ["x"], visibility: :private,
+ effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "c2", receiver: "sink", message: "consume", line: 2, span: [2, 24, 2, 33]
+ }],
+ complexity_facts: [{
+ "line" => 2, "parameters" => ["x"], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [],
+ "call_contexts" => [{
+ "line" => 2, "span" => [2, 24, 2, 33], "message" => "consume",
+ "execution_multiplicity" => "O(1)", "power" => 0
+ }]
+ }]
+ }]
+ }]
+
+ functions = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+ caller = functions.find { |fn| fn[:name] == "run" }.fetch(:quality_metrics)
+ callable = functions.find { |fn| fn[:name] == "" }.fetch(:quality_metrics)
+
+ refute callable[:big_o_complete], "the closure's own cost must be unproven for this case"
+ assert_equal "O(N*C)", caller[:big_o]
+ assert_equal :parametric, caller[:big_o_status]
+ end
+
+ def test_recursive_callback_component_stays_parametric_instead_of_expanding_forever
+ modules = [{
+ type: :class, name: "walker", file: "walker.rs", states: Set.new, language: :rust,
+ methods: [{
+ id: "visit", name: "visit", line: 1, span: [1, 0, 3, 1], parameters: ["children"],
+ visibility: :private, effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "walk", receiver: "children", message: "any", line: 2,
+ span: [2, 4, 2, 40], known_time_complexity: "O(N*C)",
+ complexity_bound_quality: "upper_bound_parametric_callback_linear"
+ }],
+ complexity_facts: [{
+ "line" => 1, "parameters" => ["children"], "collection_parameters" => ["children"],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [{
+ "id" => "param:children", "name" => "children", "source_kind" => "parameter"
+ }],
+ "call_contexts" => [{
+ "line" => 2, "span" => [2, 4, 2, 40], "message" => "any",
+ "execution_multiplicity" => "O(1)", "power" => 0,
+ "argument_size_domains" => [["param:children"]]
+ }]
+ }]
+ }, {
+ id: "lambda", name: "", line: 2, span: [2, 20, 2, 39],
+ dispatch_kind: "lambda", parameters: ["child"], visibility: :private,
+ effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ call_id: "recur", receiver: "self", message: "visit", line: 2,
+ span: [2, 22, 2, 38], candidate_target_ids: ["visit"],
+ consumer_closed_candidate_set: true,
+ candidate_reason: "scip_project_candidate_set"
+ }],
+ complexity_facts: [{
+ "line" => 2, "parameters" => ["child"], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "recursion" => { "calls" => 0 },
+ "size_domains" => [], "call_contexts" => [{
+ "line" => 2, "span" => [2, 22, 2, 38], "message" => "visit",
+ "execution_multiplicity" => "O(1)", "power" => 0
+ }]
+ }]
+ }]
+ }]
+
+ functions = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+ visit = functions.find { |function| function[:id] == "visit" }.fetch(:quality_metrics)
+
+ assert_equal "O(N*C)", visit[:big_o]
+ assert_equal :parametric, visit[:big_o_status]
+ end
+
+ def test_divergent_recursive_candidate_cycle_widens_to_unknown
+ method = lambda do |id, name, target, line|
+ {
+ id: id, name: name, line: line, span: [line, 0, line + 2, 1],
+ parameters: ["items"], effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ receiver: "self", message: target, line: line + 1,
+ span: [line + 1, 2, line + 1, 12],
+ candidate_target_ids: [target], candidate_reason: "closed_candidate_set",
+ consumer_closed_candidate_set: true
+ }],
+ complexity_facts: [{
+ "line" => line, "parameters" => ["items"], "collection_parameters" => ["items"],
+ "iterations" => [], "allocations" => [], "size_domains" => [],
+ "recursion" => { "calls" => 0 }, "call_contexts" => [{
+ "line" => line + 1, "span" => [line + 1, 2, line + 1, 12],
+ "message" => target, "execution_multiplicity" => "O(N)", "power" => 1
+ }]
+ }]
+ }
+ end
+ modules = [{
+ type: :class, name: "Cycle", file: "cycle.rs", states: Set.new, language: :rust,
+ methods: [
+ method.call("left", "left", "right", 1),
+ method.call("right", "right", "left", 5)
+ ]
+ }]
+
+ functions = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+
+ functions.each do |function|
+ quality = function.fetch(:quality_metrics)
+ refute quality[:big_o_complete]
+ assert_includes quality[:big_o_evidence_gaps], "unresolved_recursive_progress"
+ end
+ end
+
+ def test_worst_callable_rule
+ worst = ->(rows) { Espalier::SymbolicComplexity.worst_callable(rows) }
+ linear = Espalier::SymbolicComplexity.from_fact(
+ { "factors" => [{ "domain_id" => "param:y", "exponent" => 1 }] },
+ [{ "id" => "param:y", "name" => "y", "source_kind" => "parameter" }]
+ )
+
+ assert_nil worst.call([])
+ # One unknown callable poisons the site.
+ assert_equal({ expression: nil, constant: false },
+ worst.call([{ expression: linear, constant: false },
+ { expression: nil, constant: false }]))
+ # Otherwise the costliest known cost wins, and constants alone close it.
+ assert_equal linear, worst.call([{ expression: linear, constant: false },
+ { expression: nil, constant: true }]).fetch(:expression)
+ assert_equal({ expression: nil, constant: true },
+ worst.call([{ expression: nil, constant: true }]))
+ end
+
+ def test_complexity_overrides_lookup_semantics
+ assert_equal "O(N log N)",
+ Espalier::ComplexityOverrides.lookup(:go, "sort", "Sort")["time"]
+ # Unknown function / wrong language / missing owner all miss.
+ assert_nil Espalier::ComplexityOverrides.lookup(:go, "sort", "Frobnicate")
+ assert_nil Espalier::ComplexityOverrides.lookup(:rust, "sort", "Sort")
+ assert_nil Espalier::ComplexityOverrides.lookup(:go, nil, nil)
+ end
+
def test_big_o_joins_same_line_calls_by_exact_span
method = {
id: "caller", name: "run", line: 2,
@@ -1144,6 +1541,49 @@ def test_big_o_uses_exact_target_id_when_overloads_share_owner_and_name
assert caller[:quality_metrics][:big_o_complete]
end
+ def test_unresolved_duplicate_short_name_is_not_an_internal_summary_edge
+ constant_fact = {
+ "parameters" => [], "collection_parameters" => [], "iterations" => [],
+ "allocations" => [], "size_domains" => [], "recursion" => { "calls" => 0 },
+ "call_contexts" => []
+ }
+ modules = [{
+ type: :class, name: "Duplicate", file: "duplicate.rs", states: Set.new,
+ methods: [{
+ id: "work-a", name: "work", line: 1, span: [1, 0, 2, 1],
+ effects: { reads: Set.new, writes: Set.new }, delegations: [],
+ complexity_facts: [constant_fact.merge("line" => 1)]
+ }, {
+ id: "work-b", name: "work", line: 4, span: [4, 0, 6, 1],
+ effects: { reads: Set.new, writes: Set.new }, delegations: [],
+ complexity_facts: [constant_fact.merge(
+ "line" => 4,
+ "iterations" => [{ "line" => 5, "power" => 1, "execution_multiplicity" => "O(N)" }]
+ )]
+ }, {
+ id: "caller", name: "run", line: 8, span: [8, 0, 10, 1],
+ effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ receiver: "self", message: "work", line: 9, span: [9, 2, 9, 8]
+ }],
+ complexity_facts: [constant_fact.merge(
+ "line" => 8,
+ "call_contexts" => [{
+ "line" => 9, "span" => [9, 2, 9, 8], "message" => "work",
+ "execution_multiplicity" => "O(1)", "power" => 0,
+ "evidence_gap" => "ambiguous_project_call"
+ }]
+ )]
+ }]
+ }]
+
+ caller = Espalier::Aggregator.new.aggregate(modules).first[:functions]
+ .find { |function| function[:id] == "caller" }.fetch(:quality_metrics)
+
+ refute caller[:big_o_complete]
+ assert_includes caller[:big_o_evidence_gaps], "ambiguous_project_call"
+ end
+
def test_scip_complete_call_graph_rejects_false_overload_recursion
fact = {
"line" => 2, "parameters" => ["value"], "collection_parameters" => [],
@@ -1345,7 +1785,8 @@ def test_big_o_joins_compiler_implementation_candidates_as_a_modeled_upper_bound
effects: { reads: Set.new, writes: Set.new }, complexity_facts: [caller_fact],
delegations: [{
receiver: "worker", message: "work", line: 3, type: :always,
- candidate_target_ids: %w[fast slow], candidate_reason: "scip_implementation_set"
+ candidate_target_ids: %w[fast slow], candidate_reason: "scip_implementation_set",
+ consumer_closed_candidate_set: true
}]
}]
}, {
@@ -1369,4 +1810,89 @@ def test_big_o_joins_compiler_implementation_candidates_as_a_modeled_upper_bound
assert_includes caller[:quality_metrics][:big_o_assumptions].first, "implementation set is closed"
end
+ def test_big_o_does_not_certify_an_open_runtime_candidate_set
+ empty_fact = lambda do |line, contexts = []|
+ {
+ "line" => line, "parameters" => [], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "call_contexts" => contexts,
+ "size_domains" => [], "recursion" => { "calls" => 0 }
+ }
+ end
+ modules = [{
+ type: :class, name: "RuntimeObserved", file: "runtime.rb", states: Set.new,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 3],
+ parameters: [], effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ receiver: "worker", message: "work", line: 2,
+ candidate_target_ids: ["observed"], candidate_reason: "runtime_observed_candidate_set",
+ consumer_closed_candidate_set: false
+ }],
+ complexity_facts: [empty_fact.call(1, [{
+ "line" => 2, "message" => "work", "execution_multiplicity" => "O(1)",
+ "power" => 0, "evidence_gap" => "unknown_call_target"
+ }])]
+ }, {
+ id: "observed", name: "work", line: 5, span: [5, 0, 7, 3],
+ parameters: [], effects: { reads: Set.new, writes: Set.new },
+ delegations: [], complexity_facts: [empty_fact.call(5)]
+ }]
+ }]
+
+ caller = Espalier::Aggregator.new.aggregate(modules).first[:functions].find do |function|
+ function[:id] == "caller"
+ end
+
+ refute caller[:quality_metrics][:big_o_complete]
+ refute_includes Array(caller[:quality_metrics][:big_o_bound_qualities]), "upper_bound_closed_candidate_max"
+ end
+
+ def test_big_o_certifies_runtime_candidates_only_as_modeled_world
+ empty_fact = lambda do |line, contexts = []|
+ {
+ "line" => line, "parameters" => [], "collection_parameters" => [],
+ "iterations" => [], "allocations" => [], "call_contexts" => contexts,
+ "size_domains" => [], "recursion" => { "calls" => 0 }
+ }
+ end
+ closure_assumption =
+ "observed call targets exhaust the attested workload and runtime environment"
+ modules = [{
+ type: :class, name: "RuntimeObserved", file: "runtime.rb", states: Set.new,
+ methods: [{
+ id: "caller", name: "run", line: 1, span: [1, 0, 3, 3],
+ parameters: [], effects: { reads: Set.new, writes: Set.new },
+ delegations: [{
+ receiver: "worker", message: "work", line: 2,
+ candidate_target_ids: ["observed"],
+ candidate_reason: "runtime_modeled_observed_candidate_set",
+ consumer_closed_candidate_set: true,
+ complexity_bound_quality: "upper_bound_modeled_world",
+ complexity_assumptions: [closure_assumption]
+ }],
+ complexity_facts: [empty_fact.call(1, [{
+ "line" => 2, "message" => "work", "execution_multiplicity" => "O(1)",
+ "power" => 0, "evidence_gap" => "unknown_call_target"
+ }])]
+ }, {
+ id: "observed", name: "work", line: 5, span: [5, 0, 7, 3],
+ parameters: [], effects: { reads: Set.new, writes: Set.new },
+ delegations: [], complexity_facts: [empty_fact.call(5)]
+ }]
+ }]
+
+ caller = Espalier::Aggregator.new.aggregate(modules).first[:functions].find do |function|
+ function[:id] == "caller"
+ end
+
+ assert caller[:quality_metrics][:big_o_complete]
+ assert_includes caller[:quality_metrics][:big_o_bound_qualities],
+ "upper_bound_modeled_world"
+ assert_includes caller[:quality_metrics][:big_o_bound_qualities],
+ "upper_bound_closed_candidate_max"
+ assert_includes caller[:quality_metrics][:big_o_assumptions], closure_assumption
+ assert_equal :known_candidate_max,
+ Espalier::BigOProofMetrics.classify(caller[:quality_metrics])
+ end
+
end
diff --git a/gems/espalier/test/architecture_artifact_test.rb b/gems/espalier/test/architecture_artifact_test.rb
index 385b7fc6e..bd448715e 100644
--- a/gems/espalier/test/architecture_artifact_test.rb
+++ b/gems/espalier/test/architecture_artifact_test.rb
@@ -1,9 +1,46 @@
# frozen_string_literal: true
require "minitest/autorun"
+require "tmpdir"
require_relative "../lib/espalier"
class ArchitectureArtifactTest < Minitest::Test
+ def test_emits_import_edges_scanned_from_source
+ Dir.mktmpdir do |root|
+ File.write(File.join(root, "svc.go"), <<~GO)
+ package svc
+
+ import (
+ "fmt"
+ "os/exec"
+ )
+
+ import "strings"
+
+ func Run() { fmt.Println("x") }
+ GO
+ evidence = {
+ "root" => root,
+ "corpus" => { "complete" => true },
+ "owners" => [],
+ "methods" => [{ "id" => "fn:1", "owner" => "svc", "name" => "Run", "language" => "go",
+ "path" => File.join(root, "svc.go"), "line" => 10, "span" => [10, 0, 10, 30] }],
+ "fields" => [],
+ "facts" => { "calls" => [], "state_accesses" => [] }
+ }
+ artifact = Espalier::ArchitectureArtifact.build(evidence, root: root, commit: "abc")
+ imports = artifact["edges"].select { |edge| edge["kind"] == "imports" }
+ modules = imports.map { |edge| edge.dig("metadata", "module") }.sort
+ assert_equal ["fmt", "os/exec", "strings"], modules
+ # Each import edge carries the source line and targets a named module node.
+ fmt = imports.find { |edge| edge.dig("metadata", "module") == "fmt" }
+ assert_equal "svc.go", fmt.dig("spans", 0, "path")
+ assert_equal 4, fmt.dig("spans", 0, "start_line")
+ target = artifact["nodes"].find { |node| node["id"] == fmt["target"] }
+ assert_equal "fmt", target["name"]
+ end
+ end
+
def test_projects_first_class_state_edges_and_citations
evidence = {
"root" => "/repo",
@@ -66,4 +103,62 @@ def test_partial_confidence_owners_are_not_rendered_as_owner_nodes
assert_nil guessed_owner_fn["owner_id"],
"a function whose only owner is a partial-confidence guess must not link to a fabricated node"
end
+
+ def test_big_o_index_uses_the_known_component_when_the_bound_is_incomplete
+ manifest = [
+ {
+ file: "lib/x.rb",
+ functions: [
+ { name: "proven",
+ quality_metrics: { big_o: "O(1)", big_o_complete: true,
+ big_o_space: "O(1)", big_o_space_complete: true } },
+ { name: "looped",
+ quality_metrics: { big_o: "unknown", big_o_known_component: "O(N^2)",
+ big_o_complete: false } },
+ { name: "opaque",
+ quality_metrics: { big_o: "unknown", big_o_complete: false } }
+ ]
+ }
+ ]
+ index = Espalier::ArchitectureArtifact.big_o_index(manifest)
+
+ proven = index["lib/x.rb\u0000proven"]
+ assert_equal "O(1)", proven["big_o_time"]
+ assert_equal true, proven["time_complete"]
+
+ # A known-but-incomplete bound surfaces as a partial bound, not "unknown".
+ looped = index["lib/x.rb\u0000looped"]
+ assert_equal "O(N^2)", looped["big_o_time"]
+ assert_equal false, looped["time_complete"]
+
+ # No bound at all is not indexed.
+ assert_nil index["lib/x.rb\u0000opaque"]
+ end
+
+
+ def test_big_o_index_threads_status_and_provenance
+ manifest = [
+ {
+ file: "x.go",
+ functions: [
+ { name: "each",
+ quality_metrics: { big_o: "O(N * C)", big_o_complete: true,
+ big_o_status: :parametric } },
+ { name: "Sort",
+ quality_metrics: { big_o: "O(N log N)", big_o_complete: true,
+ big_o_status: :complete_override,
+ big_o_provenance: :manual_override } }
+ ]
+ }
+ ]
+ index = Espalier::ArchitectureArtifact.big_o_index(manifest)
+ each = index.values.find { |node| node["big_o_time"] == "O(N * C)" }
+ sort = index.values.find { |node| node["big_o_provenance"] }
+
+ assert_equal "parametric", each["big_o_status"]
+ assert_equal "complete_override", sort["big_o_status"]
+ assert_equal "manual_override", sort["big_o_provenance"]
+ refute each.key?("big_o_provenance")
+ end
+
end
diff --git a/gems/espalier/test/architecture_tools_test.rb b/gems/espalier/test/architecture_tools_test.rb
index ebe798a43..0bf532bbc 100644
--- a/gems/espalier/test/architecture_tools_test.rb
+++ b/gems/espalier/test/architecture_tools_test.rb
@@ -7,12 +7,12 @@
require "fileutils"
# End-to-end coverage for the architecture tools: cycle_report,
-# reach_through_report (espalier/tools) and change_coupling (lineage/tools),
-# including SARIF emission, changed-file scoping, and Lineage SARIF ingestion.
+# reach_through_report (espalier/tools) and change_coupling (gigasail/tools),
+# including SARIF emission, changed-file scoping, and Gigasail SARIF ingestion.
class ArchitectureToolsTest < Minitest::Test
TOOLS = File.expand_path("../tools", __dir__)
- LINEAGE_TOOLS = File.expand_path("../../lineage/tools", __dir__)
- LINEAGE_BIN = File.expand_path("../../lineage/target/release/lineage", __dir__)
+ GIGASAIL_TOOLS = File.expand_path("../../gigasail/tools", __dir__)
+ LINEAGE_BIN = File.expand_path("../../gigasail/target/release/giga", __dir__)
FACT_MINE_BIN = File.expand_path("../../fact-mine/target/release/fact-mine-rust", __dir__)
def setup
@@ -259,7 +259,7 @@ def test_change_coupling_reports_cross_module_pairs_and_scopes_to_changes
commit_all("solo change")
sarif = File.join(dir, "coupling.sarif")
- out = run_tool(File.join(LINEAGE_TOOLS, "change_coupling.rb"), dir, "5", "--sarif=#{sarif}")
+ out = run_tool(File.join(GIGASAIL_TOOLS, "change_coupling.rb"), dir, "5", "--sarif=#{sarif}")
# 6 explicit co-changes plus the creating commit.
assert_match(/s=7\s+c=1\.00\s+cross-module\s+core\/a\.rb <-> util\/b\.rb/, out)
@@ -270,7 +270,7 @@ def test_change_coupling_reports_cross_module_pairs_and_scopes_to_changes
# Changed-scope: a diff touching only the uncoupled file reports nothing.
out_scoped = run_tool(
- File.join(LINEAGE_TOOLS, "change_coupling.rb"), dir, "5", "--base=HEAD~1"
+ File.join(GIGASAIL_TOOLS, "change_coupling.rb"), dir, "5", "--base=HEAD~1"
)
assert_includes out_scoped, "(none)"
end
@@ -289,7 +289,7 @@ def test_change_coupling_reports_cross_module_pairs_and_scopes_to_changes
# so one unit's tracked rename still canonicalizes every other unit's
# stale path for the same file.
def test_change_coupling_db_mode_unifies_coupling_across_a_rename
- skip "lineage binary missing; build gems/lineage first" unless File.executable?(LINEAGE_BIN)
+ skip "lineage binary missing; build gems/gigasail first" unless File.executable?(LINEAGE_BIN)
with_repo do |dir|
FileUtils.mkdir_p(%w[core util])
@@ -317,8 +317,8 @@ def test_change_coupling_db_mode_unifies_coupling_across_a_rename
build_output, build_status = Open3.capture2(LINEAGE_BIN, "build", "--db", db, "--repo", dir)
assert build_status.success?, "lineage build failed: #{build_output}"
- out_default = run_tool(File.join(LINEAGE_TOOLS, "change_coupling.rb"), dir, "5")
- out_db = run_tool(File.join(LINEAGE_TOOLS, "change_coupling.rb"), dir, "5", "--db=#{db}")
+ out_default = run_tool(File.join(GIGASAIL_TOOLS, "change_coupling.rb"), dir, "5")
+ out_db = run_tool(File.join(GIGASAIL_TOOLS, "change_coupling.rb"), dir, "5", "--db=#{db}")
assert_match(/s=6\s+c=1\.00\s+cross-module\s+core\/a\.rb <-> util\/b\.rb/, out_default,
"default mode should only see the pre-rename co-changes under the old name")
@@ -328,7 +328,7 @@ def test_change_coupling_db_mode_unifies_coupling_across_a_rename
end
def test_sarif_outputs_ingest_into_lineage
- skip "lineage binary missing; build gems/lineage first" unless File.executable?(LINEAGE_BIN)
+ skip "lineage binary missing; build gems/gigasail first" unless File.executable?(LINEAGE_BIN)
with_repo do |dir|
FileUtils.mkdir_p(%w[lib/core lib/util])
diff --git a/gems/espalier/test/big_o_gap_impact_test.rb b/gems/espalier/test/big_o_gap_impact_test.rb
index 2db3b5d37..5f18d7895 100644
--- a/gems/espalier/test/big_o_gap_impact_test.rb
+++ b/gems/espalier/test/big_o_gap_impact_test.rb
@@ -30,6 +30,26 @@ def test_propagates_direct_root_to_all_incomplete_callers
assert_includes report[:roots].first[:categories], "normalized_cost_fact_missing"
end
+ def test_reports_project_candidate_summary_as_project_not_external
+ results = {
+ "root" => result(unknowns: ["callee"], gaps: [])
+ }
+ calls = [
+ {
+ source: "root",
+ target: nil,
+ semantic_symbol: "runtime project symbol",
+ external_symbol_scope: "project"
+ }
+ ]
+
+ report = Espalier::BigOGapImpact.analyze(results: results, calls: calls)
+ categories = report[:roots].first[:categories]
+
+ assert_includes categories, "project_candidate_summary_missing"
+ refute_includes categories, "external_symbol_cost_missing"
+ end
+
def test_reports_untraced_incomplete_function_without_inventing_a_root
report = Espalier::BigOGapImpact.analyze(
results: { "gap" => result },
diff --git a/gems/espalier/test/big_o_proof_metrics_test.rb b/gems/espalier/test/big_o_proof_metrics_test.rb
index 3dfbf4e88..65d3bf6a8 100644
--- a/gems/espalier/test/big_o_proof_metrics_test.rb
+++ b/gems/espalier/test/big_o_proof_metrics_test.rb
@@ -40,13 +40,15 @@ def test_incomplete_result_outranks_conditional_bound_expression
end
def test_complete_scc_bound_is_likely_while_progress_proof_remains_visible
- row = quality(
- qualities: ["upper_bound_acyclic_project_scc"],
- assumptions: ["finite acyclic input"]
- )
+ %w[upper_bound_acyclic_project_scc upper_bound_structural_descent].each do |proof_quality|
+ row = quality(
+ qualities: [proof_quality],
+ assumptions: ["finite acyclic input"]
+ )
- assert_equal :known_likely, Metrics.classify(row)
- assert_equal :recursive_progress, Metrics.bucket(row)
+ assert_equal :known_likely, Metrics.classify(row)
+ assert_equal :recursive_progress, Metrics.bucket(row)
+ end
end
def test_external_latency_scope_outranks_exact_target_in_underlying_bucket
@@ -70,6 +72,64 @@ def test_string_keyed_quality_is_supported
assert_equal :analyzer_result, Metrics.bucket(row)
end
+ def test_coverage_gate_uses_only_production_and_reports_proof_tiers
+ rows = [
+ row("go", quality, kind: "top"),
+ row("go", quality(qualities: ["upper_bound_parametric_callback_once"]), kind: "lambda"),
+ row("rust", quality(complete: false), kind: "instance"),
+ row("java", quality, role: "test", kind: "instance")
+ ]
+
+ report = Metrics.coverage_gate(
+ rows,
+ minimum_percent: 60,
+ call_coverage: {
+ "eligible_call_sites" => 10,
+ "exact_project_targets" => 4,
+ "modeled_without_project_target" => 5,
+ "semantically_accounted_call_sites" => 9,
+ "unresolved_call_sites" => 1
+ }
+ )
+
+ assert report[:passed]
+ assert_equal 3, report.dig(:coverage, :functions)
+ assert_equal 2, report.dig(:coverage, :mapped)
+ assert_equal 66.67, report.dig(:coverage, :mapped_percent)
+ assert_equal 1, report.dig(:coverage, :proof, :buckets, :parametric, :count)
+ assert_equal 2, report.dig(:by_language, "go", :functions)
+ refute report[:by_language].key?("java")
+ assert_equal 90.0, report.dig(:call_soundness, :semantically_accounted_percent)
+ end
+
+ def test_coverage_gate_fails_closed_for_raw_executable_calls
+ report = Metrics.coverage_gate(
+ [row("c", quality)],
+ call_coverage: { raw_calls_not_normalized_inside_function: 1 }
+ )
+
+ refute report[:passed]
+ assert_includes report[:failures], "1 executable raw calls were not normalized"
+ end
+
+ def test_coverage_gate_can_exclude_lambdas_by_explicit_policy
+ rows = [
+ row("rust", quality, kind: "top"),
+ row("rust", quality(complete: false), kind: "lambda")
+ ]
+
+ report = Metrics.coverage_gate(rows, include_lambdas: false)
+
+ assert report[:passed]
+ assert_equal 1, report.dig(:coverage, :functions)
+ refute report.dig(:policy, :include_lambdas)
+ end
+
+ def test_coverage_gate_rejects_invalid_minimum
+ error = assert_raises(ArgumentError) { Metrics.coverage_gate([], minimum_percent: 101) }
+ assert_match(/between 0 and 100/, error.message)
+ end
+
private
def quality(complete: true, qualities: [], assumptions: [], proof_status: nil)
@@ -80,4 +140,8 @@ def quality(complete: true, qualities: [], assumptions: [], proof_status: nil)
big_o_proof_status: proof_status
}
end
+
+ def row(language, quality, role: "production", kind: "top")
+ { language: language, source_role: role, kind: kind, quality: quality }
+ end
end
diff --git a/gems/espalier/test/big_o_test.rb b/gems/espalier/test/big_o_test.rb
index fbe85f77a..8ce1ea743 100644
--- a/gems/espalier/test/big_o_test.rb
+++ b/gems/espalier/test/big_o_test.rb
@@ -317,6 +317,19 @@ def test_structural_big_o_only_consumes_normalized_facts
consumer.send(:recursion_complexity, {
"calls" => 2, "visited_guarded_calls" => 2, "unknown_progress_calls" => 0
}, 2)
+ assert_equal ["O(N)", "O(N)", "recursive descent into a projection of the input"],
+ consumer.send(:recursion_complexity, {
+ "calls" => 2, "structural_calls" => 2, "unknown_progress_calls" => 0
+ }, 2)
+ structural = consumer.send(:summary_hint, {
+ "line" => 8, "parameters" => ["node"], "iterations" => [], "allocations" => [],
+ "size_domains" => [], "recursion" => {
+ "calls" => 2, "structural_calls" => 2, "unknown_progress_calls" => 0
+ }
+ }, { line: 8, name: "walk" })
+ assert_equal "O(N)", structural[:complexity]
+ assert_equal "upper_bound_structural_descent", structural[:complexity_bound_quality]
+ assert_includes structural[:complexity_assumptions].first, "finite and acyclic"
unknown = consumer.send(:summary_hint, {
"line" => 9, "parameters" => ["items"], "recursion" => { "calls" => 0 },
"iterations" => [{ "power" => 1, "cardinality_relation" => "unknown", "execution_multiplicity" => "unknown" }]
@@ -474,12 +487,162 @@ def test_structural_big_o_propagates_unique_cross_owner_targets
resolved_recursive_edges: { ["Caller", "run", "Target", "work"] => true }
).hints_for(nil, { name: "run", line: 2 }, "Caller")
.find { |hint| hint[:operation] == "work" }
- assert_equal "O(2^N)", recursive[:complexity]
- assert_equal "O(N)", recursive[:space]
- assert recursive[:time_complete]
- assert recursive[:space_complete]
- assert_equal "upper_bound_acyclic_project_scc", recursive[:complexity_bound_quality]
- assert recursive[:complexity_assumptions].first.include?("finite and acyclic")
+ assert_equal "unknown", recursive[:complexity]
+ assert_equal "unknown", recursive[:space]
+ refute recursive[:time_complete]
+ refute recursive[:space_complete]
+ assert_includes recursive[:evidence_gaps], "unresolved_recursive_progress"
+ end
+
+ def test_structural_big_o_discharge_resolved_constant_project_calls
+ facts = {
+ "caller-id" => [{
+ "line" => 2, "parameters" => [], "iterations" => [],
+ "recursion" => { "calls" => 0 },
+ "call_contexts" => [{
+ "line" => 3, "span" => [3, 4, 3, 12], "message" => "build",
+ "execution_multiplicity" => "O(1)",
+ "argument_cardinality_relation" => "same",
+ "evidence_gap" => "unresolved_call_target"
+ }]
+ }]
+ }
+ consumer = Espalier::StructuralBigO.new(
+ facts_by_method: facts,
+ method_complexities: { "constructor-id" => "O(1)" },
+ method_spaces: { "constructor-id" => "O(1)" },
+ method_time_complete: { "constructor-id" => true },
+ method_space_complete: { "constructor-id" => true },
+ resolved_calls: {
+ ["caller-id", "build", [3, 4, 3, 12]] =>
+ ["Target", "Target", "constructor-id"]
+ }
+ )
+
+ hint = consumer
+ .hints_for(nil, { id: "caller-id", name: "run", line: 2 }, "Caller")
+ .find { |row| row[:operation] == "build" }
+ refute_nil hint
+ assert_equal "O(1)", hint[:complexity]
+ assert_equal "O(1)", hint[:space]
+ assert hint[:time_complete]
+ assert hint[:space_complete]
+ refute hint[:is_dynamic]
+ end
+
+ # An exact recursive edge whose progress cannot be proven is not a proven
+ # exponential: it is an unproven bound. Reporting O(2^N) as complete both
+ # asserts a bound nothing established and hides the missing proof from the
+ # gap diagnostics, which key on evidence gaps.
+ def test_resolved_recursive_edge_without_progress_proof_is_unknown_not_exponential
+ facts = {
+ ["Walker", "visit"] => [{
+ "line" => 1, "parameters" => ["node"], "iterations" => [],
+ "recursion" => { "calls" => 0 },
+ "call_contexts" => [{
+ "line" => 2, "message" => "visit", "execution_multiplicity" => "O(1)",
+ "argument_progress" => "unknown", "argument_cardinality_relation" => "same"
+ }]
+ }]
+ }
+ hint = Espalier::StructuralBigO.new(
+ facts_by_method: facts,
+ method_complexities: { "Walker" => { "visit" => "O(N)" } },
+ resolved_calls: { ["Walker", "visit", "visit", 2] => ["Walker", "visit"] },
+ resolved_recursive_edges: { ["Walker", "visit", "Walker", "visit"] => true }
+ ).hints_for(nil, { name: "visit", line: 1 }, "Walker")
+ .find { |row| row[:operation] == "visit" }
+
+ assert_equal "unknown", hint[:complexity]
+ assert_equal "unknown", hint[:space]
+ refute hint[:time_complete]
+ refute hint[:space_complete]
+ assert_includes hint[:evidence_gaps], "unresolved_recursive_progress"
+ assert_nil hint[:complexity_bound_quality]
+ end
+
+ # A recursive call over a loop's own partition binding reaches each element
+ # once. The shrinking-token heuristic must not outrank that proof: ordering
+ # the loop-contained branch first reports O(N!) for a plain traversal.
+ def test_loop_contained_recursion_over_a_partition_is_linear_not_factorial
+ facts = {
+ ["Tree", "walk"] => [{
+ "line" => 1, "parameters" => ["node"], "iterations" => [],
+ "recursion" => { "calls" => 0 },
+ "call_contexts" => [{
+ "line" => 2, "message" => "walk", "execution_multiplicity" => "O(N)",
+ "argument_progress" => "shrinking",
+ "argument_cardinality_relation" => "partition_of"
+ }]
+ }]
+ }
+ hint = Espalier::StructuralBigO.new(
+ facts_by_method: facts,
+ method_complexities: { "Tree" => { "walk" => "O(N)" } },
+ resolved_calls: { ["Tree", "walk", "walk", 2] => ["Tree", "walk"] },
+ resolved_recursive_edges: { ["Tree", "walk", "Tree", "walk"] => true }
+ ).hints_for(nil, { name: "walk", line: 1 }, "Tree")
+ .find { |row| row[:operation] == "walk" }
+
+ assert_equal "O(N)", hint[:complexity]
+ assert hint[:time_complete]
+ assert hint[:space_complete]
+ end
+
+ # Structural descent is the shape most recursive visitors have: no operand
+ # shrinks arithmetically, so it used to have no bound at all. Descending one
+ # level into a finite structure reaches each node once.
+ def test_structural_descent_recursion_is_linear_in_the_structure
+ facts = {
+ ["Walker", "visit"] => [{
+ "line" => 1, "parameters" => ["node"], "iterations" => [],
+ "recursion" => { "calls" => 0 },
+ "call_contexts" => [{
+ "line" => 2, "message" => "visit", "execution_multiplicity" => "O(1)",
+ "argument_progress" => "structural",
+ "argument_cardinality_relation" => "same"
+ }]
+ }]
+ }
+ hint = Espalier::StructuralBigO.new(
+ facts_by_method: facts,
+ method_complexities: { "Walker" => { "visit" => "O(N)" } },
+ resolved_calls: { ["Walker", "visit", "visit", 2] => ["Walker", "visit"] },
+ resolved_recursive_edges: { ["Walker", "visit", "Walker", "visit"] => true }
+ ).hints_for(nil, { name: "visit", line: 1 }, "Walker")
+ .find { |row| row[:operation] == "visit" }
+
+ assert_equal "O(N)", hint[:complexity]
+ assert hint[:time_complete]
+ assert_equal "upper_bound_structural_descent", hint[:complexity_bound_quality]
+ assert_includes hint[:complexity_assumptions].first, "acyclic"
+ # The conditional bound must not masquerade as an unconditional proof.
+ assert_equal "partial", hint[:confidence]
+ end
+
+ # Unresolved mutual recursion already degrades to unknown, but published no
+ # evidence gap, so the diagnostics could not attribute the incompleteness.
+ def test_unproven_mutual_recursion_publishes_a_recursive_progress_gap
+ facts = {
+ ["Pair", "ping"] => [{
+ "line" => 1, "parameters" => [], "iterations" => [],
+ "recursion" => { "calls" => 0 },
+ "call_contexts" => [{
+ "line" => 2, "message" => "pong", "execution_multiplicity" => "O(1)",
+ "argument_cardinality_relation" => "same"
+ }]
+ }]
+ }
+ hint = Espalier::StructuralBigO.new(
+ facts_by_method: facts,
+ internal_calls: { "Pair" => { "ping" => ["pong"] } },
+ recursive_edges: { ["Pair", "ping", "pong"] => true }
+ ).hints_for(nil, { name: "ping", line: 1 }, "Pair")
+ .find { |row| row[:operation] == "pong" }
+
+ assert_equal "unknown", hint[:complexity]
+ refute hint[:time_complete]
+ assert_includes hint[:evidence_gaps], "unresolved_recursive_progress"
end
def test_remaining_complexity_lattice_and_type_resolution_paths
diff --git a/gems/espalier/test/c_stdlib_map_test.rb b/gems/espalier/test/c_stdlib_map_test.rb
new file mode 100644
index 000000000..dbfe24f8b
--- /dev/null
+++ b/gems/espalier/test/c_stdlib_map_test.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "json"
+require "minitest/autorun"
+require "open3"
+require "tmpdir"
+
+class CStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ PROBE = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "c",
+ "semantic_environment.rb"
+ )
+
+ def test_environment_probe_attests_unversioned_c_symbols
+ Dir.mktmpdir do |directory|
+ compiler = fake_tool(directory, "clang", <<~SH)
+ case "$1" in
+ --version) echo "test clang 20" ;;
+ -dumpmachine) echo "x86_64-test-linux-gnu" ;;
+ *) echo "#define TEST_ABI 1" ;;
+ esac
+ SH
+ indexer = fake_tool(directory, "scip-clang", 'echo "scip-clang 0.4.0"')
+ libc = fake_tool(directory, "libc.so.6", 'echo "test glibc 2.39"')
+ header = File.join(directory, "string.h")
+ File.write(header, "void *memcpy(void *, const void *, unsigned long);\n")
+ File.write(
+ File.join(directory, "compile_commands.json"),
+ JSON.generate([{
+ "arguments" => [compiler, "-std=c17", "-DTEST=1", "-c", "probe.c"]
+ }])
+ )
+ output = File.join(directory, "environment.json")
+ stdout, stderr, status = Open3.capture3(
+ {
+ "SCIP_CLANG" => indexer,
+ "C_LIBC_BINARY" => libc,
+ "C_LIBC_HEADERS" => header
+ },
+ "ruby",
+ PROBE,
+ directory,
+ output
+ )
+ assert status.success?, "#{stdout}\n#{stderr}"
+
+ environment = JSON.parse(File.read(output))
+ assert_equal "fact-mine.semantic-environment.v1", environment.fetch("schema")
+ claims = environment.fetch("claims")
+ assert_equal "test glibc 2.39", claims.fetch("c.libc.release")
+ assert_equal "x86_64-test-linux-gnu", claims.fetch("c.compiler.target")
+ assert_equal "scip-clang 0.4.0", claims.fetch("c.scip_clang.version")
+ assert claims.fetch("c.libc.binary.sha256").start_with?("sha256:")
+ assert claims.fetch("c.preprocessor_macros.sha256").start_with?("sha256:")
+ end
+ end
+
+ private
+
+ def fake_tool(directory, name, body)
+ path = File.join(directory, name)
+ File.write(path, "#!/bin/sh\n#{body}\n")
+ File.chmod(0o755, path)
+ path
+ end
+end
diff --git a/gems/espalier/test/check_big_o_coverage_test.rb b/gems/espalier/test/check_big_o_coverage_test.rb
new file mode 100644
index 000000000..9765ef2f0
--- /dev/null
+++ b/gems/espalier/test/check_big_o_coverage_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "json"
+require "fileutils"
+require "minitest/autorun"
+require "open3"
+require_relative "../lib/espalier/static_evidence"
+require "rbconfig"
+require "tmpdir"
+
+class CheckBigOCoverageTest < Minitest::Test
+ ROOT = File.expand_path("../../..", __dir__)
+ # Whichever profile is built, the way every other caller resolves it. CI
+ # builds --release, so naming the debug path made these tests error rather
+ # than run.
+ FACT_MINE = Espalier::StaticEvidence::FACT_MINE_RUST_BINARY
+ SCRIPT = File.join(ROOT, "gems/espalier/script/check_big_o_coverage.rb")
+
+ def test_accepts_profile_paths_relative_to_source_root
+ Dir.mktmpdir("espalier-big-o-coverage", ROOT) do |root|
+ source = File.join(root, "repository", "lib", "worker.rb")
+ FileUtils.mkdir_p(File.dirname(source))
+ File.write(source, <<~RUBY)
+ class Worker
+ def run
+ 1
+ end
+ end
+ RUBY
+
+ profile, error, status = Open3.capture3(
+ FACT_MINE, "profile", "espalier", "repository/lib/worker.rb", chdir: root
+ )
+ assert status.success?, error
+ assert_equal "repository/lib/worker.rb", JSON.parse(profile).fetch("methods").first.fetch("path")
+
+ profile_path = File.join(root, "profile.json")
+ File.write(profile_path, profile)
+ output, coverage_error, coverage_status = Open3.capture3(
+ RbConfig.ruby, SCRIPT, "--source-root", root, "--repository", "repository", "--minimum", "0", profile_path
+ )
+
+ assert coverage_status.success?, coverage_error
+ assert_equal 1, JSON.parse(output).dig("coverage", "functions")
+ end
+ end
+end
diff --git a/gems/espalier/test/compare_scip_big_o_test.rb b/gems/espalier/test/compare_scip_big_o_test.rb
new file mode 100644
index 000000000..ddbf754f4
--- /dev/null
+++ b/gems/espalier/test/compare_scip_big_o_test.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "json"
+require "fileutils"
+require "minitest/autorun"
+require "open3"
+require_relative "../lib/espalier/static_evidence"
+require "rbconfig"
+require "tmpdir"
+
+class CompareScipBigOTest < Minitest::Test
+ ROOT = File.expand_path("../../..", __dir__)
+ # Whichever profile is built, the way every other caller resolves it. CI
+ # builds --release, so naming the debug path made these tests error rather
+ # than run.
+ FACT_MINE = Espalier::StaticEvidence::FACT_MINE_RUST_BINARY
+ SCRIPT = File.join(ROOT, "gems/espalier/script/compare_scip_big_o.rb")
+
+ def test_accepts_profiles_with_paths_relative_to_the_declared_source_root
+ Dir.mktmpdir("espalier-scip-comparison", ROOT) do |root|
+ source = File.join(root, "repository", "lib", "worker.rb")
+ FileUtils.mkdir_p(File.dirname(source))
+ File.write(source, <<~RUBY)
+ class Worker
+ def run
+ 1
+ end
+ end
+ RUBY
+
+ profile, error, status = Open3.capture3(
+ FACT_MINE, "profile", "espalier", "repository/lib/worker.rb", chdir: root
+ )
+ assert status.success?, error
+ assert_equal "repository/lib/worker.rb", JSON.parse(profile).fetch("methods").first.fetch("path")
+
+ profile_path = File.join(root, "profile.json")
+ File.write(profile_path, profile)
+ output, comparison_error, comparison_status = Open3.capture3(
+ RbConfig.ruby, SCRIPT, "--source-root", root, "--repository", "repository", profile_path, profile_path
+ )
+
+ assert comparison_status.success?, comparison_error
+ comparison = JSON.parse(output)
+ assert_equal 1, comparison.dig("baseline", "functions")
+ assert_equal 1, comparison.dig("scip", "functions")
+ end
+ end
+end
diff --git a/gems/espalier/test/complexity_summary_test.rb b/gems/espalier/test/complexity_summary_test.rb
new file mode 100644
index 000000000..b8e62e87f
--- /dev/null
+++ b/gems/espalier/test/complexity_summary_test.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+require_relative "../lib/espalier"
+
+class ComplexitySummaryTest < Minitest::Test
+ def quality(bound_qualities: [], complete: true, space_complete: true)
+ {
+ big_o_complete: complete,
+ big_o_space_complete: space_complete,
+ big_o_bound_qualities: bound_qualities
+ }
+ end
+
+ def test_accepts_analyzed_structure_and_exact_analyzed_targets
+ assert Espalier::ComplexitySummary.source_proven?(quality, [])
+ assert Espalier::ComplexitySummary.source_proven?(
+ quality(bound_qualities: ["upper_bound_exact_target"]),
+ []
+ )
+ end
+
+ def test_source_method_requires_an_executable_normalization_complete_body
+ assert Espalier::ComplexitySummary.source_method_proven?(
+ {"source_export_eligible" => true},
+ quality,
+ []
+ )
+ refute Espalier::ComplexitySummary.source_method_proven?({}, quality, [])
+ refute Espalier::ComplexitySummary.source_method_proven?(
+ {"source_export_eligible" => false},
+ quality,
+ []
+ )
+ refute Espalier::ComplexitySummary.source_method_proven?(
+ {"source_export_eligible" => true, "callback_params" => []},
+ quality(bound_qualities: ["upper_bound_parametric_callback_once"]),
+ []
+ )
+ assert Espalier::ComplexitySummary.source_method_proven?(
+ {"source_export_eligible" => true, "callback_params" => ["callback"]},
+ quality(bound_qualities: ["upper_bound_parametric_callback_once"]),
+ []
+ )
+ end
+
+ def test_symbol_relocation_is_explicit_and_fail_closed
+ assert_equal(
+ "semanticdb maven jdk 21 java/lang/String#length().",
+ Espalier::ComplexitySummary.relocate_symbol(
+ "semanticdb maven temporary/java.base 21 java/lang/String#length().",
+ from: "semanticdb maven temporary/java.base 21 ",
+ to: "semanticdb maven jdk 21 "
+ )
+ )
+ assert_raises(ArgumentError) do
+ Espalier::ComplexitySummary.relocate_symbol(
+ "semanticdb maven other 21 java/lang/String#length().",
+ from: "semanticdb maven temporary/java.base 21 ",
+ to: "semanticdb maven jdk 21 "
+ )
+ end
+ assert_raises(ArgumentError) do
+ Espalier::ComplexitySummary.relocate_symbol("symbol", from: "symbol")
+ end
+ end
+
+ def test_exact_symbol_bridge_preserves_one_to_many_declaration_identities
+ row = {"time" => "O(1)", "space" => "O(1)"}
+ bridged = Espalier::ComplexitySummary.bridge_symbol_rows(
+ [["implementation Math.exp", row], ["implementation absent", row]],
+ symbol_map: {
+ "implementation Math.exp" => ["runtime Math#exp", "runtime Math.exp"]
+ }
+ )
+
+ assert_equal(
+ [["runtime Math#exp", row], ["runtime Math.exp", row]],
+ bridged
+ )
+ end
+
+ def test_symbol_bridge_and_prefix_relocation_share_one_generic_transform
+ row = {"time" => "O(N)", "space" => "O(1)"}
+ assert_equal(
+ [["consumer pkg fn", row]],
+ Espalier::ComplexitySummary.bridge_symbol_rows(
+ [["producer pkg fn", row]],
+ prefix_from: "producer ",
+ prefix_to: "consumer "
+ )
+ )
+ end
+
+ def test_rejects_incomplete_or_manual_model_derived_bounds
+ refute Espalier::ComplexitySummary.source_proven?(quality(complete: false), [])
+ refute Espalier::ComplexitySummary.source_proven?(quality(space_complete: false), [])
+
+ %w[
+ upper_bound_declared_receiver
+ upper_bound_compiler_declared_receiver
+ upper_bound_external_latency_excluded
+ upper_bound_modeled_world
+ upper_bound_unknown_cardinality_relation
+ ].each do |bound_quality|
+ refute Espalier::ComplexitySummary.source_proven?(
+ quality(bound_qualities: [bound_quality]),
+ []
+ )
+ end
+ end
+
+ def test_uses_post_scip_completeness_instead_of_stale_adapter_call_gaps
+ facts = [
+ {
+ "call_contexts" => [
+ {"message" => "PrintDefaults", "evidence_gap" => "unresolved_receiver_type"}
+ ]
+ }
+ ]
+
+ assert Espalier::ComplexitySummary.source_proven?(quality, facts)
+ refute Espalier::ComplexitySummary.source_method_proven?(
+ {"source_export_eligible" => false},
+ quality,
+ facts
+ )
+ end
+
+ def test_candidate_export_requires_explicit_downstream_closure
+ refute Espalier::ComplexitySummary.consumer_closed_candidate_set?({
+ "candidate_targets" => ["implementation"]
+ })
+ assert Espalier::ComplexitySummary.consumer_closed_candidate_set?({
+ "candidate_targets" => ["implementation"],
+ "consumer_closed_candidate_set" => true
+ })
+ end
+end
diff --git a/gems/espalier/test/cpp_stdlib_map_test.rb b/gems/espalier/test/cpp_stdlib_map_test.rb
new file mode 100644
index 000000000..0e1ea1874
--- /dev/null
+++ b/gems/espalier/test/cpp_stdlib_map_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "json"
+require "digest"
+require "fileutils"
+require "minitest/autorun"
+require "open3"
+require "tmpdir"
+require "zlib"
+
+class CppStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ CPP = File.join(ROOT, "fact-mine", "config", "stdlib_maps", "cpp")
+
+ def test_revision_digest_is_path_stable
+ Dir.mktmpdir do |directory|
+ first = File.join(directory, "first", "include")
+ second = File.join(directory, "second", "include")
+ [first, second].each do |root|
+ FileUtils.mkdir_p(File.join(root, "bits"))
+ File.write(File.join(root, "vector"), "vector body")
+ File.write(File.join(root, "bits", "config.h"), "config")
+ end
+
+ compute = lambda do |root|
+ digest = Digest::SHA256.new
+ Dir.glob(File.join(root, "**/*"), File::FNM_DOTMATCH)
+ .select { |path| File.file?(path) }
+ .sort
+ .each do |path|
+ relative = path.delete_prefix("#{root}#{File::SEPARATOR}")
+ digest << relative << "\0" << Digest::SHA256.file(path).hexdigest << "\n"
+ end
+ digest.hexdigest
+ end
+ assert_equal compute.call(first), compute.call(second)
+ end
+ end
+
+ def test_environment_is_entirely_language_owned_and_exact
+ source = File.read(File.join(CPP, "semantic_environment.rb"))
+ assert_includes source, "cpp.stdlib.effective_headers.sha256"
+ assert_includes source, "cpp.preprocessor_macros.sha256"
+ assert_includes source, "cpp.compiler.target"
+ assert_includes source, "cpp.scip_clang.sha256"
+ end
+
+ def test_symbol_bridge_only_admits_exact_std_rows
+ Dir.mktmpdir do |directory|
+ summary = File.join(directory, "summary.json.gz")
+ output = File.join(directory, "bridge.json")
+ payload = {
+ "symbols" => {
+ "cxx . . $ std/vector#size()." => {
+ "bound_quality" => "upper_bound_exact_symbol"
+ },
+ "cxx . . $ std/get()." => {
+ "bound_quality" => "upper_bound_parametric_reflective_once"
+ },
+ "cxx . . $ __gnu_cxx/helper()." => {
+ "bound_quality" => "upper_bound_exact_symbol"
+ }
+ }
+ }
+ Zlib::GzipWriter.open(summary) { |gzip| gzip.write(JSON.generate(payload)) }
+
+ stdout, stderr, status = Open3.capture3(
+ "ruby",
+ File.join(CPP, "build_symbol_bridge.rb"),
+ summary,
+ output
+ )
+ assert status.success?, "#{stdout}\n#{stderr}"
+ assert_equal(
+ {
+ "cxx . . $ std/vector#size()." =>
+ "cxx . . $ std/vector#size()."
+ },
+ JSON.parse(File.read(output)).fetch("symbols")
+ )
+ end
+ end
+end
diff --git a/gems/espalier/test/csharp_stdlib_map_test.rb b/gems/espalier/test/csharp_stdlib_map_test.rb
new file mode 100644
index 000000000..e47d7a81e
--- /dev/null
+++ b/gems/espalier/test/csharp_stdlib_map_test.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "json"
+require "minitest/autorun"
+require "tmpdir"
+require "zlib"
+
+class CsharpStdlibMapTest < Minitest::Test
+ SCRIPT = File.expand_path(
+ "../../fact-mine/config/stdlib_maps/csharp/build_symbol_bridge.rb",
+ __dir__
+ )
+
+ def test_bridge_maps_only_runtime_backed_corelib_owners
+ Dir.mktmpdir do |directory|
+ summary = File.join(directory, "summary.json.gz")
+ output = File.join(directory, "bridge.json")
+ Zlib::GzipWriter.open(summary) do |gzip|
+ gzip.write(JSON.generate({
+ "symbols" => {
+ "scip-dotnet nuget . . System/String#Trim()." => {},
+ "scip-dotnet nuget . . Generic/List#Add()." => {},
+ "scip-dotnet nuget . . Collections/ArrayList#Count." => {},
+ "scip-dotnet nuget . . Collections/ListDictionaryInternal#Add()." => {}
+ }
+ }))
+ end
+
+ assert system(RbConfig.ruby, SCRIPT, summary, output, "10.0.0.0")
+ bridge = JSON.parse(File.read(output))
+ assert_equal "fact-mine.symbol-bridge.v1", bridge.fetch("schema")
+ assert_equal(
+ "scip-dotnet nuget System.Runtime 10.0.0.0 System/String#Trim().",
+ bridge.dig("symbols", "scip-dotnet nuget . . System/String#Trim().")
+ )
+ assert_equal(
+ "scip-dotnet nuget System.Collections 10.0.0.0 Generic/List#Add().",
+ bridge.dig("symbols", "scip-dotnet nuget . . Generic/List#Add().")
+ )
+ assert_equal(
+ "scip-dotnet nuget System.Collections.NonGeneric 10.0.0.0 Collections/ArrayList#Count.",
+ bridge.dig("symbols", "scip-dotnet nuget . . Collections/ArrayList#Count.")
+ )
+ refute bridge.fetch("symbols").key?(
+ "scip-dotnet nuget . . Collections/ListDictionaryInternal#Add()."
+ )
+ end
+ end
+end
diff --git a/gems/espalier/test/dependency_graph_test.rb b/gems/espalier/test/dependency_graph_test.rb
index 0c81e5ec9..374ca3a43 100644
--- a/gems/espalier/test/dependency_graph_test.rb
+++ b/gems/espalier/test/dependency_graph_test.rb
@@ -4,6 +4,16 @@
require_relative "../lib/espalier"
class DependencyGraphTest < Minitest::Test
+ def test_markdown_output_renders_nested_manifest_records
+ markdown = Espalier::Formatter.to_markdown(service_manifest)
+
+ assert_includes markdown, "## Class: Service"
+ assert_includes markdown, "### State:"
+ assert_includes markdown, "#### - `run`"
+ assert_includes markdown, "always_calls: [`prepare`, `@repo.fetch`, `String.upcase`]"
+ assert_includes markdown, "internal_callers: [`run`]"
+ end
+
def test_dot_output_renders_owner_function_and_dependency_edges
dot = Espalier::Formatter.to_dot(service_manifest)
diff --git a/gems/espalier/test/diagnose_big_o_gaps_test.rb b/gems/espalier/test/diagnose_big_o_gaps_test.rb
new file mode 100644
index 000000000..3c8546294
--- /dev/null
+++ b/gems/espalier/test/diagnose_big_o_gaps_test.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require "json"
+require "fileutils"
+require "minitest/autorun"
+require "open3"
+require_relative "../lib/espalier/static_evidence"
+require "tmpdir"
+
+class DiagnoseBigOGapsTest < Minitest::Test
+ ROOT = File.expand_path("../../..", __dir__)
+ # Whichever profile is built, the way every other caller resolves it. CI
+ # builds --release, so naming the debug path made these tests error rather
+ # than run.
+ FACT_MINE = Espalier::StaticEvidence::FACT_MINE_RUST_BINARY
+ SCRIPT = File.join(ROOT, "gems/espalier/script/diagnose_big_o_gaps.rb")
+
+ def test_accepts_fact_mine_profiles_with_paths_relative_to_source_root
+ Dir.mktmpdir("espalier-gap-diagnostics", ROOT) do |root|
+ source_root = File.join(root, "repository")
+ FileUtils.mkdir_p(File.join(source_root, "lib"))
+ File.write(File.join(source_root, "lib", "worker.rb"), <<~RUBY)
+ class Worker
+ def run
+ 1
+ end
+ end
+ RUBY
+
+ profile, profile_error, profile_status = Open3.capture3(
+ FACT_MINE, "profile", "espalier", "repository/lib/worker.rb", chdir: root
+ )
+ assert profile_status.success?, profile_error
+ assert_equal "repository/lib/worker.rb", JSON.parse(profile).fetch("methods").first.fetch("path")
+
+ Dir.chdir(root) do
+ profile_path = File.join(root, "profile.json")
+ File.write(profile_path, profile)
+ output, error, status = Open3.capture3(
+ RbConfig.ruby, SCRIPT, "--source-root", root, "--repository", "repository", profile_path
+ )
+
+ assert status.success?, error
+ report = JSON.parse(output)
+ assert_equal 1, report.dig("summary", "functions")
+ end
+ end
+ end
+
+ def test_distinguishes_unobserved_runtime_calls_from_failed_semantic_identity_joins
+ Dir.mktmpdir("espalier-gap-runtime-observation", ROOT) do |root|
+ source_root = File.join(root, "repository")
+ FileUtils.mkdir_p(File.join(source_root, "lib"))
+ File.write(File.join(source_root, "lib", "worker.rb"), <<~RUBY)
+ class Worker
+ def run(value)
+ value.unmodeled
+ end
+ end
+ RUBY
+
+ profile_json, profile_error, profile_status = Open3.capture3(
+ FACT_MINE, "profile", "espalier", "repository/lib/worker.rb", chdir: root
+ )
+ assert profile_status.success?, profile_error
+ profile = JSON.parse(profile_json)
+ assert_equal 1, profile.fetch("calls").length
+
+ profile_path = File.join(root, "profile.json")
+ File.write(profile_path, JSON.generate(profile))
+ unobserved_output, unobserved_error, unobserved_status = Open3.capture3(
+ RbConfig.ruby, SCRIPT, "--source-root", root, "--repository", "repository", profile_path
+ )
+ assert unobserved_status.success?, unobserved_error
+ unobserved = JSON.parse(unobserved_output)
+ assert_equal 1, unobserved.dig("call_resolution", "runtime_callsite_unobserved", "calls")
+ assert_nil unobserved.dig("call_resolution", "semantic_identity_missing")
+ assert_includes(
+ unobserved.dig("root_cause_categories", "runtime_callsite_unobserved", "call_examples"),
+ {
+ "path" => "repository/lib/worker.rb",
+ "line" => 3,
+ "receiver" => "value",
+ "message" => "unmodeled",
+ "semantic_symbol" => nil,
+ "unresolved_reason" => "receiver_requires_corpus_resolution"
+ }
+ )
+
+ profile.fetch("calls").first["runtime_evidence_observed"] = true
+ File.write(profile_path, JSON.generate(profile))
+ observed_output, observed_error, observed_status = Open3.capture3(
+ RbConfig.ruby, SCRIPT, "--source-root", root, "--repository", "repository", profile_path
+ )
+ assert observed_status.success?, observed_error
+ observed = JSON.parse(observed_output)
+ assert_equal 1, observed.dig("call_resolution", "semantic_identity_missing", "calls")
+ assert_nil observed.dig("call_resolution", "runtime_callsite_unobserved")
+ end
+ end
+end
diff --git a/gems/espalier/test/fixtures/big_o/oracle.json b/gems/espalier/test/fixtures/big_o/oracle.json
index 26267e9a7..f89b88f44 100644
--- a/gems/espalier/test/fixtures/big_o/oracle.json
+++ b/gems/espalier/test/fixtures/big_o/oracle.json
@@ -16,7 +16,7 @@
},
"recursive_suffix_rescan.rb": {
"walk": "O(N^2)",
- "scan_remaining": "O(N)"
+ "scan_remaining": "O(N + C + C2)"
},
"recursive_suffix_rescan.py": {
"walk": "O(N^2)",
diff --git a/gems/espalier/test/javascript_stdlib_map_test.rb b/gems/espalier/test/javascript_stdlib_map_test.rb
new file mode 100644
index 000000000..846db4538
--- /dev/null
+++ b/gems/espalier/test/javascript_stdlib_map_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "json"
+require "minitest/autorun"
+require "open3"
+require "tmpdir"
+
+class JavascriptStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ PROBE = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "javascript",
+ "semantic_environment.rb"
+ )
+
+ def test_environment_probe_attests_node_and_v8
+ Dir.mktmpdir do |directory|
+ node = fake_tool(
+ directory,
+ "node",
+ "echo '{\"node\":\"22.1.0\",\"v8\":\"12.4-test\",\"modules\":\"127\"}'"
+ )
+ indexer = fake_tool(directory, "scip-typescript", 'echo "scip-typescript 0.3.17"')
+ output = File.join(directory, "environment.json")
+ stdout, stderr, status = Open3.capture3(
+ {"NODE" => node, "SCIP_TYPESCRIPT" => indexer},
+ "ruby",
+ PROBE,
+ directory,
+ output
+ )
+ assert status.success?, "#{stdout}\n#{stderr}"
+
+ claims = JSON.parse(File.read(output)).fetch("claims")
+ assert_equal "node", claims.fetch("javascript.runtime")
+ assert_equal "22.1.0", claims.fetch("javascript.node.version")
+ assert_equal "12.4-test", claims.fetch("javascript.v8.version")
+ assert_equal "127", claims.fetch("javascript.node.modules_abi")
+ assert claims.fetch("javascript.node.sha256").start_with?("sha256:")
+ end
+ end
+
+ private
+
+ def fake_tool(directory, name, body)
+ path = File.join(directory, name)
+ File.write(path, "#!/bin/sh\n#{body}\n")
+ File.chmod(0o755, path)
+ path
+ end
+end
diff --git a/gems/espalier/test/kotlin_stdlib_map_test.rb b/gems/espalier/test/kotlin_stdlib_map_test.rb
new file mode 100644
index 000000000..bb898ad08
--- /dev/null
+++ b/gems/espalier/test/kotlin_stdlib_map_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "json"
+require "minitest/autorun"
+require "yaml"
+require "zlib"
+
+class KotlinStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ MAP_DIR = File.join(ROOT, "fact-mine", "config", "stdlib_maps")
+ SUMMARY = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "complexity_summaries",
+ "kotlin-stdlib.kotlin2.2.0.json.gz"
+ )
+
+ def test_manifest_pins_source_runtime_and_corrected_indexer
+ manifest = YAML.safe_load(
+ File.read(File.join(MAP_DIR, "kotlin-2.2.0.yml")),
+ permitted_classes: [],
+ aliases: false
+ )
+ assert_equal "kotlin", manifest.fetch("language")
+ assert_equal "0.12.3", manifest.dig("index", "expected", "version").to_s
+ assert_equal(
+ "semanticdb maven . . kotlin/",
+ manifest.dig("summary", "symbol_relocation", "from")
+ )
+ assert_equal(
+ "scip-java maven . . kotlin/",
+ manifest.dig("summary", "symbol_relocation", "to")
+ )
+
+ materializer = File.read(File.join(MAP_DIR, "kotlin", "materialize_source.rb"))
+ assert_includes materializer, "967ad9599254e3a60d96d6c789547cc35c22d770d9c8fb1e3f15fac3b4c3b65d"
+ assert_includes materializer, "65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca"
+
+ patch = File.read(
+ File.join(MAP_DIR, "kotlin", "scip-kotlin-top-level-symbols.patch")
+ )
+ assert_includes patch, "symbolProvider.getTopLevelCallableSymbols"
+ assert_includes patch, "- is FirFileSymbol -> containingSymbol.fir.declarations"
+ assert_includes patch, "+ is FirFileSymbol ->"
+ end
+
+ def test_bundle_is_fail_closed_on_exact_runtime_digest
+ summary = Zlib::GzipReader.open(SUMMARY) { |gzip| JSON.parse(gzip.read) }
+ assert_equal "fact-mine.external-complexity-summary.v3", summary.fetch("schema")
+ assert_operator summary.fetch("symbols").length, :>=, 80
+ assert summary.fetch("symbols").keys.all? do |symbol|
+ symbol.start_with?("scip-java maven . . kotlin/")
+ end
+ assert_equal(
+ "sha256:65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca",
+ summary.dig("compatibility", "claims", "kotlin.stdlib.binary.sha256")
+ )
+ end
+end
diff --git a/gems/espalier/test/php_stdlib_map_test.rb b/gems/espalier/test/php_stdlib_map_test.rb
new file mode 100644
index 000000000..c157e9c45
--- /dev/null
+++ b/gems/espalier/test/php_stdlib_map_test.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+
+class PhpStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ PHP = File.join(ROOT, "fact-mine", "config", "stdlib_maps", "php")
+
+ def test_consumer_indexer_patch_uses_commit_qualified_version
+ patch = File.read(File.join(PHP, "scip-php-exact-version.patch"))
+ assert_includes patch, "$version = '0.0.1+71a5b117';"
+ end
+end
diff --git a/gems/espalier/test/reporter_test.rb b/gems/espalier/test/reporter_test.rb
index 099ae17ea..c74b30286 100644
--- a/gems/espalier/test/reporter_test.rb
+++ b/gems/espalier/test/reporter_test.rb
@@ -6,6 +6,18 @@
require_relative "../lib/espalier"
class ReporterTest < Minitest::Test
+ def test_from_yaml_file_constructs_reporter_from_real_yaml
+ Dir.mktmpdir do |dir|
+ path = File.join(dir, "manifest.yml")
+ File.write(path, YAML.dump([{ module: "Example", functions: [] }]))
+
+ reporter = Espalier::Reporter.from_yaml_file(path, root: dir)
+
+ assert_instance_of Espalier::Reporter, reporter
+ assert_includes reporter.to_markdown, "Modules/classes indexed: 1"
+ end
+ end
+
def test_report_ranks_architecture_specific_findings
manifest = [
{
diff --git a/gems/espalier/test/ruby_stdlib_map_test.rb b/gems/espalier/test/ruby_stdlib_map_test.rb
new file mode 100644
index 000000000..e1055af90
--- /dev/null
+++ b/gems/espalier/test/ruby_stdlib_map_test.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+require "json"
+require "fileutils"
+require "open3"
+require "tmpdir"
+require "yaml"
+require "zlib"
+
+class RubyStdlibMapTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ SUPPORT = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "support.yml"
+ )
+ BRIDGE = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "ruby",
+ "build_symbol_bridge.rb"
+ )
+ ENVIRONMENT = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "ruby",
+ "semantic_environment.rb"
+ )
+ MANIFEST = File.join(ROOT, "fact-mine", "config", "stdlib_maps", "ruby-3.2.3.yml")
+
+ def test_cruby_core_a_profiles_the_indexed_regexp_and_struct_implementation_surfaces
+ manifest = YAML.safe_load(File.read(MANIFEST))
+ includes = manifest.fetch("source").fetch("include")
+
+ assert_includes includes, "{array,dir,enum,error,file,hash,io,math,numeric,re,string,struct}.c"
+ end
+
+ def test_cruby_registration_bridge_preserves_exact_aliases_and_rejects_unproven_bodies
+ Dir.mktmpdir do |directory|
+ File.write(File.join(directory, "math.c"), <<~C)
+ rb_define_module_function(rb_mMath, "exp", math_exp, 1);
+ rb_define_module_function(rb_mMath, "log", math_log, 1);
+ C
+ File.write(File.join(directory, "file.c"), <<~C)
+ define_filetest_function("executable?", rb_file_executable_p, 1);
+ rb_define_singleton_method(rb_cFile, "realpath", rb_file_s_realpath, -1);
+ C
+ File.write(File.join(directory, "struct.c"), <<~C)
+ rb_define_singleton_method(rb_cStruct, "new", rb_struct_s_def, -1);
+ C
+ producer = File.join(directory, "producer.json.gz")
+ profile = File.join(directory, "profile.json")
+ output = File.join(directory, "bridge.json")
+ exact = "cxx . . $ math_exp(1)."
+ executable = "cxx . . $ rb_file_executable_p(1)."
+ realpath = "cxx . . $ rb_file_s_realpath(1)."
+ struct_new = "cxx . . $ rb_struct_s_def(1)."
+ Zlib::GzipWriter.open(producer) do |gzip|
+ gzip.write(JSON.generate({
+ "symbols" => {
+ exact => { "bound_quality" => "upper_bound_exact_symbol" },
+ executable => { "bound_quality" => "upper_bound_exact_symbol" },
+ realpath => { "bound_quality" => "upper_bound_exact_symbol" },
+ struct_new => { "bound_quality" => "upper_bound_exact_symbol" },
+ "cxx . . $ math_log(1)." => { "bound_quality" => "upper_bound_modeled_world" }
+ }
+ }))
+ end
+ File.write(profile, JSON.generate({
+ "methods" => [
+ { "path" => File.join(directory, "math.c"), "name" => "math_exp", "semantic_symbol" => exact },
+ { "path" => File.join(directory, "math.c"), "name" => "math_log", "semantic_symbol" => "cxx . . $ math_log(1)." },
+ { "path" => File.join(directory, "file.c"), "name" => "rb_file_executable_p", "semantic_symbol" => executable },
+ { "path" => File.join(directory, "file.c"), "name" => "rb_file_s_realpath", "semantic_symbol" => realpath },
+ { "path" => File.join(directory, "struct.c"), "name" => "rb_struct_s_def", "semantic_symbol" => struct_new }
+ ]
+ }))
+
+ stdout, stderr, status = Open3.capture3(
+ "ruby", BRIDGE, producer, profile, directory, output, "3.2.3"
+ )
+ assert status.success?, "#{stdout}\n#{stderr}"
+ symbols = JSON.parse(File.read(output)).fetch("symbols")
+ assert_equal(
+ [
+ "nil-kill-runtime ruby ruby 3.2.3 Math#exp().",
+ "nil-kill-runtime ruby ruby 3.2.3 Math.exp()."
+ ],
+ symbols.fetch(exact)
+ )
+ assert_equal(
+ [
+ "nil-kill-runtime ruby ruby 3.2.3 File.executable?().",
+ "nil-kill-runtime ruby ruby 3.2.3 FileTest#executable?()."
+ ],
+ symbols.fetch(executable)
+ )
+ assert_equal ["nil-kill-runtime ruby ruby 3.2.3 File.realpath()."], symbols.fetch(realpath)
+ assert_equal ["nil-kill-runtime ruby ruby 3.2.3 Struct.new()."], symbols.fetch(struct_new)
+ refute symbols.key?("cxx . . $ math_log(1).")
+ end
+ end
+
+ def test_cruby_environment_is_pinned_to_the_runtime_trace_version
+ Dir.mktmpdir do |directory|
+ FileUtils.mkdir_p(File.join(directory, "include", "ruby"))
+ File.write(File.join(directory, "version.h"), "#define RUBY_VERSION_TEENY 3\n")
+ File.write(File.join(directory, "include", "ruby", "version.h"), <<~C)
+ #define RUBY_API_VERSION_MAJOR 3
+ #define RUBY_API_VERSION_MINOR 2
+ C
+ output = File.join(directory, "environment.json")
+ stdout, stderr, status = Open3.capture3("ruby", ENVIRONMENT, directory, output, "3.2.3")
+ assert status.success?, "#{stdout}\n#{stderr}"
+ assert_equal(
+ {
+ "runtime.language" => "ruby",
+ "runtime.engine" => "ruby",
+ "runtime.version" => "3.2.3",
+ "runtime.engine_version" => "3.2.3"
+ },
+ JSON.parse(File.read(output)).fetch("claims")
+ )
+
+ _stdout, _stderr, mismatch = Open3.capture3("ruby", ENVIRONMENT, directory, output, "3.2.4")
+ refute mismatch.success?
+ end
+ end
+end
diff --git a/gems/espalier/test/scip_language_inventory_test.rb b/gems/espalier/test/scip_language_inventory_test.rb
new file mode 100644
index 000000000..a35efc210
--- /dev/null
+++ b/gems/espalier/test/scip_language_inventory_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+require "yaml"
+
+class ScipLanguageInventoryTest < Minitest::Test
+ ROOT = File.expand_path("../..", __dir__)
+ SUPPORT = File.join(
+ ROOT,
+ "fact-mine",
+ "config",
+ "stdlib_maps",
+ "support.yml"
+ )
+
+ def test_all_static_scip_language_families_are_inventoried
+ languages = YAML.safe_load_file(SUPPORT).fetch("languages")
+ %w[
+ c
+ cpp
+ csharp
+ dart
+ go
+ java
+ kotlin
+ rust
+ scala
+ typescript
+ visual_basic
+ ].each do |language|
+ assert languages.key?(language), "missing SCIP support status for #{language}"
+ end
+ end
+
+ def test_unparsed_static_languages_fail_closed_before_stdlib_mapping
+ languages = YAML.safe_load_file(SUPPORT).fetch("languages")
+ %w[dart scala visual_basic].each do |language|
+ entry = languages.fetch(language)
+ assert_equal "blocked", entry.fetch("status")
+ assert_equal "fact_mine_language_adapter_missing", entry.fetch("blocker")
+ end
+ end
+end
diff --git a/gems/espalier/test/static_evidence_test.rb b/gems/espalier/test/static_evidence_test.rb
index 219cd46fa..f32cf8efe 100644
--- a/gems/espalier/test/static_evidence_test.rb
+++ b/gems/espalier/test/static_evidence_test.rb
@@ -304,7 +304,9 @@ def test_project_modules_ranks_production_by_default_and_retains_selectable_test
def test_source_roles_are_language_neutral_path_facts
assert_equal "test", Espalier::StaticEvidence.source_role("src/widget_test.go")
+ assert_equal "test", Espalier::StaticEvidence.source_role("src/normalizer-test.rs")
assert_equal "test", Espalier::StaticEvidence.source_role("tests/test_widget.py")
+ assert_equal "production", Espalier::StaticEvidence.source_role("gems/test-miser/lib/test_miser.rb")
assert_equal "test", Espalier::StaticEvidence.source_role("Tests/ArgumentParserTests/AnyArgumentTests.swift")
assert_equal "test", Espalier::StaticEvidence.source_role("src/jvmTest/kotlin/Foo.kt")
assert_equal "test", Espalier::StaticEvidence.source_role("src/nonWasmTest/kotlin/Foo.kt")
@@ -314,6 +316,55 @@ def test_source_roles_are_language_neutral_path_facts
assert_equal "test", Espalier::StaticEvidence.source_role("Sources/ArgumentParserTestHelpers/Helpers.swift")
end
+ def test_method_source_roles_exclude_rust_inline_test_modules_and_their_lambdas
+ methods = [
+ {
+ "id" => "production",
+ "path" => "/project/src/lib.rs",
+ "language" => "rust",
+ "span" => [1, 0, 5, 1],
+ "semantic_symbol" => "rust-analyzer cargo demo 0.1.0 run()."
+ },
+ {
+ "id" => "test",
+ "path" => "/project/src/lib.rs",
+ "language" => "rust",
+ "span" => [10, 4, 20, 5],
+ "semantic_symbol" => "rust-analyzer cargo demo 0.1.0 tests/check()."
+ },
+ {
+ "id" => "test-lambda",
+ "path" => "/project/src/lib.rs",
+ "language" => "rust",
+ "kind" => "lambda",
+ "span" => [14, 20, 14, 32]
+ }
+ ]
+
+ assert_equal(
+ { "production" => "production", "test" => "test", "test-lambda" => "test" },
+ Espalier::StaticEvidence.method_source_roles(methods)
+ )
+ end
+
+ def test_generated_declarations_are_available_to_fact_mine_but_excluded_from_production_metrics
+ evidence = {
+ "methods" => [
+ { "id" => "authored", "name" => "render", "owner" => "Report", "kind" => "instance", "path" => "lib/report.rb", "line" => 1, "language" => "ruby" },
+ { "id" => "reader", "name" => "title", "owner" => "Report", "kind" => "instance", "path" => "lib/report.rb", "line" => 2, "language" => "ruby", "generated_declaration" => true }
+ ],
+ "fields" => [],
+ "facts" => { "calls" => [], "state_accesses" => [], "complexity_facts" => [], "struct_declarations" => [] }
+ }
+
+ assert_equal(
+ { "authored" => "production", "reader" => "generated" },
+ Espalier::StaticEvidence.method_source_roles(evidence.fetch("methods"))
+ )
+ methods = Espalier::StaticEvidence.project_modules(evidence).fetch(0).fetch(:methods)
+ assert_equal ["render"], methods.map { |method| method[:name] }
+ end
+
def test_project_modules_prefers_a_primary_owner_over_an_extension_and_never_gives_protocols_state
evidence = {
"owners" => [
diff --git a/gems/espalier/test/stdlib_map_test.rb b/gems/espalier/test/stdlib_map_test.rb
new file mode 100644
index 000000000..81816ea30
--- /dev/null
+++ b/gems/espalier/test/stdlib_map_test.rb
@@ -0,0 +1,490 @@
+# frozen_string_literal: true
+
+require "minitest/autorun"
+require "tmpdir"
+require_relative "../lib/espalier"
+
+class StdlibMapTest < Minitest::Test
+ class FakeRunner
+ def initialize(raw_call_gaps: 0)
+ @raw_call_gaps = raw_call_gaps
+ @index_working_directory = nil
+ @profile_files = nil
+ @export_commands = []
+ end
+
+ attr_reader :index_working_directory, :profile_files, :export_commands
+
+ def run!(command, chdir:, env: {})
+ raise "missing cwd" unless File.directory?(chdir)
+ raise "unexpected env" unless env.is_a?(Hash)
+
+ if command.first == "fake-index"
+ @index_working_directory = chdir
+ File.write(command.fetch(1), "scip")
+ elsif command.include?("profile")
+ output = command.fetch(command.index("--output") + 1)
+ sources = command.drop(command.index("--output") + 2)
+ if (summary_index = sources.index("--complexity-summary"))
+ sources.slice!(summary_index, 2)
+ end
+ semantic_environment = {}
+ if (environment_index = sources.index("--semantic-environment"))
+ environment = JSON.parse(File.read(sources.fetch(environment_index + 1)))
+ semantic_environment = environment.fetch("claims")
+ sources.slice!(environment_index, 2)
+ end
+ @profile_files = sources
+ File.write(output, JSON.generate({
+ "input_coverage" => {
+ "selected_files" => sources.length,
+ "parsed_files" => sources.length
+ },
+ "semantic_indexes" => [{"tool" => "fake-scip", "version" => "1.2.3"}],
+ "semantic_environment" => semantic_environment,
+ "methods" => [{"source_export_eligible" => true}],
+ "calls" => [],
+ "call_resolution_coverage" => {
+ "raw_calls_not_normalized_inside_function" => @raw_call_gaps,
+ "source_export_eligible_methods_overlapping_raw_call_loss" => @raw_call_gaps
+ }
+ }))
+ elsif command.length > 1 && File.basename(command.fetch(1)) == "export_complexity_summary.rb"
+ @export_commands << command
+ output = command.last
+ claims = if command.include?("--compatibility")
+ JSON.parse(File.read(command.fetch(command.index("--compatibility") + 1))).fetch("claims")
+ else
+ {}
+ end
+ prefix = if command.include?("--symbol-prefix-from")
+ command.fetch(command.index("--symbol-prefix-to") + 1)
+ else
+ "fake pkg std 1 "
+ end
+ Zlib::GzipWriter.open(output) do |gzip|
+ gzip.write(JSON.generate({
+ "schema" => Espalier::StdlibMap::SUMMARY_SCHEMA,
+ "source" => {
+ "complete_symbol_count" => 1,
+ "source_proven_method_count" => 1,
+ "profile_sha256" => "sha256:test",
+ "indexer" => "fake-scip@1.2.3",
+ "consumer_indexers" => command.each_index.filter_map do |index|
+ command[index + 1] if command[index] == "--consumer-indexer"
+ end
+ },
+ "compatibility" => {"claims" => claims},
+ "symbols" => {
+ "#{prefix}demo/run()." => {
+ "time" => "O(1)",
+ "space" => "O(1)"
+ }
+ }
+ }))
+ end
+ else
+ raise "unexpected command: #{command.inspect}"
+ end
+ end
+
+ def capture(command, chdir:, env: {})
+ raise "missing cwd" unless File.directory?(chdir)
+ raise "unexpected env" unless env.is_a?(Hash)
+
+ if command == ["fake-version"]
+ ["fake-1\n", "", FakeStatus.new(true)]
+ elsif File.basename(command.fetch(1, "")) == "check_big_o_coverage.rb"
+ [
+ JSON.generate({
+ "coverage" => {
+ "functions" => 1,
+ "mapped" => 1,
+ "incomplete" => 0,
+ "mapped_percent" => 100.0
+ }
+ }),
+ "",
+ FakeStatus.new(true)
+ ]
+ else
+ raise "unexpected captured command: #{command.inspect}"
+ end
+ end
+
+ FakeStatus = Struct.new(:success?)
+ end
+
+ def test_run_cli_reports_invalid_arguments
+ _stdout, stderr = capture_io do
+ assert_equal 1, Espalier::StdlibMap.run_cli([])
+ end
+
+ assert_includes stderr, "stdlib-map failed:"
+ end
+
+ def test_command_runner_executes_and_displays_real_command
+ Dir.mktmpdir do |dir|
+ _stdout, stderr = capture_io do
+ Espalier::StdlibMap::CommandRunner.new.run!(
+ [RbConfig.ruby, "-e", "exit 0"],
+ chdir: dir
+ )
+ end
+
+ assert_includes stderr, RbConfig.ruby
+ end
+ end
+
+ def test_manifest_drives_index_profile_validation_export_and_publication
+ Dir.mktmpdir do |directory|
+ source = File.join(directory, "source")
+ work = File.join(directory, "work")
+ output = File.join(directory, "published", "stdlib.json.gz")
+ binary = File.join(directory, "fact-mine-rust")
+ FileUtils.mkdir_p(source)
+ FileUtils.mkdir_p(File.join(directory, "consumer"))
+ File.write(File.join(source, "keep.go"), "package demo\n")
+ File.write(File.join(source, "skip_test.go"), "package demo\n")
+ File.write(File.join(directory, "consumer", "use.go"), "package consumer\n")
+ File.write(binary, "binary")
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: go
+ source:
+ root: source
+ revision: fake-1
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-1
+ include: ["**/*.go"]
+ exclude: ["**/*_test.go"]
+ index:
+ command: ["fake-index", "{index}"]
+ output: fake.scip
+ expected:
+ tool: fake-scip
+ version: 1.2.3
+ soundness:
+ minimum_export_eligible_methods: 1
+ compatibility:
+ claims:
+ runtime.name: fake-runtime
+ runtime.version: fake-1
+ summary:
+ corpus: fake-stdlib
+ output: #{output}
+ minimum_symbols: 1
+ consumer_indexers: ["consumer-scip@4.5.6"]
+ expected_symbol_prefix: "fake consumer std 1 "
+ symbol_relocation:
+ from: "fake pkg std 1 "
+ to: "fake consumer std 1 "
+ consumers:
+ - name: fake-consumer
+ source_root: consumer
+ include: ["*.go"]
+ compatibility:
+ claims:
+ runtime.name: fake-runtime
+ runtime.version: fake-1
+ index:
+ command: ["fake-index", "{index}"]
+ output: consumer.scip
+ minimum_complete_percent: 100
+ YAML
+
+ runner = FakeRunner.new
+ report = Espalier::StdlibMap.new(
+ manifest,
+ work_dir: work,
+ fact_mine: binary,
+ runner: runner
+ ).run
+
+ assert_equal 1, report.fetch("source_files")
+ assert_equal "fake-1", report.fetch("source_revision")
+ assert_equal 1, report.dig("summary", "symbols")
+ assert_equal 0, report.dig("producer_summary", "verified_join_call_sites")
+ assert_equal 1, report.dig("profile", "source_export_eligible_methods")
+ assert_equal 1, report.fetch("consumers").length
+ assert_equal 100.0, report.dig("consumers", 0, "after", "mapped_percent")
+ assert File.size?(output)
+ assert File.exist?(File.join(work, "stdlib-map-report.json"))
+ assert(runner.export_commands.all? do |command|
+ command.each_cons(2).include?(["--consumer-indexer", "consumer-scip@4.5.6"])
+ end)
+ summary = Zlib::GzipReader.open(output) { |gzip| JSON.parse(gzip.read) }
+ assert_equal ["consumer-scip@4.5.6"], summary.dig("source", "consumer_indexers")
+ end
+ end
+
+ def test_manifest_rejects_partial_symbol_relocation
+ Dir.mktmpdir do |directory|
+ FileUtils.mkdir_p(File.join(directory, "source"))
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: java
+ source:
+ root: source
+ revision: fake-1
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-1
+ include: ["**/*.java"]
+ index:
+ path: index.scip
+ expected:
+ tool: scip-java
+ version: 1
+ summary:
+ corpus: jdk
+ output: jdk.json.gz
+ symbol_relocation:
+ from: "temporary "
+ YAML
+
+ error = assert_raises(ArgumentError) { Espalier::StdlibMap.new(manifest) }
+ assert_includes error.message, "requires from and to"
+ end
+ end
+
+ def test_soundness_gate_rejects_parser_call_loss_before_publication
+ Dir.mktmpdir do |directory|
+ source = File.join(directory, "source")
+ work = File.join(directory, "work")
+ output = File.join(directory, "published", "stdlib.json.gz")
+ binary = File.join(directory, "fact-mine-rust")
+ FileUtils.mkdir_p(source)
+ File.write(File.join(source, "keep.go"), "package demo\n")
+ File.write(binary, "binary")
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: go
+ source:
+ root: source
+ revision: fake-1
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-1
+ include: ["**/*.go"]
+ index:
+ command: ["fake-index", "{index}"]
+ expected:
+ tool: fake-scip
+ version: 1.2.3
+ summary:
+ corpus: fake-stdlib
+ output: #{output}
+ YAML
+
+ error = assert_raises(RuntimeError) do
+ Espalier::StdlibMap.new(
+ manifest,
+ work_dir: work,
+ fact_mine: binary,
+ runner: FakeRunner.new(raw_call_gaps: 1)
+ ).run
+ end
+ assert_includes error.message, "analyzer eligibility revocation is unsound"
+ refute File.exist?(output)
+ end
+ end
+
+ def test_source_revision_must_match_before_indexing
+ Dir.mktmpdir do |directory|
+ FileUtils.mkdir_p(File.join(directory, "source"))
+ File.write(File.join(directory, "source", "keep.go"), "package demo\n")
+ File.write(File.join(directory, "fact-mine-rust"), "binary")
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: go
+ source:
+ root: source
+ revision: fake-2
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-2
+ include: ["**/*.go"]
+ index:
+ command: ["fake-index", "{index}"]
+ expected:
+ tool: fake-scip
+ version: 1.2.3
+ summary:
+ corpus: fake-stdlib
+ output: stdlib.json.gz
+ YAML
+
+ error = assert_raises(RuntimeError) do
+ Espalier::StdlibMap.new(
+ manifest,
+ work_dir: File.join(directory, "work"),
+ fact_mine: File.join(directory, "fact-mine-rust"),
+ runner: FakeRunner.new
+ ).run
+ end
+ assert_includes error.message, "source revision mismatch"
+ end
+ end
+
+ def test_git_source_requires_a_full_pinned_commit
+ Dir.mktmpdir do |directory|
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: java
+ source:
+ revision: jdk-21
+ git:
+ repository: https://example.test/jdk.git
+ commit: deadbeef
+ include: ["**/*.java"]
+ index:
+ path: index.scip
+ expected:
+ tool: scip-java
+ version: 1
+ summary:
+ corpus: jdk
+ output: jdk.json.gz
+ YAML
+
+ error = assert_raises(ArgumentError) { Espalier::StdlibMap.new(manifest) }
+ assert_includes error.message, "full 40-character commit"
+ end
+ end
+
+ def test_selected_source_can_be_staged_for_indexers_that_scan_the_whole_workspace
+ Dir.mktmpdir do |directory|
+ source = File.join(directory, "source")
+ work = File.join(directory, "work")
+ binary = File.join(directory, "fact-mine-rust")
+ FileUtils.mkdir_p(source)
+ File.write(File.join(source, "keep.py"), "def keep(): pass\n")
+ File.write(File.join(source, "broken.py"), "this indexer must not see me\n")
+ File.write(binary, "binary")
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: python
+ source:
+ root: source
+ revision: fake-1
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-1
+ include: ["keep.py"]
+ stage_selected_files: true
+ index:
+ command: ["fake-index", "{index}"]
+ expected:
+ tool: fake-scip
+ version: 1.2.3
+ summary:
+ corpus: fake-stdlib
+ output: stdlib.json.gz
+ YAML
+ runner = FakeRunner.new
+
+ report = Espalier::StdlibMap.new(
+ manifest,
+ work_dir: work,
+ fact_mine: binary,
+ runner: runner
+ ).run
+
+ assert_equal File.join(work, "selected-source"), runner.index_working_directory
+ assert File.file?(File.join(work, "selected-source", "keep.py"))
+ refute File.exist?(File.join(work, "selected-source", "broken.py"))
+ assert_equal File.join(work, "selected-source"), report.fetch("analysis_root")
+ end
+ end
+
+ def test_index_staging_can_include_build_inputs_without_analyzing_them
+ Dir.mktmpdir do |directory|
+ source = File.join(directory, "source")
+ work = File.join(directory, "work")
+ binary = File.join(directory, "fact-mine-rust")
+ FileUtils.mkdir_p(File.join(source, "include"))
+ File.write(File.join(source, "implementation.cc"), "int implementation() { return 1; }\n")
+ File.write(File.join(source, "include", "dependency.h"), "#define VALUE 1\n")
+ File.write(binary, "binary")
+ manifest = File.join(directory, "stdlib.yml")
+ File.write(manifest, <<~YAML)
+ schema: fact-mine.stdlib-map.v1
+ language: cpp
+ source:
+ root: source
+ revision: fake-1
+ revision_check:
+ command: ["fake-version"]
+ equals: fake-1
+ include: ["implementation.cc"]
+ stage_selected_files: true
+ stage_include: ["include/**/*"]
+ index:
+ command: ["fake-index", "{index}"]
+ expected:
+ tool: fake-scip
+ version: 1.2.3
+ summary:
+ corpus: fake-stdlib
+ output: stdlib.json.gz
+ YAML
+ runner = FakeRunner.new
+
+ report = Espalier::StdlibMap.new(
+ manifest,
+ work_dir: work,
+ fact_mine: binary,
+ runner: runner
+ ).run
+
+ stage = File.join(work, "selected-source")
+ assert File.file?(File.join(stage, "implementation.cc"))
+ assert File.file?(File.join(stage, "include", "dependency.h"))
+ assert_equal 1, report.fetch("source_files")
+ assert_equal [File.join(stage, "implementation.cc")], runner.profile_files
+ end
+ end
+
+ def test_support_inventory_has_artifacts_for_bundled_languages_and_reasons_for_blocked_ones
+ root = File.expand_path("../..", __dir__)
+ directory = File.join(root, "fact-mine", "config", "stdlib_maps")
+ support = YAML.safe_load(
+ File.read(File.join(directory, "support.yml")),
+ permitted_classes: [],
+ aliases: false
+ )
+ assert_equal "fact-mine.stdlib-map-support.v1", support.fetch("schema")
+ refute_empty support.fetch("languages")
+
+ support.fetch("languages").each do |language, entry|
+ case entry.fetch("status")
+ when "bundled"
+ manifests = Array(entry["manifests"] || entry.fetch("manifest"))
+ refute_empty manifests, "#{language} manifests are missing"
+ manifests.each do |filename|
+ manifest_path = File.join(directory, filename)
+ assert File.file?(manifest_path), "#{language} manifest is missing: #{filename}"
+ manifest = YAML.safe_load(File.read(manifest_path), permitted_classes: [], aliases: false)
+ output = File.expand_path(
+ manifest.fetch("summary").fetch("output"),
+ File.dirname(manifest_path)
+ )
+ assert File.size?(output), "#{language} generated summary is missing: #{filename}"
+ end
+ when "blocked"
+ refute_empty entry.fetch("blocker"), "#{language} blocker is missing"
+ refute_empty entry.fetch("required_fix"), "#{language} required fix is missing"
+ else
+ flunk "#{language} has unsupported stdlib-map status #{entry['status'].inspect}"
+ end
+ end
+ end
+end
diff --git a/gems/espalier/test/symbolic_complexity_test.rb b/gems/espalier/test/symbolic_complexity_test.rb
index 909140f40..23903c460 100644
--- a/gems/espalier/test/symbolic_complexity_test.rb
+++ b/gems/espalier/test/symbolic_complexity_test.rb
@@ -100,6 +100,40 @@ def test_logarithms_remain_attached_to_their_source_domain
assert_equal "O(log N)", Espalier::SymbolicComplexity.render(logarithmic).first
end
+ def test_large_rendering_collapses_domains_to_a_conservative_parametric_bound
+ independent = 129.times.map do |index|
+ value = domain("param:f:value#{index}", "value#{index}", index + 1)
+ expression({ value["id"] => 1 }, [value])
+ end
+ callback = Espalier::SymbolicComplexity.parameterized_cost(
+ id: "callback:f:block",
+ name: "block",
+ source_kind: "callback_cost"
+ )
+
+ rendered, variables = Espalier::SymbolicComplexity.render(
+ Espalier::SymbolicComplexity.sum(independent, callback)
+ )
+
+ assert_equal "O(N*C)", rendered
+ assert_equal %w[N C], variables.map { |variable| variable[:symbol] }
+ assert_equal [129, 1], variables.map { |variable| variable[:domain_count] }
+ assert_equal %w[collapsed_upper_bound callback_cost],
+ variables.map { |variable| variable[:source_kind] }
+ end
+
+ def test_domain_annotation_reuses_normalized_terms
+ xs = domain("param:f:xs", "xs", 1)
+ value = expression({ xs["id"] => 1 }, [xs])
+ annotated = Espalier::SymbolicComplexity.with_domains(
+ value,
+ value[:domains].transform_values { |entry| entry.merge("origin_function" => "callee") }
+ )
+
+ assert_same value[:terms], annotated[:terms]
+ assert_equal "callee", annotated[:domains].fetch(xs["id"]).fetch("origin_function")
+ end
+
def test_canonical_dag_interns_equivalent_expressions
xs = domain("param:f:xs", "xs", 1)
diff --git a/gems/espalier/test/tree_sitter_cov_test.rb b/gems/espalier/test/tree_sitter_cov_test.rb
index 09cd54d51..4dfd2d5de 100644
--- a/gems/espalier/test/tree_sitter_cov_test.rb
+++ b/gems/espalier/test/tree_sitter_cov_test.rb
@@ -4,62 +4,21 @@
require_relative "../lib/espalier/tree_sitter"
class TreeSitterCovTest < Minitest::Test
- def setup
- # Temporarily mock require to prevent actually loading the parser if it's missing
- @original_require = Kernel.instance_method(:require)
- Kernel.define_method(:require) { |*| true }
- @original_gem = Kernel.instance_method(:gem)
- Kernel.define_method(:gem) { |*| true }
+ def test_parser_for_loads_the_real_installed_ruby_runtime
+ parser = Espalier::TreeSitter.parser_for("ruby")
- # Mock RbConfig::CONFIG to hit windows/mac/arm branches if possible,
- # but the coverage tool tracks which branches were executed locally.
- # To cover lines 25-35, 39-40, we just need to call parser_for with different languages.
- @original_config = RbConfig::CONFIG.dup
+ assert_instance_of TreeSitter::Parser, parser
end
- def teardown
- Kernel.define_method(:require, @original_require)
- Kernel.define_method(:gem, @original_gem)
- RbConfig.send(:remove_const, :CONFIG)
- RbConfig.const_set(:CONFIG, @original_config)
- end
-
- def test_parser_for_languages
- # We mock TreeSitter::Language and TreeSitter::Parser to return a dummy
- dummy = Object.new
- unless defined?(::TreeSitter)
- Object.const_set(:TreeSitter, Module.new)
- ::TreeSitter.const_set(:Language, Class.new { def self.load(*); end })
- ::TreeSitter.const_set(:Parser, Class.new { def initialize(*); end })
- end
-
- # Ignore errors if the file doesn't exist
- Espalier::TreeSitter.stub :require, true do
- %w[python javascript typescript go rust zig c cpp csharp kotlin].each do |lang|
- begin
- Espalier::TreeSitter.parser_for(lang)
- rescue StandardError
- end
- end
- end
-
- # Try with different OS/CPU configs to cover those lines
- {
- "host_os" => "darwin", "host_cpu" => "arm64"
- }.each { |k, v| RbConfig::CONFIG[k] = v }
-
- begin
- Espalier::TreeSitter.parser_for("ruby")
- rescue StandardError
- end
-
- {
- "host_os" => "mswin", "host_cpu" => "x86_64"
- }.each { |k, v| RbConfig::CONFIG[k] = v }
+ def test_parser_for_normalizes_supported_language_names
+ %w[python javascript typescript go rust c cpp csharp kotlin].each do |language|
+ parser = Espalier::TreeSitter.parser_for(language)
- begin
- Espalier::TreeSitter.parser_for("ruby")
- rescue StandardError
+ assert_instance_of TreeSitter::Parser, parser, language
+ rescue LoadError, RuntimeError
+ # Optional parser shared libraries are environment-specific. The Ruby
+ # parser above is required by this repository and exercises the actual
+ # gem/require path without replacing Kernel methods with test doubles.
end
end
end
diff --git a/gems/espalier/tools/corpus_common.rb b/gems/espalier/tools/corpus_common.rb
index e960266da..6bd870a64 100644
--- a/gems/espalier/tools/corpus_common.rb
+++ b/gems/espalier/tools/corpus_common.rb
@@ -10,10 +10,16 @@
module CorpusCommon
module_function
+ # `assets` holds what a program serves rather than what it is: giga-ui's
+ # browser bundle is a hand-written app.js beside a content-hashed, minified
+ # diff viewer, and SimpleCov/RubyCritic ship JavaScript inside their HTML
+ # reports. A minified bundle is one 122k-character line, which tree-sitter
+ # takes tens of minutes to parse - it was the whole cost of the architecture
+ # SARIF job. The UI's own source is not under assets/ and stays in scope.
EXCLUDE_DIRS = %w[
test tests spec specs testing vendor node_modules examples example bench
benchmark benchmarks dist build target third_party docs doc fixtures
- __pycache__ scripts tools ci .git generated samples sample demo
+ __pycache__ scripts tools ci .git generated samples sample demo assets
zig-out .zig-cache coverage tmp transpile-tests zig-mutants
].to_set.freeze
@@ -74,12 +80,20 @@ def run_fact_mine(mode, repo, files, extra_args: [])
merged || {}
end
+ # The only document keys these reports read. A full projection of this
+ # repository is ~1.08 GB of JSON and this is 8% of it; the rest is dataflow
+ # (clone_candidates alone is a third) that gets serialized here and parsed
+ # back in Ruby only to be dropped. Ask for what we use.
+ SYNTAX_FACT_FIELDS = %w[file language imports functions calls].freeze
+
# syntax-facts requires an explicit --language; batch files per language.
def run_syntax_facts(repo, files)
documents = []
files.group_by { |f| EXT_LANGUAGE[File.extname(f)] }.each do |language, batch|
next unless language
- chunk = run_fact_mine("syntax-facts", repo, batch, extra_args: ["--language", language])
+ chunk = run_fact_mine("syntax-facts", repo, batch,
+ extra_args: ["--language", language,
+ "--fields=#{SYNTAX_FACT_FIELDS.join(",")}"])
documents.concat(chunk["documents"] || [])
end
{ "documents" => documents }
diff --git a/gems/fact-mine/Cargo.lock b/gems/fact-mine/Cargo.lock
index 31482a222..88b5f96e2 100644
--- a/gems/fact-mine/Cargo.lock
+++ b/gems/fact-mine/Cargo.lock
@@ -92,6 +92,12 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -114,12 +120,18 @@ version = "0.1.0"
dependencies = [
"anyhow",
"flate2",
+ "glob",
"hazard-contract",
+ "protobuf",
+ "protobuf-codegen",
+ "protobuf-json-mapping",
"regex",
+ "scip",
"serde",
"serde_json",
"serde_yaml",
"sha2",
+ "shell-words",
"streaming-iterator",
"tempfile",
"tree-sitter",
@@ -173,6 +185,12 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -187,6 +205,15 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -215,6 +242,12 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
[[package]]
name = "memchr"
version = "2.8.2"
@@ -231,6 +264,12 @@ dependencies = [
"simd-adler32",
]
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -240,6 +279,68 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "protobuf"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
+dependencies = [
+ "once_cell",
+ "protobuf-support",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-codegen"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace"
+dependencies = [
+ "anyhow",
+ "once_cell",
+ "protobuf",
+ "protobuf-parse",
+ "regex",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-json-mapping"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d6e4be637b310d8a5c02fa195243328e2d97fa7df1127a27281ef1187fcb1d"
+dependencies = [
+ "protobuf",
+ "protobuf-support",
+ "thiserror",
+]
+
+[[package]]
+name = "protobuf-parse"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "log",
+ "protobuf",
+ "protobuf-support",
+ "tempfile",
+ "thiserror",
+ "which",
+]
+
+[[package]]
+name = "protobuf-support"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
+dependencies = [
+ "thiserror",
+]
+
[[package]]
name = "quote"
version = "1.0.45"
@@ -297,6 +398,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "scip"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26a72133c2d6fd45c9a3a343bcb3db2faa30f68f0919bfa3370ca85add5460c3"
+dependencies = [
+ "protobuf",
+]
+
[[package]]
name = "serde"
version = "1.0.228"
@@ -365,6 +475,12 @@ dependencies = [
"digest",
]
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
[[package]]
name = "shlex"
version = "2.0.1"
@@ -406,6 +522,26 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tree-sitter"
version = "0.25.8"
@@ -600,6 +736,18 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
[[package]]
name = "windows-link"
version = "0.2.1"
diff --git a/gems/fact-mine/Cargo.toml b/gems/fact-mine/Cargo.toml
index f755d3328..6ad1c183c 100644
--- a/gems/fact-mine/Cargo.toml
+++ b/gems/fact-mine/Cargo.toml
@@ -19,6 +19,9 @@ serde_yaml = "0.9"
regex = "1.10"
sha2 = "0.10"
flate2 = "1.1"
+protobuf = "=3.7.2"
+protobuf-json-mapping = "=3.7.2"
+scip = "=0.9.0"
tree-sitter = "=0.25.8"
streaming-iterator = "0.1.9"
tree-sitter-language = "=0.1.3"
@@ -37,10 +40,15 @@ tree-sitter-c-sharp = "=0.23.5"
tree-sitter-swift = "=0.7.1"
tree-sitter-kotlin-ng = "1.1.0"
tree-sitter-php = "=0.24.2"
+shell-words = "1.1.1"
+glob = "0.3.4"
[dev-dependencies]
tempfile = "=3.10.1"
+[build-dependencies]
+protobuf-codegen = "=3.7.2"
+
[profile.profiling]
inherits = "release"
debug = 1
diff --git a/gems/fact-mine/build.rs b/gems/fact-mine/build.rs
new file mode 100644
index 000000000..93ebe19cf
--- /dev/null
+++ b/gems/fact-mine/build.rs
@@ -0,0 +1,50 @@
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+
+fn main() {
+ let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
+ let protocol_dir = manifest_dir.join("../protocol/runtime-evidence/v1");
+ let protocol = protocol_dir.join("runtime_evidence.proto");
+ println!("cargo:rerun-if-changed={}", protocol.display());
+ protobuf_codegen::Codegen::new()
+ .pure()
+ .includes([&protocol_dir])
+ .input(&protocol)
+ .cargo_out_dir("runtime_evidence_protocol")
+ .run_from_script();
+ let generated_protocol = PathBuf::from(env::var_os("OUT_DIR").unwrap())
+ .join("runtime_evidence_protocol/runtime_evidence.rs");
+ let embedded_protocol = PathBuf::from(env::var_os("OUT_DIR").unwrap())
+ .join("runtime_evidence_protocol_embedded.rs");
+ let generated_source = fs::read_to_string(&generated_protocol).unwrap();
+ let embedded_source = generated_source
+ .lines()
+ .filter(|line| !line.starts_with("#![") && !line.starts_with("//!"))
+ .collect::>()
+ .join("\n");
+ fs::write(embedded_protocol, embedded_source).unwrap();
+
+ let summary_dir = manifest_dir.join("config/complexity_summaries");
+ println!("cargo:rerun-if-changed={}", summary_dir.display());
+
+ let mut summaries = fs::read_dir(&summary_dir)
+ .unwrap()
+ .map(|entry| entry.unwrap().path())
+ .filter(|path| path.to_string_lossy().ends_with(".json.gz"))
+ .collect::>();
+ summaries.sort();
+
+ let mut generated = String::from("const BUNDLED_SUMMARIES: &[(&str, &[u8])] = &[\n");
+ for path in summaries {
+ let name = path.file_name().unwrap().to_string_lossy();
+ generated.push_str(&format!(
+ " ({name:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/config/complexity_summaries/\", {name:?}))),\n"
+ ));
+ }
+ generated.push_str("];\n");
+
+ let output =
+ PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("bundled_complexity_summaries.rs");
+ fs::write(output, generated).unwrap();
+}
diff --git a/gems/fact-mine/config/complexity_summaries/README.md b/gems/fact-mine/config/complexity_summaries/README.md
new file mode 100644
index 000000000..22c6bf36a
--- /dev/null
+++ b/gems/fact-mine/config/complexity_summaries/README.md
@@ -0,0 +1,155 @@
+# Analyzed complexity summaries
+
+These generated artifacts contain complete Espalier time-and-space bounds keyed
+by exact SCIP declaration symbols. FactMine applies bundled summaries after
+SCIP ingestion. Bundled artifacts additionally require the exact SCIP indexer
+name and version recorded by the consumer index. This protects symbol schemes
+that do not put the toolchain revision directly in every symbol.
+
+The current producer writes the v3 envelope. V3 retains all v2 provenance and
+adds exact semantic-environment claims plus an optional generated symbol-bridge
+digest. FactMine still reads existing v1/v2 artifacts. Explicitly supplied v3
+summaries fail on a missing or mismatched claim; bundled summaries remain
+inactive until both their indexer and environment match.
+
+`go-stdlib.go1.22.2.json.gz` was built from the installed Go 1.22.2
+implementation sources with scip-go 0.2.7. Its source surface is:
+
+```text
+syscall os time flag bytes bufio encoding/json encoding/xml sort strings
+io/fs strconv math regexp fmt
+```
+
+The bundle contains 322 exact symbols whose time and space bounds are proven
+from analyzed bodies, exact analyzed targets, CFG/DFG structure, or
+compiler-provided closed candidate sets. A function is intentionally omitted
+when its apparent completeness depends on a reviewed/manual receiver registry,
+an external-latency or modeled-world contract, an unknown cardinality relation,
+or an unresolved call-evidence gap. Those omissions remain eligible for the
+manual incomplete-data fallback.
+
+Rebuild it through the shared manifest producer:
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/go-1.22.2.yml
+```
+
+The gzip header is deterministic. The versioned envelope records the complete input
+profile SHA-256, proof policy, source-proven method count, and exported symbol
+count; FactMine validates the schema and every bound at startup. Add a focused
+exact-version join test whenever a new bundle is registered in
+`external_summary.rs`.
+
+`rust-stdlib.rustc1.96.0.json.gz` was built from the installed Rust 1.96.0
+`core`, `alloc`, and `std` implementation sources at compiler commit
+`ac68faa20c58`, indexed by rust-analyzer from the same build. It contains 1,543
+source-proven exact symbols. rust-analyzer identifies those crates by their
+source repository URL rather than a release number, so FactMine applies the
+bundle only when the consumer SCIP metadata reports the exact compatible
+`rust-analyzer 1.96.0 (ac68faa 2026-05-25)` build.
+
+The source pass also models Rust's implicit function-exit destruction. In
+particular, `core::mem::drop` is omitted instead of being exported as O(1):
+its empty source body hides a destructor selected by `T`. Conflicting duplicate
+symbols emitted by rust-analyzer for two test-only declarations are omitted
+rather than selected by order.
+
+Rebuild it through the same producer:
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/rust-1.96.0.yml
+```
+
+`java-stdlib.jdk21.0.12.json.gz` contains 2,598 exact symbols from
+`java.lang` and `java.util` in the pinned Adoptium JDK 21.0.12+8 source.
+scip-java indexes the source in a patch-module Maven project. The generic
+producer first verifies its exact producer symbols, then relocates the Maven
+project prefix to the `semanticdb maven jdk 21` identity emitted in user
+projects. A complete disagreement with the existing Java fallback remains a
+hard error; this process found and corrected the former `Objects.hash`
+auxiliary-space overestimate.
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/java-21.0.12.yml
+```
+
+`python-stdlib.cpython3.11.9.json.gz` contains 200 exact symbols from 43
+selected pure-Python CPython 3.11.9 implementation files. The source selection
+is staged into an isolated index workspace because scip-python 0.6.6 crashes
+while indexing an unrelated `signal.py`; staging is a generic manifest feature,
+not Python behavior in the shared producer.
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/python-3.11.9.yml
+```
+
+`csharp-corelib.dotnet10.0.10.json.gz` contains 316 exact consumer symbols
+generated from .NET runtime 10.0.10 collection, string, and array
+implementations. The manifest requires the released scip-dotnet 0.2.14 even
+though that release still writes `0.1.0-SNAPSHOT` into SCIP metadata. V3
+compatibility claims therefore pin the actual indexer DLL and .NET reference
+assembly digests, and an exact bridge assigns each implementation symbol to its
+runtime assembly identity.
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/csharp-10.0.10.yml
+```
+
+`kotlin-stdlib.kotlin2.2.0.json.gz` contains 85 source-proven symbols from
+4,811 Kotlin/JVM stdlib implementation methods. Both the Maven source jar and
+runtime jar are pinned by SHA-256. Kotlin 2.2 consumers use unversioned
+`scip-java maven . .` symbols, so applicability additionally requires a
+semantic-environment sidecar containing the exact runtime-jar digest.
+
+The published `semanticdb-kotlinc` 0.6.0 plugin numbered top-level overloads
+within each source file while `scip-java` numbered binary dependency overloads
+across their package. The language-owned index recipe applies the included
+upstream patch so both sides enumerate the full package callable set before the
+generic producer relocates the otherwise identical symbol prefix. This is an
+indexer correction, not a hand-maintained complexity override.
+
+```bash
+JAVA_HOME=/path/to/jdk-21 \
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/kotlin-2.2.0.yml
+```
+
+On Picnic 0.7.0 production sources, the generated bundle leaves complete
+coverage unchanged at 207/229 (90.39%): its currently exported symbols do not
+intersect Picnic's unresolved stdlib calls. It therefore adds verified stdlib
+coverage without overstating the measured consumer gain.
+
+The three `cpp-libstdcxx.13.3.0-*.json.gz` bundles contain 772 unique exact
+`std::` symbols from selected libstdc++ 13.3.0 container, string/stream, and
+memory surfaces. The producer preprocesses those surfaces with the pinned
+compiler configuration, then analyzes the resulting implementation bodies
+with the same generic CFG/DFG pipeline. Compatibility does not trust
+scip-clang's unversioned `cxx . .` package text: it requires exact digests for
+the compiler, indexer, macro set, and effective generic/architecture header
+overlay, plus the target and selected C++ standard.
+
+Only rows whose analyzed bound quality is exact are published. Template bodies
+with dependent implicit value construction, assignment, or destruction are
+marked ineligible by the C++ language adapter, preventing generic
+`std::swap` from being incorrectly exported as O(1). Rebuild each surface
+through its manifest:
+
+```bash
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-containers.yml
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-strings.yml
+bundle exec ruby gems/espalier/exe/espalier stdlib-map \
+ --manifest gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx20-memory.yml
+```
+
+FactMine discovers all `.json.gz` files in this directory at build time.
+Adding a generated bundle therefore requires no language-specific registration
+code. `../stdlib_maps/support.yml` records the fail-closed status of maintained
+SCIP languages whose source bodies or consumer version identity are currently
+insufficient for safe publication.
diff --git a/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-containers.json.gz b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-containers.json.gz
new file mode 100644
index 000000000..f5160208c
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-containers.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-strings.json.gz b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-strings.json.gz
new file mode 100644
index 000000000..220369b92
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-strings.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx20-memory.json.gz b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx20-memory.json.gz
new file mode 100644
index 000000000..cb55c9b4c
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/cpp-libstdcxx.13.3.0-cxx20-memory.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/csharp-corelib.dotnet10.0.10.json.gz b/gems/fact-mine/config/complexity_summaries/csharp-corelib.dotnet10.0.10.json.gz
new file mode 100644
index 000000000..208f7c70f
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/csharp-corelib.dotnet10.0.10.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/go-stdlib.go1.22.2.json.gz b/gems/fact-mine/config/complexity_summaries/go-stdlib.go1.22.2.json.gz
new file mode 100644
index 000000000..085ecc642
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/go-stdlib.go1.22.2.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/java-stdlib.jdk21.0.12.json.gz b/gems/fact-mine/config/complexity_summaries/java-stdlib.jdk21.0.12.json.gz
new file mode 100644
index 000000000..64ee06f16
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/java-stdlib.jdk21.0.12.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/kotlin-stdlib.kotlin2.2.0.json.gz b/gems/fact-mine/config/complexity_summaries/kotlin-stdlib.kotlin2.2.0.json.gz
new file mode 100644
index 000000000..102d7a754
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/kotlin-stdlib.kotlin2.2.0.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/python-stdlib.cpython3.11.9.json.gz b/gems/fact-mine/config/complexity_summaries/python-stdlib.cpython3.11.9.json.gz
new file mode 100644
index 000000000..843e9401f
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/python-stdlib.cpython3.11.9.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/ruby-cruby.3.2.3-core-a.json.gz b/gems/fact-mine/config/complexity_summaries/ruby-cruby.3.2.3-core-a.json.gz
new file mode 100644
index 000000000..6de5c3a84
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/ruby-cruby.3.2.3-core-a.json.gz differ
diff --git a/gems/fact-mine/config/complexity_summaries/rust-stdlib.rustc1.96.0.json.gz b/gems/fact-mine/config/complexity_summaries/rust-stdlib.rustc1.96.0.json.gz
new file mode 100644
index 000000000..d8eacd6b1
Binary files /dev/null and b/gems/fact-mine/config/complexity_summaries/rust-stdlib.rustc1.96.0.json.gz differ
diff --git a/gems/fact-mine/config/stdlib_complexity/c.yml b/gems/fact-mine/config/stdlib_complexity/c.yml
index 791f6e4e9..abd3fb3ca 100644
--- a/gems/fact-mine/config/stdlib_complexity/c.yml
+++ b/gems/fact-mine/config/stdlib_complexity/c.yml
@@ -18,32 +18,40 @@ Intrinsic:
strncmp: linear_scan
qsort: sort
bsearch: logarithmic
- malloc: declaration
- calloc: declaration
- realloc: declaration
- free: declaration
- printf: declaration
- fprintf: declaration
- sprintf: declaration
- snprintf: declaration
- puts: declaration
- putchar: declaration
+ # Allocation is bounded by the requested byte count. Treat deallocation as
+ # a linear allocator upper bound rather than assuming a particular libc's
+ # constant-time free-list implementation.
+ malloc: linear_materialize
+ calloc: linear_materialize
+ realloc: linear_materialize
+ free: linear_scan
+ # Formatting/parsing and byte-stream transfer are bounded by the consumed or
+ # produced sequence. External latency is outside the in-process Big-O model.
+ printf: linear_scan
+ fprintf: linear_scan
+ sprintf: linear_scan
+ snprintf: linear_scan
+ puts: linear_scan
+ putchar: constant
fopen: declaration
fclose: declaration
- fread: declaration
- fwrite: declaration
- abort: declaration
- exit: declaration
+ fread: linear_scan
+ fwrite: linear_scan
+ abort: constant
+ exit: constant
assert: declaration
va_start: declaration
va_arg: declaration
va_copy: declaration
va_end: declaration
- tolower: declaration
- toupper: declaration
+ tolower: constant
+ toupper: constant
fabs: declaration
- strtol: declaration
- strtoul: declaration
+ strtol: linear_scan
+ strtoul: linear_scan
+ strtod: linear_scan
+ strtof: linear_scan
+ strtold: linear_scan
ungetc: declaration
ftell: declaration
fseek: declaration
@@ -53,15 +61,78 @@ Intrinsic:
vsnprintf: declaration
strerror: declaration
perror: declaration
- sscanf: declaration
- fscanf: declaration
- isspace: declaration
- isdigit: declaration
- isalpha: declaration
- isalnum: declaration
- floor: declaration
- ceil: declaration
- sqrt: declaration
- pow: declaration
+ sscanf: linear_scan
+ fscanf: linear_scan
+ scanf: linear_scan
+ localeconv: constant
+ isspace: constant
+ isdigit: constant
+ isalpha: constant
+ isalnum: constant
kevent: declaration
kqueue: declaration
+ # C99 : constant-time on a fixed-width float/double (the value is one
+ # scalar; there is no input-size dimension to grow in). Classification (not
+ # `signbit`), rounding, scaling, decomposition, and elementary functions.
+ isnan: constant
+ isinf: constant
+ isfinite: constant
+ isnormal: constant
+ signbit: constant
+ fpclassify: constant
+ fabs: constant
+ floor: constant
+ ceil: constant
+ round: constant
+ roundf: constant
+ roundl: constant
+ lround: constant
+ llround: constant
+ trunc: constant
+ truncf: constant
+ truncl: constant
+ rint: constant
+ rintf: constant
+ rintl: constant
+ lrint: constant
+ nearbyint: constant
+ scalbn: constant
+ scalbnf: constant
+ scalbnl: constant
+ scalbln: constant
+ ldexp: constant
+ frexp: constant
+ modf: constant
+ ilogb: constant
+ logb: constant
+ copysign: constant
+ nextafter: constant
+ nexttoward: constant
+ fmod: constant
+ remainder: constant
+ fdim: constant
+ fmax: constant
+ fmin: constant
+ fma: constant
+ sqrt: constant
+ sqrtf: constant
+ cbrt: constant
+ hypot: constant
+ pow: constant
+ exp: constant
+ exp2: constant
+ expm1: constant
+ log: constant
+ log2: constant
+ log10: constant
+ log1p: constant
+ sin: constant
+ cos: constant
+ tan: constant
+ asin: constant
+ acos: constant
+ atan: constant
+ atan2: constant
+ sinh: constant
+ cosh: constant
+ tanh: constant
diff --git a/gems/fact-mine/config/stdlib_complexity/cpp.yml b/gems/fact-mine/config/stdlib_complexity/cpp.yml
index e807000ff..9805371a2 100644
--- a/gems/fact-mine/config/stdlib_complexity/cpp.yml
+++ b/gems/fact-mine/config/stdlib_complexity/cpp.yml
@@ -10,18 +10,44 @@ Array:
find: linear_scan
begin: constant
end: constant
- sort: sort
+ # Node transfer is constant for whole-list/single-element overloads and
+ # linear for a range from another list. Use the conservative common bound.
+ splice: linear_scan
+ emplace_back: constant
+ clear: linear_scan
+ erase: linear_scan
+ insert: linear_materialize
+ resize: linear_materialize
+ reserve: linear_materialize
+ swap: constant
Hash:
at: constant
find: linear_scan
contains: linear_scan
size: constant
empty: constant
+ begin: constant
+ end: constant
+ clear: linear_scan
+ erase: linear_scan
+ insert: linear_scan
+ emplace: linear_scan
+ lower_bound: linear_scan
+ upper_bound: linear_scan
+ swap: constant
Set:
find: linear_scan
contains: linear_scan
size: constant
empty: constant
+ begin: constant
+ end: constant
+ clear: linear_scan
+ erase: linear_scan
+ insert: linear_scan
+ lower_bound: linear_scan
+ upper_bound: linear_scan
+ swap: constant
String:
c_str: constant
compare: linear_scan
@@ -33,17 +59,81 @@ String:
find: linear_scan
starts_with: linear_scan
substr: linear_materialize
+ assign: linear_materialize
+ append: linear_materialize
+ clear: linear_scan
+ resize: linear_materialize
+ reserve: linear_materialize
+ push_back: linear_materialize
+ pop_back: constant
+ insert: linear_materialize
+ erase: linear_scan
+ replace: linear_materialize
+ swap: constant
+ operator+=: linear_materialize
+StringStream:
+ str: linear_materialize
+ eof: constant
+OutputStream:
+ eof: constant
+FileStream:
+ # CPU work is bounded by the path length; filesystem latency is excluded
+ # from the structural Big-O contract.
+ open: linear_scan
+ close: constant
+ is_open: constant
+ exceptions: constant
+ eof: constant
+Json:
+ # nlohmann::json object representation is configurable. Linear bounds are
+ # conservative across ordered objects, arrays, parsing, and conversion.
+ at: linear_scan
+ get: linear_materialize
+ get_to: linear_materialize
+ contains: linear_scan
+ find: linear_scan
+ dump: linear_materialize
+ parse: linear_materialize
+StdAtomic:
+ load: constant
+ store: constant
+ exchange: constant
+ compare_exchange_weak: constant
+ compare_exchange_strong: constant
+ fetch_add: constant
+ fetch_sub: constant
+ fetch_and: constant
+ fetch_or: constant
+ fetch_xor: constant
Namespace:
std: declaration
Intrinsic:
- # These free functions are modeled only after a SCIP symbol proves `std`
- # ownership. Their standard semantics do not execute user callbacks.
+ # These selector-only spellings are consumed after a SCIP symbol proves
+ # `std` ownership. Their standard semantics do not execute user callbacks.
std.move: constant
+ 'std::move': constant
std.forward: constant
std.addressof: constant
std.get: constant
std.as_const: constant
std.launder: constant
+ # Qualified-id calls preserve their full `std::` spelling before SCIP
+ # enrichment. `std` is a reserved namespace, so this source identity remains
+ # sufficient in compiler-inactive preprocessor branches that have no
+ # occurrence in the selected SCIP configuration.
+ 'std::string': linear_materialize
+ 'std::wstring': linear_materialize
+ 'std::strlen': linear_scan
+ 'std::wcslen': linear_scan
+ 'std::strchr': linear_scan
+ 'std::strrchr': linear_scan
+ 'std::wcsrchr': linear_scan
+ 'std::strcmp': linear_scan
+ 'std::wcscmp': linear_scan
+ 'std::operator<<': linear_scan
+ 'std::setfill': constant
+ 'std::setw': constant
+ 'std::setprecision': constant
assert: declaration
malloc: declaration
calloc: declaration
@@ -51,8 +141,140 @@ Intrinsic:
free: declaration
puts: declaration
strlen: linear_scan
+IntrinsicParametricCall:
+ # Standard algorithms invoke a caller-supplied predicate once per visited
+ # element. Their qualified `std::` identity is source-proven even when clang
+ # omits a dependent-template occurrence.
+ 'std::find_if': callback_linear
+ # `using std::swap; swap(a, b)` is the standard ADL customization pattern.
+ # It performs one selected swap/move operation whose cost is type-dependent.
+ 'std::swap': reflective_once
+SemanticSymbol:
+ # Exact scip-clang descriptors whose overload discriminator has been
+ # reviewed. These cover operations which cannot be classified from a
+ # nominal receiver alone.
+ 'std/__cxx11/basic_ostringstream#str(d33e1a6fd36255f7).': linear_materialize
+ 'std/setfill(ade351e831a1374a).': constant
+ 'std/setw(f06ce2e9220601fb).': constant
+ 'std/string#': linear_materialize
+ 'std/getline(30074676001a27e3).': linear_materialize
+ 'std/isprint(a1851578aea3f541).': constant
+ 'std/toupper(a1851578aea3f541).': constant
+ 'std/static_pointer_cast(d1ff2bba4f627176).': constant
+ 'std/weak_ptr#lock(a1b13de252cffcea).': constant
+ 'std/__weak_ptr#expired(3482b152b9333168).': constant
+ 'std/atomic_flag#clear(455a4f0c5c7bd751).': constant
+ 'std/atomic_flag#test_and_set(2adbe26b81e7d7f).': constant
+ 'std/has_single_bit(ee8962c63b365797).': constant
+ 'std/optional#has_value(3482b152b9333168).': constant
+ 'std/unique_ptr#get(ea7391e9eeeaf20e).': constant
+ 'std/unique_ptr#release(aad4c3669c5e940c).': constant
+SemanticSymbolParametricCost:
+ # These operations execute a user-defined constructor, destructor, move, or
+ # element operation. Preserve that work symbolically rather than pricing it
+ # as a scalar library call.
+ 'std/swap(c75b8fd57d7c2e06).': reflective_once
+ 'std/make_shared(ffca0f3977536948).': reflective_once
+ 'std/__shared_ptr#reset(ced63f7c635d850d).': reflective_once
+ 'std/construct_at(984c90861d021fca).': reflective_once
+ 'std/destroy_at(ebd0a1552f8ce24f).': reflective_once
+ 'std/optional#emplace(8cc437dbb5d7266f).': reflective_once
+ 'std/ranges/uninitialized_copy.': callback_linear
+ 'std/uninitialized_copy_n(31908fd9f87493f7).': callback_linear
+ParametricCall:
+ # A dependent template access may not receive a clang occurrence. The
+ # declared smart-pointer type still proves destruction of at most one
+ # user-defined pointee.
+ 'shared_ptr.reset': reflective_once
+ 'unique_ptr.reset': reflective_once
+ 'Array.sort': callback_sort
+ModeledRuntime:
+ # Exact source spellings seen only in compiler-inactive platform branches.
+ # Calls receive an explicit modeled-world assumption; these entries never
+ # replace an available SCIP identity or a project declaration.
+ assert: constant
+ va_start: constant
+ va_copy: constant
+ va_end: constant
+ malloc: linear_materialize
+ free: linear_scan
+ vsnprintf: linear_scan
+ vsnprintf_s: linear_scan
+ vsnwprintf_s: linear_scan
+ _vsnwprintf_s: linear_scan
+ _vsnwprintf: linear_scan
+ _vscprintf: linear_scan
+ _vscwprintf: linear_scan
+ '::strlen': linear_scan
+ '::wcslen': linear_scan
+ '::strchr': linear_scan
+ '::localtime': constant
+ '::localtime_r': constant
+ '::localtime_s': constant
+ '::gmtime': constant
+ '::gmtime_r': constant
+ '::gmtime_s': constant
+ '::ftime': constant
+ '::gettimeofday': constant
+ '::syscall': constant
+ '::iconv_open': constant
+ '::iconv': linear_scan
+ '::iconv_close': constant
+ '::open': linear_scan
+ '::_wsopen': linear_scan
+ '::_wsopen_s': linear_scan
+ '::lseek': constant
+ '::_lseeki64': constant
+ _lseek: constant
+ '::close': constant
+ '::_close': constant
+ '::write': linear_scan
+ '::_write': linear_scan
+ '::rename': linear_scan
+ '::unlink': linear_scan
+ '::_wunlink': linear_scan
+ '::isatty': constant
+ MultiByteToWideChar: linear_materialize
+ WideCharToMultiByte: linear_materialize
+ GetCurrentThreadId: constant
+ pthread_threadid_np: constant
+ rtems_task_self: constant
+ xTaskGetCurrentTaskHandle: constant
+ GetStdHandle: constant
+ GetConsoleScreenBufferInfo: constant
+ SetConsoleTextAttribute: constant
+ InitializeCriticalSection: constant
+ DeleteCriticalSection: constant
+ EnterCriticalSection: constant
+ LeaveCriticalSection: constant
+ '::pthread_mutex_init': constant
+ '::pthread_mutex_destroy': constant
+ '::pthread_mutex_lock': constant
+ '::pthread_mutex_unlock': constant
+ xSemaphoreCreateBinary: constant
+ xSemaphoreGive: constant
+ xSemaphoreTake: constant
+ vSemaphoreDelete: constant
+ rtems_semaphore_create: constant
+ rtems_semaphore_delete: constant
+ rtems_semaphore_obtain: constant
+ rtems_semaphore_release: constant
+ MoveFileW: linear_scan
+ WriteConsoleW: linear_scan
+ OutputDebugStringW: linear_scan
+ RegisterEventSourceW: linear_scan
+ DeregisterEventSource: constant
+ RegCreateKeyExW: linear_scan
+ RegOpenKeyExW: linear_scan
+ RegSetValueExW: linear_scan
+ RegCloseKey: constant
+ RegDeleteKeyW: linear_scan
+ ReportEventW: linear_scan
+ __android_log_print: linear_scan
NonCallPrefix:
"static_cast<": declaration
"reinterpret_cast<": declaration
"const_cast<": declaration
"dynamic_cast<": declaration
+NonCallConstruct:
+ defined: declaration
diff --git a/gems/fact-mine/config/stdlib_complexity/csharp.yml b/gems/fact-mine/config/stdlib_complexity/csharp.yml
index ec3c1f779..f7f7f2c31 100644
--- a/gems/fact-mine/config/stdlib_complexity/csharp.yml
+++ b/gems/fact-mine/config/stdlib_complexity/csharp.yml
@@ -8,6 +8,7 @@ Array:
ToArray: linear_materialize
ToList: linear_materialize
OrderBy: sort
+ Take: constant
Hash:
get_Item: constant
TryGetValue: linear_scan
@@ -24,6 +25,7 @@ Set:
ToArray: linear_materialize
String:
Length: constant
+ ToString: constant
Contains: linear_scan
IndexOf: linear_scan
Split: linear_materialize
@@ -64,6 +66,56 @@ Object:
GetType: constant
Int32:
Parse: linear_scan
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Int16:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Int64:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+UInt16:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+UInt32:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+UInt64:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Byte:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+SByte:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Single:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Double:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+Decimal:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+DateOnly:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+DateTime:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+DateTimeOffset:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+TimeOnly:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+TimeSpan:
+ ToString: linear_materialize
+ TryFormat: linear_materialize
+TextWriter:
+ Write: linear_scan
+ WriteLine: linear_scan
Intrinsic:
"Array.BinarySearch": logarithmic
"Array.Clear": linear_scan
@@ -98,9 +150,221 @@ Intrinsic:
"String.IsNullOrEmpty": constant
"String.IsNullOrWhiteSpace": linear_scan
"String.Join": linear_materialize
+ "StringWriter.ctor": constant
+ "Exception.ctor": constant
+ nameof: constant
+ sizeof: constant
+ typeof: constant
Namespace:
System: declaration
NonCallConstruct:
nameof: declaration
sizeof: declaration
typeof: declaration
+ParametricCall:
+ 'Array.Select': callback_linear
+
+# Exact descriptors emitted by scip-dotnet. Keep these overload-specific:
+# scip-dotnet's owner spelling (for example `IO/TextWriter`) is intentionally
+# not guessed from a short receiver name.
+SemanticSymbol:
+ 'Generic/Dictionary#Add().': linear_materialize
+ 'Generic/Dictionary#ContainsKey().': linear_scan
+ 'Generic/Dictionary#Remove().': linear_scan
+ 'Generic/Dictionary#TryAdd().': linear_materialize
+ 'Generic/Dictionary#TryGetValue().': linear_scan
+ 'Generic/HashSet#Add().': linear_materialize
+ 'Generic/HashSet#Clear().': linear_scan
+ 'Generic/HashSet#Contains().': linear_scan
+ 'Generic/IReadOnlyDictionary#ContainsKey().': linear_scan
+ 'Generic/IReadOnlyDictionary#TryGetValue().': linear_scan
+ 'Generic/List#Add().': linear_materialize
+ 'Generic/List#AddRange().': linear_materialize
+ 'Generic/List#Clear().': linear_scan
+ 'Generic/List#Insert().': linear_materialize
+ 'Generic/List#ToArray().': linear_materialize
+ 'Generic/Queue#Clear().': linear_scan
+ 'Generic/Queue#Enqueue().': linear_materialize
+ 'Collections/Hashtable#Clear().': linear_scan
+ 'IO/StringWriter#Dispose().': constant
+ 'IO/StringWriter#GetStringBuilder().': constant
+ 'IO/StringWriter#ToString().': linear_materialize
+ 'IO/TextWriter#Dispose().': constant
+ 'IO/TextWriter#Write(+1).': constant
+ 'IO/TextWriter#Write(+3).': linear_scan
+ 'IO/TextWriter#Write(+9).': linear_scan
+ 'IO/TextWriter#Write(+11).': linear_scan
+ 'IO/TextWriter#Write(+17).': linear_scan
+ 'IO/TextWriter#WriteLine(+12).': linear_scan
+ 'Linq/Enumerable#Concat().': constant
+ 'Linq/Enumerable#Distinct().': linear_materialize
+ 'Linq/Enumerable#First().': linear_scan
+ 'Linq/Enumerable#Single().': linear_scan
+ 'Linq/Enumerable#Skip().': constant
+ 'Linq/Enumerable#ToArray().': linear_materialize
+ 'Linq/Enumerable#ToList().': linear_materialize
+ 'Metrics/Counter#Add().': constant
+ 'Metrics/Counter#Add(+6).': constant
+ 'Reflection/MemberInfo#Name.': constant
+ 'Reflection/MethodBase#GetParameters().': linear_materialize
+ 'Reflection/ParameterInfo#ParameterType.': constant
+ 'Reflection/PropertyInfo#GetIndexParameters().': linear_materialize
+ 'System/Array#CreateInstance().': linear_materialize
+ 'System/Array#GetLength().': constant
+ 'System/Array#GetValue(+3).': constant
+ 'System/Array#Length.': constant
+ 'System/Array#Resize().': linear_materialize
+ 'System/Byte#TryFormat(+1).': linear_materialize
+ 'System/Byte#ToString(+3).': linear_materialize
+ 'System/Char#IsDigit().': constant
+ 'System/Enum#Parse(+2).': linear_scan
+ 'System/Enum#ToString().': linear_materialize
+ 'System/Enum#TryParse().': linear_scan
+ 'System/Exception#GetType().': constant
+ 'System/Exception#ToString().': linear_materialize
+ 'System/AppContext#TryGetSwitch().': linear_scan
+ 'System/Int32#TryParse(+6).': linear_scan
+ 'System/DateOnly#ToString(+2).': linear_materialize
+ 'System/DateOnly#TryFormat(+1).': linear_materialize
+ 'System/DateTime#ToString(+3).': linear_materialize
+ 'System/DateTime#TryFormat(+1).': linear_materialize
+ 'System/DateTimeOffset#ToString(+3).': linear_materialize
+ 'System/DateTimeOffset#TryFormat(+1).': linear_materialize
+ 'System/Decimal#ToString(+3).': linear_materialize
+ 'System/Decimal#TryFormat(+1).': linear_materialize
+ 'System/Double#ToString(+1).': linear_materialize
+ 'System/Double#ToString(+3).': linear_materialize
+ 'System/Double#TryFormat(+1).': linear_materialize
+ 'System/Int16#ToString(+3).': linear_materialize
+ 'System/Int16#TryFormat(+1).': linear_materialize
+ 'System/Int32#ToString(+3).': linear_materialize
+ 'System/Int32#TryFormat(+1).': linear_materialize
+ 'System/Int64#ToString(+3).': linear_materialize
+ 'System/Int64#TryFormat(+1).': linear_materialize
+ 'System/Math#Max(+5).': constant
+ 'System/Math#Min(+4).': constant
+ 'System/MemoryExtensions#AsSpan().': constant
+ 'System/MemoryExtensions#Contains().': linear_scan
+ 'System/MemoryExtensions#Reverse().': linear_scan
+ 'System/MemoryExtensions#StartsWith().': linear_scan
+ 'System/Nullable#ToString().': linear_materialize
+ 'System/Object#GetType().': constant
+ 'System/Object#ReferenceEquals().': constant
+ 'System/Span#Slice(+1).': constant
+ 'System/String#Format(+11).': linear_materialize
+ 'System/String#IndexOf().': linear_scan
+ 'System/String#IsNullOrWhiteSpace().': linear_scan
+ 'System/String#StartsWith(+1).': linear_scan
+ 'System/String#Substring().': linear_materialize
+ 'System/String#Substring(+1).': linear_materialize
+ 'System/String#Trim().': linear_materialize
+ 'System/SByte#ToString(+3).': linear_materialize
+ 'System/SByte#TryFormat(+1).': linear_materialize
+ 'System/Single#ToString(+1).': linear_materialize
+ 'System/Single#ToString(+3).': linear_materialize
+ 'System/Single#TryFormat(+1).': linear_materialize
+ 'System/TimeOnly#ToString(+2).': linear_materialize
+ 'System/TimeOnly#TryFormat(+1).': linear_materialize
+ 'System/TimeSpan#ToString().': linear_materialize
+ 'System/TimeSpan#TryFormat(+1).': linear_materialize
+ 'System/Type#FullName.': linear_materialize
+ 'System/Type#GetGenericTypeDefinition().': constant
+ 'System/Type#GetProperties(+1).': linear_materialize
+ 'System/Type#GetType(+2).': linear_scan
+ 'Tasks/Task#ConfigureAwait().': constant
+ 'Tasks/Task#WhenAny(+1).': linear_materialize
+ 'Tasks/ValueTask#ConfigureAwait().': constant
+ 'Text/StringBuilder#Append(+2).': linear_materialize
+ 'Text/StringBuilder#Append(+15).': linear_materialize
+ 'Text/StringBuilder#ToString().': linear_materialize
+ 'Threading/Interlocked#Exchange().': constant
+ 'Channels/ChannelReader#TryRead().': constant
+ 'System/UInt16#ToString(+3).': linear_materialize
+ 'System/UInt16#TryFormat(+1).': linear_materialize
+ 'System/UInt32#ToString(+3).': linear_materialize
+ 'System/UInt32#TryFormat(+1).': linear_materialize
+ 'System/UInt64#ToString(+3).': linear_materialize
+ 'System/UInt64#TryFormat(+1).': linear_materialize
+ 'Linq/Enumerable#Any().': linear_scan
+ 'Linq/Enumerable#FirstOrDefault().': constant
+ 'Reflection/MemberInfo#IsDefined().': linear_scan
+ 'System/Array#SetValue().': constant
+ 'System/Char#IsLetter().': constant
+ 'System/Char#IsLetterOrDigit().': constant
+ 'System/Convert#ChangeType().': linear_materialize
+ 'System/Convert#ToHexString().': linear_materialize
+ 'System/Convert#ToHexString(+1).': linear_materialize
+ 'System/Double#IsInfinity().': constant
+ 'System/Double#IsNaN().': constant
+ 'System/Single#IsInfinity().': constant
+ 'System/Single#IsNaN().': constant
+ 'System/ReadOnlySpan#Slice().': constant
+ 'System/ReadOnlySpan#Slice(+1).': constant
+ 'System/String#IndexOfAny(+1).': linear_scan
+ 'System/String#ToLowerInvariant().': linear_materialize
+ 'System/String#ToUpperInvariant().': linear_materialize
+ 'System/String#TrimEnd().': linear_scan
+ 'System/TimeProvider#GetElapsedTime().': constant
+ 'System/TimeProvider#GetTimestamp().': constant
+ 'System/TimeSpan#Add().': constant
+ 'System/TimeSpan#FromMinutes(+1).': constant
+ 'System/TimeSpan#FromSeconds(+1).': constant
+ 'System/TimeSpan#FromTicks().': constant
+ 'System/TimeSpan#Parse(+1).': linear_scan
+ 'System/Type#Assembly.': constant
+ 'System/Type#GetArrayRank().': constant
+ 'System/Type#GetConstructors(+1).': linear_materialize
+ 'System/Type#GetElementType().': constant
+ 'System/Type#GetFields(+1).': linear_materialize
+ 'System/Type#GetMethods(+1).': linear_materialize
+ 'Tasks/ValueTask#AsTask().': constant
+ 'Text/StringBuilder#Clear().': constant
+ 'Channels/ChannelReader#TryPeek().': constant
+ 'Channels/ChannelReader#WaitToReadAsync().': constant
+ 'Channels/ChannelWriter#TryWrite().': constant
+ 'CompilerServices/RuntimeHelpers#GetHashCode().': constant
+ 'Diagnostics/ActivitySpanId#ToString().': constant
+ 'Diagnostics/ActivityTraceId#ToString().': constant
+ 'InteropServices/MemoryMarshal#CreateSpan().': constant
+ 'RegularExpressions/Regex#IsMatch(+5).': linear_scan
+ 'RegularExpressions/Regex#IsMatch(+7).': linear_scan
+ 'RegularExpressions/Regex#Match().': linear_materialize
+ExternalLatency:
+ 'Assembly.Load': linear_materialize
+ 'Task.Delay': constant
+ 'Task.Wait': constant
+
+# These contracts include user-supplied predicate/selector work. Treating
+# them as plain O(N) would silently erase a non-constant callback body.
+SemanticSymbolParametricCost:
+ 'IO/TextWriter#Flush().': callback_once
+ 'IO/TextWriter#Write(+8).': callback_once
+ 'Linq/Enumerable#All().': callback_linear
+ 'Linq/Enumerable#Count().': callback_linear
+ 'Linq/Enumerable#GroupBy().': callback_linear
+ 'Linq/Enumerable#OrderByDescending().': callback_sort
+ 'Linq/Enumerable#Select().': callback_linear
+ 'Linq/Enumerable#SelectMany().': callback_linear
+ 'Linq/Enumerable#ToDictionary().': callback_linear
+ 'Linq/Enumerable#Where().': callback_linear
+ 'Reflection/ConstructorInfo#Invoke().': reflective_once
+ 'Reflection/FieldInfo#GetValue().': reflective_once
+ 'Reflection/MethodBase#Invoke().': reflective_once
+ 'Reflection/PropertyInfo#GetValue().': reflective_once
+ 'System/Action#Invoke().': callback_once
+ 'System/EventHandler#Invoke().': callback_once
+ 'System/Func#Invoke().': callback_once
+ 'System/IAsyncDisposable#DisposeAsync().': callback_once
+ 'System/ICustomFormatter#Format().': callback_once
+ 'System/IDisposable#Dispose().': callback_once
+ 'System/IFormatProvider#GetFormat().': callback_once
+ 'System/IFormattable#ToString().': callback_once
+ 'System/ISpanFormattable#TryFormat().': callback_once
+ 'System/Object#Equals(+1).': callback_once
+ 'System/Object#GetHashCode().': callback_once
+ 'System/Object#ToString().': callback_once
+ 'System/String#Create().': callback_linear
+ 'Tasks/Task#ContinueWith().': callback_once
+ 'Tasks/Task#Run(+2).': callback_once
+ 'Threading/CancellationTokenSource#Cancel().': callback_linear
+ 'Channels/ChannelWriter#Complete().': callback_once
diff --git a/gems/fact-mine/config/stdlib_complexity/go.stdlib.json.gz b/gems/fact-mine/config/stdlib_complexity/go.stdlib.json.gz
new file mode 100644
index 000000000..9c0c8de3d
Binary files /dev/null and b/gems/fact-mine/config/stdlib_complexity/go.stdlib.json.gz differ
diff --git a/gems/fact-mine/config/stdlib_complexity/go.yml b/gems/fact-mine/config/stdlib_complexity/go.yml
index 89d59eb01..1e86b43b4 100644
--- a/gems/fact-mine/config/stdlib_complexity/go.yml
+++ b/gems/fact-mine/config/stdlib_complexity/go.yml
@@ -32,6 +32,22 @@ reflect.Value:
Convert: linear_materialize
Type: constant
Uint: constant
+ Addr: constant
+ Bytes: constant
+ CanInterface: constant
+ Cap: constant
+ IsZero: constant
+ MapRange: constant
+ NumMethod: constant
+ OverflowFloat: constant
+ OverflowInt: constant
+ OverflowUint: constant
+ Pointer: constant
+ SetBytes: constant
+ SetLen: constant
+ SetZero: constant
+ UnsafePointer: constant
+ Grow: linear_materialize
reflect.Type:
AssignableTo: constant
Bits: constant
@@ -41,23 +57,39 @@ reflect.Type:
Kind: constant
Len: constant
NumField: constant
+ NumMethod: constant
+ Implements: constant
ConvertibleTo: constant
reflect.StructTag:
Get: linear_scan
strings.Builder:
Len: constant
+ Cap: constant
String: constant
+ Reset: constant
+ Grow: constant
+ WriteByte: constant
+ WriteRune: constant
WriteString: linear_materialize
+ Write: linear_materialize
time.Time:
Add: constant
After: constant
Before: constant
+ Format: linear_materialize
+ IsZero: constant
Truncate: constant
UnixNano: constant
Unix: constant
Sub: constant
Nanosecond: constant
time.Duration:
+ Hours: constant
+ Minutes: constant
+ Seconds: constant
+ Milliseconds: constant
+ Microseconds: constant
+ Nanoseconds: constant
Truncate: constant
crypto.Hash:
Available: constant
@@ -70,6 +102,24 @@ context.Context:
sync.Pool:
Get: constant
Put: constant
+# Go 1.19+ typed atomics: lock-free single-word ops, all O(1).
+atomic.Bool: &atomic_methods
+ Load: constant
+ Store: constant
+ Swap: constant
+ CompareAndSwap: constant
+ Add: constant
+atomic.Int32: *atomic_methods
+atomic.Int64: *atomic_methods
+atomic.Uint32: *atomic_methods
+atomic.Uint64: *atomic_methods
+atomic.Uintptr: *atomic_methods
+atomic.Pointer: *atomic_methods
+atomic.Value:
+ Load: constant
+ Store: constant
+ Swap: constant
+ CompareAndSwap: constant
list.List:
Back: constant
Front: constant
@@ -90,6 +140,9 @@ list.Element:
time.Ticker:
Stop: constant
regexp.Regexp:
+ FindStringSubmatch: linear_materialize
+ Match: linear_scan
+ MatchString: linear_scan
ReplaceAll: linear_materialize
http.Header:
Get: linear_scan
@@ -105,6 +158,88 @@ base64.Encoding:
big.Int:
FillBytes: linear_materialize
SetBytes: linear_materialize
+# `testing.common` is the embedded base of T/B/F, so t.Errorf resolves here.
+# Recording a failure is bounded by the message it formats; the variadic
+# formatting itself is reflective and lives in SemanticSymbolParametricCost.
+testing.common:
+ Helper: constant
+ Name: constant
+ Fail: constant
+ FailNow: constant
+ Failed: constant
+ SkipNow: constant
+ Skipped: constant
+ Cleanup: constant
+testing.T:
+ Parallel: constant
+ Setenv: constant
+ Deadline: constant
+bytes.Buffer:
+ Len: constant
+ Cap: constant
+ Reset: constant
+ Bytes: constant
+ Truncate: constant
+ String: linear_materialize
+ Grow: linear_materialize
+ Write: linear_materialize
+ WriteString: linear_materialize
+ WriteByte: constant
+ WriteRune: constant
+ Read: linear_scan
+ ReadString: linear_scan
+ Next: linear_scan
+# os.FileInfo is a public type alias for io/fs.FileInfo. Keep both canonical
+# spellings because build-tagged files may be parsed without a platform SCIP
+# document while their declared parameter type remains compiler syntax.
+fs.FileInfo: &file_info_methods
+ Name: constant
+ Size: constant
+ Mode: constant
+ ModTime: constant
+ IsDir: constant
+ Sys: constant
+os.FileInfo: *file_info_methods
+fs.DirEntry:
+ Name: constant
+ IsDir: constant
+ Type: constant
+os.ProcessState:
+ ExitCode: constant
+ Success: constant
+ Exited: constant
+ Pid: constant
+os.File:
+ Name: constant
+ Fd: constant
+# Registering a flag stores one descriptor; parsing walks the argument list.
+flag.FlagSet:
+ Bool: constant
+ BoolVar: constant
+ String: constant
+ StringVar: constant
+ Int: constant
+ IntVar: constant
+ Int64: constant
+ Uint: constant
+ Float64: constant
+ Float64Var: constant
+ Duration: constant
+ Var: constant
+ NArg: constant
+ NFlag: constant
+ Args: constant
+ Arg: constant
+ Lookup: constant
+ Parse: linear_scan
+ SetOutput: constant
+bufio.Scanner:
+ Text: linear_materialize
+ Bytes: constant
+ Err: constant
+ Scan: linear_scan
+ Buffer: constant
+ Split: constant
Intrinsic:
len: constant
cap: constant
@@ -138,7 +273,9 @@ Intrinsic:
"errors.Join": linear_materialize
"hmac.Equal": linear_scan
"math.Log10": constant
+ "math.Max": constant
"math.Modf": constant
+ "math.Round": constant
"reflect.Append": linear_materialize
"reflect.ArrayOf": constant
"reflect.MakeSlice": linear_materialize
@@ -154,6 +291,7 @@ Intrinsic:
"runtime.Goexit": constant
"runtime.Stack": linear_materialize
"strconv.Itoa": linear_materialize
+ "strconv.Atoi": linear_scan
"strconv.FormatFloat": linear_materialize
"strconv.FormatInt": linear_materialize
"strconv.FormatUint": linear_materialize
@@ -162,6 +300,7 @@ Intrinsic:
"strconv.ParseInt": linear_scan
"strconv.ParseUint": linear_scan
"strings.SplitN": linear_materialize
+ "strings.NewReplacer": linear_materialize
"time.NewTicker": constant
"time.Until": constant
"time.Parse": linear_scan
@@ -224,6 +363,118 @@ Intrinsic:
"atomic.LoadUint64": constant
"atomic.StoreUint32": constant
"atomic.StoreUint64": constant
+ # internal/bytealg assembly primitives. The exported bytes/strings search and
+ # compare API bottoms out in these; they are pure linear scans (or an O(n)
+ # allocation), so mapping them lets the stdlib's own functions close.
+ "bytealg.Compare": linear_scan
+ "bytealg.Count": linear_scan
+ "bytealg.CountString": linear_scan
+ "bytealg.Equal": linear_scan
+ "bytealg.Index": linear_scan
+ "bytealg.IndexByte": linear_scan
+ "bytealg.IndexByteString": linear_scan
+ "bytealg.IndexString": linear_scan
+ "bytealg.IndexRabinKarp": linear_scan
+ "bytealg.IndexRabinKarpBytes": linear_scan
+ "bytealg.LastIndexByte": linear_scan
+ "bytealg.LastIndexByteString": linear_scan
+ "bytealg.HashStr": linear_scan
+ "bytealg.HashStrBytes": linear_scan
+ "bytealg.Cutover": constant
+ "bytealg.MakeNoZero": linear_materialize
+ # unsafe intrinsics: compile-time size/offset constants and zero-copy pointer
+ # or header construction. All O(1); no runtime work is a function of input.
+ "unsafe.Pointer": constant
+ "unsafe.Sizeof": constant
+ "unsafe.Offsetof": constant
+ "unsafe.Alignof": constant
+ "unsafe.Add": constant
+ "unsafe.Slice": constant
+ "unsafe.SliceData": constant
+ "unsafe.String": constant
+ "unsafe.StringData": constant
+ # runtime hints and bounded introspection: no-ops or stack-depth bounded,
+ # constant with respect to the caller's input.
+ "runtime.KeepAlive": constant
+ "runtime.SetFinalizer": constant
+ "runtime.GC": constant
+ "runtime.Caller": constant
+ "runtime.Callers": constant
+ "runtime.CallersFrames": constant
+ "runtime.NumCPU": constant
+ "runtime.NumGoroutine": constant
+ "runtime.Gosched": constant
+ # Race detector instrumentation: a no-op without -race; O(1).
+ "race.Enable": constant
+ "race.Disable": constant
+ "race.Acquire": constant
+ "race.Release": constant
+ "race.ReleaseMerge": constant
+ "race.Read": constant
+ "race.Write": constant
+ "race.ReadRange": constant
+ "race.WriteRange": constant
+ # math/bits: single-word bit operations, each O(1).
+ "bits.Add": constant
+ "bits.Add32": constant
+ "bits.Add64": constant
+ "bits.Sub": constant
+ "bits.Sub32": constant
+ "bits.Sub64": constant
+ "bits.Mul": constant
+ "bits.Mul32": constant
+ "bits.Mul64": constant
+ "bits.Div": constant
+ "bits.Div32": constant
+ "bits.Div64": constant
+ "bits.Len": constant
+ "bits.Len8": constant
+ "bits.Len16": constant
+ "bits.Len32": constant
+ "bits.Len64": constant
+ "bits.LeadingZeros": constant
+ "bits.LeadingZeros8": constant
+ "bits.LeadingZeros16": constant
+ "bits.LeadingZeros32": constant
+ "bits.LeadingZeros64": constant
+ "bits.TrailingZeros": constant
+ "bits.TrailingZeros32": constant
+ "bits.TrailingZeros64": constant
+ "bits.OnesCount": constant
+ "bits.OnesCount32": constant
+ "bits.OnesCount64": constant
+ "bits.RotateLeft": constant
+ "bits.RotateLeft8": constant
+ "bits.RotateLeft16": constant
+ "bits.RotateLeft32": constant
+ "bits.RotateLeft64": constant
+ "bits.ReverseBytes": constant
+ "bits.ReverseBytes32": constant
+ "bits.ReverseBytes64": constant
+ # Elementary math: constant-time numeric primitives.
+ "math.Float32bits": constant
+ "math.Float64bits": constant
+ "math.Float32frombits": constant
+ "math.Float64frombits": constant
+ "math.Log": constant
+ "math.Log2": constant
+ "math.Exp": constant
+ "math.Sqrt": constant
+ "math.Abs": constant
+ "math.Floor": constant
+ "math.Ceil": constant
+ "math.Trunc": constant
+ "math.Mod": constant
+ "math.Pow": constant
+ "math.Ldexp": constant
+ "math.Frexp": constant
+ # cmp: ordered comparison. Linear to bound the string operand case.
+ "cmp.Less": linear_scan
+ "cmp.Compare": linear_scan
+ "cmp.Or": constant
+ # internal/abi: compile-time layout constants.
+ "abi.FuncPCABI0": constant
+ "abi.FuncPCABIInternal": constant
# Language builtins. These are syntax-proven by the Go adapter and do not
# require a SCIP declaration. Materializing operations use worst-case upper
# bounds, including allocation/copy during append growth.
@@ -261,6 +512,51 @@ Intrinsic:
uintptr: constant
print: declaration
println: declaration
+ # Path manipulation is bounded by the path text. Join/Clean/Dir/Base build a
+ # new string; Ext and IsAbs only scan the argument.
+ filepath.Join: linear_materialize
+ filepath.Clean: linear_materialize
+ filepath.Dir: linear_materialize
+ filepath.Base: linear_materialize
+ filepath.Rel: linear_materialize
+ filepath.ToSlash: linear_materialize
+ filepath.FromSlash: linear_materialize
+ filepath.Split: linear_scan
+ filepath.SplitList: linear_materialize
+ filepath.Ext: linear_scan
+ filepath.IsAbs: linear_scan
+ filepath.Match: linear_scan
+ filepath.VolumeName: linear_scan
+ # Environment access scans the process environment block.
+ os.Getenv: linear_scan
+ os.LookupEnv: linear_scan
+ os.Setenv: linear_scan
+ os.Unsetenv: linear_scan
+ os.Environ: linear_materialize
+ os.IsNotExist: constant
+ os.IsExist: constant
+ os.IsPermission: constant
+ os.Getpid: constant
+ os.NewFile: constant
+ flag.Bool: constant
+ flag.String: constant
+ regexp.Compile: linear_materialize
+ exec.Command: linear_materialize
+ exec.CommandContext: linear_materialize
+ bytes.NewReader: constant
+ bytes.NewBuffer: constant
+ bytes.NewBufferString: linear_materialize
+ bytes.TrimSpace: linear_scan
+ bufio.NewScanner: constant
+ bufio.NewReader: constant
+ bufio.ScanLines: linear_scan
+ bufio.ScanWords: linear_scan
+ flag.NewFlagSet: constant
+ strings.IndexByte: linear_scan
+ strings.TrimLeft: linear_scan
+ time.Since: constant
+ time.Date: constant
+ errors.New: linear_materialize
ParametricCall:
# `error` is a predeclared open interface. The declared receiver proves one
# dispatch, but the implementation cost remains the symbolic callback C.
@@ -268,6 +564,9 @@ ParametricCall:
CallableType:
"context.CancelFunc": callback_once
ExternalLatency:
+ # Reader methods scan/materialize bytes supplied by an external stream. The
+ # byte-processing bound is complete while stream latency remains excluded.
+ "bufio.Reader.ReadString": linear_materialize
"io.ReadAll": linear_materialize
"os.ReadFile": linear_materialize
"os.Open": linear_scan
@@ -288,13 +587,64 @@ ExternalLatency:
"sync.WaitGroup.Done": constant
"sync.WaitGroup.Wait": constant
"time.Sleep": constant
+ # Filesystem and process syscalls. The computational bound is what the call
+ # does with the bytes it moves; device, scheduler, and network latency are
+ # excluded and declared as an assumption by the caller of this table.
+ "os.WriteFile": linear_scan
+ "os.Stat": constant
+ "os.Lstat": constant
+ "os.Mkdir": constant
+ "os.MkdirAll": linear_scan
+ "os.MkdirTemp": constant
+ "os.CreateTemp": constant
+ "os.Create": constant
+ "os.OpenFile": constant
+ "os.Remove": constant
+ "os.RemoveAll": linear_scan
+ "os.Rename": constant
+ "os.Chmod": constant
+ "os.Chtimes": constant
+ "os.Chdir": constant
+ "os.Getwd": linear_materialize
+ "os.UserHomeDir": linear_materialize
+ "os.UserCacheDir": linear_materialize
+ "os.ReadDir": linear_materialize
+ "os.Symlink": constant
+ "os.Readlink": linear_materialize
+ "os.Truncate": constant
+ "os.File.Write": linear_scan
+ "os.File.WriteString": linear_scan
+ "os.File.Read": linear_scan
+ "os.File.Sync": constant
+ "os.File.Stat": constant
+ "os.File.Seek": constant
+ "os.Getuid": constant
+ "os.Executable": linear_materialize
+ "syscall.Flock": constant
+ "syscall.Statfs": constant
+ "fs.DirEntry.Info": constant
+ "filepath.Abs": linear_materialize
+ "filepath.EvalSymlinks": linear_materialize
+ "filepath.Glob": linear_materialize
+ "exec.LookPath": linear_scan
+ "exec.Cmd.Run": constant
+ "exec.Cmd.Start": constant
+ "exec.Cmd.Wait": constant
+ "exec.Cmd.Output": linear_materialize
+ "exec.Cmd.CombinedOutput": linear_materialize
+ "exec.Cmd.String": linear_materialize
+ "exec.Cmd.StdoutPipe": constant
+ # Creating the harness temp dir registers a cleanup and makes one directory.
+ "testing.common.TempDir": constant
SemanticSymbolParametricCost:
'sort/Sort().': callback_sort
+ 'sort/Slice().': callback_sort
'`crypto/elliptic`/Curve#Params().': callback_once
'crypto/Signer#Sign().': callback_once
'flag/Usage.': callback_once
'fmt/Errorf().': reflective_once
'fmt/Fprintf().': reflective_once
+ 'fmt/Print().': reflective_once
'fmt/Printf().': reflective_once
'fmt/Println().': reflective_once
'fmt/Sprintf().': reflective_once
@@ -302,10 +652,37 @@ SemanticSymbolParametricCost:
'reflect/Value#Call().': reflective_once
'`encoding/json`/Marshal().': reflective_once
'`encoding/json`/Unmarshal().': reflective_once
+ '`encoding/xml`/Unmarshal().': reflective_once
'`encoding/json`/Decoder#Decode().': reflective_once
+ '`encoding/json`/Encoder#Encode().': reflective_once
'`encoding/json`/MarshalIndent().': reflective_once
'errors/Is().': reflective_once
'flag/Parse().': reflective_once
+ 'flag/FlagSet#Visit().': callback_linear
+ 'flag/FlagSet#VisitAll().': callback_linear
+ # A subtest runs an arbitrary body, so its cost is the callback's.
+ 'testing/T#Run().': callback_once
+ 'testing/B#Run().': callback_once
+ # The harness failure/log calls format a variadic `...any` exactly the way
+ # fmt does, so they carry the same open reflective cost rather than a
+ # fabricated constant.
+ 'testing/common#Errorf().': reflective_once
+ 'testing/common#Error().': reflective_once
+ 'testing/common#Fatalf().': reflective_once
+ 'testing/common#Fatal().': reflective_once
+ 'testing/common#Logf().': reflective_once
+ 'testing/common#Log().': reflective_once
+ 'testing/common#Skipf().': reflective_once
+ 'testing/common#Skip().': reflective_once
+ 'fmt/Fprintln().': reflective_once
+ 'fmt/Sprint().': reflective_once
+ 'fmt/Sprintln().': reflective_once
+ 'fmt/Fprint().': reflective_once
+ 'fmt/Sscanf().': reflective_once
+ 'reflect/DeepEqual().': reflective_once
+ # Walking a tree runs the visitor once per entry.
+ '`path/filepath`/WalkDir().': callback_linear
+ '`path/filepath`/Walk().': callback_linear
'flag/PrintDefaults().': reflective_once
'flag/Var().': reflective_once
'`net/http`/Request#ParseMultipartForm().': reflective_once
diff --git a/gems/fact-mine/config/stdlib_complexity/java.yml b/gems/fact-mine/config/stdlib_complexity/java.yml
index 9f3080b7a..b2cf99d4b 100644
--- a/gems/fact-mine/config/stdlib_complexity/java.yml
+++ b/gems/fact-mine/config/stdlib_complexity/java.yml
@@ -298,7 +298,7 @@ SemanticSymbol:
"java/lang/Object#clone().": linear_materialize
"java/lang/String#valueOf(+4).": constant
"java/util/Comparator#comparing(+1).": constant
- "java/util/Objects#hash().": linear_materialize
+ "java/util/Objects#hash().": linear_scan
"java/util/stream/Stream#filter().": constant
"java/util/stream/Stream#map().": constant
"java/util/regex/Matcher#lookingAt().": exponential
diff --git a/gems/fact-mine/config/stdlib_complexity/kotlin.yml b/gems/fact-mine/config/stdlib_complexity/kotlin.yml
index 9e0b77706..a8841f1fe 100644
--- a/gems/fact-mine/config/stdlib_complexity/kotlin.yml
+++ b/gems/fact-mine/config/stdlib_complexity/kotlin.yml
@@ -45,19 +45,29 @@ SemanticSymbol:
'kotlin/IntArray#``().': linear_materialize
'kotlin/Double#toInt().': constant
'kotlin/Int#toDouble().': constant
+ 'kotlin/checkNotNull(+1).': constant
'kotlin/collections/emptyList().': constant
'kotlin/collections/emptyMap().': constant
+ 'kotlin/collections/contentHashCode().': linear_scan
+ 'kotlin/collections/contentToString(+3).': linear_materialize
+ 'kotlin/collections/getOrNull(+9).': linear_scan
+ 'kotlin/collections/getValue().': linear_scan
'kotlin/collections/listOf().': constant
'kotlin/collections/listOf(+1).': linear_materialize
'kotlin/collections/mapOf(+2).': linear_materialize
+ 'kotlin/collections/mutableListOf().': constant
'kotlin/collections/copyOf(+11).': linear_materialize
'kotlin/collections/single(+19).': linear_scan
'kotlin/collections/sum(+8).': linear_scan
'kotlin/collections/toCharArray(+1).': linear_materialize
'kotlin/collections/toDoubleArray(+1).': linear_materialize
+ 'kotlin/collections/toList(+10).': linear_materialize
+ 'kotlin/collections/toMutableList(+10).': linear_materialize
'kotlin/collections/toSet(+9).': linear_materialize
'kotlin/collections/toTypedArray().': linear_materialize
'kotlin/isNaN().': constant
+ 'kotlin/require(+1).': constant
+ 'kotlin/requireNotNull(+1).': constant
'kotlin/math/ceil().': constant
'kotlin/math/floor().': constant
'kotlin/math/log10().': constant
@@ -76,7 +86,12 @@ SemanticSymbol:
'kotlin/text/toIntOrNull().': linear_scan
'kotlin/text/toLong().': linear_scan
'kotlin/text/trim(+5).': linear_materialize
+ 'kotlin/text/trimMargin().': linear_materialize
'kotlin/text/uppercase(+2).': linear_materialize
+ 'kotlin/collections/MutableList#add().': linear_materialize
+ 'kotlin/collections/MutableList#addAll().': linear_materialize
+ 'kotlin/collections/addAll(+2).': linear_materialize
+ 'kotlin/collections/MutableList#isEmpty().': constant
# Higher-order contracts retain the callback cost as a parameter. They are
# upper bounds, not constant-cost stand-ins for the lambda body.
@@ -91,6 +106,8 @@ SemanticSymbolParametricCost:
'kotlin/collections/associateBy(+19).': callback_linear
'kotlin/collections/associate(+9).': callback_linear
'kotlin/collections/associateWith(+9).': callback_linear
+ 'kotlin/collections/count(+1).': callback_linear
+ 'kotlin/collections/filter(+9).': callback_linear
'kotlin/collections/filter(+10).': callback_linear
'kotlin/collections/flatMap(+10).': callback_linear
'kotlin/collections/fold(+9).': callback_linear
@@ -102,8 +119,11 @@ SemanticSymbolParametricCost:
'kotlin/collections/joinToString(+9).': callback_linear
'kotlin/collections/map(+6).': callback_linear
'kotlin/collections/map(+9).': callback_linear
+ 'kotlin/collections/map().': callback_linear
'kotlin/collections/maxOf(+29).': callback_linear
+ 'kotlin/collections/sortedBy(+9).': callback_sort
'kotlin/collections/sumOf(+24).': callback_linear
'kotlin/collections/sumOf(+27).': callback_linear
+ 'kotlin/collections/sumOf(+66).': callback_linear
'kotlin/repeat().': callback_linear
'kotlin/text/buildString().': callback_once
diff --git a/gems/fact-mine/config/stdlib_complexity/ruby.yml b/gems/fact-mine/config/stdlib_complexity/ruby.yml
index c3569bf7f..a5b000a7d 100644
--- a/gems/fact-mine/config/stdlib_complexity/ruby.yml
+++ b/gems/fact-mine/config/stdlib_complexity/ruby.yml
@@ -2,6 +2,8 @@
# rendered Big-O strings: Fact-Mine emits the facts and Espalier owns algebra.
Array:
"[]": constant
+ "[]=": linear_materialize
+ "<<": constant
each: linear_scan
each_with_index: linear_scan
each_index: linear_scan
@@ -38,17 +40,29 @@ Array:
unshift: linear_scan
count: linear_scan
sum: linear_scan
+ min: linear_scan
+ max: linear_scan
+ product: pairwise
delete: linear_scan
insert: linear_scan
clear: linear_scan
freeze: constant
sort: sort
sort_by: sort
+ bsearch: logarithmic
+ bsearch_index: logarithmic
"-": pairwise
"&": pairwise
"|": pairwise
+ uniq!: linear_scan
+ reject!: linear_scan
+ select!: linear_scan
+ map!: linear_scan
+ sort!: sort
+ sort_by!: sort
Hash:
"[]": constant
+ "[]=": constant
key?: constant
has_key?: constant
include?: constant
@@ -71,12 +85,20 @@ Hash:
merge: linear_materialize
keys: linear_materialize
values: linear_materialize
+ values_at: linear_materialize
dup: linear_materialize
to_set: linear_materialize
transform_keys: linear_materialize
transform_values: linear_materialize
sort: sort
sort_by: sort
+ to_a: linear_materialize
+ slice: linear_materialize
+ transform_values!: linear_scan
+OptionParser:
+ banner=: constant
+ parse!: linear_scan
+ to_s: linear_materialize
Set:
include?: constant
contains: constant
@@ -84,6 +106,7 @@ Set:
size: constant
empty?: constant
add: constant
+ "<<": constant
delete: constant
add?: constant
merge: linear_scan
@@ -105,6 +128,7 @@ String:
gsub: linear_materialize
split: linear_materialize
"+": linear_materialize
+ "<<": linear_materialize
to_s: constant
to_sym: linear_materialize
strip: linear_materialize
@@ -124,6 +148,9 @@ String:
delete_prefix: linear_materialize
delete_suffix: linear_materialize
downcase: linear_materialize
+ upcase: linear_materialize
+ capitalize: linear_materialize
+ casecmp: linear_scan
inspect: linear_materialize
reverse: linear_materialize
lstrip: linear_materialize
@@ -136,6 +163,7 @@ String:
to_i: linear_scan
to_f: linear_scan
tr: linear_materialize
+ delete: linear_materialize
Symbol:
to_s: linear_materialize
inspect: linear_materialize
@@ -155,16 +183,52 @@ Kernel:
binding: constant
methods: linear_materialize
hash: linear_scan
+ instance_variable_set: constant
+ __dir__: constant
+ frozen?: constant
+ method: constant
+ rand: constant
+ Integer: linear_scan
+ Float: linear_scan
BasicObject:
equal?: constant
Integer:
+ "+": linear_materialize
+ "-": linear_materialize
+ "*": pairwise
+ "/": pairwise
+ "%": pairwise
+ "<": linear_scan
+ "<=": linear_scan
+ ">": linear_scan
+ ">=": linear_scan
+ "<=>": linear_scan
+ "==": linear_scan
positive?: constant
zero?: constant
abs: constant
to_s: linear_materialize
to_f: constant
chr: constant
+ modulo: pairwise
+ floor: constant
+ ceil: constant
Float:
+ "+": constant
+ "-": constant
+ "*": constant
+ "/": constant
+ "%": constant
+ "<": constant
+ "<=": constant
+ ">": constant
+ ">=": constant
+ "<=>": constant
+ "==": constant
+ modulo: constant
+ floor: constant
+ ceil: constant
+ round: constant
to_i: constant
T:
must: constant
@@ -179,17 +243,80 @@ File:
Regexp:
source: constant
match: linear_scan
+ match?: exponential
+ last_match: constant
MatchData:
begin: constant
+ "[]": linear_materialize
Class:
name: constant
Time:
strftime: linear_materialize
+ now: constant
+Pathname:
+ to_s: constant
+ path: constant
+ absolute?: constant
+ relative_path_from: linear_materialize
+Process/Status:
+ success?: constant
+Process:
+ pid: constant
+Digest/Class:
+ new: constant
+ hexdigest: linear_materialize
+ file: linear_materialize
+Digest/Instance:
+ hexdigest: linear_materialize
+IO:
+ path: constant
+ close: constant
+ puts: linear_scan
+Tempfile:
+ path: constant
+ close: constant
+ unlink: linear_scan
Enumerable:
+ each: linear_scan
+ each_with_index: linear_scan
+ each_with_object: linear_scan
+ map: linear_materialize
+ flat_map: linear_materialize
+ select: linear_materialize
+ reject: linear_materialize
+ compact: linear_materialize
+ to_h: linear_materialize
to_a: linear_materialize
+ to_set: linear_materialize
count: linear_scan
first: constant
+ min: linear_scan
+ max: linear_scan
+ partition: linear_materialize
include?: linear_scan
+ any?: linear_scan
+ all?: linear_scan
+ one?: linear_scan
+ sort: sort
+ sort_by: sort
+ tally: linear_materialize
+ grep: linear_materialize
+Numeric:
+ "+": linear_materialize
+ "-": linear_materialize
+ "*": pairwise
+ "/": pairwise
+ "%": pairwise
+ positive?: constant
+ negative?: constant
+ zero?: constant
+ to_i: constant
+ to_f: constant
+NilClass:
+ nil?: constant
+ to_i: constant
+ to_f: constant
+ to_a: constant
StringScanner:
"[]": constant
eos?: constant
@@ -198,6 +325,23 @@ StringScanner:
Intrinsic:
"Array.new": linear_materialize
"Hash.new": constant
+ "Set.new": linear_materialize
+ "Pathname.new": linear_materialize
+ "Dir.pwd": linear_materialize
+SemanticSymbol:
+ # CRuby installs one generated member surface per declared field. The C
+ # producer currently reports this registration path as incomplete because
+ # it crosses VM callbacks; this reviewed O(N) fallback is used only until
+ # the source proof can replace it.
+ 'Struct.new().': linear_materialize
+ 'Hash.`[]`().': linear_materialize
+ 'JSON.parse().': linear_materialize
+ 'JSON.generate().': linear_materialize
+ 'JSON.pretty_generate().': linear_materialize
+ 'Psych.safe_load().': linear_materialize
+ # Onigmo is a backtracking engine. `match?` has an exponential worst case
+ # in the pattern/input pair and linear engine stack space.
+ 'Regexp#`match?`().': exponential
ExternalLatency:
"File.exist?": linear_scan
"File.read": linear_materialize
@@ -210,19 +354,53 @@ ExternalLatency:
"IO.flush": constant
"IO.popen": linear_scan
"IO.foreach": linear_scan
+ "Zlib/GzipReader.read": linear_materialize
"File.mtime": constant
"File.delete": linear_scan
"File.file?": linear_scan
+ "File.directory?": linear_scan
+ "File.size?": linear_scan
+ "File.rename": linear_scan
+ "File.extname": linear_materialize
+ "File.fnmatch?": linear_scan
"File.symlink?": linear_scan
"File.symlink": linear_scan
"File.readlink": linear_materialize
"Dir.exist?": linear_scan
"Dir.glob": linear_materialize
+ "FileUtils.mkdir_p": linear_scan
+ "FileUtils.copy_file": linear_scan
+ "FileUtils.rm_rf": linear_scan
+ "Dir.mktmpdir": linear_materialize
+ "Tempfile.new": linear_materialize
+ "Open3.capture2": linear_materialize
+ "Open3.capture2e": linear_materialize
+ "Open3.capture3": linear_materialize
"Dir.pwd": linear_materialize
"Kernel.printf": linear_scan
"Kernel.warn": linear_scan
"Kernel.exit": constant
"Kernel.system": linear_scan
+ # Activating a gem scans the installed specification/load-path surface.
+ # Filesystem latency is excluded; N is the activation search surface.
+ "Kernel.gem": linear_scan
+ # Loading YAML reads and parses input proportional to the file contents.
+ # Filesystem latency is excluded from the CPU/space bound.
+ "Psych.load_file": linear_materialize
+ExternalLatencyParametric:
+ # Native path APIs invoke Ruby's to_path before their bounded byte/path
+ # work. C is that single coercion callback and N is the path/output size.
+ "File.realpath": coercive_linear_materialize
+ "File.executable?": coercive_linear_scan
+ "File.absolute_path?": coercive_linear_scan
+ "IO.read": coercive_linear_materialize
+ # A runtime receiver may have converged to IO or StringIO. Both perform
+ # the same bounded coercion/read/materialization work, so the generic SCIP
+ # candidate join can retain the contract instead of dropping an otherwise
+ # complete observed IO.read callsite.
+ "StringIO.read": coercive_linear_materialize
+ "Dir.[]": coercive_linear_materialize
+ "Dir.chdir": coercive_linear_scan
SemanticSymbolParametricCost:
'Class#new().': reflective_once
'Proc0#call().': callback_once
@@ -240,6 +418,17 @@ SemanticSymbolParametricCost:
'Kernel#format().': reflective_once
'Kernel#inspect().': reflective_once
'Kernel#Array().': reflective_once
+ 'Kernel#Hash().': reflective_once
+ # `require` selects and executes an arbitrary loader/body. This is an open
+ # reflective target rather than a fabricated constant-time loader bound.
+ 'Kernel#require().': loader_once
+ # CRuby's Exception#message uses rb_funcallv(exc, :to_s, ...).
+ 'Exception#message().': callback_once
+ # Get_Double may call Numeric#to_f; a later runtime type proof can narrow
+ # Float to O(1), but the unspecialized source contract remains symbolic.
+ 'Math.exp().': callback_once
+ # reg_operand calls to_str once before rb_reg_quote scans and materializes.
+ 'Regexp.escape().': coercive_linear_materialize
'Kernel#loop().': callback_linear
'Range#each().': callback_linear
'Array#each().': callback_linear
@@ -256,39 +445,62 @@ SemanticSymbolParametricCost:
'Set#each().': callback_linear
'Enumerable#`any?`().': callback_linear
'Enumerable#`all?`().': callback_linear
+ 'Enumerable#`one?`().': callback_linear
'Enumerable#filter_map().': callback_linear
'Enumerable#flat_map().': callback_linear
'Enumerable#find().': callback_linear
'Enumerable#map().': callback_linear
'Enumerable#each().': callback_linear
'Enumerable#each_with_object().': callback_linear
+ # each_slice drives #each and yields slices to an arbitrary block.
+ 'Enumerable#each_slice().': callback_linear
'Enumerable#select().': callback_linear
'Enumerable#reduce().': callback_linear
'Enumerable#group_by().': callback_linear
'Enumerable#find_index().': callback_linear
'Enumerable#`none?`().': callback_linear
+ 'Enumerable#min_by().': callback_linear
+ 'Enumerable#max_by().': callback_linear
+ 'Enumerable#partition().': callback_linear
+ 'Integer#times().': callback_linear
+ 'Comparable#`between?`().': reflective_once
+ 'Comparable#between?().': reflective_once
+ 'Zlib/GzipReader.open().': callback_once
+ 'Enumerator#with_index().': callback_linear
+ 'Method#call().': callback_once
+ 'Kernel#tap().': callback_once
Namespace:
Array: declaration
BasicObject: declaration
Class: declaration
Dir: declaration
+ Digest: declaration
Enumerable: declaration
Enumerator: declaration
File: declaration
+ FileUtils: declaration
Float: declaration
Hash: declaration
IO: declaration
Integer: declaration
Kernel: declaration
MatchData: declaration
+ Method: declaration
Module: declaration
+ NilClass: declaration
+ Numeric: declaration
+ Open3: declaration
Object: declaration
Pathname: declaration
+ Process: declaration
Proc: declaration
Range: declaration
Regexp: declaration
Set: declaration
String: declaration
+ StringIO: declaration
Symbol: declaration
T: declaration
Time: declaration
+ Tempfile: declaration
+ Zlib: declaration
diff --git a/gems/fact-mine/config/stdlib_complexity/rust.yml b/gems/fact-mine/config/stdlib_complexity/rust.yml
index 8f53373d2..cfa1de7a4 100644
--- a/gems/fact-mine/config/stdlib_complexity/rust.yml
+++ b/gems/fact-mine/config/stdlib_complexity/rust.yml
@@ -19,13 +19,27 @@ Array:
sort_by: sort
sort_by_key: sort
iter: constant
+ iter_mut: constant
into_iter: constant
extend: linear_materialize
+ extend_from_slice: linear_materialize
+ append: linear_materialize
join: linear_materialize
to_vec: linear_materialize
dedup: linear_scan
dedup_by: linear_scan
retain: linear_scan
+ as_slice: constant
+ last_mut: constant
+ first_mut: constant
+ # Lazy adapter over the existing buffer; no element is touched.
+ windows: constant
+ chunks: constant
+ # Drops every element it holds.
+ clear: linear_scan
+ truncate: linear_scan
+ with_capacity: linear_materialize
+ sort_unstable: sort
Hash:
len: constant
is_empty: constant
@@ -33,12 +47,18 @@ Hash:
contains_key: constant
insert: constant
remove: constant
+ get_mut: constant
+ iter: constant
+ new: constant
+ entry: constant
+ clone: linear_materialize
Set:
len: constant
is_empty: constant
contains: constant
insert: constant
remove: constant
+ new: constant
String:
len: constant
is_empty: constant
@@ -60,6 +80,7 @@ String:
as_str: constant
clone: linear_materialize
to_ascii_lowercase: linear_materialize
+ to_ascii_uppercase: linear_materialize
iter: constant
split_once: linear_scan
rsplit_once: linear_scan
@@ -72,6 +93,24 @@ String:
char_indices: constant
push: linear_materialize
cmp: linear_scan
+ # Scans and validates the whole input.
+ parse: linear_scan
+ from_utf8: linear_scan
+ # Lazy adapters; the scan happens on the iterator, not here.
+ bytes: constant
+ splitn: constant
+ matches: constant
+ match_indices: constant
+ is_char_boundary: constant
+ # Sets the length after a boundary check; no element is moved.
+ truncate: constant
+ to_lowercase: linear_materialize
+ push_str: linear_materialize
+ clear: constant
+ with_capacity: linear_materialize
+ encode_utf16: constant
+ rsplitn: constant
+ split_inclusive: constant
BTreeMap:
len: constant
is_empty: constant
@@ -86,6 +125,12 @@ BTreeMap:
keys: constant
into_iter: constant
extend: sort
+ get_mut: logarithmic
+ values: constant
+ values_mut: constant
+ into_values: constant
+ # `From<[(K, V); N]>` builds the tree from an unsorted array.
+ from: sort
BTreeSet:
len: constant
is_empty: constant
@@ -97,7 +142,15 @@ BTreeSet:
into_iter: constant
extend: sort
is_disjoint: linear_scan
+ is_subset: sort
intersection: constant
+ difference: constant
+ first: logarithmic
+ pop_first: logarithmic
+ clear: linear_scan
+ new: constant
+ # `From<[T; N]>` builds the tree from an unsorted array.
+ from: sort
Option:
unwrap: constant
expect: constant
@@ -114,11 +167,18 @@ Option:
or: constant
ok: constant
clone: linear_materialize
+ transpose: constant
+ take: constant
+ as_mut: constant
+ get_or_insert: constant
Result:
unwrap: constant
expect: constant
ok: constant
is_ok: constant
+ is_err: constant
+ unwrap_or: constant
+ unwrap_or_default: constant
Iterator:
iter: constant
into_iter: constant
@@ -147,6 +207,13 @@ Iterator:
min_by_key: linear_scan
max_by_key: linear_scan
next_back: constant
+ take: constant
+ peekable: constant
+ sum: linear_scan
+ max: linear_scan
+ min: linear_scan
+ skip_while: constant
+ zip: constant
Box:
new: constant
as_ref: constant
@@ -159,8 +226,26 @@ Path:
join: linear_materialize
to_string_lossy: linear_materialize
to_path_buf: linear_materialize
+ # Scan the path text for the relevant component boundary.
+ file_stem: linear_scan
+ file_name: linear_scan
+ extension: linear_scan
+ strip_prefix: linear_scan
+ starts_with: linear_scan
+ ends_with: linear_scan
+ components: constant
+ is_absolute: constant
PathBuf:
from: linear_materialize
+ clone: linear_materialize
+ pop: linear_scan
+OsStr:
+ # Unix `to_str` validates the bytes as UTF-8.
+ to_str: linear_scan
+ to_string_lossy: linear_materialize
+DirEntry:
+ path: linear_materialize
+ file_name: linear_materialize
char:
is_ascii_alphanumeric: constant
is_ascii_alphabetic: constant
@@ -168,19 +253,97 @@ char:
is_ascii_digit: constant
is_alphanumeric: constant
len_utf8: constant
+ is_whitespace: constant
+ is_uppercase: constant
+ is_lowercase: constant
+ is_ascii_lowercase: constant
+ is_alphabetic: constant
+ is_control: constant
+ len_utf16: constant
+ to_ascii_uppercase: constant
+u8:
+ is_ascii_alphanumeric: constant
+ is_ascii_alphabetic: constant
+ is_ascii_digit: constant
+ is_ascii_whitespace: constant
+ is_ascii_hexdigit: constant
+ from_str_radix: linear_scan
bool:
then_some: constant
usize:
saturating_sub: constant
cmp: constant
+ from: constant
+ checked_sub: constant
+ abs_diff: constant
+ saturating_mul: constant
+ try_from: constant
u32:
saturating_sub: constant
+ try_from: constant
+u64:
+ from: constant
+ wrapping_mul: constant
+f64:
+ round: constant
+ abs: constant
+VecDeque:
+ push_back: linear_materialize
+ pop_front: constant
+ from: linear_materialize
+ extend: linear_materialize
+ len: constant
+ is_empty: constant
+Instant:
+ now: constant
+ elapsed: constant
+Duration:
+ as_millis: constant
+ as_secs: constant
+ as_secs_f64: constant
+Peekable:
+ peek: constant
+RefCell:
+ new: constant
+ borrow: constant
+ borrow_mut: constant
+Atomic:
+ new: constant
+ load: constant
+ store: constant
+ fetch_add: constant
+mem:
+ take: constant
+ replace: constant
+ swap: constant
+env:
+ # Process environment access; the map itself is small and O(1)-indexed.
+ var: constant
+ var_os: constant
+ set_var: constant
+ remove_var: constant
+ temp_dir: constant
+ args: linear_materialize
Ord:
min: constant
max: constant
ToString:
to_string: linear_materialize
SemanticSymbol:
+ 'iter/sources/once/once().': constant
+ 'str/converts/from_utf8().': linear_scan
+ 'panic/resume_unwind().': linear_scan
+ 'sync/mpsc/channel().': constant
+ 'sync/mpsc/impl#[`Sender`][Clone]clone().': constant
+ 'sync/mpsc/impl#[`Sender`]send().': constant
+ 'sync/once_lock/impl#[`OnceLock`]new().': constant
+ 'process/impl#[Command]stdin().': constant
+ 'process/impl#[Command]stdout().': constant
+ 'process/impl#[Command]stderr().': constant
+ 'io/buffered/bufreader/impl#[`BufReader`]new().': constant
+ 'io/error/impl#[Error]kind().': constant
+ 'path/impl#[Path]display().': constant
+ 'path/impl#[Path]ancestors().': constant
'impl#[`Node<''tree>`]kind().': constant
'impl#[`Node<''tree>`]walk().': constant
'impl#[Tree]root_node().': constant
@@ -202,7 +365,97 @@ SemanticSymbol:
'file/impl#[`NamedTempFile`]path().': constant
'impl#[`Builder<''a, ''b>`]new().': constant
'impl#[`Builder<''a, ''b>`]suffix().': constant
+ # `Entry` is the owner name of both map families, so the tree and the table
+ # variants must be keyed exactly rather than through one owner table.
+ 'collections/btree/map/entry/impl#[`Entry<''a, K, V, A>`]or_default().': logarithmic
+ 'collections/hash/map/impl#[`Entry<''a, K, V>`]or_default().': constant
+ # `[T; N]` is not the nominal array shape the shared parser recognises.
+ 'array/iter/impl#[`[T; N]`][IntoIterator]into_iter().': constant
+ 'array/impl#[`[T; N]`][Ord]cmp().': linear_scan
+ 'slice/raw/from_ref().': constant
+ 'io/stdio/stdout().': constant
+ 'io/stdio/impl#[Stdout]lock().': constant
+ 'process/impl#[Command]new().': constant
+ 'thread/builder/impl#[Builder]new().': constant
+ # Takes ownership of String and stores it in Option; it does not clone bytes.
+ 'thread/builder/impl#[Builder]name().': constant
+ 'thread/builder/impl#[Builder]stack_size().': constant
+ 'io/buffered/bufwriter/impl#[`BufWriter`]new().': constant
+ # tree-sitter cursor and node accessors are pointer arithmetic on the parse
+ # tree; `utf8_text` slices the source and validates it as UTF-8.
+ 'impl#[`Node<''tree>`]utf8_text().': linear_scan
+ 'impl#[`Node<''tree>`]id().': constant
+ 'impl#[`Node<''tree>`]start_position().': constant
+ 'impl#[`Node<''tree>`]end_position().': constant
+ 'impl#[`Node<''tree>`]byte_range().': constant
+ 'impl#[`Node<''tree>`]child_count().': constant
+ 'impl#[`Node<''tree>`]named_child_count().': constant
+ 'impl#[`Node<''tree>`]next_sibling().': constant
+ 'impl#[`Node<''tree>`]prev_sibling().': constant
+ 'impl#[`Node<''tree>`]next_named_sibling().': constant
+ 'impl#[`Node<''tree>`]prev_named_sibling().': constant
+ 'impl#[`Node<''tree>`]named_children().': constant
+ 'impl#[`Node<''tree>`]is_missing().': constant
+ 'impl#[`Node<''tree>`]is_error().': constant
+ 'impl#[`Node<''tree>`]is_extra().': constant
+ 'impl#[`Node<''tree>`]named_child().': constant
+ 'impl#[Parser]new().': constant
+ 'impl#[Query]capture_names().': constant
+ 'Point#row.': constant
+ 'Point#column.': constant
+ # regex construction compiles the pattern; matching is linear in the input.
+ 'regex/string/impl#[Regex]new().': linear_materialize
+ 'regex/string/impl#[Regex]is_match().': linear_scan
+ 'regex/string/impl#[`Match<''h>`]as_str().': constant
+ 'regex/string/impl#[`Captures<''h>`]get().': constant
+ # serde_json Value accessors are tagged-union reads.
+ 'value/impl#[Value]as_u64().': constant
+ 'value/impl#[Value]as_bool().': constant
+ 'value/impl#[Value]as_array().': constant
+ 'value/impl#[Value]as_array_mut().': constant
+ 'value/impl#[Value]as_object().': constant
+ 'value/impl#[Value]as_object_mut().': constant
+ 'value/impl#[Value]get_mut().': logarithmic
+ 'value/impl#[Value]pointer().': linear_scan
+ 'map/impl#[`Map`]get().': logarithmic
+ 'map/impl#[`Map`]remove().': logarithmic
+ 'map/impl#[`Map`]entry().': logarithmic
+ 'map/impl#[`Map`]is_empty().': constant
+ 'map/impl#[`Map`]len().': constant
+ 'map/impl#[`Map`][IntoIterator]into_iter().': constant
+ # flate2 stream handles; `finish` flushes the whole compressed buffer.
+ 'gz/write/impl#[`GzEncoder`]new().': constant
+ 'gz/write/impl#[`GzEncoder`]finish().': linear_materialize
+ 'gz/read/impl#[`GzDecoder`]new().': constant
+ 'file/impl#[`NamedTempFile`]as_file_mut().': constant
+ # anyhow context attachment stores the value; it does not walk the chain.
+ 'context/impl#[`Result`][`Context`]context().': constant
+ 'context/impl#[`Option`][`Context`]context().': constant
+ # a digest absorbs every input byte.
+ 'digest/impl#[D][Digest]update().': linear_materialize
SemanticSymbolParametricCost:
+ 'borrow/impl#[`Cow<''_, B>`]into_owned().': reflective_once
+ 'cmp/min().': reflective_once
+ 'convert/AsRef#as_ref().': reflective_once
+ 'path/impl#[Path]new().': reflective_once
+ 'iter/traits/collect/impl#[I][IntoIterator]into_iter().': reflective_once
+ 'mem/drop().': reflective_once
+ 'option/impl#[`Option`][Ord]cmp().': reflective_once
+ 'tuple/impl#[`(U, T)`][Ord]cmp().': reflective_once
+ 'tuple/impl#[`(V, U, T)`][Ord]cmp().': reflective_once
+ 'vec/impl#[`Vec`][Ord]cmp().': callback_linear
+ 'slice/impl#[`[T]`]starts_with().': callback_linear
+ 'vec/impl#[`Vec`]retain_mut().': callback_linear
+ 'iter/traits/iterator/Iterator#partition().': callback_linear
+ 'iter/adapters/map/impl#[`Map`][Iterator]next().': callback_once
+ 'iter/adapters/map/impl#[`Map`][Iterator]fold().': callback_linear
+ 'iter/adapters/rev/impl#[`Rev`][Iterator]fold().': callback_linear
+ 'option/impl#[`Option`]ok_or_else().': callback_once
+ 'result/impl#[`Result`]is_ok_and().': callback_once
+ 'result/impl#[`Result`]map_err().': callback_once
+ 'thread/scoped/scope().': callback_once
+ 'thread/builder/impl#[Builder]spawn().': callback_once
+ 'thread/scoped/impl#[Builder]spawn_scoped().': callback_once
'string/impl#[T][ToString]to_string().': reflective_once
'iter/traits/iterator/Iterator#collect().': callback_linear
'iter/traits/iterator/Iterator#any().': callback_linear
@@ -223,10 +476,66 @@ SemanticSymbolParametricCost:
'vec/impl#[`Vec`][Clone]clone().': callback_linear
'context/impl#[`Option`][`Context`]with_context().': callback_once
'context/impl#[`Result`][`Context`]with_context().': callback_once
+ # Blanket trait dispatch: the cost is the selected `From`/`Clone` impl, which
+ # the symbol does not name. Constant would be a claim we cannot make.
+ 'convert/impl#[T][`Into`]into().': reflective_once
+ 'clone/Clone#clone().': reflective_once
+ # Closure-running standard-library APIs.
+ 'sync/once_lock/impl#[`OnceLock`]get_or_init().': callback_once
+ 'option/impl#[`Option`]is_none_or().': callback_once
+ 'result/impl#[`Result`]map().': callback_once
+ 'result/impl#[`Result`]unwrap_or_else().': callback_once
+ 'collections/btree/map/entry/impl#[`Entry<''a, K, V, A>`]or_insert_with().': callback_once
+ 'iter/traits/iterator/Iterator#reduce().': callback_linear
+ 'iter/traits/iterator/Iterator#fold().': callback_linear
+ 'iter/traits/iterator/Iterator#for_each().': callback_linear
+ 'slice/iter/impl#[`Iter<''a, T>`][Iterator]fold().': callback_linear
+ 'iter/adapters/filter/impl#[`Filter`][Iterator]fold().': callback_linear
+ 'iter/adapters/filter_map/impl#[`FilterMap`][Iterator]fold().': callback_linear
+ 'slice/iter/impl#[`Iter<''a, T>`][Iterator]rposition().': callback_linear
+ 'iter/traits/double_ended/DoubleEndedIterator#rfind().': callback_linear
+ # serde traverses the value against a Deserialize/Serialize impl it selects
+ # at the call site; the impl, not the call, carries the cost.
+ 'value/from_value().': reflective_once
+ 'value/to_value().': reflective_once
+ 'ser/to_vec().': reflective_once
+ 'ser/to_string().': reflective_once
+ 'ser/to_string_pretty().': reflective_once
+ 'ser/to_writer().': reflective_once
+ 'de/from_str().': reflective_once
+ 'de/from_slice().': reflective_once
+ 'map/impl#[`Entry<''a>`]or_insert_with().': callback_once
ExternalLatency:
fs.write: linear_materialize
fs.read_to_string: linear_materialize
fs.create_dir_all: linear_materialize
+ fs.read: linear_materialize
+ fs.read_dir: linear_materialize
+ fs.canonicalize: linear_materialize
+ fs.remove_dir_all: linear_materialize
+ fs.remove_file: constant
+ fs.rename: constant
+ fs.metadata: constant
+ Path.exists: constant
+ Path.is_dir: constant
+ Path.is_file: constant
+ env.current_dir: constant
+ Write.write_all: linear_materialize
+ Write.flush: constant
+ Read.read_to_end: linear_materialize
+ BufWriter.write_all: linear_materialize
+ BufWriter.flush: constant
+ File.create: constant
+ File.open: constant
+ BufRead.read_line: linear_materialize
+ BufReader.read_exact: linear_materialize
+ copy.copy: linear_materialize
+ Command.spawn: constant
+ Child.kill: constant
+ Child.wait: constant
+ ChildStdin.flush: constant
+ JoinHandle.join: constant
Builder.tempfile: constant
dir.tempdir: constant
+ NamedTempFile.new: constant
NamedTempFile.write_all: linear_materialize
diff --git a/gems/fact-mine/config/stdlib_maps/README.md b/gems/fact-mine/config/stdlib_maps/README.md
new file mode 100644
index 000000000..aeae01103
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/README.md
@@ -0,0 +1,143 @@
+# Standard-library map support
+
+`support.yml` is the machine-readable compatibility inventory for maintained
+SCIP languages. `bundled` means the standard library is produced by a
+`fact-mine.stdlib-map.v1` manifest and its generated artifact is discovered
+automatically at FactMine build time. `blocked` means publishing a bundle would
+violate the exact source/consumer identity contract; the required upstream
+capability is recorded instead of silently shipping an unsafe approximation.
+
+The shared producer supports pinned local or Git sources, sparse checkouts,
+selected-file staging for indexers that scan an entire workspace, language-owned
+build commands, exact indexer validation, parser/recovery soundness gates,
+opaque semantic-environment attestations, optional exact-prefix relocation or
+generated exact symbol bridges, producer joins, consumer comparisons, and
+atomic publication. None of those stages branches on a language.
+
+`compatibility` claims are exact string key/value pairs produced by a manifest
+or a language-owned probe. The shared join does not interpret keys such as
+runtime artifact digest, target triple, sysroot, or ABI; it merely requires the
+consumer profile to carry every claim required by the bundle. A
+`fact-mine.symbol-bridge.v1` sidecar maps analyzed implementation symbols to
+one or more exact consumer declaration symbols. This is the common mechanism
+for declaration-only or cross-language runtimes, and the bridge digest is
+retained in the generated summary. A source identity without a bridge target is
+not published; bridge fan-out is therefore lossless but fail-closed. Bridge
+commands run after the producer profile and unrelocated summary exist, and may
+use `{profile}` and `{producer_summary}` in addition to the standard manifest
+substitutions.
+
+A standard library may be split into independent, bounded manifests (for
+example `core-a`, `collections`, and `io`) instead of one monolithic job.
+Every manifest runs the same source → SCIP → FactMine → soundness gate → exact
+bridge → atomic publication flow, and all generated summaries are discovered
+as a union. This is the intended feedback loop for CRuby now and for CPython
+and a JS engine later: adding a shard is language-owned source/index/bridge
+configuration, not a new shared integration path.
+
+When implementation and consumer source require different SCIP producers, a
+manifest may declare `summary.consumer_indexers`. The generated summary retains
+the implementation indexer as producer provenance but activates only for one
+of those exact consumer `tool@version` identities. The symbol bridge remains
+responsible for proving each cross-indexer declaration identity.
+
+Current publishable mappings:
+
+| Language | Source | Indexer | Exact symbols |
+|---|---|---|---:|
+| Go | Go 1.22.2 core surface | scip-go 0.2.7 | 322 |
+| Rust | Rust 1.96.0 `core`/`alloc`/`std` | rust-analyzer 1.96.0 | 1,543 |
+| Java | JDK 21.0.12 `java.lang`/`java.util` | scip-java 0.12.3 | 2,598 |
+| Python | CPython 3.11.9 selected pure-Python core | scip-python 0.6.6 | 200 |
+| C# | .NET 10.0.10 CoreLib collections/string/array | scip-dotnet 0.2.14 | 316 |
+| Kotlin | Kotlin/JVM 2.2.0 stdlib | semanticdb-kotlinc 0.6.0 + patch | 85 |
+| C++ | libstdc++ 13.3.0 selected C++17/C++20 surfaces | scip-clang 0.4.0 | 772 unique |
+| Ruby | CRuby 3.2.3 core-A native surface | scip-clang 0.4.0 → nil-kill-runtime 2 | 6 |
+
+The checked-in exact-symbol consumers prove that the generated data changes
+the final function result, not merely call metadata:
+
+| Consumer | Before | After | Delta |
+|---|---:|---:|---:|
+| Java 21 `BitSet.toLongArray()` | 0/1 complete | 1/1 complete | +1 |
+| Python 3.11 `OrderedDict.move_to_end()` | 0/1 complete | 1/1 complete | +1 |
+
+On Commons CLI, the Java bundle joins 12 call sites but changes no complete
+function count (321/524 before and after) because those functions either were
+already complete through a fallback or retain other evidence gaps. This is
+still useful canonicalization, but it is not reported as completeness impact.
+
+The remaining SCIP languages are deliberately fail-closed:
+
+- C standard-library symbols from scip-clang use `cxx . .`.
+ `c/semantic_environment.rb` now closes the missing consumer identity by
+ attesting the libc binary and release, effective public headers, compiler,
+ target, semantic flags, preprocessor macros, and scip-clang binary. The
+ remaining blocker is an implementation producer: the matching patched glibc
+ source must be configured and indexed under that same environment before a
+ bundle can be published.
+- TypeScript and JavaScript built-ins resolve to versioned TypeScript `.d.ts`
+ declarations. Those files have no executable bodies, while the real
+ implementations belong to a particular JS engine and release.
+ `javascript/semantic_environment.rb` now pins the Node binary and release,
+ V8 release, native-module ABI, and scip-typescript binary for both languages.
+ This removes runtime identity as an excuse for a guessed declaration model;
+ the remaining producer must analyze the matching V8/Node bodies and generate
+ the declaration bridge while retaining callback and reflective cost
+ parameters.
+- PHP built-ins can be connected to their `php-src` C implementations through
+ the generic cross-indexer symbol bridge, and
+ `php/scip-php-exact-version.patch` replaces scip-php's hard-coded `0.0.1`
+ metadata with its exact source revision. A PHP 8.4.21 producer trial
+ nevertheless exported zero of 70 analyzed `zif_*` string built-ins under the
+ source-proof policy. Weak scalar coercion can invoke user object handlers,
+ while Zend allocation depends on allocator state and hooks; both costs must
+ remain parametric. Publishing the apparent native helper cost would silently
+ discard those executable paths, so PHP remains fail-closed until generated
+ summaries can carry and compose those runtime cost parameters.
+- Ruby has a pinned CRuby 3.2.3 producer that indexes native bodies with
+ `scip-clang` and bridges only `rb_define_*` registrations to NilKill's
+ versioned `nil-kill-runtime ruby ruby 3.2.3` identities. The initial
+ core-A shard exports six source-proven rows. It is deliberately small:
+ unproved native calls, callback paths, and VM-state-sensitive bodies remain
+ absent rather than being replaced by a manual Ruby YAML model. Additional
+ CRuby shards use the same manifest-driven producer and bridge.
+- The SCIP ecosystem also includes Scala through scip-java, Visual Basic
+ through scip-dotnet, and Dart through scip-dart. They were previously absent
+ from the inventory. FactMine does not yet have syntax/CFG/DFG adapters for
+ those languages, so claiming stdlib quality would be meaningless. Once an
+ adapter exists, Scala can immediately reuse exact JDK rows and Visual Basic
+ can reuse exact CoreLib rows because those bundles join on compiler symbols,
+ not the source language. Dart additionally needs a pinned SDK-source
+ manifest.
+
+These are compatibility failures, not requests for manual overrides. Move an
+entry to `bundled` only after its required identity/body capability exists and a
+manifest passes the same producer and consumer checks. Missing version text in
+a SCIP symbol is no longer itself a blocker: a reproducible environment
+attestation may supply the missing compatibility identity without weakening the
+exact-symbol join.
+
+The C# manifest works around scip-dotnet 0.2.14's stale
+`0.1.0-SNAPSHOT` SCIP metadata without trusting that text: its language-owned
+index recipe checks the released CLI version, and its compatibility sidecar
+pins both the installed indexer binary and .NET reference-assembly digests.
+The generated runtime bundle maps only exact, assembly-qualified consumer
+symbols. On the current Serilog production corpus the released indexer itself
+raises completion from 755/888 (85.02%) to 765/888 (86.15%); the generated
+bundle has zero additional count impact because all overlapping calls already
+had complete fallback models, and it produces no complete/complete
+disagreements.
+
+The three C++ manifests preprocess bounded libstdc++ surfaces under the exact
+consumer compiler configuration before SCIP indexing. Because scip-clang uses
+unversioned `cxx . .` symbols, each bundle requires an opaque language-owned
+attestation of the compiler binary, target, C++ standard, macro set, effective
+generic and architecture-specific header overlays, and scip-clang binary.
+Only exact-bound `std::` rows cross the generated identity bridge. C++ template
+functions with unmodeled implicit construction, assignment, or destruction
+fail the generic source-export gate through the C++ behavior interface rather
+than being published as O(1). The eventpp, plog, and proxy audits join 2, 30,
+and 4 calls respectively with zero complete/complete disagreements; all were
+already covered by complete fallback models, so their function-completion
+counts do not change.
diff --git a/gems/fact-mine/config/stdlib_maps/c/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/c/semantic_environment.rb
new file mode 100644
index 000000000..9c5013009
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/c/semantic_environment.rb
@@ -0,0 +1,96 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+source_root, output = ARGV
+abort "usage: semantic_environment.rb SOURCE_ROOT OUTPUT.json" unless source_root && output
+
+def capture!(*command, chdir:)
+ stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout.strip
+end
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |candidate| File.file?(candidate) && File.executable?(candidate) }
+ abort "#{fallback} was not found; set #{environment}" unless path
+
+ path
+end
+
+def files_digest(paths)
+ digest = Digest::SHA256.new
+ paths.map { |path| File.expand_path(path) }.sort.each do |path|
+ abort "C libc compatibility input was not found at #{path}" unless File.file?(path)
+
+ digest << path << "\0" << Digest::SHA256.file(path).hexdigest << "\n"
+ end
+ digest.hexdigest
+end
+
+source_root = File.expand_path(source_root)
+database = JSON.parse(File.read(File.join(source_root, "compile_commands.json")))
+entries = database.map { |entry| entry.fetch("arguments") }
+abort "compile_commands.json has no entries" if entries.empty?
+
+compiler = File.expand_path(entries.first.fetch(0))
+abort "compile_commands.json uses multiple C compilers" unless entries.all? do |arguments|
+ File.expand_path(arguments.fetch(0)) == compiler
+end
+semantic_flags = entries.flat_map do |arguments|
+ arguments.select do |argument|
+ argument.start_with?("-std=", "-D", "-U", "-m", "--target=")
+ end
+end.uniq.sort
+
+headers = ENV.fetch(
+ "C_LIBC_HEADERS",
+ [
+ "/usr/include/features.h",
+ "/usr/include/string.h",
+ "/usr/include/stdlib.h",
+ "/usr/include/stdio.h",
+ "/usr/include/unistd.h"
+ ].join(File::PATH_SEPARATOR)
+).split(File::PATH_SEPARATOR).reject(&:empty?)
+libc = File.expand_path(ENV.fetch("C_LIBC_BINARY", "/lib/x86_64-linux-gnu/libc.so.6"))
+indexer = executable("SCIP_CLANG", "scip-clang")
+macros = capture!(
+ compiler,
+ *semantic_flags,
+ "-dM",
+ "-E",
+ "-x",
+ "c",
+ "/dev/null",
+ chdir: source_root
+).lines.sort.join
+
+claims = {
+ "c.libc.implementation" => ENV.fetch("C_LIBC_IMPLEMENTATION", "glibc"),
+ "c.libc.release" => capture!(libc, "--version", chdir: source_root).lines.first.to_s,
+ "c.libc.binary.sha256" => "sha256:#{Digest::SHA256.file(libc).hexdigest}",
+ "c.libc.headers.sha256" => "sha256:#{files_digest(headers)}",
+ "c.compiler.version" => capture!(compiler, "--version", chdir: source_root).lines.first.to_s,
+ "c.compiler.target" => capture!(compiler, "-dumpmachine", chdir: source_root),
+ "c.compiler.sha256" => "sha256:#{Digest::SHA256.file(compiler).hexdigest}",
+ "c.semantic_flags.sha256" =>
+ "sha256:#{Digest::SHA256.hexdigest(semantic_flags.join("\0"))}",
+ "c.preprocessor_macros.sha256" => "sha256:#{Digest::SHA256.hexdigest(macros)}",
+ "c.scip_clang.version" => capture!(indexer, "--version", chdir: source_root).lines.first.to_s,
+ "c.scip_clang.sha256" => "sha256:#{Digest::SHA256.file(indexer).hexdigest}"
+}
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => claims
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/consumers/java-21/pom.xml b/gems/fact-mine/config/stdlib_maps/consumers/java-21/pom.xml
new file mode 100644
index 000000000..e1e59c024
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/consumers/java-21/pom.xml
@@ -0,0 +1,13 @@
+
+
+ 4.0.0
+ dev.factmine
+ stdlib-consumer
+ 1.0.0
+
+ 21
+ UTF-8
+
+
diff --git a/gems/fact-mine/config/stdlib_maps/consumers/java-21/src/main/java/dev/factmine/StdlibConsumer.java b/gems/fact-mine/config/stdlib_maps/consumers/java-21/src/main/java/dev/factmine/StdlibConsumer.java
new file mode 100644
index 000000000..c7dd0044f
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/consumers/java-21/src/main/java/dev/factmine/StdlibConsumer.java
@@ -0,0 +1,9 @@
+package dev.factmine;
+
+import java.util.BitSet;
+
+final class StdlibConsumer {
+ static long[] materialize(BitSet values) {
+ return values.toLongArray();
+ }
+}
diff --git a/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/impact.py b/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/impact.py
new file mode 100644
index 000000000..65211e1ad
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/impact.py
@@ -0,0 +1,5 @@
+from collections import OrderedDict
+
+
+def move_last(values: OrderedDict[str, int]) -> None:
+ values.move_to_end("key")
diff --git a/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/pyproject.toml b/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/pyproject.toml
new file mode 100644
index 000000000..b9b1d14b0
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/consumers/python-3.11/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "fact-mine-stdlib-consumer"
+version = "1.0.0"
+
+[tool.pyright]
+include = ["impact.py"]
+pythonVersion = "3.11"
diff --git a/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-containers.yml b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-containers.yml
new file mode 100644
index 000000000..621e482ce
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-containers.yml
@@ -0,0 +1,55 @@
+schema: fact-mine.stdlib-map.v1
+language: cpp
+
+source:
+ root_command:
+ - ruby
+ - "{manifest_dir}/cpp/materialize_source.rb"
+ - "{workspace_root}"
+ revision: libstdcxx-13.3.0-ubuntu24.04-cxx17-containers
+ revision_check:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/revision.rb"
+ - "{source_root}"
+ equals: "libstdcxx-13.3.0 effective sha256:cab188eb89bb4b5cf99c97a16f9d1d3196d1d2ce9536ea70a9f74b782bc7bec2"
+ include:
+ - "preprocessed/c++17/containers.cpp"
+ stage_selected_files: true
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/index_stdlib.rb"
+ - "{source_root}"
+ - "{index}"
+ - c++17
+ - containers
+ working_directory: "{source_root}"
+ output: libstdcxx.scip
+ expected:
+ tool: scip-clang
+ version: "0.4.0"
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 1500
+
+summary:
+ corpus: libstdcxx-13.3.0-cxx17-containers
+ output: ../complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-containers.json.gz
+ minimum_symbols: 350
+ symbol_bridge:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/build_symbol_bridge.rb"
+ - "{producer_summary}"
+ - "{symbol_bridge}"
+ working_directory: "{source_root}"
diff --git a/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-strings.yml b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-strings.yml
new file mode 100644
index 000000000..4ed9f8805
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx17-strings.yml
@@ -0,0 +1,55 @@
+schema: fact-mine.stdlib-map.v1
+language: cpp
+
+source:
+ root_command:
+ - ruby
+ - "{manifest_dir}/cpp/materialize_source.rb"
+ - "{workspace_root}"
+ revision: libstdcxx-13.3.0-ubuntu24.04-cxx17-strings
+ revision_check:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/revision.rb"
+ - "{source_root}"
+ equals: "libstdcxx-13.3.0 effective sha256:cab188eb89bb4b5cf99c97a16f9d1d3196d1d2ce9536ea70a9f74b782bc7bec2"
+ include:
+ - "preprocessed/c++17/strings.cpp"
+ stage_selected_files: true
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/index_stdlib.rb"
+ - "{source_root}"
+ - "{index}"
+ - c++17
+ - strings
+ working_directory: "{source_root}"
+ output: libstdcxx.scip
+ expected:
+ tool: scip-clang
+ version: "0.4.0"
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 2000
+
+summary:
+ corpus: libstdcxx-13.3.0-cxx17-strings
+ output: ../complexity_summaries/cpp-libstdcxx.13.3.0-cxx17-strings.json.gz
+ minimum_symbols: 420
+ symbol_bridge:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/build_symbol_bridge.rb"
+ - "{producer_summary}"
+ - "{symbol_bridge}"
+ working_directory: "{source_root}"
diff --git a/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx20-memory.yml b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx20-memory.yml
new file mode 100644
index 000000000..374b03169
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp-libstdcxx-13.3.0-cxx20-memory.yml
@@ -0,0 +1,55 @@
+schema: fact-mine.stdlib-map.v1
+language: cpp
+
+source:
+ root_command:
+ - ruby
+ - "{manifest_dir}/cpp/materialize_source.rb"
+ - "{workspace_root}"
+ revision: libstdcxx-13.3.0-ubuntu24.04-cxx20-memory
+ revision_check:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/revision.rb"
+ - "{source_root}"
+ equals: "libstdcxx-13.3.0 effective sha256:cab188eb89bb4b5cf99c97a16f9d1d3196d1d2ce9536ea70a9f74b782bc7bec2"
+ include:
+ - "preprocessed/c++20/memory.cpp"
+ stage_selected_files: true
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/index_stdlib.rb"
+ - "{source_root}"
+ - "{index}"
+ - c++20
+ - memory
+ working_directory: "{source_root}"
+ output: libstdcxx.scip
+ expected:
+ tool: scip-clang
+ version: "0.4.0"
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 2500
+
+summary:
+ corpus: libstdcxx-13.3.0-cxx20-memory
+ output: ../complexity_summaries/cpp-libstdcxx.13.3.0-cxx20-memory.json.gz
+ minimum_symbols: 520
+ symbol_bridge:
+ command:
+ - ruby
+ - "{manifest_dir}/cpp/build_symbol_bridge.rb"
+ - "{producer_summary}"
+ - "{symbol_bridge}"
+ working_directory: "{source_root}"
diff --git a/gems/fact-mine/config/stdlib_maps/cpp/build_symbol_bridge.rb b/gems/fact-mine/config/stdlib_maps/cpp/build_symbol_bridge.rb
new file mode 100644
index 000000000..0838bc8ed
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp/build_symbol_bridge.rb
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "json"
+require "zlib"
+
+summary, output = ARGV
+abort "usage: build_symbol_bridge.rb PRODUCER.json.gz OUTPUT.json" unless summary && output
+
+producer = Zlib::GzipReader.open(summary) { |gzip| JSON.parse(gzip.read) }
+symbols = producer.fetch("symbols")
+ .select do |symbol, row|
+ symbol.start_with?("cxx . . $ std/") &&
+ row.fetch("bound_quality") == "upper_bound_exact_symbol"
+ end
+ .keys
+ .sort
+ .to_h { |symbol| [symbol, symbol] }
+abort "producer exported no exact std symbols" if symbols.empty?
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.symbol-bridge.v1",
+ "symbols" => symbols
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/cpp/index_stdlib.rb b/gems/fact-mine/config/stdlib_maps/cpp/index_stdlib.rb
new file mode 100755
index 000000000..6946efac6
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp/index_stdlib.rb
@@ -0,0 +1,70 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+INDEXER_VERSION = "0.4.0"
+INDEXER_SHA256 = "06fd18c576f979a726c651594644ec4a35db4f471f2160b3f72eb89fa6001784"
+source_root, output, standard, surface = ARGV
+abort "usage: index_stdlib.rb SOURCE_ROOT OUTPUT.scip C++_STANDARD SURFACE" unless source_root && output && standard && surface
+abort "unsupported C++ standard #{standard.inspect}" unless %w[c++17 c++20].include?(standard)
+abort "invalid surface #{surface.inspect}" unless surface.match?(/\A[a-z][a-z0-9-]*\z/)
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ candidate = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |path| File.file?(path) && File.executable?(path) }
+ abort "#{fallback} was not found; set #{environment}" unless candidate
+
+ candidate
+end
+
+def capture!(*command, chdir:)
+ stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout
+end
+
+source_root = File.expand_path(source_root)
+output = File.expand_path(output)
+compiler = executable("CLANGXX", "clang++-20")
+indexer = executable("SCIP_CLANG", "scip-clang")
+version = capture!(indexer, "--version", chdir: source_root)
+abort "scip-clang #{INDEXER_VERSION} is required" unless version.lines.first&.strip == "scip-clang #{INDEXER_VERSION}"
+abort "unexpected scip-clang binary" unless Digest::SHA256.file(indexer).hexdigest == INDEXER_SHA256
+
+implementation = File.join(source_root, "preprocessed", standard, "#{surface}.cpp")
+abort "preprocessed surface was not found at #{implementation}" unless File.file?(implementation)
+arguments = [
+ compiler,
+ "-std=#{standard}",
+ "-c",
+ implementation
+]
+File.write(
+ File.join(source_root, "compile_commands.json"),
+ JSON.pretty_generate([{
+ "directory" => source_root,
+ "file" => implementation,
+ "arguments" => arguments
+ }])
+)
+capture!("git", "init", "--quiet", chdir: source_root)
+capture!(
+ indexer,
+ "--compdb-path",
+ "compile_commands.json",
+ "--index-output-path",
+ output,
+ "--no-progress-report",
+ "-j",
+ "1",
+ chdir: source_root
+)
+abort "scip-clang did not produce #{output}" unless File.size?(output)
diff --git a/gems/fact-mine/config/stdlib_maps/cpp/materialize_source.rb b/gems/fact-mine/config/stdlib_maps/cpp/materialize_source.rb
new file mode 100755
index 000000000..ef6dc3d38
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp/materialize_source.rb
@@ -0,0 +1,106 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "open3"
+
+VERSION = "13.3.0-6ubuntu2~24.04.1"
+TREE_SHA256 = "be6bb0b28e319291ad1c08d2aaee0509feb8eb98a24b67edff98fa047b33593a"
+ARCH_TREE_SHA256 = "39dd70479b380d3552526891efea1103d478cd9804d8858ae48190d103b08823"
+EFFECTIVE_TREE_SHA256 = "cab188eb89bb4b5cf99c97a16f9d1d3196d1d2ce9536ea70a9f74b782bc7bec2"
+SURFACES = {
+ ["c++17", "containers"] => %w[array atomic list memory tuple utility],
+ ["c++17", "strings"] => %w[cctype iomanip iostream sstream string vector],
+ ["c++20", "memory"] => %w[memory optional tuple utility]
+}.freeze
+
+workspace = ARGV.fetch(0) do
+ abort "usage: materialize_source.rb WORKSPACE_ROOT"
+end
+
+def tree_digest(root)
+ digest = Digest::SHA256.new
+ Dir.glob(File.join(root, "**/*"), File::FNM_DOTMATCH)
+ .select { |path| File.file?(path) }
+ .sort
+ .each do |path|
+ relative = path.delete_prefix("#{root}#{File::SEPARATOR}")
+ digest << relative << "\0" << Digest::SHA256.file(path).hexdigest << "\n"
+ end
+ digest.hexdigest
+end
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ candidate = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |path| File.file?(path) && File.executable?(path) }
+ abort "#{fallback} was not found; set #{environment}" unless candidate
+
+ candidate
+end
+
+source = ENV.fetch("LIBSTDCXX_INCLUDE", "/usr/include/c++/13")
+architecture = ENV.fetch(
+ "LIBSTDCXX_ARCH_INCLUDE",
+ "/usr/include/x86_64-linux-gnu/c++/13"
+)
+abort "libstdc++ headers were not found at #{source}" unless File.directory?(source)
+abort "architecture-specific libstdc++ headers were not found at #{architecture}" unless File.directory?(architecture)
+abort "unexpected libstdc++ tree digest" unless tree_digest(source) == TREE_SHA256
+abort "unexpected architecture-specific libstdc++ tree digest" unless tree_digest(architecture) == ARCH_TREE_SHA256
+
+cache = File.join(
+ File.expand_path(workspace),
+ ".cache",
+ "stdlib-sources",
+ "libstdcxx-#{VERSION}"
+)
+effective = File.join(cache, "include")
+preprocessed = File.join(cache, "preprocessed")
+marker = File.join(cache, ".complete")
+
+complete = File.file?(marker) &&
+ SURFACES.keys.all? do |standard, surface|
+ File.file?(File.join(preprocessed, standard, "#{surface}.cpp"))
+ end &&
+ File.directory?(effective) &&
+ tree_digest(effective) == EFFECTIVE_TREE_SHA256
+unless complete
+ temporary = "#{cache}.#{Process.pid}.tmp"
+ FileUtils.rm_rf(temporary)
+ FileUtils.mkdir_p(File.join(temporary, "include"))
+ FileUtils.cp_r("#{source}/.", File.join(temporary, "include"))
+ FileUtils.cp_r("#{architecture}/.", File.join(temporary, "include"))
+
+ compiler = executable("CLANGXX", "clang++-20")
+ SURFACES.each do |(standard, surface), headers|
+ audit = File.join(temporary, "#{surface}-#{standard}.cpp")
+ File.write(audit, headers.map { |header| "#include <#{header}>\n" }.join)
+ stdout, stderr, status = Open3.capture3(
+ compiler,
+ "-std=#{standard}",
+ "-I#{File.join(temporary, 'include')}",
+ "-E",
+ "-P",
+ audit
+ )
+ abort "failed to preprocess #{surface} for #{standard}:\n#{stderr}" unless status.success?
+
+ destination = File.join(temporary, "preprocessed", standard, "#{surface}.cpp")
+ FileUtils.mkdir_p(File.dirname(destination))
+ File.write(destination, stdout)
+ FileUtils.rm_f(audit)
+ end
+ File.write(File.join(temporary, ".complete"), "#{VERSION}\n")
+ FileUtils.mkdir_p(File.dirname(cache))
+ FileUtils.rm_rf(cache)
+ FileUtils.mv(temporary, cache)
+end
+
+abort "materialized libstdc++ include tree is missing" unless File.directory?(effective)
+abort "materialized libstdc++ preprocessed surfaces are missing" unless File.directory?(preprocessed)
+puts cache
diff --git a/gems/fact-mine/config/stdlib_maps/cpp/revision.rb b/gems/fact-mine/config/stdlib_maps/cpp/revision.rb
new file mode 100755
index 000000000..67c8f84d8
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp/revision.rb
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+
+EXPECTED = "cab188eb89bb4b5cf99c97a16f9d1d3196d1d2ce9536ea70a9f74b782bc7bec2"
+
+root = ARGV.fetch(0) { abort "usage: revision.rb SOURCE_ROOT" }
+include_root = File.join(File.expand_path(root), "include")
+digest = Digest::SHA256.new
+Dir.glob(File.join(include_root, "**/*"), File::FNM_DOTMATCH)
+ .select { |path| File.file?(path) }
+ .sort
+ .each do |path|
+ relative = path.delete_prefix("#{include_root}#{File::SEPARATOR}")
+ digest << relative << "\0" << Digest::SHA256.file(path).hexdigest << "\n"
+ end
+actual = digest.hexdigest
+abort "unexpected effective libstdc++ digest #{actual}" unless actual == EXPECTED
+
+puts "libstdcxx-13.3.0 effective sha256:#{actual}"
diff --git a/gems/fact-mine/config/stdlib_maps/cpp/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/cpp/semantic_environment.rb
new file mode 100755
index 000000000..6014119c6
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/cpp/semantic_environment.rb
@@ -0,0 +1,92 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+source_root, output = ARGV
+abort "usage: semantic_environment.rb SOURCE_ROOT OUTPUT.json" unless source_root && output
+
+def overlay_digest(*roots)
+ files = {}
+ roots.each do |root|
+ Dir.glob(File.join(root, "**/*"), File::FNM_DOTMATCH)
+ .select { |path| File.file?(path) }
+ .each do |path|
+ relative = path.delete_prefix("#{root}#{File::SEPARATOR}")
+ files[relative] = path
+ end
+ end
+ digest = Digest::SHA256.new
+ files.sort.each do |relative, path|
+ digest << relative << "\0" << Digest::SHA256.file(path).hexdigest << "\n"
+ end
+ digest.hexdigest
+end
+
+def capture!(*command, chdir:)
+ stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout.strip
+end
+
+source_root = File.expand_path(source_root)
+database = JSON.parse(File.read(File.join(source_root, "compile_commands.json")))
+arguments = database.fetch(0).fetch("arguments")
+compiler = File.expand_path(arguments.fetch(0))
+standard = arguments.find { |argument| argument.start_with?("-std=") }
+abort "compile command has no explicit C++ standard" unless standard
+
+semantic_flags = arguments.select do |argument|
+ argument.start_with?("-std=", "-D", "-U")
+end
+base_headers = ENV.fetch("LIBSTDCXX_INCLUDE", "/usr/include/c++/13")
+architecture_headers = ENV.fetch(
+ "LIBSTDCXX_ARCH_INCLUDE",
+ "/usr/include/x86_64-linux-gnu/c++/13"
+)
+abort "libstdc++ headers were not found at #{base_headers}" unless File.directory?(base_headers)
+abort "architecture-specific libstdc++ headers were not found at #{architecture_headers}" unless File.directory?(architecture_headers)
+macros = capture!(
+ compiler,
+ *semantic_flags,
+ "-I#{base_headers}",
+ "-I#{architecture_headers}",
+ "-dM",
+ "-E",
+ "-x",
+ "c++",
+ "-include",
+ "bits/c++config.h",
+ "/dev/null",
+ chdir: source_root
+).lines.sort.join
+indexer = ENV["SCIP_CLANG"]
+indexer ||= ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, "scip-clang") }
+ .find { |path| File.file?(path) && File.executable?(path) }
+abort "scip-clang was not found; set SCIP_CLANG" unless indexer
+
+config = File.join(architecture_headers, "bits", "c++config.h")
+version = capture!(compiler, "--version", chdir: source_root).lines.first.strip
+target = capture!(compiler, "-dumpmachine", chdir: source_root)
+indexer_version = capture!(indexer, "--version", chdir: source_root).lines.first.strip
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => {
+ "cpp.stdlib.vendor" => "libstdc++",
+ "cpp.stdlib.effective_headers.sha256" =>
+ "sha256:#{overlay_digest(base_headers, architecture_headers)}",
+ "cpp.stdlib.config.sha256" => "sha256:#{Digest::SHA256.file(config).hexdigest}",
+ "cpp.compiler.version" => version,
+ "cpp.compiler.target" => target,
+ "cpp.compiler.sha256" => "sha256:#{Digest::SHA256.file(compiler).hexdigest}",
+ "cpp.preprocessor_macros.sha256" => "sha256:#{Digest::SHA256.hexdigest(macros)}",
+ "cpp.language_standard" => standard,
+ "cpp.scip_clang.version" => indexer_version,
+ "cpp.scip_clang.sha256" => "sha256:#{Digest::SHA256.file(indexer).hexdigest}"
+ }
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/csharp-10.0.10.yml b/gems/fact-mine/config/stdlib_maps/csharp-10.0.10.yml
new file mode 100644
index 000000000..982de4861
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/csharp-10.0.10.yml
@@ -0,0 +1,53 @@
+schema: fact-mine.stdlib-map.v1
+language: csharp
+
+source:
+ revision: dotnet-runtime-10.0.10
+ git:
+ repository: https://github.com/dotnet/runtime.git
+ commit: 8f030f80c0dd2722eb2f618984e9db6784765963
+ sparse_paths:
+ - src/libraries/System.Private.CoreLib/src/System
+ include:
+ - "src/libraries/System.Private.CoreLib/src/System/Collections/**/*.cs"
+ - "src/libraries/System.Private.CoreLib/src/System/String.cs"
+ - "src/libraries/System.Private.CoreLib/src/System/Array.cs"
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/csharp/index_corelib.rb"
+ - "{source_root}"
+ - "{index}"
+ working_directory: "{source_root}"
+ output: csharp-corelib.scip
+ expected:
+ # scip-dotnet 0.2.14 still writes its historical development version into
+ # SCIP metadata. The language-owned index command separately verifies the
+ # released package and the compatibility attestation pins its binary hash.
+ tool: scip-dotnet
+ version: 0.1.0-SNAPSHOT
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/csharp/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 1300
+
+summary:
+ corpus: dotnet-runtime-corelib-collections-string-array
+ output: ../complexity_summaries/csharp-corelib.dotnet10.0.10.json.gz
+ minimum_symbols: 250
+ symbol_bridge:
+ command:
+ - ruby
+ - "{manifest_dir}/csharp/build_symbol_bridge.rb"
+ - "{producer_summary}"
+ - "{symbol_bridge}"
+ - 10.0.0.0
+ working_directory: "{source_root}"
diff --git a/gems/fact-mine/config/stdlib_maps/csharp/build_symbol_bridge.rb b/gems/fact-mine/config/stdlib_maps/csharp/build_symbol_bridge.rb
new file mode 100644
index 000000000..9833276cb
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/csharp/build_symbol_bridge.rb
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "json"
+require "zlib"
+
+producer_summary, output, runtime_version = ARGV
+abort "usage: build_symbol_bridge.rb PRODUCER.json.gz OUTPUT.json RUNTIME_VERSION" unless runtime_version
+
+summary = Zlib::GzipReader.open(producer_summary) { |gzip| JSON.parse(gzip.read) }
+prefix = "scip-dotnet nuget . . "
+
+def runtime_package(descriptor)
+ case descriptor
+ when /\A(?:System\/(?:Array|String))#/
+ "System.Runtime"
+ when /\A(?:Generic|ObjectModel)\//
+ "System.Collections"
+ when /\AConcurrent\//
+ "System.Collections.Concurrent"
+ when %r{\ACollections/(?:ArrayList|Hashtable|BitArray)#}
+ "System.Collections.NonGeneric"
+ when /\ACollections\/DictionaryEntry#/
+ "System.Runtime"
+ end
+end
+
+symbols = summary.fetch("symbols").keys.sort.filter_map do |symbol|
+ next unless symbol.start_with?(prefix)
+
+ descriptor = symbol.delete_prefix(prefix)
+ package = runtime_package(descriptor)
+ next unless package
+
+ [symbol, "scip-dotnet nuget #{package} #{runtime_version} #{descriptor}"]
+end.to_h
+abort "no compatible C# runtime symbols were found" if symbols.empty?
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.symbol-bridge.v1",
+ "symbols" => symbols
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/csharp/index_corelib.rb b/gems/fact-mine/config/stdlib_maps/csharp/index_corelib.rb
new file mode 100644
index 000000000..910cd88e2
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/csharp/index_corelib.rb
@@ -0,0 +1,78 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "fileutils"
+require "open3"
+
+EXPECTED_INDEXER = "0.2.14"
+TARGET_FRAMEWORK = "net10.0"
+
+source_root, output = ARGV
+abort "usage: index_corelib.rb SOURCE_ROOT OUTPUT.scip" unless source_root && output
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |candidate| File.file?(candidate) && File.executable?(candidate) }
+ abort "#{fallback} was not found; set #{environment}" unless path
+
+ path
+end
+
+def capture!(*command, chdir:, env: {})
+ stdout, stderr, status = Open3.capture3(env, *command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout
+end
+
+source_root = File.expand_path(source_root)
+output = File.expand_path(output)
+dotnet = executable("DOTNET", "dotnet")
+indexer = executable("SCIP_DOTNET", "scip-dotnet")
+runtime_root = ENV["DOTNET_ROOT"]
+runtime_root = File.dirname(dotnet) if runtime_root.nil? || runtime_root.empty?
+environment = {
+ "DOTNET_ROOT" => runtime_root,
+ "PATH" => "#{File.dirname(dotnet)}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}"
+}
+environment["NUGET_PACKAGES"] = ENV["NUGET_PACKAGES"] if ENV["NUGET_PACKAGES"]
+version = capture!(indexer, "--version", chdir: source_root, env: environment).strip
+unless version == EXPECTED_INDEXER || version.start_with?("#{EXPECTED_INDEXER}+")
+ abort "scip-dotnet #{EXPECTED_INDEXER} is required, got #{version.inspect}"
+end
+
+project = File.join(source_root, "FactMine.CoreLib.csproj")
+File.write(project, <<~XML)
+
+
+ #{TARGET_FRAMEWORK}
+ false
+ enable
+ disable
+ 0436
+
+
+
+
+
+
+
+XML
+
+capture!(dotnet, "restore", project, chdir: source_root, env: environment)
+FileUtils.mkdir_p(File.dirname(output))
+capture!(
+ indexer,
+ "index",
+ File.basename(project),
+ "--skip-dotnet-restore",
+ "--output",
+ output,
+ chdir: source_root,
+ env: environment
+)
+abort "scip-dotnet did not produce #{output}" unless File.size?(output)
diff --git a/gems/fact-mine/config/stdlib_maps/csharp/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/csharp/semantic_environment.rb
new file mode 100644
index 000000000..77af2edf7
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/csharp/semantic_environment.rb
@@ -0,0 +1,71 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+EXPECTED_INDEXER = "0.2.14"
+TARGET_FRAMEWORK = "net10.0"
+
+source_root, output = ARGV
+abort "usage: semantic_environment.rb SOURCE_ROOT OUTPUT.json" unless source_root && output
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |candidate| File.file?(candidate) && File.executable?(candidate) }
+ abort "#{fallback} was not found; set #{environment}" unless path
+
+ path
+end
+
+def capture!(*command, chdir:, env: {})
+ stdout, stderr, status = Open3.capture3(env, *command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout.strip
+end
+
+source_root = File.expand_path(source_root)
+dotnet = executable("DOTNET", "dotnet")
+indexer = executable("SCIP_DOTNET", "scip-dotnet")
+runtime_root = ENV["DOTNET_ROOT"]
+runtime_root = File.dirname(dotnet) if runtime_root.nil? || runtime_root.empty?
+environment = {
+ "DOTNET_ROOT" => runtime_root,
+ "PATH" => "#{File.dirname(dotnet)}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}"
+}
+indexer_version = capture!(indexer, "--version", chdir: source_root, env: environment)
+unless indexer_version == EXPECTED_INDEXER || indexer_version.start_with?("#{EXPECTED_INDEXER}+")
+ abort "scip-dotnet #{EXPECTED_INDEXER} is required, got #{indexer_version.inspect}"
+end
+
+reference = Dir[File.join(
+ runtime_root,
+ "packs/Microsoft.NETCore.App.Ref/10.0.*/ref/#{TARGET_FRAMEWORK}/System.Runtime.dll"
+)].max
+abort "the .NET 10 System.Runtime reference assembly was not found below #{runtime_root}" unless reference
+
+indexer_dll = Dir[File.join(
+ File.dirname(indexer),
+ ".store/scip-dotnet/#{EXPECTED_INDEXER}/scip-dotnet/#{EXPECTED_INDEXER}/tools/**/scip-dotnet.dll"
+)].first
+indexer_dll ||= indexer
+revision = capture!("git", "rev-parse", "HEAD", chdir: source_root)
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => {
+ "csharp.runtime.source" => "dotnet/runtime@#{revision}",
+ "csharp.target_framework" => TARGET_FRAMEWORK,
+ "csharp.reference_pack.system_runtime.sha256" =>
+ "sha256:#{Digest::SHA256.file(reference).hexdigest}",
+ "csharp.scip_dotnet.release" => EXPECTED_INDEXER,
+ "csharp.scip_dotnet.sha256" =>
+ "sha256:#{Digest::SHA256.file(indexer_dll).hexdigest}"
+ }
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/go-1.22.2.yml b/gems/fact-mine/config/stdlib_maps/go-1.22.2.yml
new file mode 100644
index 000000000..44dd85294
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/go-1.22.2.yml
@@ -0,0 +1,63 @@
+schema: fact-mine.stdlib-map.v1
+language: go
+
+source:
+ root_command: ["go", "env", "GOROOT"]
+ root_suffix: src
+ revision: go1.22.2
+ revision_check:
+ command: ["go", "version"]
+ includes: ["go1.22.2"]
+ include:
+ - "{syscall,os,time,flag,bytes,bufio,sort,strings,strconv,math,regexp,fmt}/**/*.go"
+ - "encoding/{json,xml}/**/*.go"
+ - "io/fs/**/*.go"
+ exclude:
+ - "**/*_test.go"
+
+index:
+ command:
+ - scip-go
+ - index
+ - syscall/...
+ - os/...
+ - time/...
+ - flag/...
+ - bytes/...
+ - bufio/...
+ - encoding/json/...
+ - encoding/xml/...
+ - sort/...
+ - strings/...
+ - io/fs/...
+ - strconv/...
+ - math/...
+ - regexp/...
+ - fmt/...
+ - --module-root
+ - "{source_root}"
+ - --repository-remote
+ - github.com/golang/go
+ - --module-path
+ - github.com/golang/go/src
+ - --module-version
+ - go1.22
+ - --go-version
+ - go1.22.2
+ - --output
+ - "{index}"
+ - --skip-tests
+ working_directory: "{source_root}"
+ output: go-stdlib.scip
+ expected:
+ tool: scip-go
+ version: 0.2.7
+
+soundness:
+ minimum_export_eligible_methods: 3000
+
+summary:
+ corpus: go-stdlib-core-surface
+ output: ../complexity_summaries/go-stdlib.go1.22.2.json.gz
+ minimum_symbols: 310
+ expected_symbol_prefix: "scip-go gomod github.com/golang/go/src go1.22 "
diff --git a/gems/fact-mine/config/stdlib_maps/java-21.0.12.yml b/gems/fact-mine/config/stdlib_maps/java-21.0.12.yml
new file mode 100644
index 000000000..5591b2e9a
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/java-21.0.12.yml
@@ -0,0 +1,60 @@
+schema: fact-mine.stdlib-map.v1
+language: java
+
+source:
+ revision: jdk-21.0.12+8
+ git:
+ repository: https://github.com/adoptium/jdk21u.git
+ commit: 9de4f68c88a0a1510373f291d1a95b1f6b0db8c8
+ sparse_paths:
+ - src/java.base/share/classes/java/lang
+ - src/java.base/share/classes/java/util
+ root_suffix: src/java.base/share/classes
+ include:
+ - "java/{lang,util}/**/*.java"
+
+index:
+ command:
+ - scip-java
+ - index
+ - --output
+ - "{index}"
+ - --build-tool
+ - maven
+ - --
+ - package
+ - "-Dstdlib.source={source_root}/java"
+ working_directory: jdk-21.0.12
+ output: java-stdlib.scip
+ expected:
+ tool: scip-java
+ version: 0.12.3
+
+soundness:
+ minimum_export_eligible_methods: 5000
+
+summary:
+ corpus: java-stdlib-lang-util
+ output: ../complexity_summaries/java-stdlib.jdk21.0.12.json.gz
+ minimum_symbols: 1
+ expected_symbol_prefix: "semanticdb maven jdk 21 "
+ symbol_relocation:
+ from: "semanticdb maven maven/jdk/java.base 21.0.12 "
+ to: "semanticdb maven jdk 21 "
+
+consumers:
+ - name: java-21-exact-symbol
+ source_root: consumers/java-21
+ include: ["src/main/java/**/*.java"]
+ index:
+ command:
+ - scip-java
+ - index
+ - --output
+ - "{index}"
+ - --build-tool
+ - maven
+ - --
+ - package
+ output: java-consumer.scip
+ minimum_complete_percent: 100
diff --git a/gems/fact-mine/config/stdlib_maps/javascript/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/javascript/semantic_environment.rb
new file mode 100644
index 000000000..b2ef94704
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/javascript/semantic_environment.rb
@@ -0,0 +1,56 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+source_root, output = ARGV
+abort "usage: semantic_environment.rb SOURCE_ROOT OUTPUT.json" unless source_root && output
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ path = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |candidate| File.file?(candidate) && File.executable?(candidate) }
+ abort "#{fallback} was not found; set #{environment}" unless path
+
+ path
+end
+
+def capture!(*command, chdir:)
+ stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout.strip
+end
+
+source_root = File.expand_path(source_root)
+node = executable("NODE", "node")
+indexer = executable("SCIP_TYPESCRIPT", "scip-typescript")
+versions = JSON.parse(capture!(
+ node,
+ "-p",
+ "JSON.stringify(process.versions)",
+ chdir: source_root
+))
+%w[node v8 modules].each do |key|
+ abort "Node did not report process.versions.#{key}" if versions[key].to_s.empty?
+end
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => {
+ "javascript.runtime" => "node",
+ "javascript.node.version" => versions.fetch("node"),
+ "javascript.v8.version" => versions.fetch("v8"),
+ "javascript.node.modules_abi" => versions.fetch("modules"),
+ "javascript.node.sha256" => "sha256:#{Digest::SHA256.file(node).hexdigest}",
+ "javascript.scip_typescript.version" =>
+ capture!(indexer, "--version", chdir: source_root).lines.first.to_s,
+ "javascript.scip_typescript.sha256" =>
+ "sha256:#{Digest::SHA256.file(indexer).hexdigest}"
+ }
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/jdk-21.0.12/pom.xml b/gems/fact-mine/config/stdlib_maps/jdk-21.0.12/pom.xml
new file mode 100644
index 000000000..7536aeaad
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/jdk-21.0.12/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+ jdk
+ java.base
+ 21.0.12
+
+ 21
+ UTF-8
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.6.1
+
+
+ add-jdk-source
+ generate-sources
+
+ add-source
+
+
+
+ ${stdlib.source}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+
+ --patch-module
+ java.base=${stdlib.source}
+
+
+
+
+
+
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin-2.2.0.yml b/gems/fact-mine/config/stdlib_maps/kotlin-2.2.0.yml
new file mode 100644
index 000000000..820b48653
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin-2.2.0.yml
@@ -0,0 +1,67 @@
+schema: fact-mine.stdlib-map.v1
+language: kotlin
+
+source:
+ root_command:
+ - ruby
+ - "{manifest_dir}/kotlin/materialize_source.rb"
+ - "{workspace_root}"
+ revision: kotlin-stdlib-2.2.0
+ revision_check:
+ command:
+ - ruby
+ - "{manifest_dir}/kotlin/revision.rb"
+ - "{source_root}"
+ equals: "kotlin-stdlib-2.2.0 sources sha256:967ad9599254e3a60d96d6c789547cc35c22d770d9c8fb1e3f15fac3b4c3b65d"
+ include:
+ - "commonMain/generated/*.kt"
+ - "jvmMain/generated/*.kt"
+ - "commonMain/kotlin/collections/*.kt"
+ exclude:
+ - "**/AbstractMutableCollection.kt"
+ - "**/AbstractMutableList.kt"
+ - "**/AbstractMutableMap.kt"
+ - "**/AbstractMutableSet.kt"
+ - "**/ArrayList.kt"
+ - "**/Collections.kt"
+ - "**/CollectionsH.kt"
+ - "**/HashMap.kt"
+ - "**/HashSet.kt"
+ - "**/LinkedHashMap.kt"
+ - "**/LinkedHashSet.kt"
+ - "**/Maps.kt"
+ - "**/Sets.kt"
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/kotlin/index_stdlib.rb"
+ - "{source_root}"
+ - "{index}"
+ - "{workspace_root}"
+ - "{manifest_dir}/kotlin/scip-kotlin-top-level-symbols.patch"
+ working_directory: "{source_root}"
+ output: kotlin-stdlib.scip
+ expected:
+ tool: scip-java
+ version: 0.12.3
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/kotlin/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 3500
+
+summary:
+ corpus: kotlin-stdlib-jvm
+ output: ../complexity_summaries/kotlin-stdlib.kotlin2.2.0.json.gz
+ minimum_symbols: 80
+ expected_symbol_prefix: "scip-java maven . . kotlin/"
+ symbol_relocation:
+ from: "semanticdb maven . . kotlin/"
+ to: "scip-java maven . . kotlin/"
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin/index_stdlib.rb b/gems/fact-mine/config/stdlib_maps/kotlin/index_stdlib.rb
new file mode 100644
index 000000000..4a56e5310
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin/index_stdlib.rb
@@ -0,0 +1,158 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "json"
+require "open-uri"
+require "open3"
+
+SCIP_JAVA_VERSION = "0.12.3"
+SCIP_JAVA_SHA256 = "2d4d8a31333dfa0daf3aa0381a51de465e40b0dac5622e49363786a65f743f34"
+SCIP_KOTLIN_COMMIT = "2648c7dc04c2cc999cae9e3fd19863177a07492d"
+PATCHED_PLUGIN_SHA256 = "4a141d34551466881b4cfdd5127718358ddd0917971e0254664b48e6bcf3a319"
+
+source_root, output, workspace_root, patch = ARGV
+abort "usage: index_stdlib.rb SOURCE_ROOT OUTPUT.scip WORKSPACE_ROOT PATCH" unless patch
+source_root = File.expand_path(source_root)
+output = File.expand_path(output)
+cache = File.join(File.expand_path(workspace_root), ".cache", "stdlib-indexers", "kotlin-2.2.0")
+FileUtils.mkdir_p(cache)
+
+java_home = ENV["JAVA_HOME"]
+abort "JAVA_HOME must identify JDK 21" unless java_home && File.executable?(File.join(java_home, "bin", "java"))
+
+def capture!(*command, chdir:, env: {})
+ stdout, stderr, status = Open3.capture3(env, *command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+ stdout
+end
+
+java_stdout, java_stderr, java_status = Open3.capture3(
+ File.join(java_home, "bin", "java"), "-version", chdir: source_root
+)
+abort "failed to inspect JAVA_HOME" unless java_status.success?
+java_version = "#{java_stdout}\n#{java_stderr}"
+abort "JDK 21 is required, got #{java_version.inspect}" unless java_version.include?("21.")
+
+scip_java = ENV["SCIP_JAVA"]
+if scip_java && File.executable?(scip_java)
+ actual = Digest::SHA256.file(scip_java).hexdigest
+ abort "scip-java digest mismatch: expected #{SCIP_JAVA_SHA256}, got #{actual}" unless actual == SCIP_JAVA_SHA256
+else
+ scip_java = File.join(cache, "scip-java-v#{SCIP_JAVA_VERSION}")
+ unless File.file?(scip_java) && Digest::SHA256.file(scip_java).hexdigest == SCIP_JAVA_SHA256
+ temporary = "#{scip_java}.download-#{Process.pid}"
+ url = "https://github.com/scip-code/scip-java/releases/download/v#{SCIP_JAVA_VERSION}/scip-java-v#{SCIP_JAVA_VERSION}"
+ URI.open(url) { |input| File.open(temporary, "wb") { |file| IO.copy_stream(input, file) } }
+ actual = Digest::SHA256.file(temporary).hexdigest
+ abort "scip-java digest mismatch: expected #{SCIP_JAVA_SHA256}, got #{actual}" unless actual == SCIP_JAVA_SHA256
+ File.rename(temporary, scip_java)
+ FileUtils.chmod(0o755, scip_java)
+ end
+end
+version = capture!(scip_java, "--version", chdir: source_root, env: {"JAVA_HOME" => java_home}).strip
+abort "scip-java #{SCIP_JAVA_VERSION} is required, got #{version.inspect}" unless version == SCIP_JAVA_VERSION
+
+plugin = ENV["SEMANTICDB_KOTLINC"]
+if plugin && File.file?(plugin)
+ actual = Digest::SHA256.file(plugin).hexdigest
+ unless actual == PATCHED_PLUGIN_SHA256
+ abort "semanticdb-kotlinc digest mismatch: expected #{PATCHED_PLUGIN_SHA256}, got #{actual}"
+ end
+else
+ checkout = File.join(cache, "scip-kotlin")
+ unless File.directory?(File.join(checkout, ".git"))
+ capture!("git", "clone", "--quiet", "https://github.com/sourcegraph/scip-kotlin.git", checkout, chdir: cache)
+ end
+ head = capture!("git", "rev-parse", "HEAD", chdir: checkout).strip
+ unless head == SCIP_KOTLIN_COMMIT
+ capture!("git", "fetch", "--quiet", "--depth", "1", "origin", SCIP_KOTLIN_COMMIT, chdir: checkout)
+ capture!("git", "checkout", "--quiet", "--detach", "FETCH_HEAD", chdir: checkout)
+ end
+ _stdout, _stderr, applied = Open3.capture3("git", "apply", "--check", File.expand_path(patch), chdir: checkout)
+ capture!("git", "apply", File.expand_path(patch), chdir: checkout) if applied.success?
+ unless system("git", "apply", "--reverse", "--check", File.expand_path(patch), chdir: checkout,
+ out: File::NULL, err: File::NULL)
+ abort "the pinned scip-kotlin patch could not be applied cleanly"
+ end
+ capture!(
+ File.join(checkout, "gradlew"), "--no-daemon", ":semanticdb-kotlinc:shadowJar",
+ chdir: checkout,
+ env: {"JAVA_HOME" => java_home}
+ )
+ plugin = Dir[File.join(checkout, "semanticdb-kotlinc/build/libs/semanticdb-kotlinc-*.jar")]
+ .reject { |path| path.end_with?("-slim.jar") }
+ .max
+ abort "patched semanticdb-kotlinc jar was not produced" unless plugin
+end
+actual_plugin = Digest::SHA256.file(plugin).hexdigest
+unless actual_plugin == PATCHED_PLUGIN_SHA256
+ abort "patched semanticdb-kotlinc is not reproducible: expected #{PATCHED_PLUGIN_SHA256}, got #{actual_plugin}"
+end
+
+marker = JSON.parse(File.read(File.join(source_root, ".fact-mine-source.json")))
+binary = marker.fetch("binary")
+semanticdb = File.join(source_root, ".fact-mine", "semanticdb")
+FileUtils.rm_rf(semanticdb)
+FileUtils.mkdir_p(semanticdb)
+settings = File.join(source_root, "settings.gradle")
+build = File.join(source_root, "build.gradle")
+properties = File.join(source_root, "gradle.properties")
+File.write(settings, <<~GRADLE)
+ pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ }
+ }
+ rootProject.name = "fact-mine-kotlin-stdlib"
+GRADLE
+File.write(build, <<~GRADLE)
+ plugins {
+ id "org.jetbrains.kotlin.multiplatform" version "2.2.0"
+ }
+ repositories { mavenCentral() }
+ dependencies {
+ commonMainImplementation files("#{binary}")
+ }
+ kotlin {
+ jvm()
+ targets.configureEach {
+ compilations.configureEach {
+ compileTaskProvider.configure {
+ compilerOptions {
+ freeCompilerArgs.addAll(
+ "-Xallow-kotlin-package",
+ "-opt-in=kotlin.contracts.ExperimentalContracts",
+ "-Xfriend-paths=#{binary}",
+ "-Xplugin=#{plugin}",
+ "-P", "plugin:semanticdb-kotlinc:sourceroot=#{source_root}",
+ "-P", "plugin:semanticdb-kotlinc:targetroot=#{semanticdb}"
+ )
+ }
+ }
+ }
+ }
+ sourceSets {
+ commonMain.kotlin.srcDirs("commonMain/generated", "commonMain/kotlin/collections")
+ commonMain.kotlin.exclude(
+ "**/AbstractMutableCollection.kt", "**/AbstractMutableList.kt",
+ "**/AbstractMutableMap.kt", "**/AbstractMutableSet.kt", "**/ArrayList.kt",
+ "**/Collections.kt", "**/CollectionsH.kt", "**/HashMap.kt", "**/HashSet.kt",
+ "**/LinkedHashMap.kt", "**/LinkedHashSet.kt", "**/Maps.kt", "**/Sets.kt"
+ )
+ jvmMain.kotlin.srcDir("jvmMain/generated")
+ }
+ }
+GRADLE
+File.write(properties, "kotlin.stdlib.default.dependency=false\n")
+
+gradle = ENV["GRADLE"]
+gradle ||= File.join(cache, "scip-kotlin", "gradlew")
+capture!(gradle, "--no-daemon", "clean", "compileKotlinJvm", chdir: source_root,
+ env: {"JAVA_HOME" => java_home})
+FileUtils.mkdir_p(File.dirname(output))
+capture!(scip_java, "index-semanticdb", semanticdb, "--output", output,
+ chdir: source_root, env: {"JAVA_HOME" => java_home})
+abort "scip-java did not produce #{output}" unless File.size?(output)
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin/materialize_source.rb b/gems/fact-mine/config/stdlib_maps/kotlin/materialize_source.rb
new file mode 100644
index 000000000..4f5668dd5
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin/materialize_source.rb
@@ -0,0 +1,59 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "json"
+require "open-uri"
+require "open3"
+
+VERSION = "2.2.0"
+SOURCES_SHA256 = "967ad9599254e3a60d96d6c789547cc35c22d770d9c8fb1e3f15fac3b4c3b65d"
+BINARY_SHA256 = "65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca"
+BASE_URL = "https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/#{VERSION}"
+
+workspace = ARGV.fetch(0) do
+ abort "usage: materialize_source.rb WORKSPACE_ROOT"
+end
+cache = File.join(File.expand_path(workspace), ".cache", "stdlib-sources", "kotlin-stdlib-#{VERSION}")
+archive_dir = File.join(cache, "artifacts")
+source_root = File.join(cache, "source")
+marker = File.join(source_root, ".fact-mine-source.json")
+FileUtils.mkdir_p(archive_dir)
+
+def download(url, destination, sha256)
+ if File.file?(destination) && Digest::SHA256.file(destination).hexdigest == sha256
+ return
+ end
+
+ temporary = "#{destination}.download-#{Process.pid}"
+ URI.open(url) { |input| File.open(temporary, "wb") { |output| IO.copy_stream(input, output) } }
+ actual = Digest::SHA256.file(temporary).hexdigest
+ abort "digest mismatch for #{url}: expected #{sha256}, got #{actual}" unless actual == sha256
+
+ File.rename(temporary, destination)
+ensure
+ FileUtils.rm_f(temporary) if defined?(temporary)
+end
+
+sources = File.join(archive_dir, "kotlin-stdlib-#{VERSION}-sources.jar")
+binary = File.join(archive_dir, "kotlin-stdlib-#{VERSION}.jar")
+download("#{BASE_URL}/kotlin-stdlib-#{VERSION}-sources.jar", sources, SOURCES_SHA256)
+download("#{BASE_URL}/kotlin-stdlib-#{VERSION}.jar", binary, BINARY_SHA256)
+
+expected_marker = {
+ "version" => VERSION,
+ "sources_sha256" => SOURCES_SHA256,
+ "binary_sha256" => BINARY_SHA256,
+ "binary" => binary
+}
+current_marker = JSON.parse(File.read(marker)) if File.file?(marker)
+unless current_marker == expected_marker
+ FileUtils.rm_rf(source_root)
+ FileUtils.mkdir_p(source_root)
+ _stdout, stderr, status = Open3.capture3("unzip", "-q", sources, "-d", source_root)
+ abort "failed to extract #{sources}: #{stderr}" unless status.success?
+ File.write(marker, JSON.pretty_generate(expected_marker))
+end
+
+puts source_root
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin/revision.rb b/gems/fact-mine/config/stdlib_maps/kotlin/revision.rb
new file mode 100644
index 000000000..f6060af74
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin/revision.rb
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "json"
+
+EXPECTED = {
+ "version" => "2.2.0",
+ "sources_sha256" => "967ad9599254e3a60d96d6c789547cc35c22d770d9c8fb1e3f15fac3b4c3b65d",
+ "binary_sha256" => "65d12d85a3b865c160db9147851712a64b10dadd68b22eea22a95bf8a8670dca"
+}.freeze
+
+source_root = File.expand_path(ARGV.fetch(0) { abort "usage: revision.rb SOURCE_ROOT" })
+marker = JSON.parse(File.read(File.join(source_root, ".fact-mine-source.json")))
+EXPECTED.each do |key, value|
+ abort "Kotlin stdlib marker mismatch for #{key}" unless marker[key] == value
+end
+
+puts "kotlin-stdlib-2.2.0 sources sha256:#{EXPECTED.fetch('sources_sha256')}"
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin/scip-kotlin-top-level-symbols.patch b/gems/fact-mine/config/stdlib_maps/kotlin/scip-kotlin-top-level-symbols.patch
new file mode 100644
index 000000000..c816ada18
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin/scip-kotlin-top-level-symbols.patch
@@ -0,0 +1,44 @@
+diff --git a/build.gradle.kts b/build.gradle.kts
+index e9066a2..954d945 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -65,7 +65,7 @@ allprojects {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_1_8
+ }
+ jvmToolchain {
+- (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(8))
++ (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(21))
+ }
+ }
+diff --git a/semanticdb-kotlin/build.gradle.kts b/semanticdb-kotlin/build.gradle.kts
+index a4dfa19..cc678ac 100644
+--- a/semanticdb-kotlin/build.gradle.kts
++++ b/semanticdb-kotlin/build.gradle.kts
+@@ -34,12 +34,4 @@ afterEvaluate {
+ tasks.compileKotlin {
+ dependsOn(tasks.getByName("generateProto"))
+ }
+-
+- tasks.withType {
+- val sourceroot = rootDir.path
+- val targetroot = this.project.buildDir.resolve( "semanticdb-targetroot")
+- options.compilerArgs = options.compilerArgs + listOf(
+- "-Xplugin:semanticdb -sourceroot:$sourceroot -targetroot:$targetroot"
+- )
+- }
+ }
+diff --git a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt
+index 5f27dd3..20a2a96 100644
+--- a/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt
++++ b/semanticdb-kotlinc/src/main/kotlin/com/sourcegraph/semanticdb_kotlinc/SymbolsCache.kt
+@@ -183,6 +183,8 @@ class GlobalSymbolsCache(
+ is FirClassSymbol ->
+ (containingSymbol.fir as FirClass).declarations.map { it.symbol }
+- is FirFileSymbol -> containingSymbol.fir.declarations.map { it.symbol }
++ is FirFileSymbol ->
++ symbol.moduleData.session.symbolProvider.getTopLevelCallableSymbols(
++ symbol.packageFqName(), symbol.name)
+ null ->
+ symbol.moduleData.session.symbolProvider.getTopLevelCallableSymbols(
+ symbol.packageFqName(), symbol.name)
diff --git a/gems/fact-mine/config/stdlib_maps/kotlin/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/kotlin/semantic_environment.rb
new file mode 100644
index 000000000..a6ff97649
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/kotlin/semantic_environment.rb
@@ -0,0 +1,23 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+
+source_root, output = ARGV
+abort "usage: semantic_environment.rb SOURCE_ROOT OUTPUT.json" unless source_root && output
+
+marker = JSON.parse(File.read(File.join(File.expand_path(source_root), ".fact-mine-source.json")))
+binary = marker.fetch("binary")
+actual = Digest::SHA256.file(binary).hexdigest
+expected = marker.fetch("binary_sha256")
+abort "Kotlin stdlib binary digest mismatch: expected #{expected}, got #{actual}" unless actual == expected
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => {
+ "kotlin.platform" => "jvm",
+ "kotlin.stdlib.version" => marker.fetch("version"),
+ "kotlin.stdlib.binary.sha256" => "sha256:#{actual}"
+ }
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/php/scip-php-exact-version.patch b/gems/fact-mine/config/stdlib_maps/php/scip-php-exact-version.patch
new file mode 100644
index 000000000..14de5f831
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/php/scip-php-exact-version.patch
@@ -0,0 +1,8 @@
+diff --git a/bin/scip-php b/bin/scip-php
+--- a/bin/scip-php
++++ b/bin/scip-php
+@@ -36,3 +36,3 @@
+ $projectRoot = \getcwd();
+-$version = '0.0.1';
++$version = '0.0.1+71a5b117';
+ $args = \array_splice($argv, 1);
diff --git a/gems/fact-mine/config/stdlib_maps/python-3.11.9.yml b/gems/fact-mine/config/stdlib_maps/python-3.11.9.yml
new file mode 100644
index 000000000..2daa5c1c7
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/python-3.11.9.yml
@@ -0,0 +1,56 @@
+schema: fact-mine.stdlib-map.v1
+language: python
+
+source:
+ revision: cpython-3.11.9
+ git:
+ repository: https://github.com/python/cpython.git
+ commit: de54cf5be371a6f5e2e9f208c38def5f81d3ef02
+ sparse_paths:
+ - Lib
+ root_suffix: Lib
+ include:
+ - "{bisect,copy,dataclasses,enum,fnmatch,functools,glob,heapq,operator,pathlib,random,shutil,statistics,string,textwrap,types,weakref}.py"
+ - "{collections,concurrent,json,logging,re,urllib}/**/*.py"
+ exclude:
+ - "**/test/**"
+ - "**/tests/**"
+ stage_selected_files: true
+
+index:
+ command:
+ - scip-python
+ - index
+ - "."
+ - --project-name=python-stdlib
+ - --project-version=3.11
+ - "--output={index}"
+ working_directory: "{source_root}"
+ output: python-stdlib.scip
+ expected:
+ tool: scip-python
+ version: 0.6.6
+
+soundness:
+ minimum_export_eligible_methods: 250
+
+summary:
+ corpus: python-stdlib-pure-python-core
+ output: ../complexity_summaries/python-stdlib.cpython3.11.9.json.gz
+ minimum_symbols: 1
+ expected_symbol_prefix: "scip-python python python-stdlib 3.11 "
+
+consumers:
+ - name: python-3.11-exact-symbol
+ source_root: consumers/python-3.11
+ include: ["impact.py"]
+ index:
+ command:
+ - scip-python
+ - index
+ - "."
+ - --project-name=fact-mine-stdlib-consumer
+ - --project-version=1.0.0
+ - "--output={index}"
+ output: python-consumer.scip
+ minimum_complete_percent: 100
diff --git a/gems/fact-mine/config/stdlib_maps/ruby-3.2.3.yml b/gems/fact-mine/config/stdlib_maps/ruby-3.2.3.yml
new file mode 100644
index 000000000..a9efc1753
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/ruby-3.2.3.yml
@@ -0,0 +1,53 @@
+schema: fact-mine.stdlib-map.v1
+language: c
+
+source:
+ revision: cruby-3.2.3
+ git:
+ repository: https://github.com/ruby/ruby.git
+ commit: 52bb2ac0a6971d0391efa2275f7a66bff319087c
+ include:
+ # `index_cruby.rb` indexes the complete core-A frontier. Keep the
+ # producer profile aligned with that frontier: `re.c` owns Regexp.escape
+ # and Regexp#match?, both of which are emitted by NilKill consumers.
+ - "{array,dir,enum,error,file,hash,io,math,numeric,re,string,struct}.c"
+
+index:
+ command:
+ - ruby
+ - "{manifest_dir}/ruby/index_cruby.rb"
+ - "{source_root}"
+ - "{index}"
+ working_directory: "{source_root}"
+ output: cruby.scip
+ expected:
+ tool: scip-clang
+ version: "0.4.0"
+
+compatibility:
+ command:
+ - ruby
+ - "{manifest_dir}/ruby/semantic_environment.rb"
+ - "{source_root}"
+ - "{environment}"
+ - 3.2.3
+ working_directory: "{source_root}"
+
+soundness:
+ minimum_export_eligible_methods: 100
+
+summary:
+ corpus: cruby-core-3.2.3-core-a
+ output: ../complexity_summaries/ruby-cruby.3.2.3-core-a.json.gz
+ minimum_symbols: 1
+ consumer_indexers: ["nil-kill-runtime@2"]
+ symbol_bridge:
+ command:
+ - ruby
+ - "{manifest_dir}/ruby/build_symbol_bridge.rb"
+ - "{producer_summary}"
+ - "{profile}"
+ - "{source_root}"
+ - "{symbol_bridge}"
+ - 3.2.3
+ working_directory: "{source_root}"
diff --git a/gems/fact-mine/config/stdlib_maps/ruby/build_symbol_bridge.rb b/gems/fact-mine/config/stdlib_maps/ruby/build_symbol_bridge.rb
new file mode 100644
index 000000000..8ce918e14
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/ruby/build_symbol_bridge.rb
@@ -0,0 +1,117 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Connect source-proven CRuby C functions to the stable runtime identities
+# emitted by NilKill. Registration is the authoritative ownership relation:
+# no Ruby method is admitted unless both its registration and the exact C
+# declaration symbol appear in the producer's generated summary.
+
+require "json"
+require "zlib"
+
+producer_path, profile_path, source_root, output, runtime_version = ARGV
+abort "usage: build_symbol_bridge.rb PRODUCER.json.gz PROFILE.json SOURCE_ROOT OUTPUT.json RUBY_VERSION" unless runtime_version
+abort "invalid Ruby runtime version #{runtime_version.inspect}" unless runtime_version.match?(/\A\d+\.\d+\.\d+\z/)
+
+producer = Zlib::GzipReader.open(producer_path) { |gzip| JSON.parse(gzip.read) }
+profile = JSON.parse(File.read(profile_path))
+source_root = File.expand_path(source_root)
+
+def ruby_owner(receiver)
+ return unless receiver.match?(/\Arb_[cme][A-Za-z0-9_]+\z/)
+
+ receiver.delete_prefix("rb_")[1..]
+end
+
+def runtime_atom(name)
+ operators = %w[[] []= + - * / % ** << >> < <= > >= == === =~ !~ & | ^ ~ +@ -@ <=>]
+ operators.include?(name) ? "`#{name}`" : name
+end
+
+def runtime_symbol(owner, separator, name, version)
+ "nil-kill-runtime ruby ruby #{version} #{owner}#{separator}#{runtime_atom(name)}()."
+end
+
+def registrations(source_root)
+ rows = []
+ Dir.glob(File.join(source_root, "**", "*.c")).sort.each do |path|
+ source = File.read(path)
+ relative = path.delete_prefix("#{source_root}#{File::SEPARATOR}")
+ source.scan(
+ /rb_define_(singleton_method|method|module_function|private_method)\s*\(\s*(rb_[cme][A-Za-z0-9_]+)\s*,\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/m
+ ) do |kind, receiver, name, function|
+ owner = ruby_owner(receiver)
+ next unless owner
+
+ separators = case kind
+ when "singleton_method" then ["."]
+ when "module_function" then ["#", "."]
+ else ["#"]
+ end
+ separators.each do |separator|
+ rows << { "path" => relative, "function" => function, "owner" => owner,
+ "separator" => separator, "name" => name }
+ end
+ end
+ source.scan(
+ /rb_define_global_function\s*\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/m
+ ) do |name, function|
+ rows << { "path" => relative, "function" => function, "owner" => "Kernel",
+ "separator" => "#", "name" => name }
+ end
+ # file.c intentionally registers FileTest module functions and File
+ # singleton methods through this macro. Preserve both exact public
+ # identities rather than hard-coding their costs.
+ source.scan(
+ /define_filetest_function\s*\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/m
+ ) do |name, function|
+ rows << { "path" => relative, "function" => function, "owner" => "FileTest",
+ "separator" => "#", "name" => name }
+ rows << { "path" => relative, "function" => function, "owner" => "File",
+ "separator" => ".", "name" => name }
+ end
+ end
+ rows.uniq
+end
+
+exact_summary_symbols = producer.fetch("symbols").select do |_symbol, row|
+ row.fetch("bound_quality", "") == "upper_bound_exact_symbol"
+end.keys.to_h { |symbol| [symbol, true] }
+
+methods = profile.fetch("methods").filter_map do |method|
+ symbol = method["semantic_symbol"].to_s
+ next unless exact_summary_symbols[symbol]
+
+ path = method.fetch("path").to_s
+ relative = path.delete_prefix("#{source_root}#{File::SEPARATOR}")
+ next if relative == path
+
+ [[relative, method.fetch("name").to_s], symbol]
+end.to_h
+
+# A registration may be repeated, but one runtime method must never be
+# assigned an arbitrary implementation when CRuby has multiple declarations.
+targets = Hash.new { |hash, key| hash[key] = [] }
+registrations(source_root).each do |registration|
+ symbol = methods[[registration.fetch("path"), registration.fetch("function")]]
+ next unless symbol
+
+ target = runtime_symbol(
+ registration.fetch("owner"), registration.fetch("separator"), registration.fetch("name"), runtime_version
+ )
+ targets[target] << symbol unless targets[target].include?(symbol)
+end
+
+symbols = Hash.new { |hash, key| hash[key] = [] }
+targets.each do |target, candidates|
+ next unless candidates.length == 1
+
+ symbols[candidates.fetch(0)] << target
+end
+symbols.transform_values!(&:sort)
+abort "no source-proven CRuby registrations matched the producer summary" if symbols.empty?
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.symbol-bridge.v1",
+ "symbols" => symbols.sort.to_h
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/ruby/index_cruby.rb b/gems/fact-mine/config/stdlib_maps/ruby/index_cruby.rb
new file mode 100644
index 000000000..808002a98
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/ruby/index_cruby.rb
@@ -0,0 +1,66 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "digest"
+require "json"
+require "open3"
+
+INDEXER_VERSION = "0.4.0"
+INDEXER_SHA256 = "06fd18c576f979a726c651594644ec4a35db4f471f2160b3f72eb89fa6001784"
+CORE_SOURCES = %w[
+ array.c bignum.c class.c compar.c dir.c enum.c error.c file.c hash.c io.c
+ math.c numeric.c object.c proc.c random.c range.c re.c string.c struct.c
+ time.c variable.c vm_method.c
+].freeze
+
+source_root, output = ARGV
+abort "usage: index_cruby.rb CRUBY_SOURCE_ROOT OUTPUT.scip" unless source_root && output
+source_root = File.expand_path(source_root)
+output = File.expand_path(output)
+
+def executable(environment, fallback)
+ configured = ENV[environment]
+ return File.expand_path(configured) if configured && !configured.empty?
+
+ candidate = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR)
+ .map { |directory| File.join(directory, fallback) }
+ .find { |path| File.file?(path) && File.executable?(path) }
+ abort "#{fallback} was not found; set #{environment}" unless candidate
+
+ candidate
+end
+
+def capture!(*command, chdir:)
+ stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
+ abort "#{command.join(' ')} failed:\n#{stderr}" unless status.success?
+
+ stdout
+end
+
+compiler = executable("CLANG", "clang-20")
+indexer = executable("SCIP_CLANG", "scip-clang")
+version = capture!(indexer, "--version", chdir: source_root)
+abort "scip-clang #{INDEXER_VERSION} is required" unless version.lines.first&.strip == "scip-clang #{INDEXER_VERSION}"
+abort "unexpected scip-clang binary" unless Digest::SHA256.file(indexer).hexdigest == INDEXER_SHA256
+
+files = CORE_SOURCES.map { |name| File.join(source_root, name) }
+missing = files.reject { |path| File.file?(path) }
+abort "CRuby core sources were not found: #{missing.join(', ')}" unless missing.empty?
+database = files.map do |file|
+ {
+ "directory" => source_root,
+ "file" => file,
+ "arguments" => [compiler, "-I", source_root, "-DRUBY_EXPORT", "-c", file]
+ }
+end
+database_path = File.join(File.dirname(output), "compile_commands.json")
+File.write(database_path, JSON.pretty_generate(database))
+capture!(
+ indexer,
+ "--compdb-path", database_path,
+ "--index-output-path", output,
+ "--no-progress-report",
+ "-j", "1",
+ chdir: source_root
+)
+abort "scip-clang did not produce #{output}" unless File.size?(output)
diff --git a/gems/fact-mine/config/stdlib_maps/ruby/semantic_environment.rb b/gems/fact-mine/config/stdlib_maps/ruby/semantic_environment.rb
new file mode 100644
index 000000000..aa36ce32a
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/ruby/semantic_environment.rb
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "json"
+
+source_root, output, runtime_version = ARGV
+abort "usage: semantic_environment.rb CRUBY_SOURCE_ROOT OUTPUT.json RUBY_VERSION" unless runtime_version
+abort "invalid Ruby runtime version #{runtime_version.inspect}" unless runtime_version.match?(/\A\d+\.\d+\.\d+\z/)
+version_header = File.join(File.expand_path(source_root), "version.h")
+abort "CRuby version.h was not found" unless File.file?(version_header)
+header = File.read(version_header)
+api_header = File.join(File.expand_path(source_root), "include", "ruby", "version.h")
+abort "CRuby API version header was not found" unless File.file?(api_header)
+api_header = File.read(api_header)
+major, minor, teeny = runtime_version.split(".")
+{
+ "RUBY_API_VERSION_MAJOR" => major,
+ "RUBY_API_VERSION_MINOR" => minor,
+ "RUBY_VERSION_TEENY" => teeny
+}.each do |name, value|
+ source = name.start_with?("RUBY_API") ? api_header : header
+ actual = source[/^#\s*define\s+#{name}\s+(\d+)/, 1]
+ abort "CRuby version header #{name}=#{actual.inspect} does not match #{runtime_version}" unless actual == value
+end
+
+File.write(output, JSON.pretty_generate({
+ "schema" => "fact-mine.semantic-environment.v1",
+ "claims" => {
+ "runtime.language" => "ruby",
+ "runtime.engine" => "ruby",
+ "runtime.version" => runtime_version,
+ "runtime.engine_version" => runtime_version
+ }
+}))
diff --git a/gems/fact-mine/config/stdlib_maps/rust-1.96.0.yml b/gems/fact-mine/config/stdlib_maps/rust-1.96.0.yml
new file mode 100644
index 000000000..cd4d48a9a
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/rust-1.96.0.yml
@@ -0,0 +1,36 @@
+schema: fact-mine.stdlib-map.v1
+language: rust
+
+source:
+ root_command: ["rustc", "--print", "sysroot"]
+ root_suffix: lib/rustlib/src/rust
+ revision: rustc-1.96.0-ac68faa20c58
+ revision_check:
+ command: ["rustc", "--version", "--verbose"]
+ includes:
+ - "release: 1.96.0"
+ - "commit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96"
+ include:
+ - "library/{core,alloc,std}/**/*.rs"
+
+index:
+ command:
+ - rust-analyzer
+ - scip
+ - "{source_root}/library"
+ - --output
+ - "{index}"
+ working_directory: "{source_root}/library"
+ output: rust-stdlib.scip
+ expected:
+ tool: rust-analyzer
+ version: "1.96.0 (ac68faa 2026-05-25)"
+
+soundness:
+ minimum_export_eligible_methods: 15000
+
+summary:
+ corpus: rust-stdlib-core-alloc-std
+ output: ../complexity_summaries/rust-stdlib.rustc1.96.0.json.gz
+ minimum_symbols: 1500
+ expected_symbol_prefix: "rust-analyzer cargo "
diff --git a/gems/fact-mine/config/stdlib_maps/support.yml b/gems/fact-mine/config/stdlib_maps/support.yml
new file mode 100644
index 000000000..c516e531b
--- /dev/null
+++ b/gems/fact-mine/config/stdlib_maps/support.yml
@@ -0,0 +1,66 @@
+schema: fact-mine.stdlib-map-support.v1
+languages:
+ go:
+ status: bundled
+ manifest: go-1.22.2.yml
+ rust:
+ status: bundled
+ manifest: rust-1.96.0.yml
+ java:
+ status: bundled
+ manifest: java-21.0.12.yml
+ python:
+ status: bundled
+ manifest: python-3.11.9.yml
+ csharp:
+ status: bundled
+ manifest: csharp-10.0.10.yml
+ kotlin:
+ status: bundled
+ manifest: kotlin-2.2.0.yml
+ c:
+ status: blocked
+ blocker: implementation_source_not_indexed
+ compatibility_probe: c/semantic_environment.rb
+ required_fix: A pinned glibc source/build recipe must expose executable libc bodies under the same compiler, ABI, headers, and scip-clang environment now captured by the compatibility probe.
+ cpp:
+ status: bundled
+ manifests:
+ - cpp-libstdcxx-13.3.0-cxx17-containers.yml
+ - cpp-libstdcxx-13.3.0-cxx17-strings.yml
+ - cpp-libstdcxx-13.3.0-cxx20-memory.yml
+ typescript:
+ status: blocked
+ blocker: declaration_only_standard_library
+ compatibility_probe: javascript/semantic_environment.rb
+ required_fix: A Node/V8 producer must bridge executable runtime implementations to TypeScript declaration symbols while preserving callback and reflective costs; the runtime identity is captured by the compatibility probe.
+ javascript:
+ status: blocked
+ blocker: declaration_only_standard_library
+ compatibility_probe: javascript/semantic_environment.rb
+ required_fix: A Node/V8 producer must bridge executable runtime implementations to TypeScript declaration symbols while preserving callback and reflective costs; the runtime identity is captured by the compatibility probe.
+ php:
+ status: blocked
+ blocker: parametric_runtime_callbacks_and_allocator_state
+ indexer_patch: php/scip-php-exact-version.patch
+ required_fix: Generated summaries must preserve and compose PHP weak-coercion callback costs and Zend allocator-state costs before C implementation symbols can be bridged to scip-php builtin declarations.
+ ruby:
+ status: bundled
+ manifest: ruby-3.2.3.yml
+ scala:
+ status: blocked
+ blocker: fact_mine_language_adapter_missing
+ indexer: scip-java
+ reusable_bundle: java-21.0.12.yml
+ required_fix: Add a Scala syntax/CFG/DFG adapter and validate scip-java's Scala symbols; JDK calls can then reuse the existing exact Java bundle before a scala-library manifest is added.
+ visual_basic:
+ status: blocked
+ blocker: fact_mine_language_adapter_missing
+ indexer: scip-dotnet
+ reusable_bundle: csharp-10.0.10.yml
+ required_fix: Add a Visual Basic syntax/CFG/DFG adapter and validate scip-dotnet's language-neutral assembly symbols; CoreLib calls can then reuse the existing exact C# producer bundle.
+ dart:
+ status: blocked
+ blocker: fact_mine_language_adapter_missing
+ indexer: scip-dart
+ required_fix: Add a Dart syntax/CFG/DFG adapter, pin a scip-dart and Dart SDK pair, and express the SDK source mapping as a standard manifest.
diff --git a/gems/fact-mine/docs/agents/go-stdlib-bigo-investigation.md b/gems/fact-mine/docs/agents/go-stdlib-bigo-investigation.md
new file mode 100644
index 000000000..6566b9b50
--- /dev/null
+++ b/gems/fact-mine/docs/agents/go-stdlib-bigo-investigation.md
@@ -0,0 +1,181 @@
+# Go stdlib/vendor Big-O completeness investigation
+
+## Question
+
+Would resolving external (stdlib/vendor) callee bounds substantially increase
+`complete` Big-O for Go **production** code? Is >80% reachable? If so, what is
+the smarter method, and should we build it?
+
+Tests are excluded throughout: we care about the library/production runtime
+complexity, not table-driven test harnesses.
+
+## Method
+
+Sampled `~/unslop` and `gems/boobytrap/src` (boobytrap is itself Go). For each,
+`espalier -f architecture` (per-function `time_complete` + `big_o_time` known
+component) and `-f unknown_operations` (ranked blockers + the functions each
+blocks). Classified every blocker against GOROOT's 224-package set
+(`/usr/lib/go-1.22/src`) and the repo's own packages. A function is "completable
+by X" only if it has a concrete known structural component **and** all its
+call-blockers are resolvable by X.
+
+## Findings
+
+### 1. For production Go, structural loops/recursion are a non-issue
+
+The "structural ceiling" is a test-code artifact. Structurally-unbounded
+(`big_o_time = None`) production functions:
+
+| Repo | all fns | `_test.go` incl. structural-unknown | **production** structural-unknown |
+|---|---:|---:|---:|
+| unslop | 157 | 39 | **3** |
+| boobytrap | 131 | 46 | **0** |
+
+Test functions are table-driven (constant cases) or call unresolved `testing`
+methods; they dominated the earlier "unbounded loop" bucket. Production Go loops
+are almost all range-over-collection with a known cardinality domain, which the
+existing analyzer already bounds.
+
+### 2. Production completeness is gated by callee resolution, and it climbs fast
+
+| Step (production only) | unslop (121 fns) | boobytrap (85 fns) |
+|---|---:|---:|
+| complete now | 24% | 46% |
+| + stdlib/project package-call bounds | **64%** | **88%** |
+| + typed stdlib-type receiver methods | **74%** | 89% |
+| truly structural-unbounded (irreducible) | 3 fns | 0 fns |
+
+The residual after package-call bounds is "unresolved receiver" calls. Inspected
+directly, these are **overwhelmingly stdlib type methods on locals whose type
+was not propagated**: `mu.Lock` (sync.Mutex), `wg.Done` (sync.WaitGroup),
+`info.ModTime`/`info.Size` (os.FileInfo), `d.IsDir` (fs.DirEntry),
+`f.Close`/`f.Fd` (*os.File), `flags.Bool` (*flag.FlagSet),
+`inputBuf.WriteByte` (bytes.Buffer), `effTime.After` (time.Time). Only a few are
+project methods. None are unbounded - each is O(1)/amortized-O(1) once the
+receiver type is known. Resolving them pushes unslop from 74% toward **~90%**.
+
+This matches `espalier/docs/agents/complexity-coverage.md`: "the dominant
+remaining gap is **receiver/callee resolution, not loop or recursion
+recognition**."
+
+### 3. But raw analysis of the stdlib does not, by itself, deliver the bounds
+
+The Go stdlib is only **33% complete** when analyzed (170/515 over
+`fmt`,`filepath`,`strconv`,`strings`,`sort`), and the highest-frequency blockers
+are themselves incomplete: `fmt.Sprintf/Errorf/Fprintf` (reflection - ~40% of
+stdlib-call blockers), `filepath.Base/Dir/Ext`, `strconv.Itoa/Atoi`. Only
+`filepath.Join`/`ToSlash` came back complete. Completeness is transitive, so
+consuming an incomplete stdlib bound does not complete the caller. The bounds
+must be **asserted** (a sound semantic bound, e.g. `fmt.Sprintf` = O(total input
+size)), with analysis supplying the draft.
+
+### 4. Vendor is repo-specific and secondary
+
+Both repos are near-zero-dependency; the vendor/project blocker share was
+negligible. Vendor's value only appears on dependency-heavy code and must be
+measured on such a repo (e.g. fzf) before it is judged - it is not the lever
+for typical Go code.
+
+## Determination
+
+**>80% complete Big-O for production Go is reachable, and my earlier ~55-60%
+ceiling was wrong** - it counted test-code loop noise. Excluding tests,
+structural incompleteness is ~0; completeness is bounded almost entirely by
+**callee/receiver resolution into the standard library**, which is tractable
+because Go calls are statically typed and package-qualified.
+
+The smarter method is two coupled pieces, not "analyze GOROOT and consume it":
+
+1. **An asserted Go stdlib bound table covering both forms** in
+ `config/stdlib_complexity/go.yml`:
+ - package functions (`fmt.*`, `filepath.*`, `os.*`, `exec.*`, `strconv.*`,
+ `strings.*`), and
+ - **type methods** (`sync.Mutex`, `sync.WaitGroup`, `*os.File`,
+ `os.FileInfo`, `fs.DirEntry`, `bytes.Buffer`, `strings.Builder`,
+ `time.Time`, `*flag.FlagSet`).
+
+ Espalier's stdlib analysis supplies the draft known component; a human closes
+ the reflection/callback cases with a sound bound. Ranked by
+ `unknown_operations` occurrence, ~30-40 entries cover the bulk.
+
+2. **Local/flow receiver type resolution** so `mu`, `info`, `f`, `flags` resolve
+ to their stdlib types and pick up (1). This is the dominant gap named in
+ `complexity-coverage.md`; for Go it is available from declared/assigned local
+ types (or `go/types`/SSA for the hard cases).
+
+With both, the sampled repos project to ~90% (unslop) / ~89% (boobytrap)
+complete on production code. Vendor is a later, repo-specific add-on.
+
+This revises the "don't build" of `minimal-call-graph-feasibility.md`, whose
+data was Ruby (dynamic dispatch). Go's static typing makes the same lever pay
+off very differently.
+
+## Experiment: precomputed stdlib bounds + "trust the known component"
+
+Built `config/stdlib_complexity/go.stdlib.json.gz` (2,412 functions) by running
+`espalier -f architecture` over 330 files of the commonly-imported stdlib, then
+tested two consumption models.
+
+**1. Propagating stdlib bounds by corpus union gives ~zero uplift.** Analyzing
+unslop *together with* the stdlib source left production completeness at
+**24% -> 24%**. Because the stdlib is only 38% self-complete (1,172/3,071), its
+bounds are themselves `unknown`, and transitive completeness cascades the
+incompleteness straight back to the caller. Requiring transitive completeness is
+the wrong bar.
+
+**2. "Trust the known component unless a hidden callee can exceed it" reaches
+~90%.** For every incomplete function we already emit a known structural
+component (`O(1)`, `O(N)`, ...). It is only *wrong* if a hidden callee is
+asymptotically larger - which needs FFI (opaque C/syscall/runtime/unsafe),
+reflection, or a super-linear callback. Classifying incomplete functions by
+whether any blocker is in those danger categories:
+
+| Corpus | incomplete | known component RIGHT | risky: FFI | superlinear | reflection |
+|---|---:|---:|---:|---:|---:|
+| Go stdlib (worst case for FFI) | 1,708 | **81%** | 18% | 0% | 1% |
+| unslop | 92 | **88%** | 11% | 1% | 0% |
+| boobytrap | 46 | **83%** | 2% | 15% | 0% |
+
+Combined with the already-complete fraction, **effective trustworthy Big-O is
+~88-91%** on all three, and the residual risk is concentrated exactly where you
+predicted: FFI, plus a little super-linear-stdlib-in-a-cheap-function
+(boobytrap's `sort` usage). This holds on the stdlib itself, the most
+FFI-dense Go there is; ordinary application code is safer.
+
+**Caveat - the known component is also sometimes *over*-estimated.** The
+analyzer emits garbage for interface-callback and unbounded-recursion functions:
+`sort.Sort` -> a nonsensical multivariate blob (should be `O(N log N)`),
+`os.MkdirAll` -> `O(2^N)` (should be `O(depth)`). "Trust the known component"
+therefore also needs the structural analyzer's recursion/callback handling
+tightened, or those cases would be confidently wrong.
+
+## Revised recommendation
+
+The lever for >80% Go is **not** propagating stdlib completeness (0 uplift) and
+**not** a name-matched bounds table (fragile keys, self-incomplete source). It
+is a **completeness-model change**: report the known component as an
+*authoritative, confidence-tiered* answer, and reserve `unknown` for functions
+whose blockers are FFI / reflection / unbounded callback. That alone yields
+~88-91% trustworthy bounds. Support it with two fixes: (a) an explicit
+FFI/reflection boundary tag on facts, and (b) tighter structural handling of
+recursion and interface-callbacks so the trusted component is not over-estimated
+(`sort.Sort`, `os.MkdirAll`). `go.stdlib.json.gz` remains useful as a reference
+for the ~38% of stdlib functions that *are* complete, not as a propagation feed.
+
+## Recommended validation before building
+
+Assert the top ~30 Go stdlib bounds (package fns + type methods) in `go.yml`,
+add the local receiver-type resolution, re-run architecture on unslop +
+boobytrap + a dependency-heavy repo (fzf), production functions only, and
+confirm `time_complete` clears 80%. Build in that order; stop if step 1 alone
+does not reach ~65%.
+
+## Reproduction
+
+```sh
+espalier -f architecture -o arch.json
+espalier -f unknown_operations -o unknown.json
+espalier -f architecture -o stdlib.json \
+ /usr/lib/go-1.22/src/{fmt,path/filepath,strconv,strings,sort}/*.go # exclude _test
+# then: filter arch nodes to paths not ending _test.go; measure time_complete
+```
diff --git a/gems/fact-mine/docs/agents/scip-vs-fact-mine-boundary.md b/gems/fact-mine/docs/agents/scip-vs-fact-mine-boundary.md
new file mode 100644
index 000000000..8abec86c1
--- /dev/null
+++ b/gems/fact-mine/docs/agents/scip-vs-fact-mine-boundary.md
@@ -0,0 +1,240 @@
+# Design: The SCIP Boundary — what SCIP owns, what fact-mine owns
+
+Status: decided, evidence-based. Supersedes the implicit assumption (held through
+the 2026-07-25/26 work) that fact-mine should compute its own call resolution and
+type inference for every language.
+
+All numbers below are measured, not estimated. Method: for each corpus, run
+`espalier -f architecture` four ways — pre-session baseline (`9221f8f17`) and HEAD,
+each with and without a SCIP index — and compare `time_complete` per function.
+
+---
+
+## 0. The evidence
+
+### 0.1 Completeness by corpus and configuration
+
+| Corpus | Lang | BASE src | HEAD src | BASE+SCIP | HEAD+SCIP |
+|---|---|---|---|---|---|
+| fact-mine `src/` | Rust | 27.0% | 30.2% | 58.8% | **66.5%** |
+| Go stdlib (sort+strings) | Go | 24.5% | 48.6% | 46.4% | **60.4%** |
+| gremlins | Go | 32.0% | 35.0% | 53.1% | **56.0%** |
+| boobytrap | Go | 45.9% | 51.0% | 56.5% | **61.2%** |
+| unslop | Go | 24.0% | 23.6% | 28.1% | **27.7%** |
+| cheat | Ruby | 6.0% | 5.7% | *no indexer* | — |
+
+**SCIP is worth more than every resolution feature we have ever written**, in every
+language that has an indexer: +36.3 pts (Rust), +21.0 (gremlins), +11.8 (Go stdlib),
++10.2 (boobytrap), +4.1 (unslop).
+
+### 0.2 Who actually resolves calls when SCIP is present
+
+| Corpus | calls | resolved by SCIP | resolved by our passes |
+|---|---|---|---|
+| fact-mine (Rust) | 8727 | 8406 | **0** |
+| gremlins (Go) | 649 | 519 | **0** |
+| boobytrap (Go) | 1030 | 673 | **0** |
+| Go stdlib | 731 | 391 | **7** |
+
+`scip.rs` overwrites `call.target`/`semantic_symbol` unconditionally for any call it
+matches. Our resolution contributes ~nothing whenever SCIP is available.
+
+### 0.3 Proof by deletion
+
+Reverting the entire local-call-result type-inference feature (`2327bbc70` — design
+doc + implementation, the single largest investment of the session):
+
+```
+HEAD + SCIP: 956/1438 = 66.5%
+HEAD − inference + SCIP: 956/1438 = 66.5% ← byte-identical
+```
+
+Zero contribution under SCIP.
+
+### 0.4 What our work *does* add on top of SCIP
+
+Comparing BASE+SCIP → HEAD+SCIP: Go stdlib **+14.0 pts**, Rust **+7.7**, boobytrap
++4.7, gremlins +2.9, unslop −0.4. Mechanically this is **+2778 newly-priced call
+contexts**, dominated by cost-layer work: field reads (~1000), `Some`/`None`/`Ok`/`Err`
+(+393), constructors (+347), collection methods (+50).
+
+### 0.5 What a SCIP index actually contains (verified on gremlins.scip)
+
+- 15809 occurrences, 2675 symbol entries
+- **582 external symbols**, versioned:
+ `scip-go gomod github.com/golang/go/src go1.25.0 context/`
+- **128 `is_implementation` relationships** (interface satisfaction)
+- **2673/2675 symbols carry `signature_documentation`** with full types:
+ `func newRootCmd(ctx context.Context, version string) (*gremlinsCmd, error)`
+
+---
+
+## 1. What SCIP does that is useful to us
+
+1. **Call resolution** — the occurrence at a call site maps to a canonical symbol.
+ This is the single highest-value input we get, and it is compiler-grade
+ (produced by `go/types`, rust-analyzer, tsc, …). §0.1/§0.2.
+2. **Receiver/expression typing, implicitly** — because the symbol path encodes the
+ owner (`…/Buf#Add().`), receiver typing comes free with resolution. This is the
+ entire problem the session's inference/field-access/generic-owner work tried to
+ solve by hand.
+3. **Declared type signatures** — `signature_documentation` gives parameter and
+ return types verbatim. **Currently discarded** (see §4.1).
+4. **Interface satisfaction** — `is_implementation` relationships. Already consumed
+ (`scip.rs:522`), and it is exactly what `compute_dispatch_impls` re-derives.
+5. **Stable, versioned external identity** — stdlib/dependency symbols carry a
+ version (`go1.25.0 context/`). This is a far better key for a cost registry than
+ the name-based matching we use today.
+6. **Cross-file / cross-package / cross-module linking** — for free, at whole-project
+ scope, without our namespace-reconciliation machinery.
+
+## 2. What we must NEVER do if we use SCIP
+
+These are the rules that would have prevented two days of waste.
+
+1. **Never write language-specific call resolution for a language that has an
+ indexer.** Trait/generic/overload/macro resolution is a type-checker's job. Our
+ Rust source-only mode caps at 30% against SCIP's 66%; the gap is unclosable by
+ construction.
+2. **Never infer a type we can read.** Local call-result types, receiver types,
+ field types, return types — SCIP has them. Consume, don't derive.
+3. **Never re-derive interface satisfaction.** `is_implementation` is authoritative;
+ structural methodset matching is a strictly worse approximation.
+4. **Never report a completeness number without recording which resolution tier
+ produced it.** The root cause of the session's failure was measuring source-only
+ and believing it was the ceiling. Every reported metric must carry its tier.
+5. **Never trust a SCIP index without asserting it is non-empty and covering.**
+ `scip-go .` on unslop produced a *valid, well-formed, 79-byte, completely empty*
+ index. Silent degradation to source-only is the most dangerous failure mode in
+ this system.
+6. **Never fix a "call resolution" symptom in the cost layer without checking the
+ tier.** Several session commits (operators, member reads, conversions, enum
+ constructors) *look* like resolution failures. They are legitimately cost-layer
+ fixes — the target genuinely has no body — but the check must be conscious.
+
+## 3. What SCIP does NOT do, which we need
+
+SCIP is a *symbol graph*. It has no notion of cost, control flow, or program shape.
+Everything below is ours and always will be:
+
+1. **Big-O cost of an operation.** SCIP says `Vec#push()`; it never says
+ `linear_materialize`. The `config/stdlib_complexity/*.yml` registries are ours.
+2. **The complexity algebra** — loop nesting and power, recursion classification,
+ size domains, callback-cost substitution (`C`, `N*C`), parametric bounds, the
+ per-function fixpoint. Nothing in SCIP participates.
+3. **AST/CFG extraction** — normalized nodes, control-flow graph, reaching
+ definitions, liveness. SCIP has ranges, not structure. Without our extractor,
+ lambdas were not analyzed as functions *at all*, and Kotlin expression bodies
+ were dropped entirely (`e043b0989`, `822aad153`, `5c1ecf7d0`).
+4. **Syntactic constructs that have no callee** — operators, subscripts, field
+ reads, type conversions, enum constructors. There is no symbol to resolve; these
+ must be priced. This is where the majority of our SCIP-additive value came from
+ (§0.4).
+5. **Cost of *unindexed* code** — stdlib/dependency *bodies*. See §4.2.
+6. **The no-build path.** SCIP requires a compiling project. Snippets, partial
+ checkouts, broken builds, and unsupported languages have no index.
+7. **Nil-Kill / Decomplex facts** — nullability, hazards, clone detection, path
+ conditions. Entirely outside SCIP's model.
+
+## 4. The biggest gaps — and which we can close
+
+### 4.1 GAP (ours, not SCIP's): we throw away the types SCIP gives us — **CLOSE NOW**
+
+`SymbolInformation` in `scip.rs` deserializes only `symbol` and `relationships`.
+`signature_documentation` — which contains complete parameter and return types for
+2673/2675 symbols — is **discarded**.
+
+This is the highest-value, lowest-risk work available. It replaces, with authoritative
+data, the exact thing the session tried to hand-roll (return types → local types →
+receiver types → method pricing). **Closeable: yes, immediately.**
+
+### 4.2 GAP: SCIP indexes references to dependencies, not their bodies — **CLOSE, and it is our moat**
+
+`scip-go` on a probe module produced an index of `main.go` only; stdlib source was
+not indexed. Dependency/stdlib calls resolve to a *symbol* but there is no body to
+derive a cost from. Verified: `strings.ToUpper` / `Builder.WriteString` in the Go
+test carried no SCIP provenance at all.
+
+Two complementary closures:
+- **Index stdlib source directly** — the Go stdlib *is* indexable (`/usr/lib/go/src`
+ has a `go.mod`; `scip-go ./sort/... ./strings/...` works). Analyze stdlib source
+ with Espalier and emit a cost registry keyed by SCIP symbol.
+- **Key cost registries on versioned SCIP symbols** rather than names, eliminating
+ the name-collision guessing in the current registries.
+
+This is the Espalier-maps-the-stdlib idea, and SCIP makes it *more* valuable, not
+less: SCIP supplies precise identity, we supply the cost. **Closeable: yes; highest
+long-term value.**
+
+### 4.3 GAP: languages with no indexer — **PARTIALLY CLOSEABLE, and currently neglected**
+
+| Have an indexer | No usable indexer |
+|---|---|
+| Go, Rust, TS/JS, Java/Kotlin, Python, C#, C/C++¹ | **Swift, Lua, PHP, Zig, Ruby²** |
+
+¹ `scip-clang` needs `compile_commands.json`, often absent.
+² `scip-ruby` requires Sorbet; unusable on ordinary Ruby.
+
+For these, our own resolution is the *only* option. NilKill now defines a
+language-neutral `runtime_call` event contract and emits canonical SCIP indexes
+with `observed-open` authority. FactMine consumes that authority generically:
+observed targets remain explicit open candidate sets, while compiler SCIP keeps
+precedence. Ruby supplies the first tracing implementation. Python can reuse the
+same event/SCIP/FactMine/Espalier path; PHP and JavaScript need provider-owned
+tracers rather than new consumer logic. **Closeable: partially** — runtime
+observations improve identity and cost joins but cannot prove unobserved dispatch
+targets absent. Swift/Zig remain tractable statically.
+
+### 4.4 GAP: SCIP is a stale snapshot requiring a build — **MITIGATE, not close**
+
+Index cost is small (rust-analyzer on fact-mine: 24s/17MB; scip-go on gremlins:
+7.6s/1.5MB), but it requires a working build and goes stale on edit. Mitigation:
+cache indexes by commit, fall back to source-only with a *loud* tier annotation, and
+never silently degrade (§2.5).
+
+### 4.5 NON-GAP: source-only resolution quality for indexed languages — **DO NOT CLOSE**
+
+Tempting and wrong. Rust source-only cannot approach 66%. Go source-only is closer
+but still behind SCIP on real projects (35.0 vs 56.0 on gremlins). Effort here is
+strictly dominated by just running the indexer.
+
+---
+
+## 5. Path forward
+
+### Phase 1 — Stop the bleeding (immediate)
+1. **Make SCIP the default resolution tier** for every language with an indexer.
+ Already implemented; make it the default path, not an opt-in flag.
+2. **Add tier assertions**: index non-empty, and ≥ threshold of calls carrying
+ `target_provenance == "scip"`. Fail loudly otherwise.
+3. **Stamp the resolution tier on every emitted metric** and on architecture output.
+4. **Delete Rust source-only resolution**: `2327bbc70` (proven zero, §0.3),
+ `2b33520ca`, and the resolver half of `b84058a0d` (keep its `scoped_call_parts`
+ extraction half). Freeze that layer for indexed languages.
+
+### Phase 2 — Consume what SCIP already gives us (highest ROI)
+5. **Parse `signature_documentation`** into parameter/return types and feed the
+ existing type maps (§4.1). This retires the entire hand-rolled inference path.
+6. **Prefer `is_implementation`** over `compute_dispatch_impls` when SCIP is present;
+ keep the structural computation only as the no-SCIP fallback.
+
+### Phase 3 — Invest in the moat (the only durable differentiator)
+7. **Espalier-map stdlib/dependency costs, keyed by versioned SCIP symbol** (§4.2).
+ Measured precedent: cost work delivered **+14 pts on the Go stdlib with SCIP on**.
+8. **Extend the cost algebra** — recursion classification (77 `O(2^N)` functions
+ remain in the Rust corpus), callback/interface parametric closure, the
+ complete-vs-complete-worst-case distinction.
+
+### Phase 4 — Serve the languages SCIP abandons
+9. **Redirect all resolution effort to Swift, Lua, PHP, Zig, Ruby.** Hold it to a
+ measured bar: ship only if completeness moves on a real corpus.
+10. **Ruby/PHP need the runtime-evidence overlay**, not a static resolver —
+ their ceiling is type *availability*, not type *inference*. NilKill records
+ language-owned runtime values in the language-neutral evidence contract;
+ FactMine alone joins those values through normalized CFG/DFG facts and
+ emits the inferred SCIP index.
+
+### The one-line rule
+
+> **SCIP (or a real type checker) owns "what is this and what does it call."
+> fact-mine owns "what does it cost." Never invert that.**
diff --git a/gems/fact-mine/examples/profile/oracles/python_sample.json b/gems/fact-mine/examples/profile/oracles/python_sample.json
index 789397484..0018fd148 100644
--- a/gems/fact-mine/examples/profile/oracles/python_sample.json
+++ b/gems/fact-mine/examples/profile/oracles/python_sample.json
@@ -85,12 +85,12 @@
"fields": [
{
"declared_type": "int",
- "id": "state:41638fe5d66c2aa3",
+ "id": "state:f001f40fc32da24e",
"language": "python",
"line": 5,
"name": "@port",
"owner": "Database",
- "owner_id": "owner:1a8faee6057d3971",
+ "owner_id": "owner:b8064726d03619ce",
"path": "examples/profile/python_sample.py",
"source": "syntax",
"span": [
@@ -103,12 +103,12 @@
},
{
"declared_type": null,
- "id": "state:04ff731a0d59825a",
+ "id": "state:708fb7824917db17",
"language": "python",
"line": 9,
"name": "@_db",
"owner": "Greeter",
- "owner_id": "owner:d2af1adb22a6d402",
+ "owner_id": "owner:a8957e23a055d851",
"path": "examples/profile/python_sample.py",
"source": "syntax",
"span": [
@@ -122,7 +122,10 @@
],
"flow_local_types": [
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/python_sample.py",
"function": "__init__",
"line": 9,
@@ -150,12 +153,15 @@
]
},
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/python_sample.py",
"function": "hello",
"line": 12,
"name": "name",
- "node_id": "cfg:Greeter#hello:stmt:1:12:8",
+ "node_id": "cfg:Greeter#hello:return:0:12:8",
"owner": "Greeter",
"place_id": "place:Greeter#hello:local:name",
"reaching_definitions": [
@@ -182,7 +188,8 @@
"methods": [
{
"dispatch_name": "__init__",
- "id": "fn:887fdfc886d5b516",
+ "generated_declaration": false,
+ "id": "fn:36d86df03e91b9f5",
"key": [
"Database",
"__init__",
@@ -195,7 +202,7 @@
"name": "__init__",
"normalized_source": "def __init__(self): self.port: int = 5432",
"owner": "Database",
- "owner_id": "owner:1a8faee6057d3971",
+ "owner_id": "owner:b8064726d03619ce",
"params": [
"self"
],
@@ -206,6 +213,7 @@
"signature": "def __init__(self):",
"type_system": "python-typing"
},
+ "source_export_eligible": true,
"span": [
4,
4,
@@ -217,7 +225,8 @@
},
{
"dispatch_name": "__init__",
- "id": "fn:8254a2e8c05c9d0b",
+ "generated_declaration": false,
+ "id": "fn:da9413a4cfff38ee",
"key": [
"Greeter",
"__init__",
@@ -230,7 +239,7 @@
"name": "__init__",
"normalized_source": "def __init__(self, db: Database): self._db = db",
"owner": "Greeter",
- "owner_id": "owner:d2af1adb22a6d402",
+ "owner_id": "owner:a8957e23a055d851",
"params": [
"self",
"db"
@@ -242,6 +251,7 @@
"signature": "def __init__(self, db: Database):",
"type_system": "python-typing"
},
+ "source_export_eligible": true,
"span": [
8,
4,
@@ -253,7 +263,8 @@
},
{
"dispatch_name": "hello",
- "id": "fn:66deac84d584cdb7",
+ "generated_declaration": false,
+ "id": "fn:d8362c17d1674f24",
"key": [
"Greeter",
"hello",
@@ -266,7 +277,7 @@
"name": "hello",
"normalized_source": "def hello(self, name: str) -> str: return f\"Hello {name}\"",
"owner": "Greeter",
- "owner_id": "owner:d2af1adb22a6d402",
+ "owner_id": "owner:a8957e23a055d851",
"params": [
"self",
"name"
@@ -278,6 +289,7 @@
"signature": "def hello(self, name: str) -> str:",
"type_system": "python-typing"
},
+ "source_export_eligible": true,
"span": [
11,
4,
@@ -293,7 +305,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:1a8faee6057d3971",
+ "id": "owner:b8064726d03619ce",
"kind": "class",
"language": "python",
"line": 3,
@@ -309,7 +321,7 @@
},
{
"confidence": "high",
- "id": "owner:d2af1adb22a6d402",
+ "id": "owner:a8957e23a055d851",
"kind": "class",
"language": "python",
"line": 7,
@@ -336,8 +348,8 @@
"confidence": "high",
"field": "@port",
"function": "__init__",
- "function_id": "fn:887fdfc886d5b516",
- "id": "edge:dc2b379a57c32afa",
+ "function_id": "fn:36d86df03e91b9f5",
+ "id": "edge:a32086b5b46ec814",
"kind": "writes",
"line": 5,
"owner": "Database",
@@ -349,15 +361,15 @@
5,
29
],
- "state_id": "state:41638fe5d66c2aa3"
+ "state_id": "state:f001f40fc32da24e"
},
{
"conditional": false,
"confidence": "high",
"field": "@_db",
"function": "__init__",
- "function_id": "fn:8254a2e8c05c9d0b",
- "id": "edge:833205b939f16c46",
+ "function_id": "fn:da9413a4cfff38ee",
+ "id": "edge:a487e75c5dac1bcc",
"kind": "writes",
"line": 9,
"owner": "Greeter",
@@ -369,7 +381,7 @@
9,
21
],
- "state_id": "state:04ff731a0d59825a"
+ "state_id": "state:708fb7824917db17"
}
],
"state_param_origin_records": [
diff --git a/gems/fact-mine/examples/profile/oracles/ruby_calculator.json b/gems/fact-mine/examples/profile/oracles/ruby_calculator.json
index 951aae044..fe6b0197c 100644
--- a/gems/fact-mine/examples/profile/oracles/ruby_calculator.json
+++ b/gems/fact-mine/examples/profile/oracles/ruby_calculator.json
@@ -18,6 +18,7 @@
"raw_parser_call_sites": 1,
"semantically_accounted_call_percent": 0.0,
"semantically_accounted_call_sites": 0,
+ "source_export_eligible_methods_overlapping_raw_call_loss": 0,
"total_call_sites": 0,
"unresolved_call_percent": 0.0,
"unresolved_call_sites": 0,
@@ -27,10 +28,14 @@
"calls": [
{
"argument_count": 1,
+ "arguments": [
+ ":total"
+ ],
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:32f7ae34c97f05b8",
+ "id": "edge:cebb9c2d26f6f0e8",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 2,
@@ -40,7 +45,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:e15b261be3125599",
+ "selector_span": [
+ 2,
+ 2,
+ 2,
+ 15
+ ],
+ "source": "fn:736040629b8b4788",
"span": [
2,
2,
@@ -100,6 +111,7 @@
"halving_calls": 0,
"loop_contained_shrinking_calls": 0,
"shrinking_calls": 0,
+ "structural_calls": 0,
"unknown_progress_calls": 0,
"visited_guarded_calls": 0
},
@@ -155,12 +167,12 @@
"fields": [
{
"declared_type": null,
- "id": "state:9401a0fe39c9038e",
+ "id": "state:45b35020fc10ea6f",
"language": "ruby",
"line": 5,
"name": "total",
"owner": "Calculator",
- "owner_id": "owner:c89bc43899119c68",
+ "owner_id": "owner:4b045cb69a6f8a63",
"path": "examples/profile/ruby_calculator.rb",
"source": "syntax",
"span": [
@@ -174,7 +186,10 @@
],
"flow_local_types": [
{
+ "callback_binding_position": null,
"complete": false,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_calculator.rb",
"function": "add",
"line": 5,
@@ -193,7 +208,10 @@
"types": []
},
{
+ "callback_binding_position": null,
"complete": false,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_calculator.rb",
"function": "add",
"line": 5,
@@ -214,7 +232,10 @@
"types": []
},
{
+ "callback_binding_position": null,
"complete": false,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_calculator.rb",
"function": "result",
"line": 9,
@@ -240,7 +261,8 @@
"boolean_ops": 1
},
"dispatch_name": "add",
- "id": "fn:b4b63e5d9de70e44",
+ "generated_declaration": false,
+ "id": "fn:e726a8cb5d1043bf",
"key": [
"Calculator",
"add",
@@ -253,7 +275,7 @@
"name": "add",
"normalized_source": "def add(value) @total = (@total || 0) + value end",
"owner": "Calculator",
- "owner_id": "owner:c89bc43899119c68",
+ "owner_id": "owner:4b045cb69a6f8a63",
"params": [
"value"
],
@@ -261,6 +283,7 @@
"raw_source": "def add(value)\n @total = (@total || 0) + value\n end",
"signature": "",
"source": {},
+ "source_export_eligible": true,
"span": [
4,
2,
@@ -271,7 +294,8 @@
},
{
"dispatch_name": "result",
- "id": "fn:02c66c914712165d",
+ "generated_declaration": false,
+ "id": "fn:e4bc7514d4fff252",
"key": [
"Calculator",
"result",
@@ -284,12 +308,13 @@
"name": "result",
"normalized_source": "def result @total end",
"owner": "Calculator",
- "owner_id": "owner:c89bc43899119c68",
+ "owner_id": "owner:4b045cb69a6f8a63",
"params": [],
"path": "examples/profile/ruby_calculator.rb",
"raw_source": "def result\n @total\n end",
"signature": "",
"source": {},
+ "source_export_eligible": true,
"span": [
8,
2,
@@ -300,7 +325,8 @@
},
{
"dispatch_name": "total",
- "id": "fn:588c35772c399506",
+ "generated_declaration": true,
+ "id": "fn:1fc2156dc148b3bf",
"key": [
"Calculator",
"total",
@@ -313,12 +339,13 @@
"name": "total",
"normalized_source": "attr_accessor :total",
"owner": "Calculator",
- "owner_id": "owner:c89bc43899119c68",
+ "owner_id": "owner:4b045cb69a6f8a63",
"params": [],
"path": "examples/profile/ruby_calculator.rb",
"raw_source": "attr_accessor :total",
"signature": "",
"source": {},
+ "source_export_eligible": true,
"span": [
2,
2,
@@ -329,7 +356,8 @@
},
{
"dispatch_name": "total=",
- "id": "fn:bc2167e86fe1e86f",
+ "generated_declaration": true,
+ "id": "fn:9a1c2c9298c4a8f0",
"key": [
"Calculator",
"total=",
@@ -342,7 +370,7 @@
"name": "total=",
"normalized_source": "attr_accessor :total",
"owner": "Calculator",
- "owner_id": "owner:c89bc43899119c68",
+ "owner_id": "owner:4b045cb69a6f8a63",
"params": [
"value"
],
@@ -350,6 +378,7 @@
"raw_source": "attr_accessor :total",
"signature": "",
"source": {},
+ "source_export_eligible": true,
"span": [
2,
2,
@@ -364,7 +393,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:c89bc43899119c68",
+ "id": "owner:4b045cb69a6f8a63",
"kind": "class",
"language": "ruby",
"line": 1,
@@ -386,8 +415,8 @@
"confidence": "high",
"field": "total",
"function": "add",
- "function_id": "fn:b4b63e5d9de70e44",
- "id": "edge:5c77df72bc6857e8",
+ "function_id": "fn:e726a8cb5d1043bf",
+ "id": "edge:0fd334b1095d3d2a",
"kind": "reads",
"line": 5,
"owner": "Calculator",
@@ -399,15 +428,15 @@
5,
20
],
- "state_id": "state:9401a0fe39c9038e"
+ "state_id": "state:45b35020fc10ea6f"
},
{
"conditional": false,
"confidence": "high",
"field": "total",
"function": "result",
- "function_id": "fn:02c66c914712165d",
- "id": "edge:766d575d6dfd5769",
+ "function_id": "fn:e4bc7514d4fff252",
+ "id": "edge:73594ef170faf329",
"kind": "reads",
"line": 9,
"owner": "Calculator",
@@ -419,15 +448,15 @@
9,
10
],
- "state_id": "state:9401a0fe39c9038e"
+ "state_id": "state:45b35020fc10ea6f"
},
{
"conditional": false,
"confidence": "high",
"field": "total",
"function": "add",
- "function_id": "fn:b4b63e5d9de70e44",
- "id": "edge:f14dbb90ca9496a1",
+ "function_id": "fn:e726a8cb5d1043bf",
+ "id": "edge:42a4ddc6eb248bf5",
"kind": "writes",
"line": 5,
"owner": "Calculator",
@@ -439,7 +468,7 @@
5,
34
],
- "state_id": "state:9401a0fe39c9038e"
+ "state_id": "state:45b35020fc10ea6f"
}
],
"state_param_origin_records": [
diff --git a/gems/fact-mine/examples/profile/oracles/ruby_safe_navigation_nil_kill.json b/gems/fact-mine/examples/profile/oracles/ruby_safe_navigation_nil_kill.json
index 461e9d92f..c3bbbb798 100644
--- a/gems/fact-mine/examples/profile/oracles/ruby_safe_navigation_nil_kill.json
+++ b/gems/fact-mine/examples/profile/oracles/ruby_safe_navigation_nil_kill.json
@@ -40,6 +40,7 @@
"raw_parser_call_sites": 15,
"semantically_accounted_call_percent": 0.0,
"semantically_accounted_call_sites": 0,
+ "source_export_eligible_methods_overlapping_raw_call_loss": 0,
"total_call_sites": 0,
"unresolved_call_percent": 0.0,
"unresolved_call_sites": 0,
@@ -49,10 +50,14 @@
"calls": [
{
"argument_count": 1,
+ "arguments": [
+ "T::Sig"
+ ],
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:7b0515a548c9d614",
+ "id": "edge:552a5e6389b12181",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 2,
@@ -62,7 +67,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 2,
+ 2,
+ 2,
+ 8
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
2,
2,
@@ -75,8 +86,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 4,
+ 2,
+ 10,
+ 5
+ ],
"function": "(top-level)",
- "id": "edge:a34b224c826a5d90",
+ "id": "edge:387473d4b6dd9291",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 4,
@@ -86,7 +104,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "source": "fn:8b060578990b3bc3",
"span": [
4,
2,
@@ -97,12 +115,16 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "UnionMatchArmPlan"
+ ],
"complexity_bound_quality": "upper_bound_declared_receiver",
"complexity_provenance": "language_stdlib_registry",
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:2b7bf67bcdb65d15",
+ "id": "edge:dc8eaf947944fe2a",
"kind": "external_call",
"known_space_complexity": "O(1)",
"known_time_complexity": "O(1)",
@@ -113,7 +135,13 @@
"receiver": "T::Array",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 8,
+ 20,
+ 8,
+ 21
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
8,
12,
@@ -124,10 +152,16 @@
},
{
"argument_count": 3,
+ "arguments": [
+ "node: AST::MatchStatement",
+ "facts: MatchLoweringFacts",
+ "arms: T::Array[UnionMatchArmPlan]"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:ba0cae20ce7a2a65",
+ "id": "edge:291694a00f03581e",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 5,
@@ -137,7 +171,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 5,
+ 4,
+ 5,
+ 10
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
5,
4,
@@ -146,12 +186,50 @@
],
"unresolved_reason": "target_not_defined_in_document"
},
+ {
+ "argument_count": 0,
+ "complexity_bound_quality": "upper_bound_declared_receiver",
+ "complexity_provenance": "language_stdlib_registry",
+ "conditional": true,
+ "confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "function": "(top-level)",
+ "id": "edge:7de05ee0cf649dba",
+ "kind": "external_call",
+ "known_space_complexity": "O(1)",
+ "known_time_complexity": "O(1)",
+ "line": 9,
+ "message": "untyped",
+ "owner": "MIRLoweringControlFlow",
+ "path": "examples/profile/ruby_safe_navigation_nil_kill.rb",
+ "receiver": "T",
+ "receiver_binding_kind": "type",
+ "receiver_kind": "type",
+ "selector_span": [
+ 9,
+ 16,
+ 9,
+ 23
+ ],
+ "source": "fn:8b060578990b3bc3",
+ "span": [
+ 9,
+ 13,
+ 9,
+ 24
+ ],
+ "unresolved_reason": "receiver_requires_corpus_resolution"
+ },
{
"argument_count": 1,
+ "arguments": [
+ "T.untyped"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:837e80a42529c213",
+ "id": "edge:3cc531b658f90ac0",
"kind": "external_call",
"line": 5,
"message": "returns",
@@ -166,7 +244,13 @@
5
],
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 9,
+ 6,
+ 9,
+ 13
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
5,
4,
@@ -177,12 +261,17 @@
},
{
"argument_count": 2,
+ "arguments": [
+ "self",
+ "MIRLowering"
+ ],
"complexity_bound_quality": "upper_bound_declared_receiver",
"complexity_provenance": "language_stdlib_registry",
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:9a03cada03345b35",
+ "id": "edge:e748fb38ed2f23b2",
"kind": "external_call",
"known_space_complexity": "O(1)",
"known_time_complexity": "O(1)",
@@ -193,7 +282,13 @@
"receiver": "T",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 12,
+ 6,
+ 12,
+ 10
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
12,
4,
@@ -206,8 +301,9 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:2702383c132ca9cf",
+ "id": "edge:827f22624742a690",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 13,
@@ -217,7 +313,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "source": "fn:f40acd31cc807dfc",
"span": [
13,
13,
@@ -230,8 +326,9 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:c542973fb5c36ec0",
+ "id": "edge:18bdc4cbce5b460f",
"kind": "external_call",
"line": 13,
"message": "expr_type_sym",
@@ -243,7 +340,13 @@
"receiver_symbol_origin": "unqualified_declared_type",
"receiver_type": "MatchLoweringFacts",
"receiver_type_origin": "declared_parameter",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 13,
+ 33,
+ 13,
+ 46
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
13,
27,
@@ -254,10 +357,14 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "facts.expr_type_sym"
+ ],
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:f10062392d8cf010",
+ "id": "edge:d389a4a401509509",
"kind": "external_call",
"line": 13,
"message": "[]",
@@ -272,7 +379,13 @@
26
],
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 13,
+ 26,
+ 13,
+ 27
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
13,
13,
@@ -285,8 +398,9 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:7a889eb841551e81",
+ "id": "edge:421c7527244699dc",
"kind": "external_call",
"line": 14,
"message": "variants",
@@ -303,7 +417,13 @@
]
],
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 14,
+ 27,
+ 14,
+ 35
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
14,
19,
@@ -316,8 +436,9 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:92a99b420fbd802c",
+ "id": "edge:9535de7cd16c47d1",
"kind": "external_call",
"line": 14,
"message": "keys",
@@ -332,7 +453,13 @@
35
],
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 14,
+ 37,
+ 14,
+ 41
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
14,
19,
@@ -343,10 +470,14 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "&:to_s"
+ ],
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:e83d21a88a399e54",
+ "id": "edge:118ae32d050545df",
"kind": "external_call",
"line": 14,
"message": "map",
@@ -361,7 +492,13 @@
41
],
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "selector_span": [
+ 14,
+ 43,
+ 14,
+ 46
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
14,
19,
@@ -372,11 +509,16 @@
},
{
"argument_count": 0,
+ "complexity_bound_quality": "upper_bound_declared_receiver",
+ "complexity_provenance": "language_stdlib_registry",
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "union_match_default_body",
- "id": "edge:ecfa5ba2b8e83bf9",
+ "id": "edge:9d051e0e28a89598",
"kind": "external_call",
+ "known_space_complexity": "O(N)",
+ "known_time_complexity": "O(N log N)",
"line": 14,
"message": "sort",
"owner": "MIRLoweringControlFlow",
@@ -390,7 +532,15 @@
54
],
"receiver_kind": "value",
- "source": "fn:870869be8ea36bbd",
+ "receiver_type": "T::Array[T.untyped]",
+ "receiver_type_origin": "static_call_result_contract",
+ "selector_span": [
+ 14,
+ 56,
+ 14,
+ 60
+ ],
+ "source": "fn:f40acd31cc807dfc",
"span": [
14,
19,
@@ -403,8 +553,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 18,
+ 2,
+ 18,
+ 54
+ ],
"function": "(top-level)",
- "id": "edge:2c02c8959c8d4c86",
+ "id": "edge:fc57a4693722e9bf",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 18,
@@ -414,7 +571,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "source": "fn:8b060578990b3bc3",
"span": [
18,
2,
@@ -425,10 +582,14 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "required: String"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:73862c6f05bad739",
+ "id": "edge:113bbe2725b47440",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 18,
@@ -438,7 +599,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 18,
+ 8,
+ 18,
+ 14
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
18,
8,
@@ -449,10 +616,14 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "T::Boolean"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:3358eff9e35eb10a",
+ "id": "edge:b8e729c0258e5f6d",
"kind": "external_call",
"line": 18,
"message": "returns",
@@ -467,7 +638,13 @@
32
],
"receiver_kind": "value",
- "source": "fn:91d9d9a6eeda5c2a",
+ "selector_span": [
+ 18,
+ 33,
+ 18,
+ 40
+ ],
+ "source": "fn:8b060578990b3bc3",
"span": [
18,
8,
@@ -480,8 +657,9 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "missing?",
- "id": "edge:e3223b4a641db6a6",
+ "id": "edge:f79f8a43eba75b22",
"kind": "external_call",
"line": 20,
"message": "nil?",
@@ -493,7 +671,13 @@
"receiver_symbol_origin": "unqualified_declared_type",
"receiver_type": "String",
"receiver_type_origin": "declared_parameter",
- "source": "fn:4c6945d1a79599a2",
+ "selector_span": [
+ 20,
+ 13,
+ 20,
+ 17
+ ],
+ "source": "fn:4c68f8ecd3cbf937",
"span": [
20,
4,
@@ -526,7 +710,87 @@
],
"complexity_facts": [
{
- "allocations": [],
+ "allocations": [
+ {
+ "bound_classification": "output",
+ "cardinality_relation": "output_size",
+ "domain_expression": [
+ "schema"
+ ],
+ "kind": "keys",
+ "line": 14,
+ "parameter_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 41
+ ],
+ "symbolic_size": {
+ "complete": true,
+ "factors": [
+ {
+ "domain_id": "allocation:examples/profile/ruby_safe_navigation_nil_kill.rb:14:19",
+ "exponent": 1
+ }
+ ],
+ "logarithmic": false
+ }
+ },
+ {
+ "bound_classification": "output",
+ "cardinality_relation": "output_size",
+ "domain_expression": [
+ "schema"
+ ],
+ "kind": "map",
+ "line": 14,
+ "parameter_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 54
+ ],
+ "symbolic_size": {
+ "complete": true,
+ "factors": [
+ {
+ "domain_id": "allocation:examples/profile/ruby_safe_navigation_nil_kill.rb:14:19",
+ "exponent": 1
+ }
+ ],
+ "logarithmic": false
+ }
+ },
+ {
+ "bound_classification": "output",
+ "cardinality_relation": "output_size",
+ "domain_expression": [
+ "schema"
+ ],
+ "kind": "sort",
+ "line": 14,
+ "parameter_domains": [],
+ "receiver_is_call": true,
+ "span": [
+ 14,
+ 19,
+ 14,
+ 60
+ ],
+ "symbolic_size": {
+ "complete": true,
+ "factors": [
+ {
+ "domain_id": "allocation:examples/profile/ruby_safe_navigation_nil_kill.rb:14:19",
+ "exponent": 1
+ }
+ ],
+ "logarithmic": false
+ }
+ }
+ ],
"block_invocations": [],
"call_contexts": [
{
@@ -634,6 +898,100 @@
"factors": [],
"logarithmic": false
}
+ },
+ {
+ "argument_cardinality_relation": "same",
+ "argument_progress": "unknown",
+ "argument_size_domains": [],
+ "evidence_gap": "unresolved_receiver_type",
+ "execution_multiplicity": "O(1)",
+ "line": 14,
+ "message": "sort",
+ "parameter_arguments": [],
+ "power": 0,
+ "receiver_size_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 60
+ ],
+ "symbolic_execution": {
+ "complete": true,
+ "factors": [],
+ "logarithmic": false
+ }
+ },
+ {
+ "argument_cardinality_relation": "same",
+ "argument_progress": "unknown",
+ "argument_size_domains": [
+ []
+ ],
+ "evidence_gap": "unresolved_receiver_type",
+ "execution_multiplicity": "O(1)",
+ "line": 14,
+ "message": "map",
+ "parameter_arguments": [],
+ "power": 0,
+ "receiver_size_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 54
+ ],
+ "symbolic_execution": {
+ "complete": true,
+ "factors": [],
+ "logarithmic": false
+ }
+ },
+ {
+ "argument_cardinality_relation": "same",
+ "argument_progress": "unknown",
+ "argument_size_domains": [],
+ "evidence_gap": "unresolved_receiver_type",
+ "execution_multiplicity": "O(1)",
+ "line": 14,
+ "message": "keys",
+ "parameter_arguments": [],
+ "power": 0,
+ "receiver_size_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 41
+ ],
+ "symbolic_execution": {
+ "complete": true,
+ "factors": [],
+ "logarithmic": false
+ }
+ },
+ {
+ "argument_cardinality_relation": "same",
+ "argument_progress": "unknown",
+ "argument_size_domains": [],
+ "evidence_gap": "unresolved_receiver_type",
+ "execution_multiplicity": "O(1)",
+ "line": 14,
+ "message": "variants",
+ "parameter_arguments": [],
+ "power": 0,
+ "receiver_size_domains": [],
+ "span": [
+ 14,
+ 19,
+ 14,
+ 35
+ ],
+ "symbolic_execution": {
+ "complete": true,
+ "factors": [],
+ "logarithmic": false
+ }
}
],
"collection_parameters": [
@@ -655,10 +1013,23 @@
"halving_calls": 0,
"loop_contained_shrinking_calls": 0,
"shrinking_calls": 0,
+ "structural_calls": 0,
"unknown_progress_calls": 0,
"visited_guarded_calls": 0
},
"size_domains": [
+ {
+ "id": "allocation:examples/profile/ruby_safe_navigation_nil_kill.rb:14:19",
+ "name": "materialized schema",
+ "path": "examples/profile/ruby_safe_navigation_nil_kill.rb",
+ "source_kind": "output",
+ "span": [
+ 14,
+ 19,
+ 14,
+ 60
+ ]
+ },
{
"id": "param:MIRLoweringControlFlow#union_match_default_body:arms",
"name": "arms",
@@ -753,6 +1124,7 @@
"halving_calls": 0,
"loop_contained_shrinking_calls": 0,
"shrinking_calls": 0,
+ "structural_calls": 0,
"unknown_progress_calls": 0,
"visited_guarded_calls": 0
},
@@ -917,7 +1289,10 @@
"fields": [],
"flow_local_types": [
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_safe_navigation_nil_kill.rb",
"function": "missing?",
"line": 20,
@@ -945,7 +1320,10 @@
]
},
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_safe_navigation_nil_kill.rb",
"function": "union_match_default_body",
"line": 13,
@@ -973,7 +1351,19 @@
]
},
{
+ "callback_binding_position": null,
"complete": false,
+ "definition_call_sources": {
+ "cfg:MIRLoweringControlFlow#union_match_default_body:stmt:2:13:4": [
+ [
+ 13,
+ 13,
+ 13,
+ 47
+ ]
+ ]
+ },
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_safe_navigation_nil_kill.rb",
"function": "union_match_default_body",
"line": 14,
@@ -994,7 +1384,10 @@
"types": []
},
{
+ "callback_binding_position": null,
"complete": false,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_safe_navigation_nil_kill.rb",
"function": "union_match_default_body",
"line": 15,
@@ -1095,7 +1488,8 @@
"rescues": 2
},
"dispatch_name": "union_match_default_body",
- "id": "fn:870869be8ea36bbd",
+ "generated_declaration": false,
+ "id": "fn:f40acd31cc807dfc",
"key": [
"MIRLoweringControlFlow",
"union_match_default_body",
@@ -1108,7 +1502,7 @@
"name": "union_match_default_body",
"normalized_source": "def union_match_default_body(node, facts, arms) T.bind(self, MIRLowering) rescue nil schema = union_schemas[facts.expr_type_sym] all_variants = schema&.variants&.keys&.map(&:to_s)&.sort || [] all_variants end",
"owner": "MIRLoweringControlFlow",
- "owner_id": "owner:1764295d8fbc4733",
+ "owner_id": "owner:9f30e8c74316e548",
"params": [
"node",
"facts",
@@ -1123,6 +1517,7 @@
"source": "annotation",
"type_system": "sorbet"
},
+ "source_export_eligible": true,
"span": [
11,
2,
@@ -1133,7 +1528,8 @@
},
{
"dispatch_name": "missing?",
- "id": "fn:4c6945d1a79599a2",
+ "generated_declaration": false,
+ "id": "fn:4c68f8ecd3cbf937",
"key": [
"MIRLoweringControlFlow",
"missing?",
@@ -1146,7 +1542,7 @@
"name": "missing?",
"normalized_source": "def missing?(required) required.nil? end",
"owner": "MIRLoweringControlFlow",
- "owner_id": "owner:1764295d8fbc4733",
+ "owner_id": "owner:9f30e8c74316e548",
"params": [
"required"
],
@@ -1159,6 +1555,7 @@
"source": "annotation",
"type_system": "sorbet"
},
+ "source_export_eligible": true,
"span": [
19,
2,
@@ -1215,7 +1612,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:1764295d8fbc4733",
+ "id": "owner:9f30e8c74316e548",
"kind": "module",
"language": "ruby",
"line": 1,
@@ -2279,4 +2676,4 @@
"types": []
}
]
-}
+}
\ No newline at end of file
diff --git a/gems/fact-mine/examples/profile/oracles/ruby_shapes.json b/gems/fact-mine/examples/profile/oracles/ruby_shapes.json
index 7bc46c231..49dbabf16 100644
--- a/gems/fact-mine/examples/profile/oracles/ruby_shapes.json
+++ b/gems/fact-mine/examples/profile/oracles/ruby_shapes.json
@@ -55,7 +55,8 @@
"methods": [
{
"dispatch_name": "initialize",
- "id": "fn:ec941d71faca5951",
+ "generated_declaration": false,
+ "id": "fn:d81de45ef5aeaa40",
"key": [
"RubyShapes",
"initialize",
@@ -68,12 +69,13 @@
"name": "initialize",
"normalized_source": "def initialize [1, \"two\", :three, true, nil, CustomClass] end",
"owner": "RubyShapes",
- "owner_id": "owner:b26fe4ac5692839d",
+ "owner_id": "owner:24675cda39b19392",
"params": [],
"path": "examples/profile/ruby_shapes.rb",
"raw_source": "def initialize\n [1, \"two\", :three, true, nil, CustomClass]\n end",
"signature": "",
"source": {},
+ "source_export_eligible": true,
"span": [
13,
2,
@@ -88,7 +90,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:b26fe4ac5692839d",
+ "id": "owner:24675cda39b19392",
"kind": "class",
"language": "ruby",
"line": 3,
diff --git a/gems/fact-mine/examples/profile/oracles/ruby_tuple_arrays_nil_kill.json b/gems/fact-mine/examples/profile/oracles/ruby_tuple_arrays_nil_kill.json
index 1e0322e92..595881657 100644
--- a/gems/fact-mine/examples/profile/oracles/ruby_tuple_arrays_nil_kill.json
+++ b/gems/fact-mine/examples/profile/oracles/ruby_tuple_arrays_nil_kill.json
@@ -90,6 +90,7 @@
"raw_parser_call_sites": 9,
"semantically_accounted_call_percent": 0.0,
"semantically_accounted_call_sites": 0,
+ "source_export_eligible_methods_overlapping_raw_call_loss": 0,
"total_call_sites": 0,
"unresolved_call_percent": 0.0,
"unresolved_call_sites": 0,
@@ -99,10 +100,14 @@
"calls": [
{
"argument_count": 1,
+ "arguments": [
+ "T::Sig"
+ ],
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:ff1a68259204d148",
+ "id": "edge:cdffe927ac3f9ffa",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 4,
@@ -112,7 +117,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 4,
+ 2,
+ 4,
+ 8
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
4,
2,
@@ -125,8 +136,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 6,
+ 2,
+ 6,
+ 74
+ ],
"function": "(top-level)",
- "id": "edge:43d35f2eaeb4e354",
+ "id": "edge:076f3c3aeabfa362",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 6,
@@ -136,7 +154,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "source": "fn:50c72744be604cfe",
"span": [
6,
2,
@@ -147,10 +165,15 @@
},
{
"argument_count": 2,
+ "arguments": [
+ "name: String",
+ "node: Integer"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:6f9ce6b0c86529e3",
+ "id": "edge:76ccf5be66afda01",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 6,
@@ -160,7 +183,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 6,
+ 8,
+ 6,
+ 14
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
6,
8,
@@ -169,14 +198,52 @@
],
"unresolved_reason": "target_not_defined_in_document"
},
+ {
+ "argument_count": 0,
+ "complexity_bound_quality": "upper_bound_declared_receiver",
+ "complexity_provenance": "language_stdlib_registry",
+ "conditional": true,
+ "confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "function": "(top-level)",
+ "id": "edge:3dbb9db644478691",
+ "kind": "external_call",
+ "known_space_complexity": "O(1)",
+ "known_time_complexity": "O(1)",
+ "line": 6,
+ "message": "untyped",
+ "owner": "TupleArrayEvidence",
+ "path": "examples/profile/ruby_tuple_arrays_nil_kill.rb",
+ "receiver": "T",
+ "receiver_binding_kind": "type",
+ "receiver_kind": "type",
+ "selector_span": [
+ 6,
+ 63,
+ 6,
+ 70
+ ],
+ "source": "fn:50c72744be604cfe",
+ "span": [
+ 6,
+ 61,
+ 6,
+ 70
+ ],
+ "unresolved_reason": "receiver_requires_corpus_resolution"
+ },
{
"argument_count": 1,
+ "arguments": [
+ "T.untyped"
+ ],
"complexity_bound_quality": "upper_bound_declared_receiver",
"complexity_provenance": "language_stdlib_registry",
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:b7b09cfc47b13c91",
+ "id": "edge:6882f14e9bd38faf",
"kind": "external_call",
"known_space_complexity": "O(1)",
"known_time_complexity": "O(1)",
@@ -187,7 +254,13 @@
"receiver": "T::Array",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 6,
+ 60,
+ 6,
+ 61
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
6,
52,
@@ -198,10 +271,14 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "T::Array[T.untyped]"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:7efbdfbf5018b4a6",
+ "id": "edge:6c8f91d877151140",
"kind": "external_call",
"line": 6,
"message": "returns",
@@ -216,7 +293,13 @@
43
],
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 6,
+ 44,
+ 6,
+ 51
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
6,
8,
@@ -227,10 +310,15 @@
},
{
"argument_count": 2,
+ "arguments": [
+ ":CHAR",
+ "\">\""
+ ],
"conditional": false,
"confidence": "high",
+ "consumer_closed_candidate_set": false,
"function": "build",
- "id": "edge:778219360a79f571",
+ "id": "edge:416e344494dba7b4",
"implicit_receiver": true,
"kind": "internal_call",
"line": 8,
@@ -240,21 +328,28 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:8674f476ac85a3cb",
+ "selector_span": [
+ 8,
+ 4,
+ 8,
+ 11
+ ],
+ "source": "fn:5806bc2a76308928",
"span": [
8,
4,
8,
23
],
- "target": "fn:1884cd1bd6dcfeff"
+ "target": "fn:44e64e0b9ace44e0"
},
{
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "build",
- "id": "edge:c60bc64c59d16ea7",
+ "id": "edge:63ca1822919c0c60",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 9,
@@ -264,7 +359,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:8674f476ac85a3cb",
+ "source": "fn:5806bc2a76308928",
"span": [
9,
19,
@@ -277,8 +372,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 12,
+ 2,
+ 12,
+ 49
+ ],
"function": "(top-level)",
- "id": "edge:5ad5ca69b25060fe",
+ "id": "edge:37b2cae7c28e488c",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 12,
@@ -288,7 +390,7 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "source": "fn:50c72744be604cfe",
"span": [
12,
2,
@@ -299,10 +401,15 @@
},
{
"argument_count": 2,
+ "arguments": [
+ "kind: Symbol",
+ "text: String"
+ ],
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:2ef535ce9096e5da",
+ "id": "edge:3cd81ecb12dec218",
"implicit_receiver": true,
"kind": "unresolved_call",
"line": 12,
@@ -312,7 +419,13 @@
"receiver": "self",
"receiver_binding_kind": "implicit",
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 12,
+ 8,
+ 12,
+ 14
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
12,
8,
@@ -325,8 +438,9 @@
"argument_count": 0,
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:c39cb5d499cfe191",
+ "id": "edge:1e874f9f09744053",
"kind": "external_call",
"line": 12,
"message": "void",
@@ -341,7 +455,13 @@
42
],
"receiver_kind": "value",
- "source": "fn:9e3ca9e165154fb5",
+ "selector_span": [
+ 12,
+ 43,
+ 12,
+ 47
+ ],
+ "source": "fn:50c72744be604cfe",
"span": [
12,
8,
@@ -422,6 +542,7 @@
"halving_calls": 0,
"loop_contained_shrinking_calls": 0,
"shrinking_calls": 0,
+ "structural_calls": 0,
"unknown_progress_calls": 0,
"visited_guarded_calls": 0
},
@@ -569,7 +690,10 @@
"fields": [],
"flow_local_types": [
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_tuple_arrays_nil_kill.rb",
"function": "build",
"line": 9,
@@ -597,7 +721,10 @@
]
},
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/ruby_tuple_arrays_nil_kill.rb",
"function": "build",
"line": 9,
@@ -659,7 +786,8 @@
"methods": [
{
"dispatch_name": "build",
- "id": "fn:8674f476ac85a3cb",
+ "generated_declaration": false,
+ "id": "fn:5806bc2a76308928",
"key": [
"TupleArrayEvidence",
"build",
@@ -672,7 +800,7 @@
"name": "build",
"normalized_source": "def build(name, node) consume(:CHAR, \">\") [[name, node], current] end",
"owner": "TupleArrayEvidence",
- "owner_id": "owner:7a8369774f6ec03b",
+ "owner_id": "owner:d4ede25f803af7ee",
"params": [
"name",
"node"
@@ -686,6 +814,7 @@
"source": "annotation",
"type_system": "sorbet"
},
+ "source_export_eligible": true,
"span": [
7,
2,
@@ -696,7 +825,8 @@
},
{
"dispatch_name": "consume",
- "id": "fn:1884cd1bd6dcfeff",
+ "generated_declaration": false,
+ "id": "fn:44e64e0b9ace44e0",
"key": [
"TupleArrayEvidence",
"consume",
@@ -709,7 +839,7 @@
"name": "consume",
"normalized_source": "def consume(kind, text); end",
"owner": "TupleArrayEvidence",
- "owner_id": "owner:7a8369774f6ec03b",
+ "owner_id": "owner:d4ede25f803af7ee",
"params": [
"kind",
"text"
@@ -723,6 +853,7 @@
"source": "annotation",
"type_system": "sorbet"
},
+ "source_export_eligible": true,
"span": [
13,
2,
@@ -759,7 +890,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:7a8369774f6ec03b",
+ "id": "owner:d4ede25f803af7ee",
"kind": "class",
"language": "ruby",
"line": 3,
@@ -1175,7 +1306,7 @@
{
"array_element_shape": null,
"blockers": [
- "unknown return expression ARGS at /home/yahn/cheat/gems/fact-mine/examples/profile/ruby_tuple_arrays_nil_kill.rb:13"
+ "unknown return expression ARGS at examples/profile/ruby_tuple_arrays_nil_kill.rb:13"
],
"candidate_type": {
"kind": "Untyped"
@@ -1496,7 +1627,7 @@
"name": "name",
"owner": "TupleArrayEvidence",
"requirements": [
- "type-definition:/home/yahn/cheat/gems/fact-mine/examples/profile/ruby_tuple_arrays_nil_kill.rb:cfg:TupleArrayEvidence#build:entry:0:7:2:place:TupleArrayEvidence#build:local:name"
+ "type-definition:examples/profile/ruby_tuple_arrays_nil_kill.rb:cfg:TupleArrayEvidence#build:entry:0:7:2:place:TupleArrayEvidence#build:local:name"
],
"resolved": true,
"span": [
@@ -1520,7 +1651,7 @@
"name": "node",
"owner": "TupleArrayEvidence",
"requirements": [
- "type-definition:/home/yahn/cheat/gems/fact-mine/examples/profile/ruby_tuple_arrays_nil_kill.rb:cfg:TupleArrayEvidence#build:entry:0:7:2:place:TupleArrayEvidence#build:local:node"
+ "type-definition:examples/profile/ruby_tuple_arrays_nil_kill.rb:cfg:TupleArrayEvidence#build:entry:0:7:2:place:TupleArrayEvidence#build:local:node"
],
"resolved": true,
"span": [
@@ -1534,4 +1665,4 @@
]
}
]
-}
+}
\ No newline at end of file
diff --git a/gems/fact-mine/examples/profile/oracles/ruby_type_aliases.json b/gems/fact-mine/examples/profile/oracles/ruby_type_aliases.json
index db07ca1d4..f5df67b25 100644
--- a/gems/fact-mine/examples/profile/oracles/ruby_type_aliases.json
+++ b/gems/fact-mine/examples/profile/oracles/ruby_type_aliases.json
@@ -29,6 +29,7 @@
"raw_parser_call_sites": 3,
"semantically_accounted_call_percent": 0.0,
"semantically_accounted_call_sites": 0,
+ "source_export_eligible_methods_overlapping_raw_call_loss": 0,
"total_call_sites": 0,
"unresolved_call_percent": 0.0,
"unresolved_call_sites": 0,
@@ -40,8 +41,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 4,
+ 12,
+ 4,
+ 48
+ ],
"function": "(top-level)",
- "id": "edge:83b665a49e685e36",
+ "id": "edge:beab494f7c5451e2",
"kind": "external_call",
"line": 4,
"message": "type_alias",
@@ -50,7 +58,13 @@
"receiver": "T",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:0372f2d939681931",
+ "selector_span": [
+ 4,
+ 14,
+ 4,
+ 24
+ ],
+ "source": "fn:a4f16908d1ca0abc",
"span": [
4,
12,
@@ -61,12 +75,16 @@
},
{
"argument_count": 1,
+ "arguments": [
+ "AST::Node"
+ ],
"complexity_bound_quality": "upper_bound_declared_receiver",
"complexity_provenance": "language_stdlib_registry",
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:c4ea3e6aae6d0ae3",
+ "id": "edge:ddcb74a4f9f53f77",
"kind": "external_call",
"known_space_complexity": "O(1)",
"known_time_complexity": "O(1)",
@@ -77,7 +95,13 @@
"receiver": "T::Array",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:0372f2d939681931",
+ "selector_span": [
+ 4,
+ 35,
+ 4,
+ 36
+ ],
+ "source": "fn:a4f16908d1ca0abc",
"span": [
4,
27,
@@ -90,8 +114,15 @@
"argument_count": 0,
"conditional": false,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
+ "execution_span": [
+ 5,
+ 14,
+ 7,
+ 5
+ ],
"function": "(top-level)",
- "id": "edge:e01a6684bb0bbe20",
+ "id": "edge:7d2185a3d4415574",
"kind": "external_call",
"line": 5,
"message": "type_alias",
@@ -100,7 +131,13 @@
"receiver": "T",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:0372f2d939681931",
+ "selector_span": [
+ 5,
+ 16,
+ 5,
+ 26
+ ],
+ "source": "fn:a4f16908d1ca0abc",
"span": [
5,
14,
@@ -111,12 +148,17 @@
},
{
"argument_count": 2,
+ "arguments": [
+ "Schemas::EnumSchema",
+ "Schemas::StructSchema"
+ ],
"complexity_bound_quality": "upper_bound_declared_receiver",
"complexity_provenance": "language_stdlib_registry",
"conditional": true,
"confidence": "partial",
+ "consumer_closed_candidate_set": false,
"function": "(top-level)",
- "id": "edge:ffa598aa0528895e",
+ "id": "edge:b81839abd4b0a9da",
"kind": "external_call",
"known_space_complexity": "O(1)",
"known_time_complexity": "O(1)",
@@ -127,7 +169,13 @@
"receiver": "T",
"receiver_binding_kind": "type",
"receiver_kind": "type",
- "source": "fn:0372f2d939681931",
+ "selector_span": [
+ 6,
+ 6,
+ 6,
+ 9
+ ],
+ "source": "fn:a4f16908d1ca0abc",
"span": [
6,
4,
@@ -209,7 +257,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:0ac3079501ab632a",
+ "id": "owner:9aa2b17c9a0ef3e7",
"kind": "module",
"language": "ruby",
"line": 3,
diff --git a/gems/fact-mine/examples/profile/oracles/typescript_sample.json b/gems/fact-mine/examples/profile/oracles/typescript_sample.json
index 65c5493eb..4a3cb227c 100644
--- a/gems/fact-mine/examples/profile/oracles/typescript_sample.json
+++ b/gems/fact-mine/examples/profile/oracles/typescript_sample.json
@@ -105,12 +105,12 @@
"fields": [
{
"declared_type": "public port: number = 5432",
- "id": "state:bf9e0c74e6d34945",
+ "id": "state:a93cdf3c47ad143a",
"language": "typescript",
"line": 4,
"name": "port",
"owner": "Database",
- "owner_id": "owner:7d2c51a8280a8ec5",
+ "owner_id": "owner:9516ca35bd69d700",
"path": "examples/profile/typescript_sample.ts",
"source": "syntax",
"span": [
@@ -123,12 +123,12 @@
},
{
"declared_type": "Database",
- "id": "state:a502c0b8f885cf82",
+ "id": "state:1e0c9f18052b6f45",
"language": "typescript",
"line": 8,
"name": "_db",
"owner": "Greeter",
- "owner_id": "owner:412c61b10a84d1c2",
+ "owner_id": "owner:9ed9e010a773c7a7",
"path": "examples/profile/typescript_sample.ts",
"source": "syntax",
"span": [
@@ -142,7 +142,10 @@
],
"flow_local_types": [
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/typescript_sample.ts",
"function": "constructor",
"line": 10,
@@ -170,12 +173,15 @@
]
},
{
+ "callback_binding_position": null,
"complete": true,
+ "definition_call_sources": {},
+ "definition_sequence_projections": {},
"file": "examples/profile/typescript_sample.ts",
"function": "hello",
"line": 14,
"name": "name",
- "node_id": "cfg:Greeter#hello:stmt:1:14:8",
+ "node_id": "cfg:Greeter#hello:return:0:14:8",
"owner": "Greeter",
"place_id": "place:Greeter#hello:local:name",
"reaching_definitions": [
@@ -202,7 +208,8 @@
"methods": [
{
"dispatch_name": "constructor",
- "id": "fn:6a43a20e36918915",
+ "generated_declaration": false,
+ "id": "fn:f7ea68c88fbece5a",
"key": [
"Greeter",
"constructor",
@@ -215,7 +222,7 @@
"name": "constructor",
"normalized_source": "constructor(db: Database) { this._db = db; }",
"owner": "Greeter",
- "owner_id": "owner:412c61b10a84d1c2",
+ "owner_id": "owner:9ed9e010a773c7a7",
"params": [
"db"
],
@@ -226,17 +233,20 @@
"signature": "constructor(db: Database) {",
"type_system": "typescript"
},
+ "source_export_eligible": true,
"span": [
9,
4,
11,
5
],
+ "symbol_owner": "examples/profile/typescript_sample.Greeter",
"visibility": "public"
},
{
"dispatch_name": "hello",
- "id": "fn:96d27b98d810ce50",
+ "generated_declaration": false,
+ "id": "fn:1eacf03142d5eb15",
"key": [
"Greeter",
"hello",
@@ -249,7 +259,7 @@
"name": "hello",
"normalized_source": "public hello(name: string): string { return `Hello ${name}`; }",
"owner": "Greeter",
- "owner_id": "owner:412c61b10a84d1c2",
+ "owner_id": "owner:9ed9e010a773c7a7",
"params": [
"name"
],
@@ -260,12 +270,14 @@
"signature": "public hello(name: string): string {",
"type_system": "typescript"
},
+ "source_export_eligible": true,
"span": [
13,
4,
15,
5
],
+ "symbol_owner": "examples/profile/typescript_sample.Greeter",
"visibility": "public"
}
],
@@ -274,7 +286,7 @@
"owners": [
{
"confidence": "high",
- "id": "owner:7d2c51a8280a8ec5",
+ "id": "owner:9516ca35bd69d700",
"kind": "class",
"language": "typescript",
"line": 3,
@@ -285,11 +297,12 @@
0,
5,
1
- ]
+ ],
+ "symbol": "examples/profile/typescript_sample.Database"
},
{
"confidence": "high",
- "id": "owner:412c61b10a84d1c2",
+ "id": "owner:9ed9e010a773c7a7",
"kind": "class",
"language": "typescript",
"line": 7,
@@ -300,7 +313,8 @@
0,
16,
1
- ]
+ ],
+ "symbol": "examples/profile/typescript_sample.Greeter"
}
],
"presence_correlations": [],
@@ -314,8 +328,8 @@
"confidence": "high",
"field": "_db",
"function": "constructor",
- "function_id": "fn:6a43a20e36918915",
- "id": "edge:6ecb5fbd9ccebc3f",
+ "function_id": "fn:f7ea68c88fbece5a",
+ "id": "edge:ef8c8dbc43fd43b3",
"kind": "writes",
"line": 10,
"owner": "Greeter",
@@ -327,7 +341,7 @@
10,
21
],
- "state_id": "state:a502c0b8f885cf82"
+ "state_id": "state:1e0c9f18052b6f45"
}
],
"state_param_origin_records": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/block_conjunction_decisions/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/block_conjunction_decisions/ruby.json
index 424c1f989..77e79bebe 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/block_conjunction_decisions/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/block_conjunction_decisions/ruby.json
@@ -380,13 +380,25 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
},
{
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
}
],
"control_flow_metrics": [
@@ -405,8 +417,13 @@
"line": 9
},
{
- "id": "cfg:SourceFactBlockConjunctionDecisions#fallback:stmt:1:5:4",
+ "id": "cfg:SourceFactBlockConjunctionDecisions#fallback:stmt:0.loop.0:6:6",
"kind": "statement",
+ "line": 6
+ },
+ {
+ "id": "cfg:SourceFactBlockConjunctionDecisions#fallback:stmt:1:5:4",
+ "kind": "loop",
"line": 5
},
{
@@ -420,9 +437,14 @@
"line": 13
},
{
- "id": "cfg:SourceFactBlockConjunctionDecisions#filter_paths:stmt:1:12:4",
+ "id": "cfg:SourceFactBlockConjunctionDecisions#filter_paths:stmt:0.loop.0:12:27",
"kind": "statement",
"line": 12
+ },
+ {
+ "id": "cfg:SourceFactBlockConjunctionDecisions#filter_paths:stmt:1:12:4",
+ "kind": "loop",
+ "line": 12
}
],
"decisions": [
@@ -461,6 +483,8 @@
}
],
"def_use": [
+ {},
+ {},
{},
{}
],
@@ -471,6 +495,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"flow_types": [
@@ -505,6 +531,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -596,9 +624,14 @@
},
{
"reads": [
- "place:SourceFactBlockConjunctionDecisions#fallback:local:files",
"place:SourceFactBlockConjunctionDecisions#fallback:local:rel"
],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactBlockConjunctionDecisions#fallback:local:files"
+ ],
"writes": [
"place:SourceFactBlockConjunctionDecisions#fallback:local:rel"
]
@@ -615,9 +648,14 @@
},
{
"reads": [
- "place:SourceFactBlockConjunctionDecisions#filter_paths:local:hash",
"place:SourceFactBlockConjunctionDecisions#filter_paths:local:rel"
],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactBlockConjunctionDecisions#filter_paths:local:hash"
+ ],
"writes": [
"place:SourceFactBlockConjunctionDecisions#filter_paths:local:_",
"place:SourceFactBlockConjunctionDecisions#filter_paths:local:rel"
@@ -803,6 +841,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/block_receiver_calls/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/block_receiver_calls/ruby.json
index 1b1a7531d..4d57adf31 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/block_receiver_calls/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/block_receiver_calls/ruby.json
@@ -45,6 +45,19 @@
"branch_arms": [],
"branch_decisions": [],
"calls": [
+ {
+ "arguments": [
+ "item.name"
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "iterates",
+ "function": "collect",
+ "line": 7,
+ "message": "<<",
+ "receiver": "names",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/ruby.json
index 6b560956f..530cc524e 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/ruby.json
@@ -12,6 +12,11 @@
}
]
},
+ {
+ "boundaries": [],
+ "method": "method",
+ "statements": []
+ },
{
"boundaries": [],
"method": "OtherFunc",
@@ -98,6 +103,12 @@
{
"kind": "entry"
},
+ {
+ "kind": "callback_return"
+ },
+ {
+ "kind": "callback_body"
+ },
{
"kind": "fallthrough"
},
@@ -106,6 +117,7 @@
}
],
"control_flow_metrics": [
+ {},
{},
{}
],
@@ -121,8 +133,13 @@
"line": 5
},
{
- "id": "cfg:(top-level)#MyFunc:stmt:1:2:2",
+ "id": "cfg:(top-level)#MyFunc:stmt:0.callback.0:3:4",
"kind": "statement",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#MyFunc:stmt:1:2:2",
+ "kind": "callback",
"line": 2
},
{
@@ -134,12 +151,25 @@
"id": "cfg:(top-level)#OtherFunc:exit:1:7:18",
"kind": "exit",
"line": 7
+ },
+ {
+ "id": "cfg:(top-level)#method:entry:0:3:4",
+ "kind": "entry",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#method:exit:1:3:19",
+ "kind": "exit",
+ "line": 3
}
],
"decisions": [],
"def_use": [],
"dispatch_sites": [],
"dominators": [
+ {},
+ {},
+ {},
{},
{},
{},
@@ -171,6 +201,9 @@
}
],
"liveness": [
+ {},
+ {},
+ {},
{},
{},
{},
@@ -189,6 +222,11 @@
"id": "(top-level)#OtherFunc",
"score": 0.0,
"signals": {}
+ },
+ {
+ "id": "(top-level)#method",
+ "score": 0.0,
+ "signals": {}
}
],
"local_methods": [
@@ -226,6 +264,15 @@
"name": "OtherFunc",
"owner": "(top-level)",
"statements": []
+ },
+ {
+ "boundaries": [],
+ "id": "(top-level)#method",
+ "line": 3,
+ "local_contract_assignments": {},
+ "name": "method",
+ "owner": "(top-level)",
+ "statements": []
}
],
"node_effects": [
@@ -245,6 +292,18 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": []
@@ -298,6 +357,9 @@
}
],
"reachability": [
+ {},
+ {},
+ {},
{},
{},
{},
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/rust.json
index 289b187ac..e4ae704b8 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/rust.json
@@ -18,6 +18,11 @@
}
]
},
+ {
+ "boundaries": [],
+ "method": "method",
+ "statements": []
+ },
{
"boundaries": [],
"method": "OtherFunc",
@@ -87,11 +92,15 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "fallthrough"
}
],
"control_flow_metrics": [
+ {},
{},
{}
],
@@ -125,6 +134,16 @@
"id": "cfg:(top-level)#OtherFunc:exit:1:8:17",
"kind": "exit",
"line": 8
+ },
+ {
+ "id": "cfg:Local#method:entry:0:4:8",
+ "kind": "entry",
+ "line": 4
+ },
+ {
+ "id": "cfg:Local#method:exit:1:4:26",
+ "kind": "exit",
+ "line": 4
}
],
"decisions": [],
@@ -136,6 +155,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"flow_types": [],
@@ -168,6 +189,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -180,6 +203,11 @@
"id": "(top-level)#OtherFunc",
"score": 0.0,
"signals": {}
+ },
+ {
+ "id": "Local#method",
+ "score": 0.0,
+ "signals": {}
}
],
"local_methods": [
@@ -233,6 +261,15 @@
"name": "OtherFunc",
"owner": "(top-level)",
"statements": []
+ },
+ {
+ "boundaries": [],
+ "id": "Local#method",
+ "line": 4,
+ "local_contract_assignments": {},
+ "name": "method",
+ "owner": "Local",
+ "statements": []
}
],
"node_effects": [
@@ -256,6 +293,14 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": []
@@ -325,6 +370,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"reaching_definitions": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/zig.json b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/zig.json
index 257cda2d4..a2bf89dd4 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/body_owner/zig.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/body_owner/zig.json
@@ -52,7 +52,7 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "return"
},
{
"kind": "fallthrough"
@@ -74,8 +74,8 @@
"line": 5
},
{
- "id": "cfg:(top-level)#MyFunc:stmt:1:4:4",
- "kind": "statement",
+ "id": "cfg:(top-level)#MyFunc:return:0:4:4",
+ "kind": "jump",
"line": 4
},
{
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/ruby.json
index 9627375ee..4851a9411 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/ruby.json
@@ -123,6 +123,18 @@
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "short_circuit"
+ },
+ {
+ "kind": "short_circuit"
}
],
"control_flow_metrics": [
@@ -140,8 +152,13 @@
"line": 5
},
{
- "id": "cfg:(top-level)#method_seven:stmt:1:2:2",
+ "id": "cfg:(top-level)#method_seven:stmt:0.then.0:3:4",
"kind": "statement",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#method_seven:stmt:1:2:2",
+ "kind": "branch",
"line": 2
}
],
@@ -170,6 +187,7 @@
],
"dispatch_sites": [],
"dominators": [
+ {},
{},
{},
{}
@@ -193,6 +211,7 @@
}
],
"liveness": [
+ {},
{},
{},
{}
@@ -265,13 +284,21 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_seven:local:x",
"place:(top-level)#method_seven:local:y",
"place:(top-level)#method_seven:local:z"
],
- "writes": []
+ "writes": [
+ "place:(top-level)#method_seven:local:x",
+ "place:(top-level)#method_seven:local:y",
+ "place:(top-level)#method_seven:local:z"
+ ]
}
],
"owners": [],
@@ -338,6 +365,7 @@
}
],
"reachability": [
+ {},
{},
{},
{}
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/rust.json
index 26ac92001..2ee1f94fb 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/rust.json
@@ -123,6 +123,18 @@
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "short_circuit"
+ },
+ {
+ "kind": "short_circuit"
}
],
"control_flow_metrics": [
@@ -140,8 +152,13 @@
"line": 5
},
{
- "id": "cfg:(top-level)#method_seven:stmt:1:2:4",
+ "id": "cfg:(top-level)#method_seven:stmt:0.then.0:3:8",
"kind": "statement",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#method_seven:stmt:1:2:4",
+ "kind": "branch",
"line": 2
}
],
@@ -170,6 +187,7 @@
],
"dispatch_sites": [],
"dominators": [
+ {},
{},
{},
{}
@@ -193,6 +211,7 @@
}
],
"liveness": [
+ {},
{},
{},
{}
@@ -265,6 +284,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_seven:local:x",
@@ -338,6 +361,7 @@
}
],
"reachability": [
+ {},
{},
{},
{}
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/zig.json b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/zig.json
index aa45c2bd1..7fc08dbe3 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/zig.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/boolean_short_circuit/zig.json
@@ -136,6 +136,18 @@
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "short_circuit"
+ },
+ {
+ "kind": "short_circuit"
}
],
"control_flow_metrics": [
@@ -153,9 +165,14 @@
"line": 5
},
{
- "id": "cfg:bool#method_seven:stmt:1:2:4",
+ "id": "cfg:bool#method_seven:stmt:0.then.0:2:22",
"kind": "statement",
"line": 2
+ },
+ {
+ "id": "cfg:bool#method_seven:stmt:1:2:4",
+ "kind": "branch",
+ "line": 2
}
],
"decisions": [
@@ -183,6 +200,7 @@
],
"dispatch_sites": [],
"dominators": [
+ {},
{},
{},
{}
@@ -206,6 +224,7 @@
}
],
"liveness": [
+ {},
{},
{},
{}
@@ -278,6 +297,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:bool#method_seven:local:x",
@@ -344,6 +367,7 @@
}
],
"reachability": [
+ {},
{},
{},
{}
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/branch_nested_scope_refs/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/branch_nested_scope_refs/ruby.json
index a82bb452b..e2b1e497a 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/branch_nested_scope_refs/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/branch_nested_scope_refs/ruby.json
@@ -896,11 +896,35 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "continue"
+ },
{
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
},
{
"kind": "branch_true"
@@ -947,10 +971,30 @@
"line": 17
},
{
- "id": "cfg:SourceFactBranchNestedScopeRefs#case_arms:stmt:1:11:4",
+ "id": "cfg:SourceFactBranchNestedScopeRefs#case_arms:stmt:0.case0.0:13:8",
+ "kind": "statement",
+ "line": 13
+ },
+ {
+ "id": "cfg:SourceFactBranchNestedScopeRefs#case_arms:stmt:0.default.0:15:8",
"kind": "statement",
+ "line": 15
+ },
+ {
+ "id": "cfg:SourceFactBranchNestedScopeRefs#case_arms:stmt:1:11:4",
+ "kind": "case",
"line": 11
},
+ {
+ "id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:branch:0.loop.0:21:6",
+ "kind": "branch",
+ "line": 21
+ },
+ {
+ "id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:continue:0.loop.0.then.0:21:6",
+ "kind": "jump",
+ "line": 21
+ },
{
"id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:entry:0:19:2",
"kind": "entry",
@@ -962,8 +1006,13 @@
"line": 25
},
{
- "id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:stmt:1:20:4",
+ "id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:stmt:0.loop.1:23:6",
"kind": "statement",
+ "line": 23
+ },
+ {
+ "id": "cfg:SourceFactBranchNestedScopeRefs#do_block_branch:stmt:1:20:4",
+ "kind": "loop",
"line": 20
},
{
@@ -1009,6 +1058,7 @@
{},
{},
{},
+ {},
{}
],
"dispatch_sites": [],
@@ -1025,6 +1075,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -1035,6 +1090,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"functions": [
@@ -1080,6 +1138,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -1241,6 +1304,28 @@
],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactBranchNestedScopeRefs#case_arms:local:edge"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactBranchNestedScopeRefs#case_arms:local:edge"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactBranchNestedScopeRefs#do_block_branch:local:component"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -1253,7 +1338,12 @@
},
{
"reads": [
- "place:SourceFactBranchNestedScopeRefs#do_block_branch:local:component",
+ "place:SourceFactBranchNestedScopeRefs#do_block_branch:local:component"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
"place:SourceFactBranchNestedScopeRefs#do_block_branch:local:components"
],
"writes": [
@@ -1481,6 +1571,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
@@ -1491,6 +1586,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"redundant_nil_guards": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/ruby.json
index 03042f9b2..0fc0f15fb 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/ruby.json
@@ -370,17 +370,44 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
}
],
"control_flow_metrics": [
@@ -400,8 +427,13 @@
"line": 17
},
{
- "id": "cfg:(top-level)#method_case_no_val:stmt:1:13:2",
+ "id": "cfg:(top-level)#method_case_no_val:stmt:0.case0.0:15:4",
"kind": "statement",
+ "line": 15
+ },
+ {
+ "id": "cfg:(top-level)#method_case_no_val:stmt:1:13:2",
+ "kind": "case",
"line": 13
},
{
@@ -415,8 +447,13 @@
"line": 24
},
{
- "id": "cfg:(top-level)#method_case_one_pattern:stmt:1:20:2",
+ "id": "cfg:(top-level)#method_case_one_pattern:stmt:0.case0.0:22:4",
"kind": "statement",
+ "line": 22
+ },
+ {
+ "id": "cfg:(top-level)#method_case_one_pattern:stmt:1:20:2",
+ "kind": "case",
"line": 20
},
{
@@ -430,8 +467,23 @@
"line": 10
},
{
- "id": "cfg:(top-level)#method_eight:stmt:1:2:2",
+ "id": "cfg:(top-level)#method_eight:stmt:0.case0.0:4:4",
+ "kind": "statement",
+ "line": 4
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:0.case1.0:6:4",
+ "kind": "statement",
+ "line": 6
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:0.default.0:8:4",
"kind": "statement",
+ "line": 8
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:1:2:2",
+ "kind": "case",
"line": 2
}
],
@@ -469,6 +521,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -514,6 +571,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -636,11 +698,17 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_case_no_val:local:x"
],
- "writes": []
+ "writes": [
+ "place:(top-level)#method_case_no_val:local:x"
+ ]
},
{
"reads": [],
@@ -652,6 +720,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_case_one_pattern:local:x"
@@ -668,6 +740,18 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_eight:local:x"
@@ -847,6 +931,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/rust.json
index 14b3ac08d..31327974f 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/rust.json
@@ -304,17 +304,50 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
}
],
"control_flow_metrics": [
@@ -334,8 +367,13 @@
"line": 13
},
{
- "id": "cfg:(top-level)#method_case_no_val:stmt:1:10:4",
+ "id": "cfg:(top-level)#method_case_no_val:stmt:0.then.0:11:8",
"kind": "statement",
+ "line": 11
+ },
+ {
+ "id": "cfg:(top-level)#method_case_no_val:stmt:1:10:4",
+ "kind": "branch",
"line": 10
},
{
@@ -349,8 +387,13 @@
"line": 20
},
{
- "id": "cfg:(top-level)#method_case_one_pattern:stmt:1:16:4",
+ "id": "cfg:(top-level)#method_case_one_pattern:stmt:0.case0.0:17:13",
"kind": "statement",
+ "line": 17
+ },
+ {
+ "id": "cfg:(top-level)#method_case_one_pattern:stmt:1:16:4",
+ "kind": "case",
"line": 16
},
{
@@ -364,8 +407,23 @@
"line": 7
},
{
- "id": "cfg:(top-level)#method_eight:stmt:1:2:4",
+ "id": "cfg:(top-level)#method_eight:stmt:0.case0.0:3:17",
+ "kind": "statement",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:0.case1.0:4:13",
+ "kind": "statement",
+ "line": 4
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:0.case2.0:5:13",
"kind": "statement",
+ "line": 5
+ },
+ {
+ "id": "cfg:(top-level)#method_eight:stmt:1:2:4",
+ "kind": "case",
"line": 2
}
],
@@ -402,6 +460,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -447,6 +510,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -570,6 +638,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_case_no_val:local:x"
@@ -586,6 +658,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_case_one_pattern:local:x"
@@ -602,6 +678,18 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:(top-level)#method_eight:local:x"
@@ -781,6 +869,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/zig.json b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/zig.json
index c7e5e9f6b..f7108b45b 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/case_statements/zig.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/case_statements/zig.json
@@ -379,17 +379,50 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_arm"
+ },
+ {
+ "kind": "case_default"
}
],
"control_flow_metrics": [
@@ -409,10 +442,15 @@
"line": 13
},
{
- "id": "cfg:i32#method_case_no_val:stmt:1:10:4",
+ "id": "cfg:i32#method_case_no_val:stmt:0.then.0:10:16",
"kind": "statement",
"line": 10
},
+ {
+ "id": "cfg:i32#method_case_no_val:stmt:1:10:4",
+ "kind": "branch",
+ "line": 10
+ },
{
"id": "cfg:i32#method_case_one_pattern:entry:0:15:0",
"kind": "entry",
@@ -424,8 +462,13 @@
"line": 20
},
{
- "id": "cfg:i32#method_case_one_pattern:stmt:1:16:4",
+ "id": "cfg:i32#method_case_one_pattern:stmt:0.case0.0:17:13",
"kind": "statement",
+ "line": 17
+ },
+ {
+ "id": "cfg:i32#method_case_one_pattern:stmt:1:16:4",
+ "kind": "case",
"line": 16
},
{
@@ -439,8 +482,23 @@
"line": 7
},
{
- "id": "cfg:i32#method_eight:stmt:1:2:4",
+ "id": "cfg:i32#method_eight:stmt:0.case0.0:3:16",
+ "kind": "statement",
+ "line": 3
+ },
+ {
+ "id": "cfg:i32#method_eight:stmt:0.case1.0:4:13",
+ "kind": "statement",
+ "line": 4
+ },
+ {
+ "id": "cfg:i32#method_eight:stmt:0.case2.0:5:16",
"kind": "statement",
+ "line": 5
+ },
+ {
+ "id": "cfg:i32#method_eight:stmt:1:2:4",
+ "kind": "case",
"line": 2
}
],
@@ -493,6 +551,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -538,6 +601,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -661,6 +729,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:i32#method_case_no_val:local:x"
@@ -677,6 +749,10 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:i32#method_case_one_pattern:local:x"
@@ -693,6 +769,18 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:i32#method_eight:local:x"
@@ -872,6 +960,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/command_strings/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/command_strings/rust.json
index eb3e32013..2918c0541 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/command_strings/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/command_strings/rust.json
@@ -22,14 +22,80 @@
"syntax": {
"branch_arms": [],
"branch_decisions": [],
- "calls": [],
+ "calls": [
+ {
+ "arguments": [
+ "\"ls\""
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "method_four",
+ "line": 2,
+ "message": "new",
+ "receiver": "std::process::Command",
+ "safe_navigation": false
+ },
+ {
+ "arguments": [],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "method_four",
+ "line": 2,
+ "message": "output",
+ "receiver": "std::process::Command.new(\"ls\")",
+ "safe_navigation": false
+ }
+ ],
"clone_candidates": [
+ {
+ "child_fingerprints": [
+ "call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))"
+ ],
+ "child_masses": [
+ 8
+ ],
+ "fingerprint": "call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit)))",
+ "line": 2,
+ "mass": 9,
+ "method_name": "method_four",
+ "node_name": "call"
+ },
+ {
+ "child_fingerprints": [
+ "lit"
+ ],
+ "child_masses": [
+ 1
+ ],
+ "fingerprint": "argument_list(lit)",
+ "line": 2,
+ "mass": 2,
+ "method_name": "method_four",
+ "node_name": "argument_list"
+ },
+ {
+ "child_fingerprints": [
+ "scoped_identifier(scoped_identifier(id id) id)",
+ "argument_list(lit)"
+ ],
+ "child_masses": [
+ 5,
+ 2
+ ],
+ "fingerprint": "call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))",
+ "line": 2,
+ "mass": 8,
+ "method_name": "method_four",
+ "node_name": "call"
+ },
{
"child_fingerprints": [],
"child_masses": [],
- "fingerprint": "method(id)",
+ "fingerprint": "method(id body(let_declaration(assignment(id call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit)))))))",
"line": 1,
- "mass": 1,
+ "mass": 13,
"method_name": "method_four",
"node_name": "defn"
}
@@ -71,7 +137,11 @@
{},
{}
],
- "flow_types": [],
+ "flow_types": [
+ {},
+ {},
+ {}
+ ],
"functions": [
{
"line": 1,
@@ -133,13 +203,43 @@
"writes": []
},
{
- "reads": [],
- "writes": []
+ "reads": [
+ "place:(top-level)#method_four:local:Command",
+ "place:(top-level)#method_four:local:process",
+ "place:(top-level)#method_four:local:std"
+ ],
+ "writes": [
+ "place:(top-level)#method_four:local:Command",
+ "place:(top-level)#method_four:local:_",
+ "place:(top-level)#method_four:local:process",
+ "place:(top-level)#method_four:local:std"
+ ]
}
],
"owners": [],
"path_conditions": [],
- "places": [],
+ "places": [
+ {
+ "id": "place:(top-level)#method_four:local:Command",
+ "kind": "local",
+ "name": "Command"
+ },
+ {
+ "id": "place:(top-level)#method_four:local:_",
+ "kind": "local",
+ "name": "_"
+ },
+ {
+ "id": "place:(top-level)#method_four:local:process",
+ "kind": "local",
+ "name": "process"
+ },
+ {
+ "id": "place:(top-level)#method_four:local:std",
+ "kind": "local",
+ "name": "std"
+ }
+ ],
"predicate_bodies": [],
"protocol_call_paths": [
{
@@ -163,9 +263,26 @@
{},
{}
],
- "reaching_definitions": [],
+ "reaching_definitions": [
+ {},
+ {},
+ {}
+ ],
"redundant_nil_guards": [],
- "semantic_effects": [],
+ "semantic_effects": [
+ {
+ "detail": "std::process::Command.new",
+ "function": "method_four",
+ "kind": "hidden_io",
+ "line": 2
+ },
+ {
+ "detail": "std::process::Command.new(\"ls\").output",
+ "function": "method_four",
+ "kind": "hidden_io",
+ "line": 2
+ }
+ ],
"state_declarations": [],
"state_param_origins": [],
"state_reads": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/exception_handling/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/exception_handling/rust.json
index 716b6bfb3..e621ab156 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/exception_handling/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/exception_handling/rust.json
@@ -27,6 +27,18 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "",
+ "statements": [
+ {
+ "co_uses": [],
+ "dependencies": [],
+ "reads": [],
+ "writes": []
+ }
+ ]
}
],
"path_condition": {
@@ -54,7 +66,7 @@
"block": false,
"conditional": false,
"control": "always",
- "function": "method_two",
+ "function": "",
"line": 3,
"message": "Err",
"receiver": "self",
@@ -138,19 +150,6 @@
"method_name": "method_two",
"node_name": "when"
},
- {
- "child_fingerprints": [
- "closure_expression(generic_type(id type_arguments(id)) body_statement(return(call(id argument_list(id)))))"
- ],
- "child_masses": [
- 10
- ],
- "fingerprint": "call(id closure_expression(generic_type(id type_arguments(id)) body_statement(return(call(id argument_list(id))))))",
- "line": 2,
- "mass": 11,
- "method_name": "method_two",
- "node_name": "call"
- },
{
"child_fingerprints": [
"id",
@@ -194,31 +193,31 @@
},
{
"child_fingerprints": [
- "let_declaration(assignment(id call(id closure_expression(generic_type(id type_arguments(id)) body_statement(return(call(id argument_list(id))))))))",
- "case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id))))",
- "id"
+ "lambda(body(return(call(id argument_list(id)))))"
],
"child_masses": [
- 13,
- 10,
- 1
+ 6
],
- "fingerprint": "body_statement(let_declaration(assignment(id call(id closure_expression(generic_type(id type_arguments(id)) body_statement(return(call(id argument_list(id)))))))) case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id)))) id)",
- "line": 1,
- "mass": 25,
+ "fingerprint": "call(id lambda(body(return(call(id argument_list(id))))))",
+ "line": 2,
+ "mass": 7,
"method_name": "method_two",
- "node_name": "body_statement"
+ "node_name": "call"
},
{
"child_fingerprints": [
- "return(call(id argument_list(id)))"
+ "let_declaration(assignment(id call(id lambda(body(return(call(id argument_list(id))))))))",
+ "case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id))))",
+ "id"
],
"child_masses": [
- 4
+ 9,
+ 10,
+ 1
],
- "fingerprint": "body_statement(return(call(id argument_list(id))))",
- "line": 2,
- "mass": 5,
+ "fingerprint": "body_statement(let_declaration(assignment(id call(id lambda(body(return(call(id argument_list(id)))))))) case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id)))) id)",
+ "line": 1,
+ "mass": 21,
"method_name": "method_two",
"node_name": "body_statement"
},
@@ -238,15 +237,21 @@
{
"child_fingerprints": [],
"child_masses": [],
- "fingerprint": "method(id body(body_statement(let_declaration(assignment(id call(id closure_expression(generic_type(id type_arguments(id)) body_statement(return(call(id argument_list(id)))))))) case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id)))) id)))",
+ "fingerprint": "method(id body(body_statement(let_declaration(assignment(id call(id lambda(body(return(call(id argument_list(id)))))))) case(id when(argument_list(tuple_struct_pattern(id id)) call(id argument_list(id)))) id)))",
"line": 1,
- "mass": 27,
+ "mass": 23,
"method_name": "method_two",
"node_name": "defn"
}
],
"comparisons": [],
"control_flow_edges": [
+ {
+ "kind": "entry"
+ },
+ {
+ "kind": "return"
+ },
{
"kind": "entry"
},
@@ -270,9 +275,25 @@
}
],
"control_flow_metrics": [
+ {},
{}
],
"control_flow_nodes": [
+ {
+ "id": "cfg:(top-level)#:entry:0:2:18",
+ "kind": "entry",
+ "line": 2
+ },
+ {
+ "id": "cfg:(top-level)#:exit:2:4:5",
+ "kind": "exit",
+ "line": 4
+ },
+ {
+ "id": "cfg:(top-level)#:return:0:3:8",
+ "kind": "jump",
+ "line": 3
+ },
{
"id": "cfg:(top-level)#method_two:entry:0:1:0",
"kind": "entry",
@@ -315,6 +336,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -329,6 +353,13 @@
"owner": "rust",
"params": [],
"visibility": "private"
+ },
+ {
+ "line": 2,
+ "name": "",
+ "owner": "rust",
+ "params": [],
+ "visibility": "private"
}
],
"liveness": [
@@ -337,9 +368,17 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
+ {
+ "id": "(top-level)#",
+ "score": 0.0,
+ "signals": {}
+ },
{
"id": "(top-level)#method_two",
"score": 1.5,
@@ -349,6 +388,32 @@
}
],
"local_methods": [
+ {
+ "boundaries": [],
+ "id": "(top-level)#",
+ "line": 2,
+ "local_contract_assignments": {},
+ "name": "",
+ "owner": "(top-level)",
+ "statements": [
+ {
+ "co_uses": [],
+ "dependencies": [],
+ "end_line": 3,
+ "index": 0,
+ "line": 3,
+ "reads": [],
+ "source": "return Err(Error)",
+ "span": [
+ 3,
+ 8,
+ 3,
+ 25
+ ],
+ "writes": []
+ }
+ ]
+ },
{
"boundaries": [],
"id": "(top-level)#method_two",
@@ -425,14 +490,26 @@
},
{
"reads": [
- "place:(top-level)#method_two:local:e"
+ "place:(top-level)#:local:Error"
],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
- "place:(top-level)#method_two:local:Error"
+ "place:(top-level)#method_two:local:e"
],
+ "writes": []
+ },
+ {
+ "reads": [],
"writes": [
"place:(top-level)#method_two:local:result"
]
@@ -452,14 +529,14 @@
"path_conditions": [],
"places": [
{
- "id": "place:(top-level)#method_two:local:Error",
+ "id": "place:(top-level)#method_two:local:e",
"kind": "local",
- "name": "Error"
+ "name": "e"
},
{
- "id": "place:(top-level)#method_two:local:e",
+ "id": "place:(top-level)#:local:Error",
"kind": "local",
- "name": "e"
+ "name": "Error"
},
{
"id": "place:(top-level)#method_two:local:result",
@@ -480,7 +557,14 @@
3,
25
]
- },
+ }
+ ],
+ "line": 2,
+ "name": "",
+ "owner": "rust"
+ },
+ {
+ "calls": [
{
"line": 6,
"mid": "log",
@@ -508,16 +592,6 @@
},
{
"calls": [
- {
- "line": 3,
- "mid": "Err",
- "span": [
- 3,
- 15,
- 3,
- 25
- ]
- },
{
"line": 9,
"mid": "cleanup",
@@ -540,11 +614,19 @@
"name": "method_two",
"owner": "rust",
"reads": [
- "Err",
"cleanup",
"log"
],
"writes": []
+ },
+ {
+ "line": 2,
+ "name": "",
+ "owner": "rust",
+ "reads": [
+ "Err"
+ ],
+ "writes": []
}
],
"reachability": [
@@ -553,6 +635,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/indexed_assignments/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/indexed_assignments/ruby.json
index 49d6a810a..60968f16c 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/indexed_assignments/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/indexed_assignments/ruby.json
@@ -56,6 +56,19 @@
"branch_arms": [],
"branch_decisions": [],
"calls": [
+ {
+ "arguments": [
+ "1"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "method_six",
+ "line": 3,
+ "message": "status=",
+ "receiver": "self.cache[index]",
+ "safe_navigation": false
+ },
{
"arguments": [
"index"
@@ -82,6 +95,19 @@
"receiver": "obj",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "obj[key]"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "method_six",
+ "line": 2,
+ "message": "status=",
+ "receiver": "self.cache",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
@@ -563,7 +589,9 @@
"name": "method_six",
"owner": "ruby",
"reads": [
- "cache"
+ "cache",
+ "self.cache.status=",
+ "self.cache[index].status="
],
"writes": [
"cache",
@@ -613,6 +641,18 @@
"function": "method_six",
"line": 4,
"receiver": "self"
+ },
+ {
+ "field": "status=",
+ "function": "method_six",
+ "line": 2,
+ "receiver": "self.cache"
+ },
+ {
+ "field": "status=",
+ "function": "method_six",
+ "line": 3,
+ "receiver": "self.cache[index]"
}
],
"state_writes": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/local_flow_edges/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/local_flow_edges/ruby.json
index 9977fa24a..c230c3132 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/local_flow_edges/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/local_flow_edges/ruby.json
@@ -217,6 +217,19 @@
"receiver": "self",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "\"| #{key.join(\":\")} | #{audit.findings.size} |\""
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "iterates",
+ "function": "build",
+ "line": 16,
+ "message": "<<",
+ "receiver": "rows",
+ "safe_navigation": false
+ },
{
"arguments": [
"audit.findings"
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/local_methods_contracts/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/local_methods_contracts/ruby.json
index 1009cf92b..002647ca9 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/local_methods_contracts/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/local_methods_contracts/ruby.json
@@ -102,6 +102,19 @@
}
],
"calls": [
+ {
+ "arguments": [
+ "item.name"
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "conditional",
+ "function": "process",
+ "line": 9,
+ "message": "<<",
+ "receiver": "names",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/locals_not_state/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/locals_not_state/ruby.json
index 989ec64e6..f243b7ae8 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/locals_not_state/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/locals_not_state/ruby.json
@@ -107,6 +107,20 @@
"branch_arms": [],
"branch_decisions": [],
"calls": [
+ {
+ "arguments": [
+ ":path",
+ "path"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "build",
+ "line": 11,
+ "message": "[]=",
+ "receiver": "config",
+ "safe_navigation": false
+ },
{
"arguments": [
"key"
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/mutation_receiver_scope/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/mutation_receiver_scope/ruby.json
index 2fe81ba99..cb37a25ee 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/mutation_receiver_scope/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/mutation_receiver_scope/ruby.json
@@ -28,7 +28,21 @@
"syntax": {
"branch_arms": [],
"branch_decisions": [],
- "calls": [],
+ "calls": [
+ {
+ "arguments": [
+ "item"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "append",
+ "line": 5,
+ "message": "<<",
+ "receiver": "items",
+ "safe_navigation": false
+ }
+ ],
"clone_candidates": [
{
"child_fingerprints": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/normalized_boolean_complexity/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/normalized_boolean_complexity/ruby.json
index 796f7acc9..20e6bfe72 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/normalized_boolean_complexity/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/normalized_boolean_complexity/ruby.json
@@ -259,6 +259,15 @@
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "short_circuit"
}
],
"control_flow_metrics": [
@@ -276,8 +285,13 @@
"line": 8
},
{
- "id": "cfg:SourceFactNormalizedBooleanComplexity#eligible?:stmt:1:5:4",
+ "id": "cfg:SourceFactNormalizedBooleanComplexity#eligible?:stmt:0.then.0:6:6",
"kind": "statement",
+ "line": 6
+ },
+ {
+ "id": "cfg:SourceFactNormalizedBooleanComplexity#eligible?:stmt:1:5:4",
+ "kind": "branch",
"line": 5
}
],
@@ -300,16 +314,19 @@
}
],
"def_use": [
+ {},
{},
{}
],
"dispatch_sites": [],
"dominators": [
+ {},
{},
{},
{}
],
"flow_types": [
+ {},
{},
{}
],
@@ -326,6 +343,7 @@
}
],
"liveness": [
+ {},
{},
{},
{}
@@ -388,12 +406,21 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactNormalizedBooleanComplexity#eligible?:local:cart"
+ ],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactNormalizedBooleanComplexity#eligible?:local:cart",
"place:SourceFactNormalizedBooleanComplexity#eligible?:local:user"
],
- "writes": []
+ "writes": [
+ "place:SourceFactNormalizedBooleanComplexity#eligible?:local:cart",
+ "place:SourceFactNormalizedBooleanComplexity#eligible?:local:user"
+ ]
}
],
"owners": [
@@ -481,11 +508,13 @@
}
],
"reachability": [
+ {},
{},
{},
{}
],
"reaching_definitions": [
+ {},
{},
{}
],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/operator_mutation_locals/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/operator_mutation_locals/ruby.json
index 1f8ea3173..fd64e091f 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/operator_mutation_locals/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/operator_mutation_locals/ruby.json
@@ -45,6 +45,19 @@
"branch_arms": [],
"branch_decisions": [],
"calls": [
+ {
+ "arguments": [
+ "item"
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "iterates",
+ "function": "append_all",
+ "line": 6,
+ "message": "<<",
+ "receiver": "out",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": true,
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/path_condition_report/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/path_condition_report/ruby.json
index 2285b2c66..18b6ab125 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/path_condition_report/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/path_condition_report/ruby.json
@@ -833,35 +833,113 @@
],
"comparisons": [],
"control_flow_edges": [
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
}
],
"control_flow_metrics": [
@@ -872,6 +950,11 @@
{}
],
"control_flow_nodes": [
+ {
+ "id": "cfg:SourceFactPathConditionReport#duplicate_guard:branch:0.then.0:55:6",
+ "kind": "branch",
+ "line": 55
+ },
{
"id": "cfg:SourceFactPathConditionReport#duplicate_guard:entry:0:53:2",
"kind": "entry",
@@ -883,10 +966,25 @@
"line": 59
},
{
- "id": "cfg:SourceFactPathConditionReport#duplicate_guard:stmt:1:54:4",
+ "id": "cfg:SourceFactPathConditionReport#duplicate_guard:stmt:0.then.0.then.0:56:8",
"kind": "statement",
+ "line": 56
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#duplicate_guard:stmt:1:54:4",
+ "kind": "branch",
"line": 54
},
+ {
+ "id": "cfg:SourceFactPathConditionReport#first:branch:0.then.0.then.0:13:8",
+ "kind": "branch",
+ "line": 13
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#first:branch:0.then.0:12:6",
+ "kind": "branch",
+ "line": 12
+ },
{
"id": "cfg:SourceFactPathConditionReport#first:entry:0:10:2",
"kind": "entry",
@@ -898,10 +996,20 @@
"line": 23
},
{
- "id": "cfg:SourceFactPathConditionReport#first:stmt:1:11:4",
+ "id": "cfg:SourceFactPathConditionReport#first:stmt:0.then.0.then.0.then.0:14:10",
"kind": "statement",
+ "line": 14
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#first:stmt:1:11:4",
+ "kind": "branch",
"line": 11
},
+ {
+ "id": "cfg:SourceFactPathConditionReport#missing_enabled:branch:0.then.0:47:6",
+ "kind": "branch",
+ "line": 47
+ },
{
"id": "cfg:SourceFactPathConditionReport#missing_enabled:entry:0:45:2",
"kind": "entry",
@@ -913,10 +1021,25 @@
"line": 51
},
{
- "id": "cfg:SourceFactPathConditionReport#missing_enabled:stmt:1:46:4",
+ "id": "cfg:SourceFactPathConditionReport#missing_enabled:stmt:0.then.0.then.0:48:8",
"kind": "statement",
+ "line": 48
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#missing_enabled:stmt:1:46:4",
+ "kind": "branch",
"line": 46
},
+ {
+ "id": "cfg:SourceFactPathConditionReport#second:branch:0.then.0.then.0:28:8",
+ "kind": "branch",
+ "line": 28
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#second:branch:0.then.0:27:6",
+ "kind": "branch",
+ "line": 27
+ },
{
"id": "cfg:SourceFactPathConditionReport#second:entry:0:25:2",
"kind": "entry",
@@ -928,10 +1051,25 @@
"line": 33
},
{
- "id": "cfg:SourceFactPathConditionReport#second:stmt:1:26:4",
+ "id": "cfg:SourceFactPathConditionReport#second:stmt:0.then.0.then.0.then.0:29:10",
"kind": "statement",
+ "line": 29
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#second:stmt:1:26:4",
+ "kind": "branch",
"line": 26
},
+ {
+ "id": "cfg:SourceFactPathConditionReport#third:branch:0.then.0.then.0:38:8",
+ "kind": "branch",
+ "line": 38
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#third:branch:0.then.0:37:6",
+ "kind": "branch",
+ "line": 37
+ },
{
"id": "cfg:SourceFactPathConditionReport#third:entry:0:35:2",
"kind": "entry",
@@ -943,8 +1081,13 @@
"line": 43
},
{
- "id": "cfg:SourceFactPathConditionReport#third:stmt:1:36:4",
+ "id": "cfg:SourceFactPathConditionReport#third:stmt:0.then.0.then.0.then.0:39:10",
"kind": "statement",
+ "line": 39
+ },
+ {
+ "id": "cfg:SourceFactPathConditionReport#third:stmt:1:36:4",
+ "kind": "branch",
"line": 36
}
],
@@ -961,10 +1104,31 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"dispatch_sites": [],
"dominators": [
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
@@ -993,6 +1157,14 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"functions": [
@@ -1050,6 +1222,19 @@
}
],
"liveness": [
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
@@ -1307,6 +1492,12 @@
}
],
"node_effects": [
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#duplicate_guard:local:user"
+ ],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -1317,10 +1508,28 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactPathConditionReport#duplicate_guard:local:user"
],
+ "writes": [
+ "place:SourceFactPathConditionReport#duplicate_guard:local:user"
+ ]
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#first:local:enabled"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#first:local:feature"
+ ],
"writes": []
},
{
@@ -1335,12 +1544,26 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactPathConditionReport#first:local:enabled",
"place:SourceFactPathConditionReport#first:local:feature",
"place:SourceFactPathConditionReport#first:local:user"
],
+ "writes": [
+ "place:SourceFactPathConditionReport#first:local:enabled",
+ "place:SourceFactPathConditionReport#first:local:feature",
+ "place:SourceFactPathConditionReport#first:local:user"
+ ]
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#missing_enabled:local:feature"
+ ],
"writes": []
},
{
@@ -1354,11 +1577,30 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactPathConditionReport#missing_enabled:local:feature",
"place:SourceFactPathConditionReport#missing_enabled:local:user"
],
+ "writes": [
+ "place:SourceFactPathConditionReport#missing_enabled:local:feature",
+ "place:SourceFactPathConditionReport#missing_enabled:local:user"
+ ]
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#second:local:enabled"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#second:local:feature"
+ ],
"writes": []
},
{
@@ -1373,12 +1615,32 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactPathConditionReport#second:local:enabled",
"place:SourceFactPathConditionReport#second:local:feature",
"place:SourceFactPathConditionReport#second:local:user"
],
+ "writes": [
+ "place:SourceFactPathConditionReport#second:local:enabled",
+ "place:SourceFactPathConditionReport#second:local:feature",
+ "place:SourceFactPathConditionReport#second:local:user"
+ ]
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#third:local:enabled"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactPathConditionReport#third:local:feature"
+ ],
"writes": []
},
{
@@ -1393,13 +1655,21 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactPathConditionReport#third:local:enabled",
"place:SourceFactPathConditionReport#third:local:feature",
"place:SourceFactPathConditionReport#third:local:user"
],
- "writes": []
+ "writes": [
+ "place:SourceFactPathConditionReport#third:local:enabled",
+ "place:SourceFactPathConditionReport#third:local:feature",
+ "place:SourceFactPathConditionReport#third:local:user"
+ ]
}
],
"owners": [
@@ -1687,6 +1957,19 @@
}
],
"reachability": [
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
@@ -1715,6 +1998,14 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"redundant_nil_guards": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/protocols_nil_clone/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/protocols_nil_clone/ruby.json
index 7792ef4fa..dedde9246 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/protocols_nil_clone/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/protocols_nil_clone/ruby.json
@@ -1446,12 +1446,30 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
@@ -1473,6 +1491,12 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
@@ -1494,6 +1518,12 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
{
"kind": "entry"
},
@@ -1632,8 +1662,18 @@
"line": 49
},
{
- "id": "cfg:SourceFactProtocolsNilClone#guard_both_terminate:stmt:1:44:4",
+ "id": "cfg:SourceFactProtocolsNilClone#guard_both_terminate:stmt:0.else.0:47:6",
"kind": "statement",
+ "line": 47
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_both_terminate:stmt:0.then.0:45:6",
+ "kind": "statement",
+ "line": 45
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_both_terminate:stmt:1:44:4",
+ "kind": "branch",
"line": 44
},
{
@@ -1647,8 +1687,18 @@
"line": 57
},
{
- "id": "cfg:SourceFactProtocolsNilClone#guard_else_terminate:stmt:1:52:4",
+ "id": "cfg:SourceFactProtocolsNilClone#guard_else_terminate:stmt:0.else.0:55:6",
"kind": "statement",
+ "line": 55
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_else_terminate:stmt:0.then.0:53:6",
+ "kind": "statement",
+ "line": 53
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_else_terminate:stmt:1:52:4",
+ "kind": "branch",
"line": 52
},
{
@@ -1687,8 +1737,13 @@
"line": 34
},
{
- "id": "cfg:SourceFactProtocolsNilClone#guard_ne_nil:stmt:1:31:4",
+ "id": "cfg:SourceFactProtocolsNilClone#guard_ne_nil:stmt:0.then.0:32:6",
"kind": "statement",
+ "line": 32
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_ne_nil:stmt:1:31:4",
+ "kind": "branch",
"line": 31
},
{
@@ -1727,8 +1782,13 @@
"line": 63
},
{
- "id": "cfg:SourceFactProtocolsNilClone#guard_safe_nav_cond:stmt:1:60:4",
+ "id": "cfg:SourceFactProtocolsNilClone#guard_safe_nav_cond:stmt:0.then.0:61:6",
"kind": "statement",
+ "line": 61
+ },
+ {
+ "id": "cfg:SourceFactProtocolsNilClone#guard_safe_nav_cond:stmt:1:60:4",
+ "kind": "branch",
"line": 60
},
{
@@ -1785,6 +1845,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"dispatch_sites": [],
@@ -1836,6 +1899,12 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -1855,6 +1924,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"functions": [
@@ -2011,6 +2083,12 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -2716,11 +2794,21 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactProtocolsNilClone#guard_both_terminate:local:value"
],
- "writes": []
+ "writes": [
+ "place:SourceFactProtocolsNilClone#guard_both_terminate:local:value"
+ ]
},
{
"reads": [],
@@ -2732,12 +2820,24 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [
"place:SourceFactProtocolsNilClone#guard_else_terminate:local:value"
],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactProtocolsNilClone#guard_else_terminate:local:value"
+ ],
+ "writes": [
+ "place:SourceFactProtocolsNilClone#guard_else_terminate:local:value"
+ ]
+ },
{
"reads": [],
"writes": [
@@ -2780,6 +2880,14 @@
],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactProtocolsNilClone#guard_ne_nil:local:value"
+ ],
+ "writes": [
+ "place:SourceFactProtocolsNilClone#guard_ne_nil:local:value"
+ ]
+ },
{
"reads": [],
"writes": [
@@ -2822,6 +2930,14 @@
],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactProtocolsNilClone#guard_safe_nav_cond:local:value"
+ ],
+ "writes": [
+ "place:SourceFactProtocolsNilClone#guard_safe_nav_cond:local:value"
+ ]
+ },
{
"reads": [],
"writes": [
@@ -3343,6 +3459,12 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
@@ -3362,6 +3484,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"redundant_nil_guards": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/receiver_attribute_local_flow/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/receiver_attribute_local_flow/ruby.json
index 0359abe40..edfd3df92 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/receiver_attribute_local_flow/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/receiver_attribute_local_flow/ruby.json
@@ -99,6 +99,19 @@
"branch_arms": [],
"branch_decisions": [],
"calls": [
+ {
+ "arguments": [
+ "(row.risk * row.multiplier).round(4)"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "apply",
+ "line": 9,
+ "message": "risk=",
+ "receiver": "row",
+ "safe_navigation": false
+ },
{
"arguments": [
"4"
@@ -112,6 +125,32 @@
"receiver": "row.risk * row.multiplier",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "fact.count"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "apply",
+ "line": 8,
+ "message": "count=",
+ "receiver": "row",
+ "safe_navigation": false
+ },
+ {
+ "arguments": [
+ "fact.summary"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "apply",
+ "line": 7,
+ "message": "status=",
+ "receiver": "row",
+ "safe_navigation": false
+ },
{
"arguments": [
"row.file",
@@ -887,11 +926,14 @@
"fact.count",
"fact.summary",
"facts.status_for",
+ "row.count=",
"row.file",
"row.multiplier",
"row.name",
"row.risk",
- "row.risk * row.multiplier.round"
+ "row.risk * row.multiplier.round",
+ "row.risk=",
+ "row.status="
],
"writes": [
"repo",
@@ -952,6 +994,12 @@
"line": 8,
"receiver": "fact"
},
+ {
+ "field": "count=",
+ "function": "apply",
+ "line": 8,
+ "receiver": "row"
+ },
{
"field": "file",
"function": "apply",
@@ -982,12 +1030,24 @@
"line": 9,
"receiver": "row"
},
+ {
+ "field": "risk=",
+ "function": "apply",
+ "line": 9,
+ "receiver": "row"
+ },
{
"field": "round",
"function": "apply",
"line": 9,
"receiver": "row.risk * row.multiplier"
},
+ {
+ "field": "status=",
+ "function": "apply",
+ "line": 7,
+ "receiver": "row"
+ },
{
"field": "status_for",
"function": "apply",
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/rust_behavior/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/rust_behavior/rust.json
index 2aec11244..f2c38efc1 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/rust_behavior/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/rust_behavior/rust.json
@@ -248,7 +248,7 @@
"function": "update",
"line": 19,
"message": "ok",
- "receiver": "std::fs::read_to_string.call(\"config\")",
+ "receiver": "std::fs.read_to_string(\"config\")",
"safe_navigation": false
}
],
@@ -270,18 +270,18 @@
"child_fingerprints": [
"assignment(id id argument_list(id))",
"call(id id)",
- "call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit)))",
+ "call(id call(id scoped_identifier(id id) argument_list(lit)))",
"macro_invocation(id token_tree(lit id))"
],
"child_masses": [
4,
2,
- 9,
+ 7,
5
],
- "fingerprint": "body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id)))",
+ "fingerprint": "body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id)))",
"line": 16,
- "mass": 21,
+ "mass": 19,
"method_name": "update",
"node_name": "body_statement"
},
@@ -289,18 +289,18 @@
"child_fingerprints": [
"assignment(id id argument_list(id))",
"call(id id)",
- "call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit)))",
+ "call(id call(id scoped_identifier(id id) argument_list(lit)))",
"macro_invocation(id token_tree(lit id))"
],
"child_masses": [
4,
2,
- 9,
+ 7,
5
],
- "fingerprint": "if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id))))",
+ "fingerprint": "if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id))))",
"line": 16,
- "mass": 26,
+ "mass": 24,
"method_name": "update",
"node_name": "if"
},
@@ -349,14 +349,14 @@
},
{
"child_fingerprints": [
- "call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))"
+ "call(id scoped_identifier(id id) argument_list(lit))"
],
"child_masses": [
- 8
+ 6
],
- "fingerprint": "call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit)))",
+ "fingerprint": "call(id call(id scoped_identifier(id id) argument_list(lit)))",
"line": 19,
- "mass": 9,
+ "mass": 7,
"method_name": "update",
"node_name": "call"
},
@@ -470,26 +470,52 @@
},
{
"child_fingerprints": [
- "let_declaration(assignment(id call(id scoped_identifier(id id))))",
- "let_declaration(id generic_type(id type_arguments(id)))",
+ "id"
+ ],
+ "child_masses": [
+ 1
+ ],
+ "fingerprint": "call(id id)",
+ "line": 8,
+ "mass": 2,
+ "method_name": "update",
+ "node_name": "call"
+ },
+ {
+ "child_fingerprints": [
+ "id"
+ ],
+ "child_masses": [
+ 1
+ ],
+ "fingerprint": "call(id id)",
+ "line": 9,
+ "mass": 2,
+ "method_name": "update",
+ "node_name": "call"
+ },
+ {
+ "child_fingerprints": [
+ "let_declaration(assignment(id call(id id)))",
+ "let_declaration(assignment(id call(id id)))",
"let_declaration(assignment(id id))",
"if(call(id id) macro_invocation(id token_tree(lit)))",
- "if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id))))",
+ "if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id))))",
"call(id id argument_list(lit))",
"call(id argument_list(id))"
],
"child_masses": [
- 6,
- 6,
+ 4,
+ 4,
3,
7,
- 26,
+ 24,
4,
3
],
- "fingerprint": "body_statement(let_declaration(assignment(id call(id scoped_identifier(id id)))) let_declaration(id generic_type(id type_arguments(id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))",
+ "fingerprint": "body_statement(let_declaration(assignment(id call(id id))) let_declaration(assignment(id call(id id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))",
"line": 7,
- "mass": 56,
+ "mass": 50,
"method_name": "update",
"node_name": "body_statement"
},
@@ -521,53 +547,40 @@
},
{
"child_fingerprints": [
- "method(id body(body_statement(let_declaration(assignment(id call(id scoped_identifier(id id)))) let_declaration(id generic_type(id type_arguments(id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))))",
+ "method(id body(body_statement(let_declaration(assignment(id call(id id))) let_declaration(assignment(id call(id id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))))",
"method(id body(call(id call(id id))))"
],
"child_masses": [
- 58,
+ 52,
5
],
- "fingerprint": "body_statement(method(id body(body_statement(let_declaration(assignment(id call(id scoped_identifier(id id)))) let_declaration(id generic_type(id type_arguments(id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id))))) method(id body(call(id call(id id)))))",
+ "fingerprint": "body_statement(method(id body(body_statement(let_declaration(assignment(id call(id id))) let_declaration(assignment(id call(id id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id))))) method(id body(call(id call(id id)))))",
"line": 6,
- "mass": 64,
+ "mass": 58,
"method_name": "(top-level)",
"node_name": "body_statement"
},
{
"child_fingerprints": [
- "scoped_identifier(id id)"
- ],
- "child_masses": [
- 3
- ],
- "fingerprint": "call(id scoped_identifier(id id))",
- "line": 8,
- "mass": 4,
- "method_name": "update",
- "node_name": "call"
- },
- {
- "child_fingerprints": [
- "scoped_identifier(scoped_identifier(id id) id)",
+ "scoped_identifier(id id)",
"argument_list(lit)"
],
"child_masses": [
- 5,
+ 3,
2
],
- "fingerprint": "call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))",
+ "fingerprint": "call(id scoped_identifier(id id) argument_list(lit))",
"line": 19,
- "mass": 8,
+ "mass": 6,
"method_name": "update",
"node_name": "call"
},
{
"child_fingerprints": [],
"child_masses": [],
- "fingerprint": "method(id body(body_statement(let_declaration(assignment(id call(id scoped_identifier(id id)))) let_declaration(id generic_type(id type_arguments(id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(scoped_identifier(id id) id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))))",
+ "fingerprint": "method(id body(body_statement(let_declaration(assignment(id call(id id))) let_declaration(assignment(id call(id id))) let_declaration(assignment(id id)) if(call(id id) macro_invocation(id token_tree(lit))) if(and(call(id id) id) body_statement(assignment(id id argument_list(id)) call(id id) call(id call(id scoped_identifier(id id) argument_list(lit))) macro_invocation(id token_tree(lit id)))) call(id id argument_list(lit)) call(id argument_list(id)))))",
"line": 7,
- "mass": 58,
+ "mass": 52,
"method_name": "update",
"node_name": "defn"
},
@@ -787,7 +800,6 @@
{},
{},
{},
- {},
{}
],
"functions": [
@@ -1085,8 +1097,7 @@
},
{
"reads": [
- "place:RustSourceFactBehavior#update:local:String",
- "place:RustSourceFactBehavior#update:local:new"
+ "place:RustSourceFactBehavior#update:local:String"
],
"writes": [
"place:RustSourceFactBehavior#update:local:String",
@@ -1095,7 +1106,7 @@
},
{
"reads": [
- "place:RustSourceFactBehavior#update:local:methods"
+ "place:RustSourceFactBehavior#update:local:Vec"
],
"writes": [
"place:RustSourceFactBehavior#update:local:methods"
@@ -1128,7 +1139,6 @@
{
"reads": [
"place:RustSourceFactBehavior#update:local:fs",
- "place:RustSourceFactBehavior#update:local:read_to_string",
"place:RustSourceFactBehavior#update:local:std"
],
"writes": [
@@ -1157,7 +1167,9 @@
"writes": []
},
{
- "reads": [],
+ "reads": [
+ "place:RustSourceFactBehavior#update:local:result"
+ ],
"writes": []
},
{
@@ -1243,6 +1255,11 @@
"kind": "local",
"name": "&mut self"
},
+ {
+ "id": "place:RustSourceFactBehavior#update:local:Vec",
+ "kind": "local",
+ "name": "Vec"
+ },
{
"id": "place:RustSourceFactBehavior#update:local:enabled",
"kind": "local",
@@ -1253,11 +1270,6 @@
"kind": "local",
"name": "input"
},
- {
- "id": "place:RustSourceFactBehavior#update:local:new",
- "kind": "local",
- "name": "new"
- },
{
"id": "place:RustSourceFactBehavior#update:local:panic",
"kind": "local",
@@ -1268,11 +1280,6 @@
"kind": "local",
"name": "println"
},
- {
- "id": "place:RustSourceFactBehavior#update:local:read_to_string",
- "kind": "local",
- "name": "read_to_string"
- },
{
"id": "place:RustSourceFactBehavior#update:local:String",
"kind": "local",
@@ -1405,7 +1412,6 @@
{},
{},
{},
- {},
{}
],
"redundant_nil_guards": [],
@@ -1435,7 +1441,7 @@
"line": 19
},
{
- "detail": "std::fs::read_to_string.call(\"config\").ok",
+ "detail": "std::fs.read_to_string(\"config\").ok",
"function": "update",
"kind": "hidden_io",
"line": 19
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/semantic_effects/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/semantic_effects/ruby.json
index e362ffd75..fd5254e61 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/semantic_effects/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/semantic_effects/ruby.json
@@ -207,6 +207,47 @@
"receiver": "self",
"safe_navigation": false
},
+ {
+ "arguments": [
+ ":name",
+ "value"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "mutate",
+ "line": 12,
+ "message": "[]=",
+ "receiver": "target",
+ "safe_navigation": false
+ },
+ {
+ "arguments": [
+ "item"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "add_item",
+ "line": 31,
+ "message": "<<",
+ "receiver": "items",
+ "safe_navigation": false
+ },
+ {
+ "arguments": [
+ "key",
+ "[]"
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "iterates",
+ "function": "shape_hash",
+ "line": 18,
+ "message": "[]=",
+ "receiver": "hash",
+ "safe_navigation": false
+ },
{
"arguments": [
"name"
@@ -233,6 +274,19 @@
"receiver": "callback",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "value"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "mutate",
+ "line": 13,
+ "message": "<<",
+ "receiver": "target.items",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
@@ -968,6 +1022,7 @@
{},
{},
{},
+ {},
{}
],
"dispatch_sites": [],
@@ -1016,6 +1071,7 @@
{},
{},
{},
+ {},
{}
],
"functions": [
@@ -1522,6 +1578,7 @@
},
{
"reads": [
+ "place:SourceFactSemanticEffects#perform:local:callback",
"place:SourceFactSemanticEffects#perform:local:name"
],
"writes": []
@@ -1812,6 +1869,7 @@
{},
{},
{},
+ {},
{}
],
"redundant_nil_guards": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/ruby.json
index 1af1d01d3..18c283e1b 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/ruby.json
@@ -17,6 +17,16 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "singleton_method",
+ "statements": []
+ },
+ {
+ "boundaries": [],
+ "method": "self_singleton_method",
+ "statements": []
}
],
"path_condition": {
@@ -91,11 +101,19 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "fallthrough"
}
],
"control_flow_metrics": [
+ {},
+ {},
{}
],
"control_flow_nodes": [
@@ -118,12 +136,36 @@
"id": "cfg:(top-level)#method_five:stmt:2:6:2",
"kind": "statement",
"line": 6
+ },
+ {
+ "id": "cfg:(top-level)#self_singleton_method:entry:0:7:4",
+ "kind": "entry",
+ "line": 7
+ },
+ {
+ "id": "cfg:(top-level)#self_singleton_method:exit:1:8:7",
+ "kind": "exit",
+ "line": 8
+ },
+ {
+ "id": "cfg:(top-level)#singleton_method:entry:0:3:4",
+ "kind": "entry",
+ "line": 3
+ },
+ {
+ "id": "cfg:(top-level)#singleton_method:exit:1:4:7",
+ "kind": "exit",
+ "line": 4
}
],
"decisions": [],
"def_use": [],
"dispatch_sites": [],
"dominators": [
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
@@ -154,6 +196,10 @@
}
],
"liveness": [
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
@@ -164,6 +210,16 @@
"id": "(top-level)#method_five",
"score": 0.0,
"signals": {}
+ },
+ {
+ "id": "(top-level)#self_singleton_method",
+ "score": 0.0,
+ "signals": {}
+ },
+ {
+ "id": "(top-level)#singleton_method",
+ "score": 0.0,
+ "signals": {}
}
],
"local_methods": [
@@ -208,9 +264,43 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "id": "(top-level)#self_singleton_method",
+ "line": 7,
+ "local_contract_assignments": {},
+ "name": "self_singleton_method",
+ "owner": "(top-level)",
+ "statements": []
+ },
+ {
+ "boundaries": [],
+ "id": "(top-level)#singleton_method",
+ "line": 3,
+ "local_contract_assignments": {},
+ "name": "singleton_method",
+ "owner": "(top-level)",
+ "statements": []
}
],
"node_effects": [
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": []
@@ -289,6 +379,10 @@
}
],
"reachability": [
+ {},
+ {},
+ {},
+ {},
{},
{},
{},
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/rust.json
index 2737b90c5..855ed36d7 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/rust.json
@@ -17,6 +17,11 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "singleton_method",
+ "statements": []
}
],
"path_condition": {
@@ -70,11 +75,15 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "fallthrough"
}
],
"control_flow_metrics": [
+ {},
{}
],
"control_flow_nodes": [
@@ -97,12 +106,24 @@
"id": "cfg:(top-level)#method_five:stmt:2:3:4",
"kind": "statement",
"line": 3
+ },
+ {
+ "id": "cfg:Obj#singleton_method:entry:0:4:8",
+ "kind": "entry",
+ "line": 4
+ },
+ {
+ "id": "cfg:Obj#singleton_method:exit:1:4:37",
+ "kind": "exit",
+ "line": 4
}
],
"decisions": [],
"def_use": [],
"dispatch_sites": [],
"dominators": [
+ {},
+ {},
{},
{},
{},
@@ -128,6 +149,8 @@
}
],
"liveness": [
+ {},
+ {},
{},
{},
{},
@@ -138,6 +161,11 @@
"id": "(top-level)#method_five",
"score": 0.0,
"signals": {}
+ },
+ {
+ "id": "Obj#singleton_method",
+ "score": 0.0,
+ "signals": {}
}
],
"local_methods": [
@@ -182,6 +210,15 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "id": "Obj#singleton_method",
+ "line": 4,
+ "local_contract_assignments": {},
+ "name": "singleton_method",
+ "owner": "Obj",
+ "statements": []
}
],
"node_effects": [
@@ -197,6 +234,16 @@
"reads": [],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": [
+ "place:Obj#singleton_method:local:&self"
+ ]
+ },
{
"reads": [],
"writes": []
@@ -215,7 +262,13 @@
}
],
"path_conditions": [],
- "places": [],
+ "places": [
+ {
+ "id": "place:Obj#singleton_method:local:&self",
+ "kind": "local",
+ "name": "&self"
+ }
+ ],
"predicate_bodies": [],
"protocol_call_paths": [
{
@@ -248,6 +301,8 @@
}
],
"reachability": [
+ {},
+ {},
{},
{},
{},
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/zig.json b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/zig.json
index 4fa7c28a2..8355a0ab2 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/zig.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/singleton_classes/zig.json
@@ -28,6 +28,11 @@
]
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "singleton_method",
+ "statements": []
}
],
"path_condition": {
@@ -81,11 +86,15 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "fallthrough"
}
],
"control_flow_metrics": [
+ {},
{}
],
"control_flow_nodes": [
@@ -108,6 +117,16 @@
"id": "cfg:(top-level)#method_five:stmt:2:5:4",
"kind": "statement",
"line": 5
+ },
+ {
+ "id": "cfg:Obj#singleton_method:entry:0:3:8",
+ "kind": "entry",
+ "line": 3
+ },
+ {
+ "id": "cfg:Obj#singleton_method:exit:1:3:37",
+ "kind": "exit",
+ "line": 3
}
],
"decisions": [],
@@ -116,6 +135,8 @@
],
"dispatch_sites": [],
"dominators": [
+ {},
+ {},
{},
{},
{},
@@ -141,6 +162,8 @@
}
],
"liveness": [
+ {},
+ {},
{},
{},
{},
@@ -151,6 +174,11 @@
"id": "(top-level)#method_five",
"score": 0.0,
"signals": {}
+ },
+ {
+ "id": "Obj#singleton_method",
+ "score": 0.0,
+ "signals": {}
}
],
"local_methods": [
@@ -209,6 +237,15 @@
]
}
]
+ },
+ {
+ "boundaries": [],
+ "id": "Obj#singleton_method",
+ "line": 3,
+ "local_contract_assignments": {},
+ "name": "singleton_method",
+ "owner": "Obj",
+ "statements": []
}
],
"node_effects": [
@@ -233,6 +270,14 @@
"writes": [
"place:(top-level)#method_five:local:_"
]
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
}
],
"owners": [
@@ -287,6 +332,8 @@
}
],
"reachability": [
+ {},
+ {},
{},
{},
{},
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/slopcop_parity_edges/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/slopcop_parity_edges/ruby.json
index cba439eb8..64d5dcede 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/slopcop_parity_edges/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/slopcop_parity_edges/ruby.json
@@ -1002,8 +1002,26 @@
{
"kind": "entry"
},
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
+ },
{
"kind": "fallthrough"
+ },
+ {
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
+ },
+ {
+ "kind": "short_circuit"
}
],
"control_flow_metrics": [
@@ -1093,8 +1111,23 @@
"line": 17
},
{
- "id": "cfg:SourceFactSlopcopParityEdges#scan:stmt:1:12:4",
+ "id": "cfg:SourceFactSlopcopParityEdges#scan:loop:0.then.0:13:6",
+ "kind": "loop",
+ "line": 13
+ },
+ {
+ "id": "cfg:SourceFactSlopcopParityEdges#scan:stmt:0.else.0:15:6",
"kind": "statement",
+ "line": 15
+ },
+ {
+ "id": "cfg:SourceFactSlopcopParityEdges#scan:stmt:0.then.0.loop.0:13:35",
+ "kind": "statement",
+ "line": 13
+ },
+ {
+ "id": "cfg:SourceFactSlopcopParityEdges#scan:stmt:1:12:4",
+ "kind": "branch",
"line": 12
}
],
@@ -1123,6 +1156,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"dispatch_sites": [],
@@ -1143,6 +1179,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -1159,6 +1198,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"functions": [
@@ -1208,6 +1250,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -1501,7 +1546,9 @@
]
},
{
- "reads": [],
+ "reads": [
+ "place:SourceFactSlopcopParityEdges#report:local:x"
+ ],
"writes": []
},
{
@@ -1543,12 +1590,31 @@
},
{
"reads": [
- "place:SourceFactSlopcopParityEdges#scan:local:path",
"place:SourceFactSlopcopParityEdges#scan:local:paths"
],
"writes": [
"place:SourceFactSlopcopParityEdges#scan:local:path"
]
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactSlopcopParityEdges#scan:local:path"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactSlopcopParityEdges#scan:local:path",
+ "place:SourceFactSlopcopParityEdges#scan:local:paths"
+ ],
+ "writes": [
+ "place:SourceFactSlopcopParityEdges#scan:local:path",
+ "place:SourceFactSlopcopParityEdges#scan:local:paths"
+ ]
}
],
"owners": [
@@ -1795,6 +1861,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
@@ -1811,6 +1880,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"redundant_nil_guards": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/state_read_chains_and_constants/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/state_read_chains_and_constants/ruby.json
index 57667d874..80f3a7f0a 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/state_read_chains_and_constants/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/state_read_chains_and_constants/ruby.json
@@ -94,6 +94,20 @@
"receiver": "ranked",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "item.name",
+ "true"
+ ],
+ "block": false,
+ "conditional": true,
+ "control": "iterates",
+ "function": "helper_result",
+ "line": 20,
+ "message": "[]=",
+ "receiver": "out",
+ "safe_navigation": false
+ },
{
"arguments": [
"items"
@@ -146,6 +160,19 @@
"receiver": "Custom::Sarif",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "{}"
+ ],
+ "block": true,
+ "conditional": false,
+ "control": "always",
+ "function": "helper_result",
+ "line": 19,
+ "message": "each_with_object",
+ "receiver": "decorate(items)",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
@@ -201,17 +228,6 @@
"receiver": "row",
"safe_navigation": false
},
- {
- "arguments": [],
- "block": true,
- "conditional": false,
- "control": "always",
- "function": "helper_result",
- "line": 19,
- "message": "each_with_object",
- "receiver": "decorate(items)",
- "safe_navigation": false
- },
{
"arguments": [],
"block": true,
@@ -249,19 +265,6 @@
"method_name": "helper_result",
"node_name": "call"
},
- {
- "child_fingerprints": [
- "call(id call(id argument_list(id)))"
- ],
- "child_masses": [
- 4
- ],
- "fingerprint": "block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))",
- "line": 19,
- "mass": 15,
- "method_name": "helper_result",
- "node_name": "block"
- },
{
"child_fingerprints": [
"call(id call(id id argument_list(id)))"
@@ -310,12 +313,25 @@
"child_masses": [
3
],
- "fingerprint": "call(id call(id argument_list(id)))",
+ "fingerprint": "call(id call(id argument_list(id)))",
"line": 19,
"mass": 4,
"method_name": "helper_result",
"node_name": "call"
},
+ {
+ "child_fingerprints": [
+ "call(id call(id argument_list(id)))"
+ ],
+ "child_masses": [
+ 4
+ ],
+ "fingerprint": "block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))",
+ "line": 19,
+ "mass": 15,
+ "method_name": "helper_result",
+ "node_name": "block"
+ },
{
"child_fingerprints": [
"call(id colon2(id id) argument_list(id))",
@@ -569,7 +585,7 @@
"method(id body(block(call(id call(id id argument_list(id))) body(parameters(id) call(id colon2(id id) argument_list(hash(lit call(id id))))))))",
"method(id body(parameters(id) call(id id argument_list(id))))",
"method(id body(block(call(id id) body(parameters(id) call(id id)))))",
- "method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))))",
+ "method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))))",
"method(id body(parameters(id) body_statement(call(id colon2(id id) argument_list(id)) call(id id))))"
],
"child_masses": [
@@ -579,7 +595,7 @@
19,
12
],
- "fingerprint": "body_statement(method(id body(block(call(id call(id id argument_list(id))) body(parameters(id) call(id colon2(id id) argument_list(hash(lit call(id id)))))))) method(id body(parameters(id) call(id id argument_list(id)))) method(id body(block(call(id id) body(parameters(id) call(id id))))) method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool)))))) method(id body(parameters(id) body_statement(call(id colon2(id id) argument_list(id)) call(id id)))))",
+ "fingerprint": "body_statement(method(id body(block(call(id call(id id argument_list(id))) body(parameters(id) call(id colon2(id id) argument_list(hash(lit call(id id)))))))) method(id body(parameters(id) call(id id argument_list(id)))) method(id body(block(call(id id) body(parameters(id) call(id id))))) method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool)))))) method(id body(parameters(id) body_statement(call(id colon2(id id) argument_list(id)) call(id id)))))",
"line": 4,
"mass": 69,
"method_name": "(top-level)",
@@ -606,7 +622,7 @@
{
"child_fingerprints": [],
"child_masses": [],
- "fingerprint": "method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))))",
+ "fingerprint": "method(id body(parameters(id) block(call(id call(id argument_list(id))) body(parameters(id id) assignment<[]=>(lit id argument_list(call(id id) bool))))))",
"line": 18,
"mass": 19,
"method_name": "helper_result",
@@ -652,19 +668,37 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
},
{
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
},
{
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
}
],
"control_flow_metrics": [
@@ -721,8 +755,13 @@
"line": 22
},
{
- "id": "cfg:SourceFactStateReadChainsAndConstants#helper_result:stmt:1:19:4",
+ "id": "cfg:SourceFactStateReadChainsAndConstants#helper_result:stmt:0.loop.0:20:6",
"kind": "statement",
+ "line": 20
+ },
+ {
+ "id": "cfg:SourceFactStateReadChainsAndConstants#helper_result:stmt:1:19:4",
+ "kind": "loop",
"line": 19
},
{
@@ -736,10 +775,15 @@
"line": 16
},
{
- "id": "cfg:SourceFactStateReadChainsAndConstants#provider_paths:stmt:1:15:4",
+ "id": "cfg:SourceFactStateReadChainsAndConstants#provider_paths:stmt:0.loop.0:15:45",
"kind": "statement",
"line": 15
},
+ {
+ "id": "cfg:SourceFactStateReadChainsAndConstants#provider_paths:stmt:1:15:4",
+ "kind": "loop",
+ "line": 15
+ },
{
"id": "cfg:SourceFactStateReadChainsAndConstants#results:entry:0:4:2",
"kind": "entry",
@@ -751,13 +795,22 @@
"line": 8
},
{
- "id": "cfg:SourceFactStateReadChainsAndConstants#results:stmt:1:5:4",
+ "id": "cfg:SourceFactStateReadChainsAndConstants#results:stmt:0.loop.0:6:6",
"kind": "statement",
+ "line": 6
+ },
+ {
+ "id": "cfg:SourceFactStateReadChainsAndConstants#results:stmt:1:5:4",
+ "kind": "loop",
"line": 5
}
],
"decisions": [],
"def_use": [
+ {},
+ {},
+ {},
+ {},
{},
{},
{}
@@ -779,6 +832,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"flow_types": [
@@ -851,6 +907,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -1090,9 +1149,14 @@
{
"reads": [
"place:SourceFactStateReadChainsAndConstants#helper_result:local:item",
- "place:SourceFactStateReadChainsAndConstants#helper_result:local:items",
"place:SourceFactStateReadChainsAndConstants#helper_result:local:out"
],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactStateReadChainsAndConstants#helper_result:local:items"
+ ],
"writes": [
"place:SourceFactStateReadChainsAndConstants#helper_result:local:item",
"place:SourceFactStateReadChainsAndConstants#helper_result:local:out"
@@ -1110,6 +1174,10 @@
"reads": [
"place:SourceFactStateReadChainsAndConstants#provider_paths:local:provider"
],
+ "writes": []
+ },
+ {
+ "reads": [],
"writes": [
"place:SourceFactStateReadChainsAndConstants#provider_paths:local:provider"
]
@@ -1124,10 +1192,15 @@
},
{
"reads": [
- "place:SourceFactStateReadChainsAndConstants#results:instance_field:@ranked",
- "place:SourceFactStateReadChainsAndConstants#results:instance_field:@top",
"place:SourceFactStateReadChainsAndConstants#results:local:row"
],
+ "writes": []
+ },
+ {
+ "reads": [
+ "place:SourceFactStateReadChainsAndConstants#results:instance_field:@ranked",
+ "place:SourceFactStateReadChainsAndConstants#results:instance_field:@top"
+ ],
"writes": [
"place:SourceFactStateReadChainsAndConstants#results:local:row"
]
@@ -1308,6 +1381,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/ruby.json b/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/ruby.json
index 37ff2f519..c874bd4a9 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/ruby.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/ruby.json
@@ -123,13 +123,16 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "yield_return"
},
{
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_exit"
}
],
"control_flow_metrics": [
@@ -149,7 +152,7 @@
},
{
"id": "cfg:(top-level)#method_three:stmt:1:2:2",
- "kind": "statement",
+ "kind": "callback",
"line": 2
},
{
@@ -164,7 +167,7 @@
},
{
"id": "cfg:(top-level)#method_with_empty_block:stmt:1:6:2",
- "kind": "statement",
+ "kind": "loop",
"line": 6
}
],
diff --git a/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/rust.json b/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/rust.json
index 39bdc4771..90416d7cc 100644
--- a/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/rust.json
+++ b/gems/fact-mine/examples/source-facts/oracles/general/yield_blocks/rust.json
@@ -35,6 +35,11 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "",
+ "statements": []
}
],
"path_condition": {
@@ -155,6 +160,9 @@
],
"comparisons": [],
"control_flow_edges": [
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "entry"
},
@@ -168,14 +176,25 @@
"kind": "fallthrough"
},
{
- "kind": "fallthrough"
+ "kind": "callback_return"
}
],
"control_flow_metrics": [
+ {},
{},
{}
],
"control_flow_nodes": [
+ {
+ "id": "cfg:(top-level)#:entry:0:7:24",
+ "kind": "entry",
+ "line": 7
+ },
+ {
+ "id": "cfg:(top-level)#:exit:1:7:30",
+ "kind": "exit",
+ "line": 7
+ },
{
"id": "cfg:(top-level)#method_three:entry:0:1:0",
"kind": "entry",
@@ -208,12 +227,14 @@
},
{
"id": "cfg:(top-level)#method_with_empty_block:stmt:2:7:4",
- "kind": "statement",
+ "kind": "callback",
"line": 7
}
],
"decisions": [],
- "def_use": [],
+ "def_use": [
+ {}
+ ],
"dispatch_sites": [],
"dominators": [
{},
@@ -222,9 +243,13 @@
{},
{},
{},
+ {},
+ {},
+ {}
+ ],
+ "flow_types": [
{}
],
- "flow_types": [],
"functions": [
{
"line": 1,
@@ -241,6 +266,13 @@
"owner": "rust",
"params": [],
"visibility": "private"
+ },
+ {
+ "line": 7,
+ "name": "",
+ "owner": "rust",
+ "params": [],
+ "visibility": "private"
}
],
"liveness": [
@@ -250,9 +282,16 @@
{},
{},
{},
+ {},
+ {},
{}
],
"local_complexity_scores": [
+ {
+ "id": "(top-level)#",
+ "score": 0.0,
+ "signals": {}
+ },
{
"id": "(top-level)#method_three",
"score": 0.0,
@@ -265,6 +304,15 @@
}
],
"local_methods": [
+ {
+ "boundaries": [],
+ "id": "(top-level)#",
+ "line": 7,
+ "local_contract_assignments": {},
+ "name": "",
+ "owner": "(top-level)",
+ "statements": []
+ },
{
"boundaries": [],
"id": "(top-level)#method_three",
@@ -343,6 +391,14 @@
}
],
"node_effects": [
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -372,7 +428,9 @@
]
},
{
- "reads": [],
+ "reads": [
+ "place:(top-level)#method_with_empty_block:local:arr"
+ ],
"writes": []
}
],
@@ -398,6 +456,12 @@
"name": "method_with_empty_block",
"owner": "rust"
},
+ {
+ "calls": [],
+ "line": 7,
+ "name": "",
+ "owner": "rust"
+ },
{
"calls": [
{
@@ -432,6 +496,13 @@
"owner": "rust",
"reads": [],
"writes": []
+ },
+ {
+ "line": 7,
+ "name": "",
+ "owner": "rust",
+ "reads": [],
+ "writes": []
}
],
"reachability": [
@@ -441,9 +512,13 @@
{},
{},
{},
+ {},
+ {},
+ {}
+ ],
+ "reaching_definitions": [
{}
],
- "reaching_definitions": [],
"redundant_nil_guards": [],
"semantic_effects": [],
"state_declarations": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-block_constant_effects.json b/gems/fact-mine/examples/source-facts/oracles/ruby-block_constant_effects.json
index 10a098809..9f5f6bf8b 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-block_constant_effects.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-block_constant_effects.json
@@ -381,7 +381,13 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
}
],
"control_flow_metrics": [
@@ -420,13 +426,20 @@
"line": 9
},
{
- "id": "cfg:SourceFactBlockConstantEffects#resolve:stmt:1:7:4",
+ "id": "cfg:SourceFactBlockConstantEffects#resolve:stmt:0.loop.0:8:34",
"kind": "statement",
+ "line": 8
+ },
+ {
+ "id": "cfg:SourceFactBlockConstantEffects#resolve:stmt:1:7:4",
+ "kind": "loop",
"line": 7
}
],
"decisions": [],
"def_use": [
+ {},
+ {},
{},
{}
],
@@ -438,6 +451,7 @@
{},
{},
{},
+ {},
{}
],
"flow_types": [
@@ -474,6 +488,7 @@
{},
{},
{},
+ {},
{}
],
"local_complexity_scores": [
@@ -601,7 +616,12 @@
},
{
"reads": [
- "place:SourceFactBlockConstantEffects#resolve:local:candidate",
+ "place:SourceFactBlockConstantEffects#resolve:local:candidate"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
"place:SourceFactBlockConstantEffects#resolve:local:path",
"place:SourceFactBlockConstantEffects#resolve:local:rel"
],
@@ -679,6 +699,7 @@
{},
{},
{},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-branch_qualified_predicates.json b/gems/fact-mine/examples/source-facts/oracles/ruby-branch_qualified_predicates.json
index 7d560452d..9c16aeba4 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-branch_qualified_predicates.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-branch_qualified_predicates.json
@@ -185,6 +185,28 @@
"receiver": "::File",
"safe_navigation": false
},
+ {
+ "arguments": [],
+ "block": false,
+ "conditional": true,
+ "control": "conditional",
+ "function": "checks",
+ "line": 5,
+ "message": "enabled?",
+ "receiver": "External::Risk",
+ "safe_navigation": false
+ },
+ {
+ "arguments": [],
+ "block": false,
+ "conditional": true,
+ "control": "conditional",
+ "function": "checks",
+ "line": 6,
+ "message": "load_config",
+ "receiver": "External::Risk",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-implicit_self_chain_state_reads.json b/gems/fact-mine/examples/source-facts/oracles/ruby-implicit_self_chain_state_reads.json
index 3e1c7128f..ecded3df7 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-implicit_self_chain_state_reads.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-implicit_self_chain_state_reads.json
@@ -653,7 +653,13 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "loop_backedge"
+ },
+ {
+ "kind": "loop_body"
+ },
+ {
+ "kind": "loop_exit"
},
{
"kind": "entry"
@@ -741,10 +747,15 @@
"line": 15
},
{
- "id": "cfg:SourceFactImplicitSelfChainStateReads#owner_nodes:stmt:1:14:4",
+ "id": "cfg:SourceFactImplicitSelfChainStateReads#owner_nodes:stmt:0.loop.0:14:26",
"kind": "statement",
"line": 14
},
+ {
+ "id": "cfg:SourceFactImplicitSelfChainStateReads#owner_nodes:stmt:1:14:4",
+ "kind": "loop",
+ "line": 14
+ },
{
"id": "cfg:SourceFactImplicitSelfChainStateReads#self.build:entry:0:4:2",
"kind": "entry",
@@ -767,6 +778,7 @@
{},
{},
{},
+ {},
{}
],
"dispatch_sites": [],
@@ -788,6 +800,7 @@
{},
{},
{},
+ {},
{}
],
"flow_types": [
@@ -860,6 +873,7 @@
{},
{},
{},
+ {},
{}
],
"local_complexity_scores": [
@@ -1126,6 +1140,10 @@
"reads": [
"place:SourceFactImplicitSelfChainStateReads#owner_nodes:local:node"
],
+ "writes": []
+ },
+ {
+ "reads": [],
"writes": [
"place:SourceFactImplicitSelfChainStateReads#owner_nodes:local:node"
]
@@ -1345,6 +1363,7 @@
{},
{},
{},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-indexed_state_reads.json b/gems/fact-mine/examples/source-facts/oracles/ruby-indexed_state_reads.json
index 986dcddd4..5f325c2fb 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-indexed_state_reads.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-indexed_state_reads.json
@@ -106,6 +106,19 @@
"receiver": "lineage",
"safe_navigation": false
},
+ {
+ "arguments": [
+ "\"- [Lineage Unit Risk (#{@lineage[:units].size})]\""
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "lineage_summary",
+ "line": 11,
+ "message": "<<",
+ "receiver": "out",
+ "safe_navigation": false
+ },
{
"arguments": [],
"block": false,
@@ -366,7 +379,13 @@
"kind": "entry"
},
{
- "kind": "fallthrough"
+ "kind": "return"
+ },
+ {
+ "kind": "branch_false"
+ },
+ {
+ "kind": "branch_true"
},
{
"kind": "entry"
@@ -390,9 +409,14 @@
"kind": "exit",
"line": 8
},
+ {
+ "id": "cfg:SourceFactIndexedStateReads#label:return:0.then.0:5:4",
+ "kind": "jump",
+ "line": 5
+ },
{
"id": "cfg:SourceFactIndexedStateReads#label:stmt:1:5:4",
- "kind": "statement",
+ "kind": "branch",
"line": 5
},
{
@@ -422,9 +446,11 @@
{},
{},
{},
+ {},
{}
],
"flow_types": [
+ {},
{},
{},
{}
@@ -453,6 +479,7 @@
{},
{},
{},
+ {},
{}
],
"local_complexity_scores": [
@@ -541,6 +568,12 @@
],
"writes": []
},
+ {
+ "reads": [
+ "place:SourceFactIndexedStateReads#label:instance_field:@data"
+ ],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -631,9 +664,11 @@
{},
{},
{},
+ {},
{}
],
"reaching_definitions": [
+ {},
{},
{},
{}
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-memoized_helper_calls.json b/gems/fact-mine/examples/source-facts/oracles/ruby-memoized_helper_calls.json
index 58565fdb3..9ce3a7aaf 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-memoized_helper_calls.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-memoized_helper_calls.json
@@ -298,17 +298,19 @@
"writes": []
},
{
- "reads": [],
- "writes": []
+ "reads": [
+ "place:SourceFactMemoizedHelperCalls#owner_edges:instance_field:@owner_edges"
+ ],
+ "writes": [
+ "place:SourceFactMemoizedHelperCalls#owner_edges:instance_field:@owner_edges"
+ ]
},
{
"reads": [],
"writes": []
},
{
- "reads": [
- "place:SourceFactMemoizedHelperCalls#owner_edges:instance_field:@owner_edges"
- ],
+ "reads": [],
"writes": []
}
],
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-ruby_coverage.json b/gems/fact-mine/examples/source-facts/oracles/ruby-ruby_coverage.json
index d98b9dc59..985a2a145 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-ruby_coverage.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-ruby_coverage.json
@@ -161,6 +161,11 @@
"writes": []
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "class_method",
+ "statements": []
}
],
"path_condition": {
@@ -1500,6 +1505,9 @@
}
],
"control_flow_edges": [
+ {
+ "kind": "fallthrough"
+ },
{
"kind": "entry"
},
@@ -1637,9 +1645,20 @@
{},
{},
{},
+ {},
{}
],
"control_flow_nodes": [
+ {
+ "id": "cfg:RubyCoverageParent::RubyCoverageChild#class_method:entry:0:66:8",
+ "kind": "entry",
+ "line": 66
+ },
+ {
+ "id": "cfg:RubyCoverageParent::RubyCoverageChild#class_method:exit:1:68:11",
+ "kind": "exit",
+ "line": 68
+ },
{
"id": "cfg:RubyCoverageParent::RubyCoverageChild#initialize:entry:0:13:4",
"kind": "entry",
@@ -1881,6 +1900,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"flow_types": [
@@ -1973,9 +1994,16 @@
{},
{},
{},
+ {},
+ {},
{}
],
"local_complexity_scores": [
+ {
+ "id": "RubyCoverageParent::RubyCoverageChild#class_method",
+ "score": 0.0,
+ "signals": {}
+ },
{
"id": "RubyCoverageParent::RubyCoverageChild#initialize",
"score": 0.0,
@@ -2008,6 +2036,15 @@
}
],
"local_methods": [
+ {
+ "boundaries": [],
+ "id": "RubyCoverageParent::RubyCoverageChild#class_method",
+ "line": 66,
+ "local_contract_assignments": {},
+ "name": "class_method",
+ "owner": "RubyCoverageParent::RubyCoverageChild",
+ "statements": []
+ },
{
"boundaries": [],
"id": "RubyCoverageParent::RubyCoverageChild#initialize",
@@ -2360,6 +2397,14 @@
}
],
"node_effects": [
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -2482,7 +2527,9 @@
},
{
"reads": [],
- "writes": []
+ "writes": [
+ "place:RubyCoverageParent::RubyCoverageChild#some_call:local:e"
+ ]
},
{
"reads": [],
@@ -2803,6 +2850,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"reaching_definitions": [
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-sequence_call_edges.json b/gems/fact-mine/examples/source-facts/oracles/ruby-sequence_call_edges.json
index 017bdb8eb..3fb09217f 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-sequence_call_edges.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-sequence_call_edges.json
@@ -1862,6 +1862,12 @@
{
"kind": "fallthrough"
},
+ {
+ "kind": "callback_return"
+ },
+ {
+ "kind": "callback_body"
+ },
{
"kind": "entry"
},
@@ -1917,8 +1923,18 @@
"line": 13
},
{
- "id": "cfg:SourceFactSequenceCallEdges#assertion_single_call_argument:stmt:1:9:4",
+ "id": "cfg:SourceFactSequenceCallEdges#assertion_single_call_argument:stmt:0.callback.0:10:6",
+ "kind": "statement",
+ "line": 10
+ },
+ {
+ "id": "cfg:SourceFactSequenceCallEdges#assertion_single_call_argument:stmt:0.callback.1:11:6",
"kind": "statement",
+ "line": 11
+ },
+ {
+ "id": "cfg:SourceFactSequenceCallEdges#assertion_single_call_argument:stmt:1:9:4",
+ "kind": "callback",
"line": 9
},
{
@@ -1980,6 +1996,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"dispatch_sites": [],
@@ -1998,6 +2017,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"flow_types": [
@@ -2016,6 +2037,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"functions": [
@@ -2081,6 +2107,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"local_complexity_scores": [
@@ -2543,12 +2571,21 @@
"reads": [
"place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:coverage",
"place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:dir",
- "place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:file",
+ "place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:file"
+ ],
+ "writes": []
+ },
+ {
+ "reads": [
"place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:path",
"place:SourceFactSequenceCallEdges#assertion_single_call_argument:local:v"
],
"writes": []
},
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -2602,15 +2639,23 @@
"writes": []
},
{
- "reads": [],
+ "reads": [
+ "place:SourceFactSequenceCallEdges#symbol_proc_maps:local:arms"
+ ],
"writes": []
},
{
- "reads": [],
+ "reads": [
+ "place:SourceFactSequenceCallEdges#symbol_proc_maps:local:f",
+ "place:SourceFactSequenceCallEdges#symbol_proc_maps:local:rsf"
+ ],
"writes": []
},
{
- "reads": [],
+ "reads": [
+ "place:SourceFactSequenceCallEdges#symbol_proc_maps:local:f",
+ "place:SourceFactSequenceCallEdges#symbol_proc_maps:local:rsf"
+ ],
"writes": []
}
],
@@ -2925,6 +2970,8 @@
{},
{},
{},
+ {},
+ {},
{}
],
"reaching_definitions": [
@@ -2943,6 +2990,11 @@
{},
{},
{},
+ {},
+ {},
+ {},
+ {},
+ {},
{}
],
"redundant_nil_guards": [],
diff --git a/gems/fact-mine/examples/source-facts/oracles/ruby-visibility.json b/gems/fact-mine/examples/source-facts/oracles/ruby-visibility.json
index 6fd7c8423..798b7a68a 100644
--- a/gems/fact-mine/examples/source-facts/oracles/ruby-visibility.json
+++ b/gems/fact-mine/examples/source-facts/oracles/ruby-visibility.json
@@ -25,6 +25,18 @@
]
}
]
+ },
+ {
+ "boundaries": [],
+ "method": "inline_guard",
+ "statements": [
+ {
+ "co_uses": [],
+ "dependencies": [],
+ "reads": [],
+ "writes": []
+ }
+ ]
}
],
"path_condition": {
@@ -198,15 +210,37 @@
{
"kind": "entry"
},
+ {
+ "kind": "fallthrough"
+ },
+ {
+ "kind": "entry"
+ },
{
"kind": "fallthrough"
}
],
"control_flow_metrics": [
+ {},
{},
{}
],
"control_flow_nodes": [
+ {
+ "id": "cfg:SourceFactVisibility#inline_guard:entry:0:14:12",
+ "kind": "entry",
+ "line": 14
+ },
+ {
+ "id": "cfg:SourceFactVisibility#inline_guard:exit:2:16:5",
+ "kind": "exit",
+ "line": 16
+ },
+ {
+ "id": "cfg:SourceFactVisibility#inline_guard:stmt:1:15:4",
+ "kind": "statement",
+ "line": 15
+ },
{
"id": "cfg:SourceFactVisibility#prepare:entry:0:10:2",
"kind": "entry",
@@ -247,6 +281,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"flow_types": [],
@@ -279,9 +316,17 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"local_complexity_scores": [
+ {
+ "id": "SourceFactVisibility#inline_guard",
+ "score": 0.0,
+ "signals": {}
+ },
{
"id": "SourceFactVisibility#prepare",
"score": 0.0,
@@ -294,6 +339,32 @@
}
],
"local_methods": [
+ {
+ "boundaries": [],
+ "id": "SourceFactVisibility#inline_guard",
+ "line": 14,
+ "local_contract_assignments": {},
+ "name": "inline_guard",
+ "owner": "SourceFactVisibility",
+ "statements": [
+ {
+ "co_uses": [],
+ "dependencies": [],
+ "end_line": 15,
+ "index": 0,
+ "line": 15,
+ "reads": [],
+ "source": "true",
+ "span": [
+ 15,
+ 4,
+ 15,
+ 8
+ ],
+ "writes": []
+ }
+ ]
+ },
{
"boundaries": [],
"id": "SourceFactVisibility#prepare",
@@ -350,6 +421,18 @@
}
],
"node_effects": [
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
+ {
+ "reads": [],
+ "writes": []
+ },
{
"reads": [],
"writes": [
@@ -464,6 +547,9 @@
{},
{},
{},
+ {},
+ {},
+ {},
{}
],
"reaching_definitions": [],
diff --git a/gems/fact-mine/examples/syntax-facts/oracles/go-core.json b/gems/fact-mine/examples/syntax-facts/oracles/go-core.json
index 9437cef99..0eb4b3945 100644
--- a/gems/fact-mine/examples/syntax-facts/oracles/go-core.json
+++ b/gems/fact-mine/examples/syntax-facts/oracles/go-core.json
@@ -504,45 +504,6 @@
17
]
},
- {
- "field": "audit",
- "function": "Process",
- "line": 66,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 66,
- 4,
- 66,
- 11
- ]
- },
- {
- "field": "audit",
- "function": "Process",
- "line": 67,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 67,
- 7,
- 67,
- 14
- ]
- },
- {
- "field": "children",
- "function": "Process",
- "line": 62,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 62,
- 2,
- 62,
- 12
- ]
- },
{
"field": "count",
"function": "Process",
@@ -569,45 +530,6 @@
15
]
},
- {
- "field": "defaultCase",
- "function": "Process",
- "line": 51,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 51,
- 2,
- 51,
- 15
- ]
- },
- {
- "field": "escalate",
- "function": "Process",
- "line": 47,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 47,
- 2,
- 47,
- 12
- ]
- },
- {
- "field": "fallback",
- "function": "Process",
- "line": 49,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 49,
- 2,
- 49,
- 12
- ]
- },
{
"field": "lookup",
"function": "Process",
@@ -621,32 +543,6 @@
9
]
},
- {
- "field": "publish",
- "function": "Process",
- "line": 56,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 56,
- 2,
- 56,
- 11
- ]
- },
- {
- "field": "send",
- "function": "audit",
- "line": 74,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 74,
- 1,
- 74,
- 7
- ]
- },
{
"field": "status",
"function": "Process",
@@ -672,19 +568,6 @@
75,
13
]
- },
- {
- "field": "warn",
- "function": "Process",
- "line": 58,
- "owner": "GoSyntaxFactsCore",
- "receiver": "self",
- "span": [
- 58,
- 2,
- 58,
- 8
- ]
}
],
"state_writes": [
diff --git a/gems/fact-mine/examples/syntax-facts/oracles/java-core.json b/gems/fact-mine/examples/syntax-facts/oracles/java-core.json
index 7d966fabc..15af65321 100644
--- a/gems/fact-mine/examples/syntax-facts/oracles/java-core.json
+++ b/gems/fact-mine/examples/syntax-facts/oracles/java-core.json
@@ -139,6 +139,27 @@
26
]
},
+ {
+ "arguments": [
+ "name",
+ "user.active()"
+ ],
+ "block": false,
+ "conditional": false,
+ "control": "always",
+ "function": "process",
+ "line": 14,
+ "message": "call",
+ "owner": "JavaSyntaxFactsCore",
+ "receiver": "Account",
+ "safe_navigation": false,
+ "span": [
+ 14,
+ 22,
+ 14,
+ 54
+ ]
+ },
{
"arguments": [
"name"
diff --git a/gems/fact-mine/examples/syntax-facts/oracles/kotlin-core.json b/gems/fact-mine/examples/syntax-facts/oracles/kotlin-core.json
index 43ecec581..a06a9578b 100644
--- a/gems/fact-mine/examples/syntax-facts/oracles/kotlin-core.json
+++ b/gems/fact-mine/examples/syntax-facts/oracles/kotlin-core.json
@@ -283,6 +283,22 @@
"dispatch_sites": [],
"file": "gems/fact-mine/examples/syntax-facts/kotlin/core.kt",
"functions": [
+ {
+ "line": 3,
+ "name": "KotlinSyntaxFactsCore",
+ "owner": "KotlinSyntaxFactsCore",
+ "params": [
+ "status",
+ "sink"
+ ],
+ "span": [
+ 3,
+ 6,
+ 4,
+ 25
+ ],
+ "visibility": "public"
+ },
{
"line": 31,
"name": "audit",
@@ -311,6 +327,19 @@
],
"visibility": "public"
},
+ {
+ "line": 42,
+ "name": "Status",
+ "owner": "Status",
+ "params": [],
+ "span": [
+ 42,
+ 11,
+ 45,
+ 1
+ ],
+ "visibility": "public"
+ },
{
"line": 6,
"name": "process",
diff --git a/gems/fact-mine/examples/syntax-facts/oracles/typescript-cfg_callbacks.json b/gems/fact-mine/examples/syntax-facts/oracles/typescript-cfg_callbacks.json
index 7450426e8..5e36124bc 100644
--- a/gems/fact-mine/examples/syntax-facts/oracles/typescript-cfg_callbacks.json
+++ b/gems/fact-mine/examples/syntax-facts/oracles/typescript-cfg_callbacks.json
@@ -2,6 +2,118 @@
"documents": [
{
"control_flow_edges": [
+ {
+ "from": "cfg:CfgCallbacks#:entry:0:10:18",
+ "function": "",
+ "kind": "entry",
+ "line": 10,
+ "owner": "CfgCallbacks",
+ "span": [
+ 10,
+ 18,
+ 14,
+ 5
+ ],
+ "to": "cfg:CfgCallbacks#:stmt:1:11:6"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:stmt:0.callback.0:12:8",
+ "function": "",
+ "kind": "callback_return",
+ "line": 12,
+ "owner": "CfgCallbacks",
+ "span": [
+ 12,
+ 8,
+ 12,
+ 24
+ ],
+ "to": "cfg:CfgCallbacks#:exit:2:14:5"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:stmt:1:11:6",
+ "function": "",
+ "kind": "callback_body",
+ "line": 11,
+ "owner": "CfgCallbacks",
+ "span": [
+ 11,
+ 6,
+ 13,
+ 8
+ ],
+ "to": "cfg:CfgCallbacks#:stmt:0.callback.0:12:8"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:entry:0:11:16",
+ "function": "",
+ "kind": "entry",
+ "line": 11,
+ "owner": "CfgCallbacks",
+ "span": [
+ 11,
+ 16,
+ 13,
+ 7
+ ],
+ "to": "cfg:CfgCallbacks#:stmt:1:12:8"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:stmt:1:12:8",
+ "function": "",
+ "kind": "fallthrough",
+ "line": 12,
+ "owner": "CfgCallbacks",
+ "span": [
+ 12,
+ 8,
+ 12,
+ 24
+ ],
+ "to": "cfg:CfgCallbacks#:exit:2:13:7"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:entry:0:19:18",
+ "function": "",
+ "kind": "fallthrough",
+ "line": 19,
+ "owner": "CfgCallbacks",
+ "span": [
+ 19,
+ 18,
+ 19,
+ 26
+ ],
+ "to": "cfg:CfgCallbacks#:exit:1:19:26"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:entry:0:3:18",
+ "function": "",
+ "kind": "entry",
+ "line": 3,
+ "owner": "CfgCallbacks",
+ "span": [
+ 3,
+ 18,
+ 5,
+ 5
+ ],
+ "to": "cfg:CfgCallbacks#:stmt:1:4:6"
+ },
+ {
+ "from": "cfg:CfgCallbacks#:stmt:1:4:6",
+ "function": "",
+ "kind": "fallthrough",
+ "line": 4,
+ "owner": "CfgCallbacks",
+ "span": [
+ 4,
+ 6,
+ 4,
+ 22
+ ],
+ "to": "cfg:CfgCallbacks#:exit:2:5:5"
+ },
{
"from": "cfg:CfgCallbacks#callbackBlock:entry:0:2:2",
"function": "callbackBlock",
@@ -172,6 +284,186 @@
}
],
"control_flow_nodes": [
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:entry:0:10:18",
+ "kind": "entry",
+ "line": 10,
+ "owner": "CfgCallbacks",
+ "role": "function_entry",
+ "source": "",
+ "span": [
+ 10,
+ 18,
+ 14,
+ 5
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:exit:2:14:5",
+ "kind": "exit",
+ "line": 14,
+ "owner": "CfgCallbacks",
+ "role": "function_exit",
+ "source": "",
+ "span": [
+ 10,
+ 18,
+ 14,
+ 5
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:stmt:0.callback.0:12:8",
+ "kind": "statement",
+ "line": 12,
+ "owner": "CfgCallbacks",
+ "role": "linear_statement",
+ "source": "this.audit(user)",
+ "span": [
+ 12,
+ 8,
+ 12,
+ 24
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:stmt:1:11:6",
+ "kind": "callback",
+ "line": 11,
+ "owner": "CfgCallbacks",
+ "role": "callback_region",
+ "source": "this.hook(() => { this.audit(user); })",
+ "span": [
+ 11,
+ 6,
+ 13,
+ 8
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:entry:0:11:16",
+ "kind": "entry",
+ "line": 11,
+ "owner": "CfgCallbacks",
+ "role": "function_entry",
+ "source": "",
+ "span": [
+ 11,
+ 16,
+ 13,
+ 7
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:exit:2:13:7",
+ "kind": "exit",
+ "line": 13,
+ "owner": "CfgCallbacks",
+ "role": "function_exit",
+ "source": "",
+ "span": [
+ 11,
+ 16,
+ 13,
+ 7
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:stmt:1:12:8",
+ "kind": "statement",
+ "line": 12,
+ "owner": "CfgCallbacks",
+ "role": "linear_statement",
+ "source": "this.audit(user)",
+ "span": [
+ 12,
+ 8,
+ 12,
+ 24
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:entry:0:19:18",
+ "kind": "entry",
+ "line": 19,
+ "owner": "CfgCallbacks",
+ "role": "function_entry",
+ "source": "",
+ "span": [
+ 19,
+ 18,
+ 19,
+ 26
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:exit:1:19:26",
+ "kind": "exit",
+ "line": 19,
+ "owner": "CfgCallbacks",
+ "role": "function_exit",
+ "source": "",
+ "span": [
+ 19,
+ 18,
+ 19,
+ 26
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:entry:0:3:18",
+ "kind": "entry",
+ "line": 3,
+ "owner": "CfgCallbacks",
+ "role": "function_entry",
+ "source": "",
+ "span": [
+ 3,
+ 18,
+ 5,
+ 5
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:exit:2:5:5",
+ "kind": "exit",
+ "line": 5,
+ "owner": "CfgCallbacks",
+ "role": "function_exit",
+ "source": "",
+ "span": [
+ 3,
+ 18,
+ 5,
+ 5
+ ]
+ },
+ {
+ "function": "",
+ "id": "cfg:CfgCallbacks#:stmt:1:4:6",
+ "kind": "statement",
+ "line": 4,
+ "owner": "CfgCallbacks",
+ "role": "linear_statement",
+ "source": "this.audit(user)",
+ "span": [
+ 4,
+ 6,
+ 4,
+ 22
+ ]
+ },
{
"function": "callbackBlock",
"id": "cfg:CfgCallbacks#callbackBlock:entry:0:2:2",
diff --git a/gems/fact-mine/src/architecture_test.rs b/gems/fact-mine/src/architecture_test.rs
index ddd7277bb..c0320e562 100644
--- a/gems/fact-mine/src/architecture_test.rs
+++ b/gems/fact-mine/src/architecture_test.rs
@@ -791,6 +791,65 @@ fn production_source(source: &str) -> String {
.join("\n")
}
+#[test]
+fn shared_analysis_does_not_own_concrete_language_policy() {
+ let checked = [
+ "ast.rs",
+ "ast/adapters/base.rs",
+ "incremental.rs",
+ "lsp_scip.rs",
+ "profile.rs",
+ "scip.rs",
+ "external_summary.rs",
+ "syntax/cfg/effects.rs",
+ "syntax/complexity_facts.rs",
+ "syntax/tree_sitter_adapter.rs",
+ "syntax/normalized_extractor.rs",
+ "syntax/local_flow.rs",
+ "syntax/protocols.rs",
+ ];
+ let languages = [
+ ("ruby", "Ruby"),
+ ("python", "Python"),
+ ("javascript", "JavaScript"),
+ ("typescript", "TypeScript"),
+ ("java", "Java"),
+ ("swift", "Swift"),
+ ("kotlin", "Kotlin"),
+ ("go", "Go"),
+ ("rust", "Rust"),
+ ("zig", "Zig"),
+ ("lua", "Lua"),
+ ("c", "C"),
+ ("cpp", "Cpp"),
+ ("csharp", "CSharp"),
+ ("php", "Php"),
+ ];
+ let mut offenders = Vec::new();
+ for relative in checked {
+ let path = crate_src().join(relative);
+ let source = production_source(&fs::read_to_string(&path).expect("read shared analysis"));
+ for (name, variant) in languages {
+ let mut tokens = vec![format!("\"{name}\""), format!("Language::{variant}")];
+ if name.len() > 1 {
+ tokens.push(format!("{name}_"));
+ tokens.push(format!("{name}::"));
+ }
+ for token in tokens {
+ if source.contains(&token) {
+ offenders.push(format!("{}: {}", path.display(), token));
+ }
+ }
+ }
+ }
+
+ assert!(
+ offenders.is_empty(),
+ "Shared analysis must call language-neutral adapter contracts; concrete language policy belongs in syntax/.rs or ast/adapters/.rs:\n{}",
+ offenders.join("\n")
+ );
+}
+
#[test]
fn generic_cfg_does_not_own_concrete_language_knowledge() {
let cfg_dir = crate_src().join("syntax/cfg");
diff --git a/gems/fact-mine/src/ast.rs b/gems/fact-mine/src/ast.rs
index 38519d2bd..d9f04c07c 100644
--- a/gems/fact-mine/src/ast.rs
+++ b/gems/fact-mine/src/ast.rs
@@ -216,6 +216,14 @@ pub(crate) fn preprocessor_callable_names(
adapters::normalization_adapter(language).preprocessor_callable_names(root, source)
}
+pub(crate) fn preprocessor_callable_definitions(
+ root: TreeSitterNode<'_>,
+ source: &str,
+ language: Language,
+) -> Vec<(String, String)> {
+ adapters::normalization_adapter(language).preprocessor_callable_definitions(root, source)
+}
+
pub fn node(child: &Child) -> Option<&Node> {
match child {
Child::Node(node) => Some(node),
@@ -223,12 +231,29 @@ pub fn node(child: &Child) -> Option<&Node> {
}
}
+pub(crate) fn reconcile_presence_correlation_spans(
+ root: tree_sitter::Node<'_>,
+ source: &str,
+ language: Language,
+ seeds: &mut Vec,
+) {
+ adapters::normalization_adapter(language)
+ .reconcile_presence_correlation_spans(root, source, seeds);
+}
+
pub fn slice(node: &Node, _lines: &[String]) -> String {
normalize_text(&node.text)
}
pub fn body_stmts(defn_node: &Node) -> Vec<&Node> {
- let scope_index = if defn_node.r#type == "DEFS" { 2 } else { 1 };
+ // A normalized function wraps its SCOPE at a type-specific child index: a
+ // singleton-method DEFS after its receiver/name, a LAMBDA directly, an
+ // ordinary DEFN after its name.
+ let scope_index = match defn_node.r#type.as_str() {
+ "DEFS" => 2,
+ "LAMBDA" => 0,
+ _ => 1,
+ };
let Some(scope) = defn_node.children.get(scope_index).and_then(node) else {
return Vec::new();
};
@@ -418,6 +443,7 @@ const STATEMENT_BLOCK_PARENT_KINDS: &[&str] = &[
"finally_clause",
"do_statement",
"lambda_expression",
+ "func_literal",
];
const EMPTY_BODY_WRAPPER_KINDS: &[&str] = &["body_statement", "block", "block_body", "statement"];
const HEREDOC_BODY_WRAPPER_KINDS: &[&str] = &["body_statement", "block_body", "statement", "then"];
diff --git a/gems/fact-mine/src/ast/adapters/base.rs b/gems/fact-mine/src/ast/adapters/base.rs
index 6cf8084cd..1c80c0cca 100644
--- a/gems/fact-mine/src/ast/adapters/base.rs
+++ b/gems/fact-mine/src/ast/adapters/base.rs
@@ -11,6 +11,7 @@ use super::super::{
LEADING_LOOP_WRAPPER_KINDS, LEADING_OWNER_WRAPPER_KINDS, LOOP_NODE_KINDS, OWNER_NODE_KINDS,
OWNER_STATEMENT_NESTED_KINDS, QUESTION_COLON_TERNARY_KINDS,
};
+use crate::syntax::nullable::PresenceCorrelationSeed;
use tree_sitter::Node as TreeSitterNode;
pub(crate) const COMMON_ASSIGNMENT_OPERATORS: &[&str] = &["=", "+=", "-=", "*=", "/=", "%="];
@@ -30,6 +31,16 @@ pub(crate) struct ConditionalBranchParts<'tree> {
use super::super::TreeSitterNormalizer;
pub(crate) trait AstNormalizationAdapter: Sync {
+ /// Reconcile normalized presence correlations with exact raw parser spans
+ /// when the native grammar exposes stronger source ownership.
+ fn reconcile_presence_correlation_spans(
+ &self,
+ _root: TreeSitterNode<'_>,
+ _source: &str,
+ _seeds: &mut Vec,
+ ) {
+ }
+
/// Language-native namespace and explicit-import facts used to form
/// canonical symbol identities. The empty default deliberately means
/// "not proven", rather than treating a filename or short owner as a
@@ -62,6 +73,25 @@ pub(crate) trait AstNormalizationAdapter: Sync {
Vec::new()
}
+ fn preprocessor_callable_definitions(
+ &self,
+ _root: TreeSitterNode<'_>,
+ _source: &str,
+ ) -> Vec<(String, String)> {
+ Vec::new()
+ }
+
+ fn variable_declarator_node(&self, node: TreeSitterNode<'_>) -> bool {
+ node.kind() == "variable_declarator"
+ }
+
+ fn variable_declarator_alternative<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ ) -> Option> {
+ None
+ }
+
/// Pre-parse source transformation, fed to tree-sitter's `parse()` call
/// only - never used for digests, snippets, or spans, which always read
/// the untouched original source. Defaults to a no-op; override only
@@ -104,7 +134,7 @@ pub(crate) trait AstNormalizationAdapter: Sync {
| "assignment_statement"
| "annotated_assignment"
),
- "variable_declarator" => kind == "variable_declarator",
+ "variable_declarator" => self.variable_declarator_node(node),
"super" => kind == "super",
"return_or_break" => matches!(
kind,
@@ -274,6 +304,18 @@ pub(crate) trait AstNormalizationAdapter: Sync {
node
}
+ /// Start/end nodes for a callable whose declaration and executable body
+ /// are split by grammar recovery. Unlike `function_declaration_node`,
+ /// this preserves the exact union span without swallowing neighboring
+ /// declarations from a broad recovery wrapper.
+ fn function_declaration_span_nodes<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ _source: &str,
+ ) -> Option<(TreeSitterNode<'tree>, TreeSitterNode<'tree>)> {
+ None
+ }
+
/// Tree-sitter error recovery can occasionally label a malformed region
/// as a function definition. Adapters with syntax that makes a reliable
/// declaration check possible may reject that recovery node here.
@@ -320,6 +362,18 @@ pub(crate) trait AstNormalizationAdapter: Sync {
None
}
+ /// Some languages allow a statement before a switch/case value (for
+ /// example Go's `switch value := next(); value.Kind()`). Preserve it as
+ /// executable work instead of treating only the trailing case value as
+ /// the condition.
+ fn case_initializer<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ _source: &str,
+ ) -> Option> {
+ None
+ }
+
fn conditional_keyword_node_type(&self, keyword: &str) -> Option<&'static str> {
match keyword {
"if" => Some("IF"),
@@ -386,6 +440,10 @@ pub(crate) trait AstNormalizationAdapter: Sync {
None
}
+ fn case_arm_guard<'tree>(&self, _node: TreeSitterNode<'tree>) -> Option> {
+ None
+ }
+
fn case_else_node<'tree>(
&self,
node: TreeSitterNode<'tree>,
@@ -443,6 +501,41 @@ pub(crate) trait AstNormalizationAdapter: Sync {
None
}
+ /// Executable nodes that precede the grammar's ordinary function body.
+ /// This is used for source constructs such as C# constructor delegation,
+ /// which tree-sitter stores beside (rather than inside) the body block.
+ fn function_body_prefix_nodes<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ _source: &str,
+ ) -> Vec> {
+ Vec::new()
+ }
+
+ /// Split a path-qualified call callee (`Cell::new`, `Foo::bar`) into its
+ /// receiver node and method name, so the normalized call carries the real
+ /// method as its message instead of a generic `call` placeholder. Returns
+ /// None for languages/nodes that are not scope-path callees.
+ fn scoped_call_parts<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ _source: &str,
+ ) -> Option<(TreeSitterNode<'tree>, String)> {
+ None
+ }
+
+ /// Unwrap a callee that carries explicit type arguments (`parse::`,
+ /// `collect::>`) to the callee itself. The type arguments are a
+ /// parametric annotation, never a message: leaving the wrapper in place
+ /// makes the normalizer read `` as the method name, which loses the
+ /// real call and leaves a callee no indexer can resolve.
+ fn type_argument_callee<'tree>(
+ &self,
+ _node: TreeSitterNode<'tree>,
+ ) -> Option