diff --git a/compiler/ast2nif.nim b/compiler/ast2nif.nim index c4bab071ee..34ccff532b 100644 --- a/compiler/ast2nif.nim +++ b/compiler/ast2nif.nim @@ -2172,6 +2172,12 @@ proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag]; var t = next(s) # skip dot var cont = true let exportTag = pool.tags.getOrIncl"export" + # Top-level `let`/`var` sections are loaded even without LoadFullAst: they may + # declare `{.compileTime.}` globals whose VM slots the importer initializes + # eagerly (pipelines.initLoadedCompileTimeGlobals), which needs them visible in + # `topLevel`. They sit in the module header before `(implementation)`. + let letTag = pool.tags.getOrIncl(toNifTag(nkLetSection)) + let varTag = pool.tags.getOrIncl(toNifTag(nkVarSection)) while cont and t.kind != EofToken: if t.kind == ParLe: if t.tagId == replayTag: @@ -2271,8 +2277,9 @@ proc processTopLevel(c: var DecodeContext; s: var Stream; flags: set[LoadFlag]; result.reexportedModules.add (mname, msuffix) elif t.tagId == implTag: cont = false - elif LoadFullAst in flags: - # Parse the full statement + elif LoadFullAst in flags or t.tagId == letTag or t.tagId == varTag: + # Parse the full statement. let/var sections are loaded unconditionally + # (see above) so `{.compileTime.}` globals reach the eager initializer. var buf = createTokenBuf(50) nextSubtree(s, buf, t) t = next(s) # skip ParRi diff --git a/compiler/deps.nim b/compiler/deps.nim index 7015db6f1a..a33b48d292 100644 --- a/compiler/deps.nim +++ b/compiler/deps.nim @@ -12,7 +12,7 @@ import std / [os, tables, sets, times, osproc, algorithm, strtabs, strutils, syncio] import options, msgs, lineinfos, pathutils, condsyms, - modulepaths, extccomp, cnif + modulepaths, extccomp, cnif, platform import "../dist/nimony/src/lib" / [nifstreams, bitabs, nifreader, nifbuilder] import "../dist/nimony/src/gear2" / modnames @@ -196,8 +196,11 @@ proc resolveInclude(c: DepContext; origin, toResolve: string): string = proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) -proc processInclude(c: var DepContext; includePath: string; current: Node) = - let resolved = resolveInclude(c, current.files[current.files.len - 1].nimFile, includePath) +proc processInclude(c: var DepContext; includePath: string; current: Node; origin: string) = + # `origin` = the file the `include` literally appears in (an included file's + # own nested includes/imports must resolve relative to IT, not the importing + # module's main file). + let resolved = resolveInclude(c, origin, includePath) if resolved.len == 0 or not fileExists(resolved): return @@ -220,8 +223,16 @@ proc getsImplicitImports(c: DepContext; nimFile: string): bool = ## system.nim and never reaches them). Stdlib == under conf.libpath. not isRelativeTo(nimFile, c.config.libpath.string) -proc processImport(c: var DepContext; importPath: string; current: Node) = - let resolved = resolveImport(c, current.files[0].nimFile, importPath) +proc processImport(c: var DepContext; importPath: string; current: Node; origin: string) = + # `origin` = the file the `import` literally appears in. Crucial for imports + # inside `include`d files: e.g. `system.nim` includes `system/excpt.nim`, which + # does `import stacktraces` — that must resolve relative to `excpt.nim` + # (lib/system/) → `lib/system/stacktraces.nim`, NOT relative to `system.nim` + # (lib/) which has no `stacktraces.nim`. Resolving against the main file silently + # dropped the `system → stacktraces` edge, so stacktraces was a separate SCC in + # the static round and got re-grouped (and recompiled with divergent type ids) + # only after the post-sem `.s.deps` revealed the edge. + let resolved = resolveImport(c, origin, importPath) if resolved.len == 0 or not fileExists(resolved): return @@ -290,6 +301,47 @@ proc evalCondIdent(c: DepContext; v: string): bool = c.scanningMain else: true +proc constIdentValue(c: DepContext; ident: string): string = + ## String value of a compile-time platform constant that appears in `when` + ## guards, or "" when unknown. Mirrors the compiler's magics so the scanner + ## evaluates e.g. `when hostOS == "standalone"` the SAME way the real compile + ## does. Without this the comparison is "unknown" → the conservative `true`, + ## which is WRONG once negated (`else:` branches emit `not (==)`), so a real + ## conditional `include`/`import` is dropped (e.g. system's `else: include + ## excpt`, hiding `import stacktraces`). + # Must match the compiler's magics EXACTLY, incl. case: `hostOS`/`hostCPU` etc. + # fold to the lower-cased platform name (see semfold.nim mHostOS/mHostCPU), and + # user code compares against lower-case literals (`when hostOS == "linux"`). + case ident + of "hostOS": result = toLowerAscii(platform.OS[c.config.target.targetOS].name) + of "hostCPU": result = toLowerAscii(platform.CPU[c.config.target.targetCPU].name) + of "buildOS": result = toLowerAscii(platform.OS[c.config.target.hostOS].name) + of "buildCPU": result = toLowerAscii(platform.CPU[c.config.target.hostCPU].name) + else: result = "" + +proc readOperandValue(c: DepContext; s: var Stream): string = + ## Read one operand of an `==`/`!=` infix and return its string value (a string + ## literal verbatim, a platform-constant ident resolved, anything else ""), fully + ## consuming the operand (subtrees are skipped) so the caller stays in sync. + let t = next(s) + case t.kind + of StringLit: result = pool.strings[t.litId] + of Ident: result = constIdentValue(c, pool.strings[t.litId]) + of ParLe: + result = "" + skipSubtree(s, t) + else: result = "" + +proc evalCondCmp(c: DepContext; s: var Stream; isEq: bool): bool = + ## Evaluate `a == b` / `a != b`. Both operands known → real result; otherwise + ## fall back to `true` (the conservative direction for a bare comparison). + let v1 = readOperandValue(c, s) + let v2 = readOperandValue(c, s) + if v1.len > 0 and v2.len > 0: + result = (v1 == v2) == isEq + else: + result = true + proc evalCondExpr(c: DepContext; s: var Stream): bool = ## Read exactly one condition expression from `s` and return its truth ## value. Consumes tokens whether the expression is recognised or not so @@ -326,6 +378,8 @@ proc evalCondExpr(c: DepContext; s: var Stream): bool = result = evalCondExpr(c, s) if not result: result = evalCondExpr(c, s) else: skipSubtree(s, next(s)) + of "==", "!=": + result = evalCondCmp(c, s, name == "==") else: result = true # Drain whatever remains until the matching ParRi. @@ -416,6 +470,8 @@ proc whenMarkerHolds(c: DepContext; s: var Stream): bool = of "or": ok = evalCondExpr(c, s) if not ok: ok = evalCondExpr(c, s) + of "==", "!=": + ok = evalCondCmp(c, s, name == "==") else: ok = true # finish the subtree @@ -580,15 +636,15 @@ proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) = # be treated as modules. Both still create a real dependency on `m`. for importPath in parseImportPath(s, t): if importPath.len > 0: - processImport(c, importPath, current) + processImport(c, importPath, current, pair.nimFile) else: while t.kind != ParRi and t.kind != EofToken: for importPath in parseImportPath(s, t): if importPath.len > 0: if tag == "include": - processInclude(c, importPath, current) + processInclude(c, importPath, current, pair.nimFile) else: - processImport(c, importPath, current) + processImport(c, importPath, current, pair.nimFile) # Drain any remaining tokens of this node (e.g. the symbol list of a # `fromimport`), up to and including the node's closing ')'. var depth = 1 diff --git a/compiler/pipelines.nim b/compiler/pipelines.nim index ae81ac12f1..23de4be020 100644 --- a/compiler/pipelines.nim +++ b/compiler/pipelines.nim @@ -280,6 +280,42 @@ proc processPipelineModule*(graph: ModuleGraph; module: PSym; idgen: IdGenerator result = true +proc loadedDefSym(defs: PNode): PSym = + ## The defined symbol of a let/var entry as it loads back from a NIF: the + ## section child is a bare `nkSym` (the `(sd …)` reference), but be defensive + ## about the from-source shapes too (`nkIdentDefs`, a pragma-wrapped name). + case defs.kind + of nkSym: result = defs.sym + of nkPragmaExpr: + result = if defs.len > 0: loadedDefSym(defs[0]) else: nil + of nkIdentDefs, nkConstDef: + result = if defs.len > 0: loadedDefSym(defs[0]) else: nil + else: result = nil + +proc initLoadedCompileTimeGlobals(graph: ModuleGraph; module: PSym; topLevel: PNode) = + ## Eagerly initialize the compile-time globals (`let/var {.compileTime.}`) of a + ## module restored from a NIF. In a normal sem these VM slots are filled by + ## `setupCompileTimeVar` (semstmts) as the section is semchecked; a NIF-loaded + ## module is never semchecked, so without this a macro or compile-time proc that + ## reads such a global finds a nil slot. The lazy `vmgen.genGlobalInit` fallback + ## is order-fragile across proc boundaries (it emits the init at the first + ## VM-gen'd reference, which need not be the first one executed), so the init has + ## to happen here, once, before any of the module's code can run. The symbol's + ## own `ast` is the `nkIdentDefs` (initializer included); re-wrap it in a section + ## exactly as semstmts does and hand it to the same evaluator. + if topLevel == nil: return + let idgen = idGeneratorFromModule(module) + for stmt in topLevel: + if stmt.kind notin {nkLetSection, nkVarSection}: continue + for defs in stmt: + let s = loadedDefSym(defs) + if s != nil and s.kind in {skLet, skVar} and + {sfCompileTime, sfGlobal} <= s.flags and + s.ast != nil and s.ast.kind == nkIdentDefs: + var sect = newNodeI(stmt.kind, s.info) + sect.add s.ast + setupCompileTimeVar(module, idgen, graph, sect) + proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags; fromModule: PSym = nil): PSym = var flags = flags if fileIdx == graph.config.projectMainIdx2: flags.incl sfMainModule @@ -345,6 +381,9 @@ proc compilePipelineModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymF # Replay state changes from the loaded NIF module if result.ast != nil: replayStateChanges(result, graph) + # Fill the VM slots of the module's `{.compileTime.}` globals now (sem + # would have, but a NIF-loaded module is never semchecked). + initLoadedCompileTimeGlobals(graph, result, precomp.topLevel) return result # Return early, don't process from source let path = toFullPath(graph.config, fileIdx) let filename = AbsoluteFile path diff --git a/compiler/vmgen.nim b/compiler/vmgen.nim index 2b1ec37283..999b0756de 100644 --- a/compiler/vmgen.nim +++ b/compiler/vmgen.nim @@ -1782,8 +1782,15 @@ proc genGlobalInit(c: PCtx; n: PNode; s: PSym) = # This is rather hard to support, due to the laziness of the VM code # generator. See tests/compile/tmacro2 for why this is necessary: # var decls{.compileTime.}: seq[NimNode] = @[] + # Load the slot's ADDRESS (not its value): the lazy initializer must REPLACE + # the null slot, which `opcWrDeref` only does for an `rkNodeAddr` target + # (`nAddr[] = n` for refs). With `opcLdGlobal` the slot value is loaded and for + # a ref-typed global that value is an `nkNilLit` ("nil ref"); writing through it + # hits the VM's nil-deref guard ("attempt to access a nil address"). This path + # is reached for compile-time globals whose defining module is restored from a + # NIF under `nim ic` (so `setupCompileTimeVar` never ran to eagerly init them). let dest = c.getTemp(s.typ) - c.gABx(n, opcLdGlobal, dest, s.position) + c.gABx(n, opcLdGlobalAddr, dest, s.position) if s.astdef != nil: let tmp = c.genx(s.astdef) c.genAdditionalCopy(n, opcWrDeref, dest, 0, tmp) diff --git a/koch.nim b/koch.nim index 4127a86e25..e5909ea388 100644 --- a/koch.nim +++ b/koch.nim @@ -600,19 +600,38 @@ proc xtemp(cmd: string) = finally: copyExe(d / "bin" / "nim_backup".exe, d / "bin" / "nim".exe) -proc icTest(args: string) = - temp("") - let inp = os.parseCmdLine(args)[0] +proc runIcTestFile(inp: string) = + ## Compile a single `tests/ic` file with `nim ic`, once per `#!EDIT!#` fragment + ## (each fragment is the file's source after that incremental edit). Only checks + ## that `nim ic` exits 0 — the produced binary's output is not verified here. let content = readFile(inp) let nimExe = getAppDir() / "bin" / "nim_temp".exe - var i = 0 for fragment in content.split("#!EDIT!#"): let file = inp.replace(".nim", "_temp.nim") writeFile(file, fragment) var cmd = nimExe & " ic --hint:Conf:off --warnings:off " cmd.add quoteShell(file) exec(cmd) - inc i + +# The `tests/ic` files that `nim ic` must keep compiling. Multi-module tests rely +# on a sibling helper (`timp` -> `myimp`, `tcompiletimeglobal` -> `mctglobal`), +# which exercises the NIF import/load path the single-file tests do not. +const icSuite = ["thallo", "tconverter", "timp", "tmiscs", "tparseutils", + "tcompiletimeglobal"] + +proc icTest(args: string) = + temp("") + let parsed = os.parseCmdLine(args) + if parsed.len > 0 and parsed[0].len > 0: + # `koch ic `: run just that file. + runIcTestFile(parsed[0]) + else: + # `koch ic`: the full regression set we want to keep working — the test + # suite plus both self-host bootstraps (`bootic` and `bootic -d:release`). + for t in icSuite: + runIcTestFile("tests" / "ic" / (t & ".nim")) + bootic("", skipIntegrityCheck = false) + bootic("-d:release", skipIntegrityCheck = false) proc buildDrNim(args: string) = if not dirExists("dist/nimz3"):