automate the testing; bugfixes

This commit is contained in:
Araq
2026-06-15 12:50:01 +02:00
parent 8938716326
commit 3403ad2033
5 changed files with 144 additions and 16 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 <file>`: 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"):