Incremental compilation (IC): Improvements (#11881)

* IC: C codegen is aware of IC
* manual: minor change to make VSCode's RST plugin render it properly
* IC: minor refactoring
* testament: code refactorings
* rodutils: removed dead code
* IC: always build the compiler with the IC feature
* IC: C codegen improvements
* IC: implement the undocumented -d:nimMustCache option for testing purposes
* IC: added first basic tests
* IC: extensive testing of the deserialization feature
* testament: refactoring; better IC tests
* IC: removes 'nimMustCache' flag; readonly does the same
* testament: minor refactoring
* update Nimble version
* testament: removed dead code and imports; IC: added simple test
* IC: progress
This commit is contained in:
Andreas Rumpf 2019-08-08 08:41:05 +02:00 • committed by GitHub
commit c8cffaf420
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 318 additions and 261 deletions

View file

@ -1108,7 +1108,8 @@ proc genProcNoForward(m: BModule, prc: PSym) =
# a check for ``m.declaredThings``. # a check for ``m.declaredThings``.
if not containsOrIncl(m.declaredThings, prc.id): if not containsOrIncl(m.declaredThings, prc.id):
#if prc.loc.k == locNone: #if prc.loc.k == locNone:
# mangle the inline proc based on the module where it is defined - not on the first module that uses it # mangle the inline proc based on the module where it is defined -
# not on the first module that uses it
fillProcLoc(findPendingModule(m, prc), prc.ast[namePos]) fillProcLoc(findPendingModule(m, prc), prc.ast[namePos])
#elif {sfExportc, sfImportc} * prc.flags == {}: #elif {sfExportc, sfImportc} * prc.flags == {}:
# # reset name to restore consistency in case of hashing collisions: # # reset name to restore consistency in case of hashing collisions:
@ -1781,10 +1782,6 @@ proc rawNewModule(g: BModuleList; module: PSym, filename: AbsoluteFile): BModule
else: AbsoluteFile"" else: AbsoluteFile""
open(result.ndi, ndiName, g.config) open(result.ndi, ndiName, g.config)
proc nullify[T](arr: var T) =
for i in low(arr)..high(arr):
arr[i] = Rope(nil)
proc rawNewModule(g: BModuleList; module: PSym; conf: ConfigRef): BModule = proc rawNewModule(g: BModuleList; module: PSym; conf: ConfigRef): BModule =
result = rawNewModule(g, module, AbsoluteFile toFullPath(conf, module.position.FileIndex)) result = rawNewModule(g, module, AbsoluteFile toFullPath(conf, module.position.FileIndex))
@ -1875,7 +1872,9 @@ proc myProcess(b: PPassContext, n: PNode): PNode =
result = n result = n
if b == nil: return if b == nil: return
var m = BModule(b) var m = BModule(b)
if passes.skipCodegen(m.config, n): return if passes.skipCodegen(m.config, n) or
not moduleHasChanged(m.g.graph, m.module):
return
m.initProc.options = initProcOptions(m) m.initProc.options = initProcOptions(m)
#softRnl = if optLineDir in m.config.options: noRnl else: rnl #softRnl = if optLineDir in m.config.options: noRnl else: rnl
# XXX replicate this logic! # XXX replicate this logic!
@ -1886,9 +1885,10 @@ proc myProcess(b: PPassContext, n: PNode): PNode =
genStmts(m.initProc, transformedN) genStmts(m.initProc, transformedN)
proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool = proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool =
result = true
if optForceFullMake notin m.config.globalOptions: if optForceFullMake notin m.config.globalOptions:
if not equalsFile(code, cfile.cname): if not moduleHasChanged(m.g.graph, m.module):
result = false
elif not equalsFile(code, cfile.cname):
if false: if false:
#m.config.symbolFiles == readOnlySf: #isDefined(m.config, "nimdiff"): #m.config.symbolFiles == readOnlySf: #isDefined(m.config, "nimdiff"):
if fileExists(cfile.cname): if fileExists(cfile.cname):
@ -1898,12 +1898,15 @@ proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool =
echo "new file ", cfile.cname.string echo "new file ", cfile.cname.string
if not writeRope(code, cfile.cname): if not writeRope(code, cfile.cname):
rawMessage(m.config, errCannotOpenFile, cfile.cname.string) rawMessage(m.config, errCannotOpenFile, cfile.cname.string)
return result = true
if fileExists(cfile.obj) and os.fileNewer(cfile.obj.string, cfile.cname.string): elif fileExists(cfile.obj) and os.fileNewer(cfile.obj.string, cfile.cname.string):
result = false result = false
else:
result = true
else: else:
if not writeRope(code, cfile.cname): if not writeRope(code, cfile.cname):
rawMessage(m.config, errCannotOpenFile, cfile.cname.string) rawMessage(m.config, errCannotOpenFile, cfile.cname.string)
result = true
# We need 2 different logics here: pending modules (including # We need 2 different logics here: pending modules (including
# 'nim__dat') may require file merging for the combination of dead code # 'nim__dat') may require file merging for the combination of dead code
@ -1915,6 +1918,7 @@ proc writeModule(m: BModule, pending: bool) =
let cfile = getCFile(m) let cfile = getCFile(m)
if true or optForceFullMake in m.config.globalOptions: if true or optForceFullMake in m.config.globalOptions:
if moduleHasChanged(m.g.graph, m.module):
genInitCode(m) genInitCode(m)
finishTypeDescriptions(m) finishTypeDescriptions(m)
if sfMainModule in m.module.flags: if sfMainModule in m.module.flags:
@ -1926,7 +1930,7 @@ proc writeModule(m: BModule, pending: bool) =
var cf = Cfile(nimname: m.module.name.s, cname: cfile, var cf = Cfile(nimname: m.module.name.s, cname: cfile,
obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {}) obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {})
var code = genModule(m, cf) var code = genModule(m, cf)
if code != nil: if code != nil or m.config.symbolFiles != disabledSf:
when hasTinyCBackend: when hasTinyCBackend:
if conf.cmd == cmdRun: if conf.cmd == cmdRun:
tccgen.compileCCode($code) tccgen.compileCCode($code)
@ -1983,6 +1987,7 @@ proc myClose(graph: ModuleGraph; b: PPassContext, n: PNode): PNode =
for destructorCall in graph.globalDestructors: for destructorCall in graph.globalDestructors:
n.add destructorCall n.add destructorCall
if passes.skipCodegen(m.config, n): return if passes.skipCodegen(m.config, n): return
if moduleHasChanged(graph, m.module):
# if the module is cached, we don't regenerate the main proc # if the module is cached, we don't regenerate the main proc
# nor the dispatchers? But if the dispatchers changed? # nor the dispatchers? But if the dispatchers changed?
# XXX emit the dispatchers into its own .c file? # XXX emit the dispatchers into its own .c file?

View file

@ -30,6 +30,8 @@ import
wordrecg, parseutils, nimblecmd, parseopt, sequtils, lineinfos, wordrecg, parseutils, nimblecmd, parseopt, sequtils, lineinfos,
pathutils, strtabs pathutils, strtabs
from incremental import nimIncremental
# but some have deps to imported modules. Yay. # but some have deps to imported modules. Yay.
bootSwitch(usedTinyC, hasTinyCBackend, "-d:tinyc") bootSwitch(usedTinyC, hasTinyCBackend, "-d:tinyc")
bootSwitch(usedNativeStacktrace, bootSwitch(usedNativeStacktrace,
@ -669,7 +671,7 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
helpOnError(conf, pass) helpOnError(conf, pass)
of "symbolfiles": discard "ignore for backwards compat" of "symbolfiles": discard "ignore for backwards compat"
of "incremental": of "incremental":
when not defined(nimIncremental): when not nimIncremental:
localError(conf, info, "the compiler was not built with " & localError(conf, info, "the compiler was not built with " &
"incremental compilation features; bootstrap with " & "incremental compilation features; bootstrap with " &
"-d:nimIncremental to enable") "-d:nimIncremental to enable")

View file

@ -10,13 +10,14 @@
## Basic type definitions the module graph needs in order to support ## Basic type definitions the module graph needs in order to support
## incremental compilations. ## incremental compilations.
const nimIncremental* = defined(nimIncremental) const nimIncremental* = true # defined(nimIncremental)
import options, lineinfos import options, lineinfos
when nimIncremental: when nimIncremental:
import ast, msgs, intsets, btrees, db_sqlite, std / sha1, pathutils import ast, msgs, intsets, btrees, db_sqlite, std / sha1, pathutils
from strutils import parseInt from strutils import parseInt
from os import isAbsolute
type type
Writer* = object Writer* = object
@ -47,7 +48,7 @@ when nimIncremental:
proc hashFileCached*(conf: ConfigRef; fileIdx: FileIndex; fullpath: AbsoluteFile): string = proc hashFileCached*(conf: ConfigRef; fileIdx: FileIndex; fullpath: AbsoluteFile): string =
result = msgs.getHash(conf, fileIdx) result = msgs.getHash(conf, fileIdx)
if result.len == 0: if result.len == 0 and isAbsolute(string fullpath):
result = $secureHashFile(string fullpath) result = $secureHashFile(string fullpath)
msgs.setHash(conf, fileIdx, result) msgs.setHash(conf, fileIdx, result)

View file

@ -77,8 +77,6 @@ proc compileModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags): P
if result == nil: if result == nil:
result = newModule(graph, fileIdx) result = newModule(graph, fileIdx)
result.flags = result.flags + flags result.flags = result.flags + flags
if sfMainModule in result.flags:
graph.config.mainPackageId = result.owner.id
result.id = id result.id = id
registerModule(graph, result) registerModule(graph, result)
else: else:

View file

@ -113,6 +113,8 @@ const
nkExportStmt, nkExportExceptStmt, nkFromStmt, nkImportStmt, nkImportExceptStmt} nkExportStmt, nkExportExceptStmt, nkFromStmt, nkImportStmt, nkImportExceptStmt}
proc prepareConfigNotes(graph: ModuleGraph; module: PSym) = proc prepareConfigNotes(graph: ModuleGraph; module: PSym) =
if sfMainModule in module.flags:
graph.config.mainPackageId = module.owner.id
# don't be verbose unless the module belongs to the main package: # don't be verbose unless the module belongs to the main package:
if module.owner.id == graph.config.mainPackageId: if module.owner.id == graph.config.mainPackageId:
graph.config.notes = graph.config.mainPackageNotes graph.config.notes = graph.config.mainPackageNotes
@ -121,7 +123,7 @@ proc prepareConfigNotes(graph: ModuleGraph; module: PSym) =
graph.config.notes = graph.config.foreignPackageNotes graph.config.notes = graph.config.foreignPackageNotes
proc moduleHasChanged*(graph: ModuleGraph; module: PSym): bool {.inline.} = proc moduleHasChanged*(graph: ModuleGraph; module: PSym): bool {.inline.} =
result = module.id >= 0 result = module.id >= 0 or isDefined(graph.config, "nimBackendAssumesChange")
proc processModule*(graph: ModuleGraph; module: PSym, stream: PLLStream): bool {.discardable.} = proc processModule*(graph: ModuleGraph; module: PSym, stream: PLLStream): bool {.discardable.} =
if graph.stopCompile(): return true if graph.stopCompile(): return true
@ -131,7 +133,7 @@ proc processModule*(graph: ModuleGraph; module: PSym, stream: PLLStream): bool {
s: PLLStream s: PLLStream
fileIdx = module.fileIdx fileIdx = module.fileIdx
prepareConfigNotes(graph, module) prepareConfigNotes(graph, module)
if not moduleHasChanged(graph, module): if module.id < 0:
# new module caching mechanism: # new module caching mechanism:
for i in 0 ..< graph.passes.len: for i in 0 ..< graph.passes.len:
if not isNil(graph.passes[i].open) and not graph.passes[i].isFrontend: if not isNil(graph.passes[i].open) and not graph.passes[i].isFrontend:

View file

@ -17,7 +17,6 @@ import strutils, os, intsets, tables, ropes, db_sqlite, msgs, options, types,
## - Dependency computation should use *signature* hashes in order to ## - Dependency computation should use *signature* hashes in order to
## avoid recompiling dependent modules. ## avoid recompiling dependent modules.
## - Patch the rest of the compiler to do lazy loading of proc bodies. ## - Patch the rest of the compiler to do lazy loading of proc bodies.
## - Patch the C codegen to cache proc bodies and maybe types.
template db(): DbConn = g.incr.db template db(): DbConn = g.incr.db
@ -72,9 +71,12 @@ proc getModuleId(g: ModuleGraph; fileIdx: FileIndex; fullpath: AbsoluteFile): in
# not changed, so use the cached AST: # not changed, so use the cached AST:
doAssert(result != 0) doAssert(result != 0)
var cycleCheck = initIntSet() var cycleCheck = initIntSet()
if not needsRecompile(g, fileIdx, fullpath, cycleCheck) and not g.incr.configChanged: if not needsRecompile(g, fileIdx, fullpath, cycleCheck):
echo "cached successfully! ", string fullpath if not g.incr.configChanged or g.config.symbolFiles == readOnlySf:
#echo "cached successfully! ", string fullpath
return -result return -result
elif g.config.symbolFiles == readOnlySf:
internalError(g.config, "file needs to be recompiled: " & (string fullpath))
db.exec(sql"update modules set fullHash = ? where id = ?", currentFullhash, module[0]) db.exec(sql"update modules set fullHash = ? where id = ?", currentFullhash, module[0])
db.exec(sql"delete from deps where module = ?", module[0]) db.exec(sql"delete from deps where module = ?", module[0])
db.exec(sql"delete from types where module = ?", module[0]) db.exec(sql"delete from types where module = ?", module[0])
@ -296,7 +298,7 @@ proc encodeSym(g: ModuleGraph, s: PSym, result: var string) =
pushSym(w, s.owner) pushSym(w, s.owner)
if s.flags != {}: if s.flags != {}:
result.add('$') result.add('$')
encodeVInt(cast[int32](s.flags), result) encodeVBiggestInt(cast[int64](s.flags), result)
if s.magic != mNone: if s.magic != mNone:
result.add('@') result.add('@')
encodeVInt(ord(s.magic), result) encodeVInt(ord(s.magic), result)
@ -723,7 +725,7 @@ proc loadSymFromBlob(g; b; info: TLineInfo): PSym =
result.owner = loadSym(g, decodeVInt(b.s, b.pos), result.info) result.owner = loadSym(g, decodeVInt(b.s, b.pos), result.info)
if b.s[b.pos] == '$': if b.s[b.pos] == '$':
inc(b.pos) inc(b.pos)
result.flags = cast[TSymFlags](int32(decodeVInt(b.s, b.pos))) result.flags = cast[TSymFlags](decodeVBiggestInt(b.s, b.pos))
if b.s[b.pos] == '@': if b.s[b.pos] == '@':
inc(b.pos) inc(b.pos)
result.magic = TMagic(decodeVInt(b.s, b.pos)) result.magic = TMagic(decodeVInt(b.s, b.pos))
@ -756,6 +758,7 @@ proc loadSymFromBlob(g; b; info: TLineInfo): PSym =
if b.s[b.pos] == '\24': if b.s[b.pos] == '\24':
inc b.pos inc b.pos
result.transformedBody = decodeNode(g, b, result.info) result.transformedBody = decodeNode(g, b, result.info)
#result.transformedBody = nil
of skModule, skPackage: of skModule, skPackage:
decodeInstantiations(g, b, result.info, result.usedGenerics) decodeInstantiations(g, b, result.info, result.usedGenerics)
of skLet, skVar, skField, skForVar: of skLet, skVar, skField, skForVar:
@ -769,7 +772,7 @@ proc loadSymFromBlob(g; b; info: TLineInfo): PSym =
if b.s[b.pos] == '(': if b.s[b.pos] == '(':
#if result.kind in routineKinds: #if result.kind in routineKinds:
# result.ast = decodeNodeLazyBody(b, result.info, result) # result.ast = nil
#else: #else:
result.ast = decodeNode(g, b, result.info) result.ast = decodeNode(g, b, result.info)
if sfCompilerProc in result.flags: if sfCompilerProc in result.flags:
@ -886,6 +889,10 @@ proc replay(g: ModuleGraph; module: PSym; n: PNode) =
internalAssert g.config, imported.id < 0 internalAssert g.config, imported.id < 0
of nkStmtList, nkStmtListExpr: of nkStmtList, nkStmtListExpr:
for x in n: replay(g, module, x) for x in n: replay(g, module, x)
of nkExportStmt:
for x in n:
doAssert x.kind == nkSym
strTableAdd(module.tab, x.sym)
else: discard "nothing to do for this node" else: discard "nothing to do for this node"
proc loadNode*(g: ModuleGraph; module: PSym): PNode = proc loadNode*(g: ModuleGraph; module: PSym): PNode =

View file

@ -46,14 +46,9 @@ proc toStrMaxPrecision*(f: BiggestFloat, literalPostfix = ""): string =
of fcNegInf: of fcNegInf:
result = "-INF" result = "-INF"
else: else:
when defined(nimNoArrayToCstringConversion):
result = newString(81) result = newString(81)
let n = c_snprintf(result.cstring, result.len.uint, "%#.16e%s", f, literalPostfix.cstring) let n = c_snprintf(result.cstring, result.len.uint, "%#.16e%s", f, literalPostfix.cstring)
setLen(result, n) setLen(result, n)
else:
var buf: array[0..80, char]
discard c_snprintf(buf.cstring, buf.len.uint, "%#.16e%s", f, literalPostfix.cstring)
result = $buf.cstring
proc encodeStr*(s: string, result: var string) = proc encodeStr*(s: string, result: var string) =
for i in 0 ..< len(s): for i in 0 ..< len(s):

View file

@ -95,8 +95,8 @@ and code execution in the executable.
The compiler parses Nim source code into an internal data structure called the The compiler parses Nim source code into an internal data structure called the
`abstract syntax tree`:idx: (`AST`:idx:). Then, before executing the code or `abstract syntax tree`:idx: (`AST`:idx:). Then, before executing the code or
compiling it into the executable, it transforms the AST through `semantic compiling it into the executable, it transforms the AST through
analysis`:idx:. This adds semantic information such as expression types, `semantic analysis`:idx:. This adds semantic information such as expression types,
identifier meanings, and in some cases expression values. An error detected identifier meanings, and in some cases expression values. An error detected
during semantic analysis is called a `static error`:idx:. Errors described in during semantic analysis is called a `static error`:idx:. Errors described in
this manual are static errors when not otherwise specified. this manual are static errors when not otherwise specified.

View file

@ -10,7 +10,7 @@
# #
const const
NimbleStableCommit = "d15c8530cb7480ce39ffa85a2dd9819d2d4fc645" # 0.10.2 NimbleStableCommit = "da82e3111e662fc1b12f96b3cddd66c749c0f686" # master
when defined(gcc) and defined(windows): when defined(gcc) and defined(windows):
when defined(x86): when defined(x86):

View file

@ -10,8 +10,9 @@
## Include for the tester that contains test suites that test special features ## Include for the tester that contains test suites that test special features
## of the compiler. ## of the compiler.
# included from tester.nim
import important_packages import important_packages
import sequtils
const const
specialCategories = [ specialCategories = [
@ -24,6 +25,7 @@ const
"gc", "gc",
"io", "io",
"js", "js",
"ic",
"lib", "lib",
"longgc", "longgc",
"manyloc", "manyloc",
@ -41,58 +43,52 @@ const
"dir with space" "dir with space"
] ]
# included from tester.nim proc isTestFile*(file: string): bool =
# ---------------- ROD file tests --------------------------------------------- let (_, name, ext) = splitFile(file)
result = ext == ".nim" and name.startsWith("t")
# ---------------- IC tests ---------------------------------------------
proc icTests(r: var TResults; testsDir: string, cat: Category, options: string) =
const const
rodfilesDir = "tests/rodfiles" tooltests = ["compiler/nim.nim", "tools/nimgrep.nim"]
writeOnly = " --incremental:writeonly "
readOnly = " --incremental:readonly "
incrementalOn = " --incremental:on "
proc delNimCache(filename, options: string) = template test(x: untyped) =
for target in low(TTarget)..high(TTarget): testSpecWithNimcache(r, makeRawTest(file, x & options, cat), nimcache)
let dir = nimcacheDir(filename, options, target)
try:
removeDir(dir)
except OSError:
echo "[Warning] could not delete: ", dir
proc runRodFiles(r: var TResults, cat: Category, options: string) = template editedTest(x: untyped) =
template test(filename: string, clearCacheFirst=false) = var test = makeTest(file, x & options, cat)
if clearCacheFirst: delNimCache(filename, options) test.spec.targets = {getTestSpecTarget()}
testSpec r, makeTest(rodfilesDir / filename, options, cat) testSpecWithNimcache(r, test, nimcache)
const tempExt = "_temp.nim"
for it in walkDirRec(testsDir / "ic"):
if isTestFile(it) and not it.endsWith(tempExt):
let nimcache = nimcacheDir(it, options, getTestSpecTarget())
removeDir(nimcache)
# test basic recompilation scheme: let content = readFile(it)
test "hallo", true for fragment in content.split("#!EDIT!#"):
test "hallo" let file = it.replace(".nim", tempExt)
when false: writeFile(file, fragment)
# test incremental type information: let oldPassed = r.passed
test "hallo2" editedTest incrementalOn
if r.passed != oldPassed+1: break
# test type converters: for file in tooltests:
test "aconv", true let nimcache = nimcacheDir(file, options, getTestSpecTarget())
test "bconv" removeDir(nimcache)
# test G, A, B example from the documentation; test init sections: let oldPassed = r.passed
test "deada", true test writeOnly
test "deada2"
when false: if r.passed == oldPassed+1:
# test method generation: test readOnly
test "bmethods", true if r.passed == oldPassed+2:
test "bmethods2" test readOnly & "-d:nimBackendAssumesChange "
# test generics:
test "tgeneric1", true
test "tgeneric2"
proc compileRodFiles(r: var TResults, cat: Category, options: string) =
template test(filename: untyped, clearCacheFirst=true) =
if clearCacheFirst: delNimCache(filename, options)
testSpec r, makeTest(rodfilesDir / filename, options, cat)
# test DLL interfacing:
test "gtkex1", true
test "gtkex2"
# --------------------- flags tests ------------------------------------------- # --------------------- flags tests -------------------------------------------
@ -116,12 +112,6 @@ proc flagTests(r: var TResults, cat: Category, options: string) =
# --------------------- DLL generation tests ---------------------------------- # --------------------- DLL generation tests ----------------------------------
proc safeCopyFile(src, dest: string) =
try:
copyFile(src, dest)
except OSError:
echo "[Warning] could not copy: ", src, " to ", dest
proc runBasicDLLTest(c, r: var TResults, cat: Category, options: string) = proc runBasicDLLTest(c, r: var TResults, cat: Category, options: string) =
const rpath = when defined(macosx): const rpath = when defined(macosx):
" --passL:-rpath --passL:@loader_path" " --passL:-rpath --passL:@loader_path"
@ -153,11 +143,12 @@ proc runBasicDLLTest(c, r: var TResults, cat: Category, options: string) =
if "boehm" notin options: if "boehm" notin options:
# force build required - see the comments in the .nim file for more details # force build required - see the comments in the .nim file for more details
var hcr_integration = makeTest("tests/dll/nimhcr_integration.nim", var hcri = makeTest("tests/dll/nimhcr_integration.nim",
options & " --forceBuild --hotCodeReloading:on" & rpath, cat) options & " --forceBuild --hotCodeReloading:on" & rpath, cat)
hcr_integration.args = prepareTestArgs(hcr_integration.spec.getCmd, hcr_integration.name, let nimcache = nimcacheDir(hcri.name, hcri.options, getTestSpecTarget())
hcr_integration.options, getTestSpecTarget()) hcri.args = prepareTestArgs(hcri.spec.getCmd, hcri.name,
testSpec r, hcr_integration hcri.options, nimcache, getTestSpecTarget())
testSpec r, hcri
proc dllTests(r: var TResults, cat: Category, options: string) = proc dllTests(r: var TResults, cat: Category, options: string) =
# dummy compile result: # dummy compile result:
@ -363,7 +354,8 @@ proc testNimInAction(r: var TResults, cat: Category, options: string) =
for i, test in tests: for i, test in tests:
let filename = testsDir / test.addFileExt("nim") let filename = testsDir / test.addFileExt("nim")
let testHash = getMD5(readFile(filename).string) let testHash = getMD5(readFile(filename).string)
doAssert testHash == refHashes[i], "Nim in Action test " & filename & " was changed: " & $(i: i, testHash: testHash, refHash: refHashes[i]) doAssert testHash == refHashes[i], "Nim in Action test " & filename &
" was changed: " & $(i: i, testHash: testHash, refHash: refHashes[i])
# Run the tests. # Run the tests.
for testfile in tests: for testfile in tests:
test "tests/" & testfile & ".nim" test "tests/" & testfile & ".nim"
@ -449,25 +441,6 @@ let
nimbleExe = findExe("nimble") nimbleExe = findExe("nimble")
packageIndex = nimbleDir / "packages_official.json" packageIndex = nimbleDir / "packages_official.json"
proc waitForExitEx(p: Process): int =
var outp = outputStream(p)
var line = newStringOfCap(120).TaintedString
while true:
if outp.readLine(line):
discard
else:
result = peekExitCode(p)
if result != -1: break
close(p)
proc getPackageDir(package: string): string =
## TODO - Replace this with dom's version comparison magic.
let commandOutput = execCmdEx("nimble path $#" % package)
if commandOutput.exitCode != QuitSuccess:
return ""
else:
result = commandOutput[0].string
iterator listPackages(): tuple[name, url, cmd: string, hasDeps: bool] = iterator listPackages(): tuple[name, url, cmd: string, hasDeps: bool] =
let defaultCmd = "nimble test" let defaultCmd = "nimble test"
let packageList = parseFile(packageIndex) let packageList = parseFile(packageIndex)
@ -481,8 +454,8 @@ iterator listPackages(): tuple[name, url, cmd: string, hasDeps: bool] =
let name = package["name"].str let name = package["name"].str
if name == n: if name == n:
found = true found = true
let p_url = package["url"].str let pUrl = package["url"].str
yield (name, p_url, cmd, hasDeps) yield (name, pUrl, cmd, hasDeps)
break break
if not found: if not found:
raise newException(ValueError, "Cannot find package '$#'." % n) raise newException(ValueError, "Cannot find package '$#'." % n)
@ -551,7 +524,7 @@ proc testNimblePackages(r: var TResults, cat: Category) =
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
const AdditionalCategories = ["debugger", "examples", "lib"] const AdditionalCategories = ["debugger", "examples", "lib", "ic"]
const MegaTestCat = "megatest" const MegaTestCat = "megatest"
proc `&.?`(a, b: string): string = proc `&.?`(a, b: string): string =
@ -584,17 +557,12 @@ proc isJoinableSpec(spec: TSpec): bool =
(spec.targets == {} or spec.targets == {targetC}) (spec.targets == {} or spec.targets == {targetC})
proc norm(s: var string) = proc norm(s: var string) =
# equivalent of s/\n+/\n/g (could use a single pass over input if needed)
while true: while true:
let tmp = s.replace("\n\n", "\n") let tmp = s.replace("\n\n", "\n")
if tmp == s: break if tmp == s: break
s = tmp s = tmp
s = s.strip s = s.strip
proc isTestFile*(file: string): bool =
let (_, name, ext) = splitFile(file)
result = ext == ".nim" and name.startsWith("t")
proc quoted(a: string): string = proc quoted(a: string): string =
# todo: consider moving to system.nim # todo: consider moving to system.nim
result.addQuoted(a) result.addQuoted(a)
@ -607,7 +575,7 @@ proc runJoinedTest(r: var TResults, cat: Category, testsDir: string) =
let cat = dir[testsDir.len .. ^1] let cat = dir[testsDir.len .. ^1]
if kind == pcDir and cat notin specialCategories: if kind == pcDir and cat notin specialCategories:
for file in walkDirRec(testsDir / cat): for file in walkDirRec(testsDir / cat):
if not isTestFile(file): continue if isTestFile(file):
let spec = parseSpec(file) let spec = parseSpec(file)
if isJoinableSpec(spec): if isJoinableSpec(spec):
specs.add spec specs.add spec
@ -646,14 +614,13 @@ proc runJoinedTest(r: var TResults, cat: Category, testsDir: string) =
let args = ["c", "--nimCache:" & outDir, "-d:testing", "--listCmd", let args = ["c", "--nimCache:" & outDir, "-d:testing", "--listCmd",
"--listFullPaths:off", "--excessiveStackTrace:off", "megatest.nim"] "--listFullPaths:off", "--excessiveStackTrace:off", "megatest.nim"]
proc onStdout(line: string) = echo line
var (cmdLine, buf, exitCode) = execCmdEx2(command = compilerPrefix, args = args, input = "") var (cmdLine, buf, exitCode) = execCmdEx2(command = compilerPrefix, args = args, input = "")
if exitCode != 0: if exitCode != 0:
echo "$ ", cmdLine echo "$ ", cmdLine
echo buf.string echo buf.string
quit("megatest compilation failed") quit("megatest compilation failed")
# Could also use onStdout here.
(buf, exitCode) = execCmdEx("./megatest") (buf, exitCode) = execCmdEx("./megatest")
if exitCode != 0: if exitCode != 0:
echo buf.string echo buf.string
@ -690,6 +657,8 @@ proc processCategory(r: var TResults, cat: Category,
when false: when false:
compileRodFiles(r, cat, options) compileRodFiles(r, cat, options)
runRodFiles(r, cat, options) runRodFiles(r, cat, options)
of "ic":
icTests(r, testsDir, cat, options)
of "js": of "js":
# only run the JS tests on Windows or Linux because Travis is bad # only run the JS tests on Windows or Linux because Travis is bad
# and other OSes like Haiku might lack nodejs: # and other OSes like Haiku might lack nodejs:

View file

@ -9,7 +9,7 @@
## HTML generator for the tester. ## HTML generator for the tester.
import cgi, backend, strutils, json, os, tables, times import strutils, json, os, times
import "testamenthtml.nimf" import "testamenthtml.nimf"

View file

@ -1,4 +1,3 @@
import strutils
template pkg(name: string; cmd = "nimble test"; hasDeps = false; url = ""): untyped = template pkg(name: string; cmd = "nimble test"; hasDeps = false; url = ""): untyped =
packages.add((name, cmd, hasDeps, url)) packages.add((name, cmd, hasDeps, url))

View file

@ -7,7 +7,7 @@
# distribution, for details about the copyright. # distribution, for details about the copyright.
# #
import sequtils, parseutils, strutils, os, osproc, streams, parsecfg import sequtils, parseutils, strutils, os, streams, parsecfg
var compilerPrefix* = findExe("nim") var compilerPrefix* = findExe("nim")
@ -122,6 +122,9 @@ proc addLine*(self: var string; a,b: string) =
self.add b self.add b
self.add "\n" self.add "\n"
proc initSpec*(filename: string): TSpec =
result.file = filename
proc parseSpec*(filename: string): TSpec = proc parseSpec*(filename: string): TSpec =
result.file = filename result.file = filename
let specStr = extractSpec(filename) let specStr = extractSpec(filename)

View file

@ -10,9 +10,9 @@
## This program verifies Nim against the testcases. ## This program verifies Nim against the testcases.
import import
parseutils, strutils, pegs, os, osproc, streams, parsecfg, json, strutils, pegs, os, osproc, streams, json,
marshal, backend, parseopt, specs, htmlgen, browsers, terminal, backend, parseopt, specs, htmlgen, browsers, terminal,
algorithm, times, sets, md5, sequtils algorithm, times, md5, sequtils
include compiler/nodejs include compiler/nodejs
@ -124,19 +124,20 @@ proc execCmdEx2(command: string, args: openarray[string]; workingDir, input: str
proc nimcacheDir(filename, options: string, target: TTarget): string = proc nimcacheDir(filename, options: string, target: TTarget): string =
## Give each test a private nimcache dir so they don't clobber each other's. ## Give each test a private nimcache dir so they don't clobber each other's.
let hashInput = options & $target let hashInput = options & $target
return "nimcache" / (filename & '_' & hashInput.getMD5) result = "nimcache" / (filename & '_' & hashInput.getMD5)
proc prepareTestArgs(cmdTemplate, filename, options: string, proc prepareTestArgs(cmdTemplate, filename, options, nimcache: string,
target: TTarget, extraOptions=""): seq[string] = target: TTarget, extraOptions=""): seq[string] =
let nimcache = nimcacheDir(filename, options, target)
let options = options & " " & quoteShell("--nimCache:" & nimcache) & extraOptions let options = options & " " & quoteShell("--nimCache:" & nimcache) & extraOptions
return parseCmdLine(cmdTemplate % ["target", targetToCmd[target], result = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
"options", options, "file", filename.quoteShell, "options", options, "file", filename.quoteShell,
"filedir", filename.getFileDir()]) "filedir", filename.getFileDir()])
proc callCompiler(cmdTemplate, filename, options: string, proc callCompiler(cmdTemplate, filename, options, nimcache: string,
target: TTarget, extraOptions=""): TSpec = target: TTarget,
let c = prepareTestArgs(cmdTemplate, filename, options, target, extraOptions) extraOptions=""): TSpec =
let c = prepareTestArgs(cmdTemplate, filename, options, nimcache, target,
extraOptions)
result.cmd = quoteShellCommand(c) result.cmd = quoteShellCommand(c)
var p = startProcess(command=c[0], args=c[1 .. ^1], var p = startProcess(command=c[0], args=c[1 .. ^1],
options={poStdErrToStdOut, poUsePath}) options={poStdErrToStdOut, poUsePath})
@ -370,7 +371,7 @@ proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) =
currentPos = giv.find(line.strip, currentPos) currentPos = giv.find(line.strip, currentPos)
if currentPos < 0: if currentPos < 0:
given.err = reMsgsDiffer given.err = reMsgsDiffer
return break
proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec, proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec,
expected: TSpec; r: var TResults) = expected: TSpec; r: var TResults) =
@ -391,9 +392,9 @@ proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec,
proc getTestSpecTarget(): TTarget = proc getTestSpecTarget(): TTarget =
if getEnv("NIM_COMPILE_TO_CPP", "false").string == "true": if getEnv("NIM_COMPILE_TO_CPP", "false").string == "true":
return targetCpp result = targetCpp
else: else:
return targetC result = targetC
proc checkDisabled(r: var TResults, test: TTest): bool = proc checkDisabled(r: var TResults, test: TTest): bool =
if test.spec.err in {reDisabled, reJoined}: if test.spec.err in {reDisabled, reJoined}:
@ -401,59 +402,34 @@ proc checkDisabled(r: var TResults, test: TTest): bool =
r.addResult(test, targetC, "", "", test.spec.err) r.addResult(test, targetC, "", "", test.spec.err)
inc(r.skipped) inc(r.skipped)
inc(r.total) inc(r.total)
return result = false
true else:
result = true
proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) = var count = 0
var expected = test.spec
if expected.parseErrors.len > 0:
# targetC is a lie, but parameter is required
r.addResult(test, targetC, "", expected.parseErrors, reInvalidSpec)
inc(r.total)
return
if not checkDisabled(r, test): return
expected.targets.incl targets
# still no target specified at all
if expected.targets == {}:
expected.targets = {getTestSpecTarget()}
for target in expected.targets:
inc(r.total)
if target notin gTargets:
r.addResult(test, target, "", "", reDisabled)
inc(r.skipped)
continue
if simulate:
var count {.global.} = 0
count.inc
echo "testSpec count: ", count, " expected: ", expected
continue
proc testSpecHelper(r: var TResults, test: TTest, expected: TSpec, target: TTarget, nimcache: string) =
case expected.action case expected.action
of actionCompile: of actionCompile:
var given = callCompiler(expected.getCmd, test.name, test.options, target, var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target,
extraOptions = " --stdout --hint[Path]:off --hint[Processing]:off") extraOptions = " --stdout --hint[Path]:off --hint[Processing]:off")
compilerOutputTests(test, target, given, expected, r) compilerOutputTests(test, target, given, expected, r)
of actionRun: of actionRun:
# In this branch of code "early return" pattern is clearer than deep var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target)
# nested conditionals - the empty rows in between to clarify the "danger"
var given = callCompiler(expected.getCmd, test.name, test.options, target)
if given.err != reSuccess: if given.err != reSuccess:
r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err) r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err)
continue else:
let isJsTarget = target == targetJS let isJsTarget = target == targetJS
var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt) var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt)
if not existsFile(exeFile): if not existsFile(exeFile):
r.addResult(test, target, expected.output, r.addResult(test, target, expected.output,
"executable not found: " & exeFile, reExeNotFound) "executable not found: " & exeFile, reExeNotFound)
continue else:
let nodejs = if isJsTarget: findNodeJs() else: "" let nodejs = if isJsTarget: findNodeJs() else: ""
if isJsTarget and nodejs == "": if isJsTarget and nodejs == "":
r.addResult(test, target, expected.output, "nodejs binary not in PATH", r.addResult(test, target, expected.output, "nodejs binary not in PATH",
reExeNotFound) reExeNotFound)
continue else:
var exeCmd: string var exeCmd: string
var args = test.args var args = test.args
if isJsTarget: if isJsTarget:
@ -461,7 +437,7 @@ proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
args = concat(@[exeFile], args) args = concat(@[exeFile], args)
else: else:
exeCmd = exeFile exeCmd = exeFile
var (cmdLine, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input) var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
# Treat all failure codes from nodejs as 1. Older versions of nodejs used # Treat all failure codes from nodejs as 1. Older versions of nodejs used
# to return other codes, but for us it is sufficient to know that it's not 0. # to return other codes, but for us it is sufficient to know that it's not 0.
if exitCode != 0: exitCode = 1 if exitCode != 0: exitCode = 1
@ -476,19 +452,48 @@ proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
r.addResult(test, target, "exitcode: " & $expected.exitCode, r.addResult(test, target, "exitcode: " & $expected.exitCode,
"exitcode: " & $exitCode & "\n\nOutput:\n" & "exitcode: " & $exitCode & "\n\nOutput:\n" &
bufB, reExitCodesDiffer) bufB, reExitCodesDiffer)
continue elif (expected.outputCheck == ocEqual and expected.output != bufB) or
if (expected.outputCheck == ocEqual and expected.output != bufB) or
(expected.outputCheck == ocSubstr and expected.output notin bufB): (expected.outputCheck == ocSubstr and expected.output notin bufB):
given.err = reOutputsDiffer given.err = reOutputsDiffer
r.addResult(test, target, expected.output, bufB, reOutputsDiffer) r.addResult(test, target, expected.output, bufB, reOutputsDiffer)
continue else:
compilerOutputTests(test, target, given, expected, r) compilerOutputTests(test, target, given, expected, r)
continue
of actionReject: of actionReject:
var given = callCompiler(expected.getCmd, test.name, test.options, var given = callCompiler(expected.getCmd, test.name, test.options,
target) nimcache, target)
cmpMsgs(r, expected, given, test, target) cmpMsgs(r, expected, given, test, target)
continue
proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
var expected = test.spec
if expected.parseErrors.len > 0:
# targetC is a lie, but a parameter is required
r.addResult(test, targetC, "", expected.parseErrors, reInvalidSpec)
inc(r.total)
return
if not checkDisabled(r, test): return
expected.targets.incl targets
# still no target specified at all
if expected.targets == {}:
expected.targets = {getTestSpecTarget()}
for target in expected.targets:
inc(r.total)
if target notin gTargets:
r.addResult(test, target, "", "", reDisabled)
inc(r.skipped)
elif simulate:
inc count
echo "testSpec count: ", count, " expected: ", expected
else:
let nimcache = nimcacheDir(test.name, test.options, target)
testSpecHelper(r, test, expected, target, nimcache)
proc testSpecWithNimcache(r: var TResults, test: TTest; nimcache: string) =
if not checkDisabled(r, test): return
for target in test.spec.targets:
inc(r.total)
testSpecHelper(r, test, test.spec, target, nimcache)
proc testC(r: var TResults, test: TTest, action: TTestAction) = proc testC(r: var TResults, test: TTest, action: TTestAction) =
# runs C code. Doesn't support any specs, just goes by exit code. # runs C code. Doesn't support any specs, just goes by exit code.
@ -529,6 +534,15 @@ proc makeTest(test, options: string, cat: Category): TTest =
result.spec = parseSpec(addFileExt(test, ".nim")) result.spec = parseSpec(addFileExt(test, ".nim"))
result.startTime = epochTime() result.startTime = epochTime()
proc makeRawTest(test, options: string, cat: Category): TTest =
result.cat = cat
result.name = test
result.options = options
result.spec = initSpec(addFileExt(test, ".nim"))
result.startTime = epochTime()
result.spec.action = actionCompile
result.spec.targets = {getTestSpecTarget()}
# TODO: fix these files # TODO: fix these files
const disabledFilesDefault = @[ const disabledFilesDefault = @[
"LockFreeHash.nim", "LockFreeHash.nim",
@ -553,23 +567,18 @@ when defined(windows):
else: else:
const const
# array of modules disabled from compilation test of stdlib. # array of modules disabled from compilation test of stdlib.
# TODO: why the ["-"]? (previous code should've prob used seq[string] = @[] instead) disabledFiles = disabledFilesDefault
disabledFiles = disabledFilesDefault & @["-"]
include categories include categories
proc loadSkipFrom(name: string): seq[string] = proc loadSkipFrom(name: string): seq[string] =
if name.len() == 0: return if name.len == 0: return
# One skip per line, comments start with # # One skip per line, comments start with #
# used by `nlvm` (at least) # used by `nlvm` (at least)
try:
for line in lines(name): for line in lines(name):
let sline = line.strip() let sline = line.strip()
if sline.len > 0 and not sline.startsWith("#"): if sline.len > 0 and not sline.startsWith("#"):
result.add sline result.add sline
except:
echo "Could not load " & name & ", ignoring"
proc main() = proc main() =
os.putenv "NIMTEST_COLOR", "never" os.putenv "NIMTEST_COLOR", "never"

39
tests/ic/tgenerics.nim Normal file
View file

@ -0,0 +1,39 @@
discard """
output: "bar"
disabled: "true"
"""
import tables
var tab: Table[string, string]
tab["foo"] = "bar"
echo tab["foo"]
#!EDIT!#
discard """
output: "bar 3"
"""
import tables
var tab: Table[string, string]
var tab2: Table[string, int]
tab["foo"] = "bar"
tab2["meh"] = 3
echo tab["foo"], " ", tab2["meh"]
#!EDIT!#
discard """
output: "3"
"""
import tables
var tab2: Table[string, int]
tab2["meh"] = 3
echo tab2["meh"]

28
tests/ic/thallo.nim Normal file
View file

@ -0,0 +1,28 @@
discard """
output: "Hello World"
"""
const str = "Hello World"
echo str
# Splitters are done with this special comment:
#!EDIT!#
discard """
output: "Hello World B"
"""
const str = "Hello World"
echo str, " B"
#!EDIT!#
discard """
output: "Hello World C"
"""
const str = "Hello World"
var x = 7
if 3+4 == x:
echo str, " C"