use IC for nimsuggest; wip

This commit is contained in:
Araq
2026-06-16 00:36:01 +02:00
parent f5d9e7a207
commit 9cfba6c0fb
18 changed files with 350 additions and 80 deletions

View File

@@ -475,6 +475,8 @@ proc comment*(n: PNode): string =
else:
result = ""
nodeCommentReader = proc(n: PNode): string {.nimcall.} = comment(n)
proc `comment=`*(n: PNode, a: string) =
let id = n.nodeId
if a.len > 0:
@@ -490,6 +492,8 @@ proc `comment=`*(n: PNode, a: string) =
n.flags.excl nfHasComment
gconfig.comments.del(id)
nodeCommentWriter = proc(n: PNode; s: string) {.nimcall.} = n.comment = s
# BUGFIX: a module is overloadable so that a proc can have the
# same name as an imported module. This is necessary because of
# the poor naming choices in the standard library.

View File

@@ -11,7 +11,7 @@
import std / [assertions, tables, sets]
from std / strutils import startsWith, endsWith, contains
from std / os import fileExists
from std / os import fileExists, dirExists, walkFiles
from std / syncio import readFile
from std / algorithm import sort
import "../dist/checksums/src/checksums" / sha1
@@ -131,6 +131,20 @@ proc nifLineInfo(w: var LineInfoWriter; info: TLineInfo): PackedLineInfo =
# Must use pool.man since toString uses pool.man to unpack
result = pack(pool.man, fid, info.line.int32, info.col)
proc nifLineInfoWithComment(w: var LineInfoWriter; info: TLineInfo; doc: string): PackedLineInfo =
## Like `nifLineInfo` but also attaches `doc` as a NIF `#…#` comment on the
## token. Used to carry `##` doc comments, which the AST serialization itself
## drops, across a NIF round-trip (the loader reads it back off the info).
if doc.len == 0:
result = nifLineInfo(w, info)
else:
let cid = pool.strings.getOrIncl(doc).uint32
if info == unknownLineInfo:
result = packWithComment(pool.man, NoFile, 0'i32, 0'i32, cid)
else:
let fid = get(w, info.fileIndex)
result = packWithComment(pool.man, fid, info.line.int32, info.col, cid)
proc oldLineInfo(w: var LineInfoWriter; info: PackedLineInfo): TLineInfo =
if info == NoLineInfo:
result = unknownLineInfo
@@ -379,8 +393,25 @@ proc writeLib(w: var Writer; dest: var TokenBuf; lib: PLib) =
dest.addStrLit lib.name
writeNode w, dest, lib.path
proc docOfSym(sym: PSym): string =
## The `##` doc comment documenting `sym`, if any (mirrors nifler's
## docCommentOf). The comment may sit on the decl node itself or as the first
## `nkCommentStmt` of a routine body. Carried separately on the sym def's NIF
## token because the AST serialization drops comments — nimsuggest needs it
## for "find definition" doc hovers.
let n = sym.astImpl
if n == nil or nodeCommentReader == nil: return ""
let own = nodeCommentReader(n)
if own.len > 0: return own
if sym.kindImpl in routineKinds and n.safeLen > bodyPos:
let body = n[bodyPos]
if body != nil and body.kind == nkStmtList and body.len > 0 and
body[0].kind == nkCommentStmt:
return nodeCommentReader(body[0])
return ""
proc writeSymDef(w: var Writer; dest: var TokenBuf; sym: PSym) =
dest.addParLe sdefTag, trLineInfo(w, sym.infoImpl)
dest.addParLe sdefTag, nifLineInfoWithComment(w.infos, sym.infoImpl, docOfSym(sym))
dest.addSymDef pool.syms.getOrIncl(w.toNifSymName(sym)), NoLineInfo
# The `x` marker means "importable as a bare identifier into an importer's
# scope". Object fields carry `sfExported` (so they are visible via `obj.field`
@@ -609,6 +640,7 @@ var importTag = registerTag("import")
var implTag = registerTag("implementation")
var reexpModTag = registerTag("reexpmod")
var offerTag = registerTag("offer")
var modulesrcTag = registerTag("modulesrc")
proc registerNifAstTags*() =
## (Re)registers ast2nif's NIF tags explicitly. The top-level `registerTag`
@@ -640,6 +672,7 @@ proc registerNifAstTags*() =
implTag = registerTag("implementation")
reexpModTag = registerTag("reexpmod")
offerTag = registerTag("offer")
modulesrcTag = registerTag("modulesrc")
proc writeNode(w: var Writer; dest: var TokenBuf; n: PNode; forAst = false) =
if n == nil:
@@ -1324,6 +1357,14 @@ proc writeNifModule*(config: ConfigRef; thisModule: int32; n: PNode;
for ct in off.concreteTypes:
w.deps.addSymUse pool.syms.getOrIncl(typeToNifSym(ct, w.infos.config)), NoLineInfo
w.deps.addParRi
# Record this module's own absolute source path. The NIF suffix is a hash of
# the (relative) path (gear2/modnames.moduleSuffix) and is NOT reversible, so
# the standalone include-graph scanner (`scanIncludeGraph`, used by nimsuggest
# cold queries) needs the path written explicitly to map an included file back
# to the *source* of its includer without loading the module.
w.deps.addParLe modulesrcTag, NoLineInfo
w.deps.addStrLit toFullPath(config, FileIndex(thisModule))
w.deps.addParRi
# the implTag is used to tell the loader that the
# bottom of the file is the implementation of the module:
@@ -1463,6 +1504,16 @@ proc cursorFromIndexEntry(c: var DecodeContext; module: FileIndex; entry: NifInd
buf: var TokenBuf): Cursor =
let s = addr c.mods[module].stream
s.r.jumpTo entry.offset
# A seek-load is self-contained: its tokens must decode their relative line
# info against `entry.info` ALONE. The stream's `parents` stack can be left at
# depth >1 by a prior non-seek read (e.g. loadNifModule reads `(stmts`/
# `(implementation` without consuming their `)`), and `parse` only overwrites
# parents[0] while `rawNext` reads parents[^1] — so a stale top entry (the last
# symbol decoded, e.g. excpt.nim:746) would become the base. This is benign on
# a freshly-opened stream (cold/stdin) but corrupts EPC recompiles, which reuse
# the per-module stream (the global DecodeContext survives resetAllModules).
# Collapse the stack so parse's parentInfo is the sole base.
s[].parents.setLen 1
nifcursors.parse(s[], buf, entry.info)
result = cursorAt(buf, 0)
@@ -1909,8 +1960,14 @@ proc loadSym*(c: var DecodeContext; s: PSym) =
# Now parse the symbol definition with all local symbols pre-registered
s.infoImpl = c.infos.oldLineInfo(n.info)
# The `##` doc comment (if any) rides as a NIF comment on the sym def token;
# capture it before advancing, then restore it onto the loaded AST so that
# suggest's `extractDocComment` (findDocComment on `s.ast`) finds it.
let docId = unpack(pool.man, n.info).comment
inc n
loadSymFromCursor(c, s, n, c.mods[symsModule].suffix, localSyms)
if docId != 0'u32 and s.astImpl != nil and nodeCommentWriter != nil:
nodeCommentWriter(s.astImpl, pool.strings[StrId(docId)])
template withNode(c: var DecodeContext; n: var Cursor; result: PNode; kind: TNodeKind; body: untyped) =
@@ -2268,6 +2325,10 @@ type
## generic instances this module created; modulegraphs.nim rebuilds
## `procInstCache` from them so a consumer reuses the instance instead of
## re-instantiating it in its own (operator-blind) module scope.
includes*: seq[string] # resolved full paths of files this module `include`s;
# replayed into `inclToMod` by modulegraphs.nim so that
# nimsuggest can map a query in an include file back to
# this module (`parentModule`) and recompile it.
proc loadImport(c: var DecodeContext; s: var Stream; deps: var seq[ModuleSuffix]; tok: var PackedToken) =
tok = next(s) # skip `(import`
@@ -2285,6 +2346,94 @@ proc loadImport(c: var DecodeContext; s: var Stream; deps: var seq[ModuleSuffix]
else:
raiseAssert "expected ParRi but got " & $tok.kind
proc loadInclude(c: var DecodeContext; s: var Stream; includes: var seq[string]; tok: var PackedToken) =
## Reads an `(include . . "path"...)` entry written by `trInclude`. The paths
## are resolved full paths (see semstmts.evalInclude under cmdM/optCompress).
tok = next(s) # skip `(include`
if tok.kind == DotToken: tok = next(s) # flags
if tok.kind == DotToken: tok = next(s) # type
while tok.kind == StringLit:
includes.add pool.strings[tok.litId]
tok = next(s)
if tok.kind == ParRi:
tok = next(s)
else:
raiseAssert "expected ParRi in (include ...) but got " & $tok.kind
proc scanIncludeGraph*(config: ConfigRef): seq[tuple[includer: string; includes: seq[string]]] =
## Standalone "full table" scan of every `<suffix>.nif` in the nimcache: reads
## only each module's header records — `(modulesrc "path")` (the includer's own
## source) and `(include . . "path"...)` (resolved included files) — and returns
## (includerSource, includedSources) pairs for the modules that `include`
## anything. No `DecodeContext`, no symbol/index loading: it parses the few dep
## tokens at the top of the file and stops at the first non-dep node.
##
## Used by nimsuggest to answer, for a cold-opened *include* file, "which module
## includes me?" without NIF-loading that module — so the includer can be
## *source*-compiled (modules that `include` files are never served from NIF).
result = @[]
let dir = getNimcacheDir(config)
if not dirExists(dir.string): return
for f in walkFiles((dir / RelativeFile"*.nif").string):
# only the primary module NIFs; skip the sidecars
# (.iface.nif/.impl.nif/.edges.nif/.s.deps.nif).
if f.endsWith(".iface.nif") or f.endsWith(".impl.nif") or
f.endsWith(".edges.nif") or f.endsWith(".deps.nif"):
continue
var s = nifstreams.open(f)
var includer = ""
var includes: seq[string] = @[]
var t = next(s) # (stmts
if t.kind == ParLe:
t = next(s) # flags dot
t = next(s) # type dot
t = next(s) # first child (matches loadNifModule's priming)
# the dep records (import/include/reexpmod/modulesrc) are written first and
# contiguously; stop at the first body node or the (implementation) marker.
while t.kind == ParLe:
if t.tagId == includeTag or t.tagId == modulesrcTag:
let isInc = t.tagId == includeTag
t = next(s) # into the node (past its ParLe)
while t.kind != ParRi and t.kind != EofToken:
if t.kind == StringLit:
if isInc: includes.add pool.strings[t.litId]
else: includer = pool.strings[t.litId]
t = next(s)
if t.kind == ParRi: t = next(s) # past the ParRi
elif t.tagId == importTag or t.tagId == reexpModTag:
t = skip(s, t)
else:
break
close s
if includer.len > 0 and includes.len > 0:
result.add (includer, includes)
proc nifModuleHasIncludes*(config: ConfigRef; fileIdx: FileIndex): bool =
## Cheap header-only check: does the module's `<suffix>.nif` contain an
## `(include ...)` record? Used by nimsuggest (`moduleFromNifFile`) to refuse to
## NIF-serve modules that `include` files, so the includer is source-compiled
## and the included symbols never round-trip through NIF (which mishandles their
## owner/line-info on reload).
let f = toNifFilename(config, fileIdx)
if not fileExists(f): return false
var s = nifstreams.open(f)
result = false
var t = next(s) # (stmts
if t.kind == ParLe:
t = next(s) # flags dot
t = next(s) # type dot
t = next(s) # first child
while t.kind == ParLe:
if t.tagId == includeTag:
result = true
break
elif t.tagId == modulesrcTag or t.tagId == importTag or
t.tagId == reexpModTag:
t = skip(s, t)
else:
break
close s
proc addReexportedEnumFields(c: var DecodeContext; sym: PSym; interf: var TStrTable) =
## When a non-pure enum type is (re-)exported, its fields must also become
## visible (unqualified) to importers. In a from-source build this happens via
@@ -2396,7 +2545,7 @@ proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag];
") in export list of module " & suffix & ", last symbol: " & lastGood
t = next(s)
elif t.tagId == includeTag:
t = skipTree(s)
loadInclude(c, s, result.includes, t)
elif t.tagId == importTag:
loadImport(c, s, result.deps, t)
elif t.tagId == reexpModTag:
@@ -2445,6 +2594,10 @@ proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag];
t = next(s)
if ok and genSym != nil and instSym != nil:
result.genericOffers.add (genSym, instSym, cts, paramsCount)
elif t.tagId == modulesrcTag:
# self-identification record for the standalone include-graph scanner;
# not needed by the lazy loader, just skip past it.
t = skip(s, t)
elif t.tagId == implTag:
cont = false
elif LoadFullAst in flags or t.tagId == letTag or t.tagId == varTag:

View File

@@ -1150,3 +1150,11 @@ proc strTableGet*(t: TStrTable, name: PIdent): PSym =
if result == nil: break
if result.name.id == name.id: break
h = nextTry(h, high(t.data))
# --- doc-comment bridge for the NIF serializer -------------------------------
# `ast2nif` (the NIF reader/writer) cannot import `ast` (where the comment
# accessor and its `gconfig.comments` side table live) because `ast` imports
# `ast2nif`. These hooks are assigned by `ast` and let the serializer carry a
# decl's `##` doc comment across a NIF round-trip.
var nodeCommentReader*: proc(n: PNode): string {.nimcall.}
var nodeCommentWriter*: proc(n: PNode; s: string) {.nimcall.}

View File

@@ -53,7 +53,8 @@ proc processCmdLineAndProjectPath*(self: NimProg, conf: ConfigRef) =
proc loadConfigsAndProcessCmdLine*(self: NimProg, cache: IdentCache; conf: ConfigRef;
graph: ModuleGraph): bool =
if self.suggestMode:
conf.setCmd cmdIdeTools
conf.setCmd cmdCheck
conf.ideActive = true
if conf.cmd == cmdNimscript:
incl(conf.globalOptions, optWasNimscript)
loadConfigs(DefaultConfig, cache, conf, graph.idgen) # load all config files

View File

@@ -718,6 +718,14 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
conf.outDir = processPath(conf, arg, info, notRelativeToProj=true)
of "usenimcache":
processOnOffSwitchG(conf, {optUseNimcache}, arg, pass, info)
of "ideimports":
# nimsuggest: where the import closure comes from.
# nif|on (default) load unchanged imports from precompiled NIF (cmdM)
# source|off recompile the whole closure from source (cmdCheck)
case arg.normalize
of "nif", "on", "": conf.ideImportsFromNif = true
of "source", "off": conf.ideImportsFromNif = false
else: localError(conf, info, "'--ideImports' expects 'nif' or 'source', got: '$1'" % arg)
of "docseesrcurl":
expectArg(conf, switch, arg, pass, info)
conf.docSeeSrcUrl = arg

View File

@@ -458,7 +458,7 @@ proc mainCommand*(graph: ModuleGraph) =
of cmdJsonscript:
setOutFile(graph.config)
commandJsonScript(graph)
of cmdUnknown, cmdNone, cmdIdeTools:
of cmdUnknown, cmdNone:
rawMessage(conf, errGenerated, "invalid command: " & conf.command)
if conf.errorCounter == 0 and conf.cmd notin {cmdTcc, cmdDump, cmdNop, cmdM} and

View File

@@ -607,10 +607,14 @@ proc loadCompilerProc*(g: ModuleGraph; name: string): PSym =
when not defined(nimKochBootstrap):
# Try to resolve from NIF for both cmdNifC and cmdM (which uses NIF files)
if g.config.cmd in {cmdNifC, cmdM}:
# First try system module (most compilerprocs are there)
# First try system module (most compilerprocs are there).
# Only consult the NIF if it actually exists: under nimsuggest's cold
# cache (ideActive) system is compiled from source and has no NIF yet,
# in which case the proc is already registered in-memory and the caller
# found/falls back to it — so degrade to nil instead of asserting.
let systemFileIdx = g.config.m.systemFileIdx
if systemFileIdx != InvalidFileIdx and not g.withinSystem:
# Only try to load from NIF if the file exists (it may not during initial ic build)
if systemFileIdx != InvalidFileIdx and not g.withinSystem and
fileExists(toNifFilename(g.config, systemFileIdx)):
result = tryResolveCompilerProc(ast.program, name, systemFileIdx)
if result != nil:
strTableAdd(g.compilerprocs, result)
@@ -622,6 +626,7 @@ proc loadCompilerProc*(g: ModuleGraph; name: string): PSym =
let module = g.ifaces[moduleIdx].module
if module != nil and module.name.s == "threadpool":
let threadpoolFileIdx = module.position.FileIndex
if not fileExists(toNifFilename(g.config, threadpoolFileIdx)): break
result = tryResolveCompilerProc(ast.program, name, threadpoolFileIdx)
if result != nil:
strTableAdd(g.compilerprocs, result)
@@ -1000,6 +1005,17 @@ when not defined(nimKochBootstrap):
if not fileExists(toNifFilename(g.config, fileIdx)):
return PrecompiledModule(module: nil)
# NOTE: direction-(c) experiment (refuse to NIF-serve include-bearing modules
# under ideActive, forcing a source compile) is disabled — it reproduces the
# known sibling-resolution corruption (system.string -> excpt.nim:746). The
# cold-include *discovery* scan (scanIncludeGraph) stays; the round-trip
# fidelity of included symbols is the separate, still-open loader problem.
when false:
if g.config.ideActive and not g.withinSystem and
fileIdx != g.config.m.systemFileIdx and
nifModuleHasIncludes(g.config, fileIdx):
return PrecompiledModule(module: nil)
# Create module symbol
let filename = AbsoluteFile toFullPath(g.config, fileIdx)
@@ -1021,6 +1037,12 @@ when not defined(nimKochBootstrap):
let ms = materializeReexportedModule(g, mname, msuffix)
if ms != nil:
strTableAdd(g.ifaces[fileIdx.int].interf, ms)
# Re-establish include->module mapping so nimsuggest's `parentModule` can map
# a query in an included file back to this (NIF-loaded) module and recompile
# it, exactly as it does for a from-source module. Without this the include
# relationship is invisible for NIF-served modules.
for incPath in result.includes:
g.addIncludeDep(fileIdx, fileInfoIdx(g.config, AbsoluteFile incPath))
# Rebuild `procInstCache` from this module's generic-instance OFFERS so a
# consumer's `genericCacheGet` finds the instance and SKIPS re-running
@@ -1063,6 +1085,35 @@ when not defined(nimKochBootstrap):
if g.config.cmd == cmdM:
loadTransitiveHooks(g, result.deps)
proc isModuleFile(g: ModuleGraph; fileIdx: FileIndex): bool =
let i = fileIdx.int32
i >= 0 and i < g.ifaces.len and g.ifaces[i].module != nil
proc registerIncluderFromNif*(g: ModuleGraph; fileIdx: FileIndex): bool =
## Targeted cold-include discovery for nimsuggest: scan the nimcache NIFs
## (`scanIncludeGraph`) for a module whose include-set contains *this* file
## and register only that single include->module edge in `inclToMod`, so a
## query inside the include file resolves its includer via `parentModule`.
##
## Deliberately targeted: registering *every* include relationship (i.e. also
## `system`'s own `include`s) eagerly assigns FileIndexes and pollutes
## `inclToMod`, which perturbs the NIF line-info decode of unrelated modules
## (`system.string` then resolves into `excpt.nim`). Touch nothing but the
## one edge we need.
let target = toFullPath(g.config, fileIdx)
for (includer, includes) in scanIncludeGraph(g.config):
for incFile in includes:
if cmpPaths(incFile, target) == 0:
g.addIncludeDep(fileInfoIdx(g.config, AbsoluteFile includer), fileIdx)
return true
result = false
proc needsIncludeScan*(g: ModuleGraph; fileIdx: FileIndex): bool =
## True when `fileIdx` is neither a known module of its own nor an
## already-known include file — i.e. a cold-opened file whose includer we
## must still discover via `registerIncluderFromNif`.
not g.isModuleFile(fileIdx) and not g.inclToMod.hasKey(fileIdx)
proc configComplete*(g: ModuleGraph) =
#rememberStartupConfig(g.startupPackedConfig, g.config)
discard

View File

@@ -351,7 +351,7 @@ proc msgWriteln*(conf: ConfigRef; s: string, flags: MsgFlags = {}) =
## This is used for 'nim dump' etc. where we don't have nimsuggest
## support.
#if conf.cmd == cmdIdeTools and optCDebug notin gGlobalOptions: return
#if conf.ideActive and optCDebug notin gGlobalOptions: return
let sep = if msgNoUnitSep notin flags: conf.unitSep else: ""
if not isNil(conf.writelnHook) and msgSkipHook notin flags:
conf.writelnHook(s & sep)
@@ -457,8 +457,8 @@ To create a stacktrace, rerun compilation with './koch temp $1 <file>', see $2 f
proc handleError(conf: ConfigRef; msg: TMsgKind, eh: TErrorHandling, s: string, ignoreMsg: bool) =
if msg in fatalMsgs:
if conf.cmd == cmdIdeTools: log(s)
if conf.cmd != cmdIdeTools or msg != errFatal:
if conf.ideActive: log(s)
if not conf.ideActive or msg != errFatal:
quit(conf, msg)
if msg >= errMin and msg <= errMax or
(msg in warnMin..hintMax and msg in conf.warningAsErrors and not ignoreMsg):
@@ -472,7 +472,7 @@ proc handleError(conf: ConfigRef; msg: TMsgKind, eh: TErrorHandling, s: string,
raiseRecoverableError(s)
else:
quit(conf, msg)
elif eh == doAbort and conf.cmd != cmdIdeTools:
elif eh == doAbort and not conf.ideActive:
quit(conf, msg)
elif eh == doRaise:
raiseRecoverableError(s)
@@ -503,7 +503,7 @@ proc writeContext(conf: ConfigRef; lastinfo: TLineInfo) =
info = context.info
proc ignoreMsgBecauseOfIdeTools(conf: ConfigRef; msg: TMsgKind): bool =
msg >= errGenerated and conf.cmd == cmdIdeTools and optIdeDebug notin conf.globalOptions
msg >= errGenerated and conf.ideActive and optIdeDebug notin conf.globalOptions
proc addSourceLine(conf: ConfigRef; fileIdx: FileIndex, line: string) =
conf.m.fileInfos[fileIdx.int32].lines.add line
@@ -661,7 +661,7 @@ proc warningDeprecated*(conf: ConfigRef, info: TLineInfo = gCmdLineInfo, msg = "
message(conf, info, warnDeprecated, msg)
proc internalErrorImpl(conf: ConfigRef; info: TLineInfo, errMsg: string, info2: InstantiationInfo) =
if conf.cmd in {cmdIdeTools, cmdCheck} and conf.structuredErrorHook.isNil: return
if (conf.ideActive or conf.cmd == cmdCheck) and conf.structuredErrorHook.isNil: return
writeContext(conf, info)
liMessage(conf, info, errInternal, errMsg, doAbort, info2)

View File

@@ -316,7 +316,7 @@ proc loadConfigs*(cfg: RelativeFile; cache: IdentCache; conf: ConfigRef; idgen:
if conf.cmd == cmdNimscript:
showHintConf()
conf.configFiles.setLen 0
if conf.cmd notin {cmdIdeTools, cmdCheck, cmdDump}:
if not conf.ideActive and conf.cmd notin {cmdCheck, cmdDump}:
if conf.cmd == cmdNimscript:
runNimScriptIfExists(conf.projectFull, isMain = true)
else:

View File

@@ -183,7 +183,6 @@ type
cmdCheck # semantic checking for whole project
cmdM # only compile a single
cmdParse # parse a single file (for debugging)
cmdIdeTools # ide tools (e.g. nimsuggest)
cmdNimscript # evaluate nimscript
cmdDoc0
cmdDoc # convert .nim doc comments to HTML
@@ -400,6 +399,13 @@ type
evalMacroCounter*: int
exitcode*: int8
cmd*: Command # raw command parsed as enum
ideActive*: bool # serving IDE tooling (nimsuggest): collect suggestions and
# keep going after errors. Decoupled from `cmd` so the IDE
# server can run under any compilation mode (cmdCheck, cmdM).
ideImportsFromNif*: bool # nimsuggest: load the unchanged import closure from
# precompiled NIF (run under cmdM) instead of recompiling it
# from source (cmdCheck). Default on; `--ideImports:source`
# opts out.
cmdInput*: string # input command
projectIsCmd*: bool # whether we're compiling from a command input
implicitCmd*: bool # whether some flag triggered an implicit `command`
@@ -685,6 +691,7 @@ proc newConfigRef*(): ConfigRef =
command: "", # the main command (e.g. cc, check, scan, etc)
commandArgs: @[], # any arguments after the main command
commandLine: "",
ideImportsFromNif: true, # opt-out; see `--ideImports`
implicitImports: @[], # modules that are to be implicitly imported
implicitIncludes: @[], # modules that are to be implicitly included
docSeeSrcUrl: "",
@@ -786,7 +793,7 @@ template quitOrRaise*(conf: ConfigRef, msg = "") =
else:
quit(msg) # quits with QuitFailure
proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.cmd in cmdDocLike + {cmdIdeTools}
proc importantComments*(conf: ConfigRef): bool {.inline.} = conf.ideActive or conf.cmd in cmdDocLike
proc usesWriteBarrier*(conf: ConfigRef): bool {.inline.} = conf.selectedGC >= gcRefc
proc usesSso*(conf: ConfigRef): bool {.inline.} = conf.selectedStrings == stringSso
@@ -912,7 +919,8 @@ proc getOsCacheDir(): string =
proc getNimcacheDir*(conf: ConfigRef): AbsoluteDir =
proc nimcacheSuffix(conf: ConfigRef): string =
if conf.cmd == cmdCheck: "_check"
if conf.ideActive: "_nimsuggest" # dedicated cache, never shared with `nim c`
elif conf.cmd == cmdCheck: "_check"
elif isDefined(conf, "release") or isDefined(conf, "danger"): "_r"
else: "_d"

View File

@@ -246,11 +246,20 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator
# (imported modules should be loaded from existing NIF files). Members of the
# current strongly-connected import group (`--icGroup`) are the exception:
# they are compiled from source here, so each must write its own NIF.
let shouldWriteNif = (optCompress in graph.config.globalOptions) or
(graph.config.cmd == cmdM and
(sfMainModule in module.flags or
(graph.config.icGroup.len > 0 and
toFullPath(graph.config, module.position.FileIndex) in graph.config.icGroup)))
let shouldWriteNif =
if graph.config.ideActive:
# nimsuggest (cmdM): persist NIF for cleanly-compiled, SAVED modules so
# later queries load them instead of recompiling. Never persist the
# actively edited buffer (it may hold unsaved/incomplete code) nor a
# module that failed to compile — that would poison the cache.
graph.config.cmd == cmdM and graph.config.errorCounter == 0 and
graph.config.m.fileInfos[module.position].dirtyFile.isEmpty
else:
(optCompress in graph.config.globalOptions) or
(graph.config.cmd == cmdM and
(sfMainModule in module.flags or
(graph.config.icGroup.len > 0 and
toFullPath(graph.config, module.position.FileIndex) in graph.config.icGroup)))
if shouldWriteNif and not graph.config.isDefined("nimscript"):
topLevelStmts.add finalNode
# Collect replay actions from both pragma computations and VM state diff
@@ -380,24 +389,31 @@ proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymF
toFullPath(graph.config, fileIdx) notin graph.config.icGroup):
let precomp = moduleFromNifFile(graph, fileIdx)
if precomp.module == nil:
let nifPath = toNifFilename(graph.config, fileIdx)
# Macro-generated imports (e.g. chronicles' parseStmt("import
# chronicles/textlines") driven by the chronicles_sinks define) are
# invisible to the static scanner, so this module's NIF was never
# built. The importer already recorded this import via
# addImportFileDep, so flush every module's `.s.deps`: `nim ic` reads
# it, re-derives the graph with the missing node + edge, and reruns
# the frontend. We still error — this process cannot finish sem
# without the import — but the discovery is structured data now, not
# a side-channel file.
for importer, deps in graph.importDeps.pairs:
var paths: seq[string] = @[]
for f in deps: paths.add toFullPath(graph.config, f)
writeSemDeps(graph.config, importer.int32, paths)
globalError(graph.config, unknownLineInfo,
"nim m requires precompiled NIF for import: " & toFullPath(graph.config, fileIdx) &
" (expected: " & nifPath & ")")
return nil # Don't fall through to compile from source
if graph.config.ideActive:
# nimsuggest bootstrap: this import has no precompiled NIF yet (cold
# cache, or it was invalidated). Don't error — fall through to the
# source-compile path below; the pass-close emits a fresh NIF so the
# next query loads it instead of recompiling.
discard
else:
let nifPath = toNifFilename(graph.config, fileIdx)
# Macro-generated imports (e.g. chronicles' parseStmt("import
# chronicles/textlines") driven by the chronicles_sinks define) are
# invisible to the static scanner, so this module's NIF was never
# built. The importer already recorded this import via
# addImportFileDep, so flush every module's `.s.deps`: `nim ic` reads
# it, re-derives the graph with the missing node + edge, and reruns
# the frontend. We still error — this process cannot finish sem
# without the import — but the discovery is structured data now, not
# a side-channel file.
for importer, deps in graph.importDeps.pairs:
var paths: seq[string] = @[]
for f in deps: paths.add toFullPath(graph.config, f)
writeSemDeps(graph.config, importer.int32, paths)
globalError(graph.config, unknownLineInfo,
"nim m requires precompiled NIF for import: " & toFullPath(graph.config, fileIdx) &
" (expected: " & nifPath & ")")
return nil # Don't fall through to compile from source
else:
# Module successfully loaded from NIF file - use it and skip processing
result = precomp.module
@@ -520,13 +536,21 @@ proc compilePipelineProject*(graph: ModuleGraph; projectFileIdx = InvalidFileIdx
graph.config.m.systemFileIdx = fileInfoIdx(graph.config,
graph.config.libpath / RelativeFile"system.nim")
when not defined(nimKochBootstrap):
let precomp = moduleFromNifFile(graph, graph.config.m.systemFileIdx)
graph.systemModule = precomp.module
# Don't clobber an already-compiled system: nimsuggest's NimScript config
# evaluation compiles `system` into this same graph before we get here.
if graph.systemModule == nil:
let nifPath = toNifFilename(graph.config, graph.config.m.systemFileIdx)
localError(graph.config, unknownLineInfo,
"nim m requires precompiled NIF for system module (expected: " & nifPath & ")")
return
let precomp = moduleFromNifFile(graph, graph.config.m.systemFileIdx)
graph.systemModule = precomp.module
if graph.systemModule == nil:
if graph.config.ideActive:
# nimsuggest bootstrap: no system NIF yet — compile it from source
# (the pass-close emits it), then continue with the main module.
graph.compilePipelineSystemModule()
else:
let nifPath = toNifFilename(graph.config, graph.config.m.systemFileIdx)
localError(graph.config, unknownLineInfo,
"nim m requires precompiled NIF for system module (expected: " & nifPath & ")")
return
discard graph.compilePipelineModule(projectFile, {sfMainModule})
else:
graph.compilePipelineSystemModule()

View File

@@ -77,7 +77,7 @@ template semIdeForTemplateOrGeneric(c: PContext; n: PNode;
# templates perform some quick check whether the cursor is actually in
# the generic or template.
when defined(nimsuggest):
if c.config.cmd == cmdIdeTools and requiresCheck:
if c.config.ideActive and requiresCheck:
#if optIdeDebug in gGlobalOptions:
# echo "passing to safeSemExpr: ", renderTree(n)
discard safeSemExpr(c, n)
@@ -871,7 +871,7 @@ proc semStmtAndGenerateGenerics(c: PContext, n: PNode): PNode =
result = hloStmt(c, result)
if c.config.cmd == cmdInteractive and not isEmptyType(result.typ):
result = buildEchoStmt(c, result)
if c.config.cmd == cmdIdeTools:
if c.config.ideActive:
appendToModule(c.module, result)
trackStmt(c, c.module, result, isTopLevel = true)
if optMultiMethods notin c.config.globalOptions and
@@ -908,7 +908,7 @@ proc semWithPContext*(c: PContext, n: PNode): PNode =
result = nil
else:
result = newNodeI(nkEmpty, n.info)
#if c.config.cmd == cmdIdeTools: findSuggest(c, n)
#if c.config.ideActive: findSuggest(c, n)
proc reportUnusedModules(c: PContext) =
if c.config.cmd == cmdM: return
@@ -917,7 +917,7 @@ proc reportUnusedModules(c: PContext) =
message(c.config, info, warnUnusedImportX, s.name.s)
proc closePContext*(graph: ModuleGraph; c: PContext, n: PNode): PNode =
if c.config.cmd == cmdIdeTools and not c.suggestionsMade:
if c.config.ideActive and not c.suggestionsMade:
suggestSentinel(c)
closeScope(c) # close module's scope
rawCloseScope(c) # imported symbols; don't check for unused ones!

View File

@@ -1571,7 +1571,7 @@ proc builtinFieldAccess(c: PContext; n: PNode; flags: var TExprFlags): PNode =
# here at all!
#if isSymChoice(n[1]): return
when defined(nimsuggest):
if c.config.cmd == cmdIdeTools:
if c.config.ideActive:
suggestExpr(c, n)
if exactEquals(c.config.m.trackPos, n[1].info): suggestExprNoCheck(c, n)
@@ -3405,7 +3405,7 @@ proc semExpr(c: PContext, n: PNode, flags: TExprFlags = {}, expectedType: PType
c.config.expandNodeResult = $n
suggestQuit()
if c.config.cmd == cmdIdeTools: suggestExpr(c, n)
if c.config.ideActive: suggestExpr(c, n)
if nfSem in n.flags: return
case n.kind
of nkIdent, nkAccQuoted:

View File

@@ -273,7 +273,7 @@ proc semGenericStmt(c: PContext, n: PNode,
when defined(nimsuggest):
if withinTypeDesc in flags: inc c.inTypeContext
#if conf.cmd == cmdIdeTools: suggestStmt(c, n)
#if conf.ideActive: suggestStmt(c, n)
semIdeForTemplateOrGenericCheck(c.config, n, ctx.cursorInBody)
case n.kind

View File

@@ -531,7 +531,7 @@ proc semUsing(c: PContext; n: PNode): PNode =
if not isTopLevel(c): localError(c.config, n.info, errXOnlyAtModuleScope % "using")
for i in 0..<n.len:
var a = n[i]
if c.config.cmd == cmdIdeTools: suggestStmt(c, a)
if c.config.ideActive: suggestStmt(c, a)
if a.kind == nkCommentStmt: continue
if a.kind notin {nkIdentDefs, nkVarTuple, nkConstDef}: illFormedAst(a, c.config)
checkMinSonsLen(a, 3, c.config)
@@ -838,7 +838,7 @@ proc semVarOrLet(c: PContext, n: PNode, symkind: TSymKind): PNode =
for i in 0..<n.len:
var a = n[i]
if c.config.cmd == cmdIdeTools: suggestStmt(c, a)
if c.config.ideActive: suggestStmt(c, a)
if a.kind == nkCommentStmt: continue
if a.kind notin {nkIdentDefs, nkVarTuple}: illFormedAst(a, c.config)
checkMinSonsLen(a, 3, c.config)
@@ -994,7 +994,7 @@ proc semConst(c: PContext, n: PNode): PNode =
var b: PNode
for i in 0..<n.len:
var a = n[i]
if c.config.cmd == cmdIdeTools: suggestStmt(c, a)
if c.config.ideActive: suggestStmt(c, a)
if a.kind == nkCommentStmt: continue
if a.kind notin {nkConstDef, nkVarTuple}: illFormedAst(a, c.config)
checkMinSonsLen(a, 3, c.config)
@@ -1535,7 +1535,7 @@ proc typeSectionLeftSidePass(c: PContext, n: PNode) =
while i < n.len: # n may grow due to type pragma macros
var a = n[i]
when defined(nimsuggest):
if c.config.cmd == cmdIdeTools:
if c.config.ideActive:
inc c.inTypeContext
suggestStmt(c, a)
dec c.inTypeContext
@@ -1812,7 +1812,7 @@ proc typeSectionFinalPass(c: PContext, n: PNode) =
var remainingOwners = initIntSet()
for (owner, _, _) in c.forwardTypeUpdates:
remainingOwners.incl owner.id
while c.forwardTypeUpdates.len > 0:
let pending = move c.forwardTypeUpdates
var madeProgress = false
@@ -1829,7 +1829,7 @@ proc typeSectionFinalPass(c: PContext, n: PNode) =
c.forwardTypeUpdates.add (owner, typ, typeNode)
elif not remainingOwners.missingOrExcl(owner.id):
madeProgress = true
if not madeProgress:
# can't error here unfortunately
break
@@ -2868,7 +2868,7 @@ proc incMod(c: PContext, n: PNode, it: PNode, includeStmtResult, resolvedIncStmt
proc evalInclude(c: PContext, n: PNode): PNode =
result = newNodeI(nkStmtList, n.info)
var resolvedIncStmt: PNode = nil
if optCompress in c.config.globalOptions:
if optCompress in c.config.globalOptions or c.config.cmd == cmdM:
# New resolve the include filenames to string literals that contain absolute paths,
# nicer for IC:
resolvedIncStmt = newNodeI(nkIncludeStmt, n.info)

View File

@@ -2233,7 +2233,7 @@ proc semTypeNode(c: PContext, n: PNode, prev: PType): PType =
result = nil
inc c.inTypeContext
if c.config.cmd == cmdIdeTools: suggestExpr(c, n)
if c.config.ideActive: suggestExpr(c, n)
case n.kind
of nkEmpty: result = n.typ
of nkTypeOfExpr:

View File

@@ -911,7 +911,7 @@ proc suggestDecl*(c: PContext, n: PNode; s: PSym) =
defer:
if attached: dec(c.inTypeContext)
# If user is typing out an enum field, then don't provide suggestions
if s.kind == skEnumField and c.config.cmd == cmdIdeTools and exactEquals(c.config.m.trackPos, n.info):
if s.kind == skEnumField and c.config.ideActive and exactEquals(c.config.m.trackPos, n.info):
suggestQuit()
suggestExpr(c, n)

View File

@@ -35,7 +35,7 @@ import strutils, os, parseopt, parseutils, sequtils, net, rdstdin, sexp
# suggestionResultHook, because suggest.nim is included by sigmatch.
# So we import that one instead.
import compiler / [options, commands, modules,
passes, passaux, msgs,
passes, passaux, msgs, pipelines,
sigmatch, ast,
idents, modulegraphs, prefixmatches, lineinfos, cmdlinehelper,
pathutils, condsyms, syntaxes, suggestsymdb]
@@ -261,6 +261,14 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int,
var isKnownFile = true
let dirtyIdx = fileInfoIdx(conf, file, isKnownFile)
# Cold-opened include file: nothing has been compiled/loaded yet, so its
# includer is unknown. Scan the nimcache NIFs for the module that `include`s
# this exact file and register that one edge; `parentModule(dirtyIdx)` then
# resolves to the includer, which we (re)compile below.
if conf.ideImportsFromNif and graph.needsIncludeScan(dirtyIdx):
discard graph.registerIncluderFromNif(dirtyIdx)
let isInclude = graph.inclToMod.hasKey(dirtyIdx)
if not dirtyfile.isEmpty: msgs.setDirtyFile(conf, dirtyIdx, dirtyfile)
else: msgs.setDirtyFile(conf, dirtyIdx, AbsoluteFile"")
@@ -269,9 +277,9 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int,
conf.errorCounter = 0
if conf.suggestVersion == 1:
graph.usageSym = nil
if not isKnownFile:
if not isKnownFile and not isInclude:
graph.clearInstCache(dirtyIdx)
graph.compileProject(dirtyIdx)
graph.compilePipelineProject(dirtyIdx)
if conf.suggestVersion == 0 and conf.ideCmd in {ideUse, ideDus} and
dirtyfile.isEmpty:
discard "no need to recompile anything"
@@ -280,9 +288,11 @@ proc executeNoHooks(cmd: IdeCmd, file, dirtyfile: AbsoluteFile, line, col: int,
graph.markDirty dirtyIdx
graph.markClientsDirty dirtyIdx
if conf.ideCmd != ideMod:
if isKnownFile:
# `isInclude`: a freshly discovered include file is not "known" yet, but we
# still must (source-)compile its includer to serve the query.
if isKnownFile or isInclude:
graph.clearInstCache(modIdx)
graph.compileProject(modIdx)
graph.compilePipelineProject(modIdx)
if conf.ideCmd in {ideUse, ideDus}:
let u = if conf.suggestVersion != 1: graph.symFromInfo(conf.m.trackPos) else: graph.usageSym
if u != nil:
@@ -577,7 +587,7 @@ proc recompileFullProject(graph: ModuleGraph) =
graph.vm = nil
graph.resetAllModules()
GC_fullCollect()
graph.compileProject()
graph.compilePipelineProject()
proc mainThread(graph: ModuleGraph) =
let conf = graph.config
@@ -621,10 +631,14 @@ var
proc mainCommand(graph: ModuleGraph) =
let conf = graph.config
clearPasses(graph)
registerPass graph, verbosePass
registerPass graph, semPass
conf.setCmd cmdIdeTools
# Use the pipeline driver (same as `nim check`): it is where IC/NIF loading
# and emission live. The legacy `passes.compileProject` path has no NIF support.
setPipeLinePass(graph, SemPass)
# cmdM loads the unchanged import closure from precompiled NIF; cmdCheck
# recompiles everything from source. `ideActive` keeps suggestion collection
# and error-resilience regardless of which mode we run under.
conf.setCmd(if conf.ideImportsFromNif: cmdM else: cmdCheck)
conf.ideActive = true
defineSymbol(conf.symbols, $conf.backend)
wantMainModule(conf)
@@ -646,7 +660,7 @@ proc mainCommand(graph: ModuleGraph) =
# compile the project before showing any input so that we already
# can answer questions right away:
benchmark "Initial compilation":
compileProject(graph)
compilePipelineProject(graph)
open(requests)
open(results)
@@ -798,7 +812,7 @@ proc recompilePartially(graph: ModuleGraph, projectFileIdx = InvalidFileIdx) =
try:
benchmark "Recompilation":
graph.compileProject(projectFileIdx)
graph.compilePipelineProject(projectFileIdx)
except Exception as e:
myLog fmt "Failed to recompile partially with the following error:\n {e.msg} \n\n {e.getStackTrace()}"
try:
@@ -1311,11 +1325,10 @@ else:
proc mockCommand(graph: ModuleGraph) =
retval = graph
let conf = graph.config
conf.setCmd cmdIdeTools
conf.setCmd(if conf.ideImportsFromNif: cmdM else: cmdCheck)
conf.ideActive = true
defineSymbol(conf.symbols, $conf.backend)
clearPasses(graph)
registerPass graph, verbosePass
registerPass graph, semPass
setPipeLinePass(graph, SemPass)
wantMainModule(conf)
@@ -1331,7 +1344,7 @@ else:
# compile the project before showing any input so that we already
# can answer questions right away:
compileProject(graph)
compilePipelineProject(graph)
proc mockCmdLine(pass: TCmdLinePass, cmd: string; conf: ConfigRef) =