mirror of
https://github.com/nim-lang/Nim.git
synced 2026-07-12 04:09:34 +00:00
280 lines
12 KiB
Nim
280 lines
12 KiB
Nim
#
|
|
#
|
|
# The Nim Compiler
|
|
# (c) Copyright 2026 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## NIF-based goto-definition / find-all-usages for `nim track`.
|
|
##
|
|
## This is the mainline-Nim port of nimony's `idetools.nim`. It answers a
|
|
## `--def:FILE,LINE,COL` / `--usages:FILE,LINE,COL` query by *scanning the
|
|
## `.s.bif` files* (binary NIF, see `dist/nimony/src/lib/bif.nim`) that the
|
|
## preceding `nim ic` frontend (`nim track`) emitted into the nimcache directory
|
|
## — NOT by re-running sem. NIF distinguishes a definition (`SymbolDef` token) from a use
|
|
## (`Symbol` token) syntactically, so goto-def / find-uses become plain token
|
|
## scans over type-checked NIF, which is more reliable than the classic PSym
|
|
## engine because generics and macros are type-checked in the NIF too.
|
|
##
|
|
## Two passes (mirroring nimony's `usages`):
|
|
## 1. Load the queried module's `.s.bif` and find the `Symbol`/`SymbolDef`
|
|
## token whose line info + identifier length contains `conf.m.trackPos`.
|
|
## That yields the mangled symbol NAME and whether it is global (>= 2 dots).
|
|
## 2. `--usages`: emit every `Symbol` (use) token; `--def`: every `SymbolDef`.
|
|
## A global symbol is scanned across every module `.s.bif`; a local one only
|
|
## within the queried module.
|
|
##
|
|
## IMPORTANT porting note: `bif.load` mints FRESH per-file pools, so a `SymId`
|
|
## from module A's buffer is meaningless in module B's. The cross-module match is
|
|
## therefore by the mangled NAME string, never by `SymId` (nimony can compare ids
|
|
## because it parses every text NIF into one shared global pool; we cannot).
|
|
|
|
import std / [os, strutils, sets]
|
|
import options, msgs, pathutils
|
|
import lineinfos as astli
|
|
import ast2nif # toNifFilename
|
|
from deps import includerSbifs # deps-guided include-file lookup
|
|
import "../dist/nimony/src/lib/nifcore"
|
|
from "../dist/nimony/src/lib" / bif import load, BifModule, containsSym
|
|
|
|
proc identLen(name: string): int =
|
|
## Length of the displayed identifier: the run before the first `.` of a
|
|
## mangled NIF name (`ident.disamb[.moduleSuffix]`). Bounds the column match.
|
|
let d = name.find('.')
|
|
result = if d < 0: name.len else: d
|
|
|
|
proc isGlobalName(name: string): bool =
|
|
## A global symbol carries `ident.disamb.moduleSuffix` (>= 2 dots); a local at
|
|
## most `ident.disamb` (<= 1 dot). `moduleSuffix` is a dot-free hash, so a raw
|
|
## dot count is equivalent to nifbuilder's suffix-compressed test for our use.
|
|
var dots = 0
|
|
for i in 1 ..< name.len:
|
|
if name[i] == '.': inc dots
|
|
result = dots >= 2
|
|
|
|
proc posMatch(c: Cursor; conf: ConfigRef; target: TLineInfo; tokenLen: int): bool =
|
|
## True when `target` (the queried position) falls within the identifier span
|
|
## of the Symbol/SymbolDef token at `c`. Mirrors nimony's `lineInfoMatch`; the
|
|
## filename is resolved through the loaded buffer's own pool (fresh per file),
|
|
## then mapped to a `FileIndex` exactly like `ast2nif.oldLineInfo`.
|
|
let li = rawLineInfo(c)
|
|
if not li.isValid: return false
|
|
if li.line.int != target.line.int: return false
|
|
let f = fileInfoIdx(conf, AbsoluteFile lineInfoFile(c))
|
|
if f != target.fileIndex: return false
|
|
if target.col.int < li.col.int: return false
|
|
if target.col.int > li.col.int + tokenLen: return false
|
|
result = true
|
|
|
|
const sep = '\t'
|
|
|
|
proc formatSuggest(s: Suggest): string =
|
|
## Reproduce `suggest.$Suggest` for the `ideDef`/`ideUse` sections without
|
|
## importing `suggest` (which would create an import cycle). Layout:
|
|
## `section⭾symkind⭾qualifiedPath⭾forth⭾filePath⭾line⭾column⭾⭾quality`.
|
|
## symkind is always `skUnknown` here — the raw NIF scan has no PSym to give a
|
|
## real kind (like nimony's `foundSymbol`, which leaves it empty).
|
|
result = $s.section
|
|
result.add sep
|
|
result.add "skUnknown"
|
|
result.add sep
|
|
if s.qualifiedPath.len != 0:
|
|
result.add s.qualifiedPath.join(".")
|
|
result.add sep
|
|
result.add s.forth
|
|
result.add sep
|
|
result.add s.filePath
|
|
result.add sep
|
|
result.add $s.line
|
|
result.add sep
|
|
result.add $s.column
|
|
result.add sep # empty doc field (docgen is off outside nimsuggest)
|
|
if s.version == 0 or s.version == 3:
|
|
result.add sep
|
|
result.add $s.quality
|
|
|
|
proc emit(conf: ConfigRef; c: Cursor; section: IdeCmd; name: string;
|
|
seen: var HashSet[string]) =
|
|
## Report one hit as a nimsuggest-compatible result (routed through the
|
|
## structured-output hook / `--stdout`). We only have the mangled name + line
|
|
## info from the raw NIF, so symkind/type are left empty — like nimony's
|
|
## `foundSymbol`. `seen` deduplicates: the same source location can back
|
|
## several NIF `Symbol` tokens (e.g. a call argument re-emitted in a lowered
|
|
## form), which must surface as one hit.
|
|
let li = rawLineInfo(c)
|
|
if not li.isValid: return
|
|
let key = $section.int & ":" & lineInfoFile(c) & ":" & $li.line.int & ":" & $li.col.int
|
|
if seen.containsOrIncl(key):
|
|
return # already reported this location for this section
|
|
let s = Suggest(section: section,
|
|
qualifiedPath: @[name[0 ..< identLen(name)]],
|
|
filePath: lineInfoFile(c),
|
|
line: li.line.int,
|
|
column: li.col.int,
|
|
tokenLen: identLen(name),
|
|
forth: "",
|
|
symkind: 0'u8,
|
|
quality: 100,
|
|
version: conf.suggestVersion)
|
|
if conf.suggestionResultHook != nil:
|
|
conf.suggestionResultHook(s)
|
|
else:
|
|
conf.suggestWriteln(formatSuggest(s))
|
|
|
|
proc tokenSymId(c: Cursor): SymId {.inline.} =
|
|
## SymId (in the cursor's own per-file pool) of a `Symbol`/`SymbolDef` token,
|
|
## or `SymId(0)` for an inline-encoded one — which is never our search target:
|
|
## a mangled name (`ident.disamb.suffix`) is always longer than
|
|
## `StrInlineMaxLen`, so every occurrence of the symbol we look for is stored by
|
|
## pool id, decoded here with a shift and no string materialization.
|
|
if isInlineLit(c): SymId(0) else: SymId(combinedPayload(c) shr 1)
|
|
|
|
template symMatches(c: Cursor): bool =
|
|
## True when the token at `c` is the searched symbol. The fast path is a pure
|
|
## integer compare against `targetSym` (the symbol's id in THIS module's pool,
|
|
## resolved once per file by the caller). `targetSym == 0` means the name is not
|
|
## representable as a pool id (a rare <=3-byte local): fall back to a string
|
|
## compare, correct for both inline and pooled encodings.
|
|
(if targetSym != SymId(0): tokenSymId(c) == targetSym else: symName(c) == targetName)
|
|
|
|
proc scanUses(conf: ConfigRef; m: var BifModule; targetSym: SymId; targetName: string;
|
|
seen: var HashSet[string]) =
|
|
## `--usages`: report every `Symbol` (use) occurrence with valid line info.
|
|
if m.buf.len == 0: return
|
|
var c = m.buf.beginRead()
|
|
while c.hasMore:
|
|
if c.kind == Symbol and symMatches(c) and rawLineInfo(c).isValid:
|
|
emit(conf, c, ideUse, targetName, seen)
|
|
inc c
|
|
c.endRead()
|
|
|
|
proc scanDef(conf: ConfigRef; m: var BifModule; targetSym: SymId; targetName: string;
|
|
seen: var HashSet[string]) =
|
|
## `--def`: report the declaration of the target symbol if this module owns it
|
|
## (has its `SymbolDef`). The `SymbolDef` token itself carries no line info; the
|
|
## declaration location lives on the *enclosing tag* (e.g. `(sd @file:line:col`,
|
|
## like `bif.buildIndex`'s `mostRecentTagPos`). When that tag has no line info
|
|
## either, fall back to the declaration-site `Symbol` occurrence — but only in
|
|
## the owning module, so a plain user of the symbol is never reported as a def.
|
|
if m.buf.len == 0: return
|
|
var c = m.buf.beginRead()
|
|
var mostRecentTagPos = 0
|
|
var sawDef = false
|
|
var emitted = false
|
|
var fallbackPos = -1
|
|
while c.hasMore:
|
|
case c.kind
|
|
of TagLit:
|
|
mostRecentTagPos = cursorToPosition(m.buf, c)
|
|
inc c
|
|
of SymbolDef:
|
|
if symMatches(c):
|
|
sawDef = true
|
|
var tc = cursorAt(m.buf, mostRecentTagPos)
|
|
if rawLineInfo(tc).isValid:
|
|
emit(conf, tc, ideDef, targetName, seen)
|
|
emitted = true
|
|
tc.endRead()
|
|
inc c
|
|
of Symbol:
|
|
if fallbackPos < 0 and symMatches(c) and rawLineInfo(c).isValid:
|
|
fallbackPos = cursorToPosition(m.buf, c)
|
|
inc c
|
|
else:
|
|
inc c
|
|
c.endRead()
|
|
if sawDef and not emitted and fallbackPos >= 0:
|
|
var fc = cursorAt(m.buf, fallbackPos)
|
|
emit(conf, fc, ideDef, targetName, seen)
|
|
fc.endRead()
|
|
|
|
proc scanBuf(conf: ConfigRef; m: var BifModule; section: IdeCmd;
|
|
targetSym: SymId; targetName: string; seen: var HashSet[string]) =
|
|
## Emit hits for the target symbol in `m` per the query kind. `ideDus`
|
|
## (`--defusages`) reports both the definition and every usage.
|
|
if section in {ideDef, ideDus}:
|
|
scanDef(conf, m, targetSym, targetName, seen)
|
|
if section in {ideUse, ideDus}:
|
|
scanUses(conf, m, targetSym, targetName, seen)
|
|
|
|
proc findPos(conf: ConfigRef; m: var BifModule; target: TLineInfo;
|
|
foundName: var string): bool =
|
|
## Scan `m` for the `Symbol`/`SymbolDef` token covering the queried position
|
|
## `target` and set `foundName` to its mangled name. Returns true on a hit.
|
|
if m.buf.len == 0: return false
|
|
var c = m.buf.beginRead()
|
|
result = false
|
|
while c.hasMore:
|
|
let k = c.kind
|
|
if k == Symbol or k == SymbolDef:
|
|
let nm = symName(c)
|
|
if posMatch(c, conf, target, identLen(nm)):
|
|
foundName = nm
|
|
result = true
|
|
break
|
|
inc c
|
|
c.endRead()
|
|
|
|
proc runIdeQuery*(conf: ConfigRef) =
|
|
## Entry point: called from `main.nim` after `commandCheck` when a
|
|
## `--def`/`--usages` query is active. Assumes the check just emitted the
|
|
## project's `.s.bif` files into `getNimcacheDir(conf)`.
|
|
let section = conf.ideCmd
|
|
if section notin {ideDef, ideUse, ideDus}: return
|
|
let target = conf.m.trackPos
|
|
if target.fileIndex.int32 < 0: return
|
|
|
|
# Pass 1: position -> symbol. Try the queried file's own module bif first (the
|
|
# fast path when the position is inside a real module). An include file has no
|
|
# module bif of its own — its tokens live in the *including* module's bif with
|
|
# include-file line info — so when the direct lookup misses, consult the
|
|
# `.deps.nif` preludes (`includerSbifs`) to load only the module(s) that
|
|
# include the queried file (directly or transitively), never every bif in the
|
|
# nimcache. `ownerFile` is the bif that owns the hit.
|
|
let modFile = toNifFilename(conf, target.fileIndex)
|
|
var foundName = ""
|
|
var ownerFile = ""
|
|
if fileExists(modFile):
|
|
var qm = load(modFile)
|
|
if findPos(conf, qm, target, foundName):
|
|
ownerFile = modFile
|
|
if foundName.len == 0:
|
|
for cand in includerSbifs(conf, toFullPath(conf, target.fileIndex).AbsoluteFile):
|
|
if cand == modFile: continue
|
|
var m = load(cand)
|
|
if findPos(conf, m, target, foundName):
|
|
ownerFile = cand
|
|
break
|
|
if foundName.len == 0: return
|
|
|
|
# Pass 2: emit definition / usages. `seen` spans every module so a location is
|
|
# reported once even when scanned across the whole nimcache.
|
|
#
|
|
# Cross-file matching is by SymId, not by decoding every token's name. Two
|
|
# filters keep it cheap:
|
|
# 1. `bif.containsSym` — a sym-table-only probe that reads just the small
|
|
# trailing pools, NOT the token block or any `BiTable`. A module that never
|
|
# references the symbol is rejected here without a full `load` (no pools
|
|
# built, no token block mapped) — so a query whose symbol lives in a few
|
|
# modules no longer pays to load the whole nimcache.
|
|
# 2. For a module that does contain it, `bif.load` mints a fresh per-file pool,
|
|
# so the name is resolved to THIS file's SymId once via `getKeyId`; the scan
|
|
# then compares integer ids per token instead of materializing a string for
|
|
# each (see `symMatches`).
|
|
var seen = initHashSet[string]()
|
|
if isGlobalName(foundName):
|
|
for f in walkFiles((getNimcacheDir(conf).string) / "*.s.bif"):
|
|
if not containsSym(f, foundName): continue
|
|
var m = load(f)
|
|
let tid = m.buf.pool.syms.getKeyId(foundName)
|
|
if tid != SymId(0):
|
|
scanBuf(conf, m, section, tid, foundName, seen)
|
|
else:
|
|
# Local symbol: its mangled name is not unique across modules, so restrict
|
|
# the scan to the module it lives in (the one that owns the queried position).
|
|
var qm = load(ownerFile)
|
|
let tid = qm.buf.pool.syms.getKeyId(foundName)
|
|
scanBuf(conf, qm, section, tid, foundName, seen)
|