Skip to content

Code Querying

Bifrost’s composable code-query engine is query_code. The single supported schema version is 1; it carries the complete query vocabulary, including host-registered retained production taint findings. It answers questions such as “find calls to this callee,” “which exact control edge leaves this entry?”, “does this registered resource protocol reach an error transition?”, and “which retained production taint findings belong to this procedure?” across the active workspace.

The CFG surface remains deliberately procedure-local. A narrow registered typestate adapter, declaration-bounded containment, and registered value flow are part of the same vocabulary. The taint step resolves an exact procedure within an immutable retained production result and invokes only the existing public projector. It never loads or compiles a policy, runs propagation, reconstructs witnesses, or performs policy classification.

Use the narrowest tool that directly answers the question:

QuestionToolWhy
“Where is Parser.parse declared?”search_symbolsSearches indexed declarations by name.
“Who references this exact symbol?”scan_usages_by_reference or scan_usages_by_locationResolves a known declaration to reference sites from a symbol or source location.
“What is the workspace caller/callee graph?”usage_graphReturns the existing whole-workspace resolved usage graph.
“Which code has this shape, enclosing declaration, import/type relationship, or procedure-local control-flow relationship?”query_codeMatches normalized kinds and applies typed structural and semantic steps.
“Where does this literal text occur?”search_file_contentsSearches source text without structural interpretation.

Start with search_symbols or the mode-appropriate scan-usages tool when you already know the symbol. Use query_code when the shape matters more than symbol identity. A useful workflow is to capture structural candidates with query_code, then pass their locations or enclosing symbols to exact navigation tools.

Language adapters map grammar-specific tree-sitter nodes and fields into Rune IR, Bifrost’s normalized source-side representation. The matcher evaluates typed CodeQuery queries against those facts rather than against raw grammar node names.

See Rune IR for the representation, .rune files and VS Code previews, query-by-example workflow, limits, and the complete per-language adapter mapping.

Typed Pipelines and Declaration-Bounded Containment

Section titled “Typed Pipelines and Declaration-Bounded Containment”

query_code validates the structural seed query, lowers it to a shared logical dependency graph, selects physical operators, and then applies an ordered typed pipeline. Queries without steps return tagged structural matches. Complete compatible pipelines can be combined with union, intersect, and except, then passed through another common typed suffix. enclosing_decl returns exact indexed declarations; procedure_of enters the source-backed semantic domain; cfg_* traverses procedure-local boundaries and edges; typestate consumes one registered solver capability; and witness projects its retained evidence. Derived results retain seed-and-edge provenance, including the contributing branch path after composition.

Semantic declaration steps intentionally stop at the analyzer’s indexed declaration boundary. Seeing a reference or usage into a dependency is not evidence that the dependency declaration is indexed. Until Bifrost can target library code for indexing, unindexed library declarations are omitted rather than reconstructed from names, and their absence is not reported as a capability error.

RQL wrapperJSON stepInput → outputUse it to
enclosing-declenclosing_declstructural match → indexed declarationFind the smallest real declaration that contains a matching expression.
procedure-ofprocedure_ofstructural match or declaration → procedureResolve the unique smallest executable procedure enclosing the exact input range.
cfg-entrycfg_entryprocedure → program pointReturn the validated entry boundary.
cfg-exitscfg_exitsprocedure → program pointReturn normal then exceptional exits.
cfg-successor-edgescfg_successor_edgesprogram point → control edgeReturn one-hop outgoing edges.
cfg-predecessor-edgescfg_predecessor_edgesprogram point → control edgeReturn one-hop incoming edges.
cfg-edge-sourcecfg_edge_sourcecontrol edge → program pointProject an edge to its source.
cfg-edge-targetcfg_edge_targetcontrol edge → program pointProject an edge to its target.
typestatetypestateprocedure → typestate findingResolve protocol_ref against the host snapshot and run the bounded existing typestate client once.
witnesswitnesstypestate finding → typestate witnessProject retained source-backed steps, optionally reducing them with max_steps and max_bytes.
references-ofreferences_ofdeclaration → reference siteReturn exact structured sites targeting a declaration.
used-byused_bydeclaration → declarationReturn each smallest exact semantic user, with its proving site under via.
usesusesdeclaration → declarationReturn exact indexed targets used by one semantic declaration, with via.
callerscallersdeclaration → declarationFollow incoming calls, direct by default or through a positive depth.
calleescalleesdeclaration → declarationFollow outgoing calls, direct by default or through a positive depth.
call-sites-tocall_sites_todeclaration → call siteReturn incoming call sites with caller, callee, proof, receiver, and bound arguments.
call-sites-fromcall_sites_fromdeclaration → call siteReturn call sites lexically owned by the declaration.
call-inputcall_inputcall site → expression siteSelect receiver: true, a zero-based parameter_index, or parameter_name.
receiver-targetsreceiver_targetsstructural match, reference site, call site, or expression site → receiver analysisAnalyze the receiver extracted from a call/member site or an exact receiver expression.
points-topoints_tostructural match, reference site, or expression site → receiver analysisReturn bounded value/allocation/factory provenance for an expression.
member-targetsmember_targetsstructural match or reference site → receiver analysisReturn exact member declarations selected through the receiver candidates.
occurrencesoccurrences(source) → occurrenceSeed classified identifier occurrences straight from workspace facts, filtered by class, role, and namespace.
occurrences-inoccurrences_instructural match or file → occurrenceReturn the occurrences lexically inside a matching node or a file.
occurrences-ofoccurrences_ofdeclaration → occurrenceReturn the declaration’s own name occurrence plus every reference-class occurrence resolving to it.
occurrence-targetoccurrence_targetoccurrence → declarationWalk a reference-class occurrence back to what it resolved to.
scopesscopes(source) → lexical scopeSeed lexical scope rows straight from workspace facts, filtered by kind.
bindingsbindings(source) → bindingSeed lexical binding rows straight from workspace facts, filtered by kind, name, and hoisting.
scope-ofscope_ofbinding, occurrence, or structural match → lexical scopeReturn the innermost lexical scope that owns the input.
scope-ancestorsscope_ancestorslexical scope → lexical scopeWalk outward through the enclosing scopes, excluding the scope itself.
bindings-inbindings_inlexical scope or structural match → bindingReturn the bindings declared in the scope, or whose binder token lies inside the match.
binding-ofbinding_ofoccurrence → bindingReturn the binding of the occurrence’s name in effect at its exact position.
binding-occurrencebinding_occurrencebinding → occurrenceWalk back to the binder-class occurrence of the binding’s declaring token.
candidates-ofcandidates_ofoccurrence → resolution candidateReturn the candidates the resolver considered, with tier, outcome, and boundary.
candidate-targetcandidate_targetresolution candidate → declarationProject unit-backed candidates to declarations; partial by construction.
edges-ofedges_ofdeclaration → reference edgeReturn the canonical inverse edges: every usage site the usage index enumerates for the declaration.
edges-fromedges_fromoccurrence → reference edgeReturn the canonical forward edges: the resolver’s own resolved targets for that exact token.
edge-targetedge_targetreference edge → declarationMove from an edge to its exact indexed target declaration.
state-events-ofstate_events_ofprocedure or declaration → state eventDerive the establishment, kill, and read events of bindings and properties from the production CFG.
flow-relations-offlow_relations_ofstate event or procedure → flow relationRelate those events: reaching-definition, dominance, and same-evaluation, each with exact or may certainty.
flow-sourceflow_sourceflow relation → state eventProject a relation to its establishment or kill end.
flow-targetflow_targetflow relation → state eventProject a relation to its read end.
rewrite-paths-ofrewrite_paths_offile → rewrite pathEnumerate the bounded rewrite chases the file engages in a declared finite rewrite domain, with their steps, bound, and terminal outcome.
file-offile_ofstructural match or semantic source value → fileMove from code, a declaration, reference, call, input expression, or receiver analysis to its project file.
imports-ofimports_offile → fileFollow one resolved direct project-local import.
importers-ofimporters_offile → fileFind every project file with a resolved direct import of that file.

For example, (importers-of (file-of (function :name "target"))) answers “which project files directly import the file declaring target?” It is deliberately a file relationship: it does not prove that an importer uses that particular declaration, resolve an out-of-scope library’s members, or manufacture external declarations. The references-of, used-by, and uses steps provide that exact declaration relationship separately, and references-of can compose through file-of when both symbol and import-file provenance matter. See Typed Set Composition for executable union, intersection, and subtraction over import traversal, and Reference Traversal for exact declaration edges. For bounded receiver values and members, see the executable Receiver Traversal cookbook.

For CFG inspection, (cfg-edge-target (cfg-successor-edges (cfg-entry (procedure-of (function :name "run"))))) returns the target point of every edge leaving run’s entry. Procedure, point, and edge rows carry checkout-independent content-scoped IDs, exact ranges, proof/completeness, and ordinary CodeQuery provenance. Each edge step is one hop and shares a separate finite semantic file/source/row/retained-byte/traversal budget. Explain mode shows the requested semantic facets without materialization; profile mode attributes actual semantic work to the physical pipeline steps.

For registered typestate, (witness :max-steps 32 :max-bytes 16384 (typestate :protocol-ref "embedding:resource-lifecycle" (procedure-of (function :name "lifecycle")))) returns only the bounded witnesses retained by the same solver run. Findings and witnesses carry stable protocol/binding hashes, canonical subjects, certainty, proof/completeness, uncertainty, exact ranges, and omission metadata, but no severity or policy presentation. The host must pre-register the alias against the current workspace generation and exact procedure root; otherwise results/profile mode returns a typed incomplete diagnostic. Explain mode can still plan the query without resolving or running the registration.

For typed occurrences, (occurrences :role binder (language "rust")) is not the spelling: occurrences is a source, so it is wrapped rather than wrapping, as in (language "rust" (occurrences :role binder)). Each row says what the parser thinks one identifier token is at one exact position, with the resolved target for reference-class rows. Its ast_id is the content-scoped identity of the underlying AST node, equal to the ast_id a full-detail structural capture over the same node reports, so captures and occurrences join on one opaque string instead of on coincident ranges or spellings. Occurrence support is declared per language and per role; a query naming a role an adapter does not classify is reported incomplete rather than answered with zero rows.

For the lexical environment, (scope-of (binding-of (occurrences :role receiver_position))) answers the question the whole family exists for: which binding of this name is actually in effect at this position, and where was it declared? The binding-of answer is computed from activation intervals and scope ancestry, never from source-order co-presence, so a rebinding, a shadowing outer name and a read before a declaration all give different answers. scopes and bindings are sources like occurrences, so they are wrapped rather than wrapping.

For resolution candidates, (candidates-of :outcome selected (occurrences :class reference)) lists what the resolver considered for each reference, with a precedence tier, an outcome, and a boundary status. Three things are deliberately not inferable from an empty answer: a candidate with no tier is unattributed rather than weakest (the :tier unattributed filter selects those rows); a trace whose trace_completeness is selection_only says nothing by omitting a rejection; and candidate-target answers only for unit-backed candidates, because a lexical binding and an external route carry no workspace declaration at all. Each of the three reports an incomplete diagnostic rather than a clean empty result where it matters.

For canonical reference edges, (edges-of :usage [reference] (function "register")) and (edges-from (occurrences :class reference)) state the same kind of fact from opposite ends, in one row shape, so the two derivations can be compared rather than merely coexisting. Every classification a comparison depends on — the reference kind, the proof tier, the usage kind, the site class, the owner relation, the derivation direction and the workspace generation — is an explicit field, never inferred from which step produced the set or from how many rows came back. :surface is optional with no default, because the complete edge answer includes editor-only rows. Only Java, Rust, Python, JavaScript and TypeScript answer the forward projection today; edges-from in any other language reports edge_axis_unsupported rather than an empty answer.

For flow-sensitive state, (flow-relations-of :relation [reaching] :certainty [exact] (state-events-of (procedure-of (function :name "handler")))) answers “has an assignment to this binding actually executed before this read?”. The production control-flow graph is the only evidence source: source order, file co-presence and structural containment are never presented as a reaching-definition, a dominance, or a same-evaluation relation. A write that appears one line above a read but that no CFG path carries there produces no reaching row. exact means the establishment is the only definition of that subject in the read’s IN set and dominates the read’s program point; anything else is may. Where the lowering does not model an axis — a language without CFG lowering, a field access with no binding-rooted base, a local the adapter declares but never establishes — the rows say completeness: partial, name the uncovered axes, and the response carries flow_state_axis_unsupported or flow_state_derivation_incomplete. An empty answer is never silently a complete one, and an exhausted control-flow budget emits no rows rather than a shortened relation set.

For bounded termination, (rewrite-paths-of :outcome [cycle] (file-of (function :name "use_alias"))) answers “does this file make a supported rewrite chase loop?”. A rewrite domain is a declared finite state space with a rewrite rule; the row states the semantic state key of every step, the bound the domain declared for itself, and how the chase ended. The three outcomes are separate claims and are never blurred: converged names the fixed point, cycle carries the ordered repeated-state witness that closes the loop, and exceeded-budget names only the work performed. Budget exhaustion is absence of evidence — it is never reported as a cycle, and never as a clean convergence.

The engine has one semantic query model: CodeQuery. The query_code operation executes that model; it is not the name of a second query language. Different input forms must become a validated CodeQuery before planning and execution.

The terminology describes three layers:

FormPrefer it whenTrade-off and execution path
Rune Query Language (RQL)A person is exploring, reviewing, or maintaining a query. Its compact S-expression nesting, comments, multiline input, and REPL/editor assistance favor hand authoring.RQL is an experimental authoring syntax. It lowers to the canonical JSON shape and then validates as CodeQuery. MCP cannot accept raw RQL inline, but query_file can name a saved workspace .rql file.
JSON CodeQueryAn agent, script, Python client, or other protocol client needs an explicit, schema-versioned payload.JSON is more verbose to write by hand, but it is the stable machine-facing serialization. MCP accepts its fields inline through query_code, and query_file can name a saved .json query.
Rust CodeQuery valueAn in-process Rust embedding already owns query construction and execution.The caller can parse or construct the typed model and use the lower-level execution API without a wire-format round trip. Deployed protocol-style behavior goes through SearchToolsService; protocol clients use JSON.

RQL and JSON are frontends to the same typed model, planner, executor, result types, budgets, and completeness rules. Choosing one does not select a more powerful matcher or a different analysis. Use RQL for human authoring, JSON for machine integration, and the Rust value only for in-process embedding.

See JSON CodeQuery for the complete schema, validation rules, result model, and copy-paste examples. See Rune Query Language for interactive authoring and canonical JSON inspection, and MCP query and RQL availability for the exact inline and saved file surfaces. Use Explain and Profile CodeQuery to inspect logical sharing and physical selection before execution or collect opt-in operator, cache, budget, wait, and concurrency observations from one execution.

For source-first walkthroughs, see the per-language query_code tutorials. Their fixtures, RQL and JSON forms, and exact results are exercised against the real structural adapters. To call query_code from an embedding application rather than from MCP or the REPL, see Library Integration: it runs one canonical query through both SearchToolsService::query_code_result(...) and SearchToolsClient.query_code(...) and shows how each caller reads diagnostics, truncated, provenance completeness, and receiver outcome before making a completeness-sensitive claim.

The examples below use one-shot CLI mode. They were validated against a toy workspace containing the small per-language shapes on the Rune IR adapter-mapping page, with one file for each supported language. The JSON reference contains the complete, test-parsed input examples.

For a reusable query, save the complete RQL or canonical JSON query under the workspace and run it directly:

Terminal window
bifrost --query-file queries/audit.rql
bifrost --root ./code-query-toy --query-file queries/audit.json

The current directory is the default workspace root. Query files must stay within that workspace after symlinks resolve. --query-file selects the complete query and does not merge command-line filters or inline JSON.

Find calls to audit across every structural adapter:

Terminal window
bifrost --root ./code-query-toy --tool query_code --args '{"match":{"kind":"call","callee":{"name":"audit"}},"limit":20}'

The result contains one call match for each current analyzable language and no diagnostics. Representative rows look like:

{"result_type":"structural_match","language":"python","path":"python/app.py","kind":"call","text":"audit(code)"}
{"result_type":"structural_match","language":"typescript","path":"typescript/app.ts","kind":"call","text":"audit(code)"}
{"result_type":"structural_match","language":"ruby","path":"ruby/app.rb","kind":"call","text":"audit(code)"}

Find assignments to password whose right-hand side is a string literal, and capture the value:

Terminal window
bifrost --root ./code-query-toy --tool query_code --args '{"match":{"kind":"assignment","left":{"name":"password"},"right":{"kind":"string_literal","capture":"value"}},"limit":20}'

The result contains one assignment match per language. The captured value is "hunter2" in each match, even though the source syntax varies:

{"result_type":"structural_match","language":"java","text":"password = \"hunter2\"","captures":[{"name":"value","text":"\"hunter2\""}]}
{"result_type":"structural_match","language":"php","text":"$password = \"hunter2\"","captures":[{"name":"value","text":"\"hunter2\""}]}
{"result_type":"structural_match","language":"rust","text":"let password = \"hunter2\";","captures":[{"name":"value","text":"\"hunter2\""}]}

Limit a query to one adapter while debugging a mapping:

Terminal window
bifrost --root ./code-query-toy --tool query_code --args '{"languages":["typescript"],"match":{"kind":"call","callee":{"name":"audit"},"args":[{"capture":"argument"}]},"result_detail":"full"}'

This searches only TypeScript files and returns the matched call plus deterministic byte and line ranges because result_detail is full.

Use RQL when you are exploring a repository interactively:

Terminal window
bifrost --root /path/to/project --repl

Use JSON CodeQuery when a host, script, or MCP client needs a stable machine-facing payload for the query_code tool.