summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2019-08-08 08:41:05 +0200
committerGitHub <noreply@github.com>2019-08-08 08:41:05 +0200
commitc8cffaf42037ae8defe59d9a1fb7d202655aa1ee (patch)
treed9bf11748ee18dccf00fefb2f3d6aaeff8e115ff
parentc0d240b8cd3dc08d25c671b0dc7614fbfa980c2e (diff)
downloadNim-c8cffaf42037ae8defe59d9a1fb7d202655aa1ee.tar.gz
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
-rw-r--r--compiler/cgen.nim115
-rw-r--r--compiler/commands.nim4
-rw-r--r--compiler/incremental.nim5
-rw-r--r--compiler/modules.nim2
-rw-r--r--compiler/passes.nim6
-rw-r--r--compiler/rodimpl.nim21
-rw-r--r--compiler/rodutils.nim11
-rw-r--r--doc/manual.rst4
-rw-r--r--koch.nim2
-rw-r--r--testament/categories.nim161
-rw-r--r--testament/htmlgen.nim2
-rw-r--r--testament/important_packages.nim1
-rw-r--r--testament/specs.nim5
-rw-r--r--testament/tester.nim195
-rw-r--r--tests/ic/tgenerics.nim39
-rw-r--r--tests/ic/thallo.nim28
16 files changed, 329 insertions, 272 deletions
diff --git a/compiler/cgen.nim b/compiler/cgen.nim
index c8906f380..c0ef4e1e3 100644
--- a/compiler/cgen.nim
+++ b/compiler/cgen.nim
@@ -1108,7 +1108,8 @@ proc genProcNoForward(m: BModule, prc: PSym) =
     # a check for ``m.declaredThings``.
     if not containsOrIncl(m.declaredThings, prc.id):
       #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])
       #elif {sfExportc, sfImportc} * prc.flags == {}:
       #  # 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""
   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 =
   result = rawNewModule(g, module, AbsoluteFile toFullPath(conf, module.position.FileIndex))
 
@@ -1875,7 +1872,9 @@ proc myProcess(b: PPassContext, n: PNode): PNode =
   result = n
   if b == nil: return
   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)
   #softRnl = if optLineDir in m.config.options: noRnl else: rnl
   # XXX replicate this logic!
@@ -1886,9 +1885,10 @@ proc myProcess(b: PPassContext, n: PNode): PNode =
     genStmts(m.initProc, transformedN)
 
 proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool =
-  result = true
   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:
         #m.config.symbolFiles == readOnlySf: #isDefined(m.config, "nimdiff"):
         if fileExists(cfile.cname):
@@ -1898,12 +1898,15 @@ proc shouldRecompile(m: BModule; code: Rope, cfile: Cfile): bool =
           echo "new file ", cfile.cname.string
       if not writeRope(code, cfile.cname):
         rawMessage(m.config, errCannotOpenFile, cfile.cname.string)
-      return
-    if fileExists(cfile.obj) and os.fileNewer(cfile.obj.string, cfile.cname.string):
+      result = true
+    elif fileExists(cfile.obj) and os.fileNewer(cfile.obj.string, cfile.cname.string):
       result = false
+    else:
+      result = true
   else:
     if not writeRope(code, cfile.cname):
       rawMessage(m.config, errCannotOpenFile, cfile.cname.string)
+    result = true
 
 # We need 2 different logics here: pending modules (including
 # 'nim__dat') may require file merging for the combination of dead code
@@ -1915,18 +1918,19 @@ proc writeModule(m: BModule, pending: bool) =
   let cfile = getCFile(m)
 
   if true or optForceFullMake in m.config.globalOptions:
-    genInitCode(m)
-    finishTypeDescriptions(m)
-    if sfMainModule in m.module.flags:
-      # generate main file:
-      genMainProc(m)
-      add(m.s[cfsProcHeaders], m.g.mainModProcs)
-      generateThreadVarsSize(m)
+    if moduleHasChanged(m.g.graph, m.module):
+      genInitCode(m)
+      finishTypeDescriptions(m)
+      if sfMainModule in m.module.flags:
+        # generate main file:
+        genMainProc(m)
+        add(m.s[cfsProcHeaders], m.g.mainModProcs)
+        generateThreadVarsSize(m)
 
     var cf = Cfile(nimname: m.module.name.s, cname: cfile,
                    obj: completeCfilePath(m.config, toObjFile(m.config, cfile)), flags: {})
     var code = genModule(m, cf)
-    if code != nil:
+    if code != nil or m.config.symbolFiles != disabledSf:
       when hasTinyCBackend:
         if conf.cmd == cmdRun:
           tccgen.compileCCode($code)
@@ -1983,46 +1987,47 @@ proc myClose(graph: ModuleGraph; b: PPassContext, n: PNode): PNode =
     for destructorCall in graph.globalDestructors:
       n.add destructorCall
   if passes.skipCodegen(m.config, n): return
-  # if the module is cached, we don't regenerate the main proc
-  # nor the dispatchers? But if the dispatchers changed?
-  # XXX emit the dispatchers into its own .c file?
-  if n != nil:
-    m.initProc.options = initProcOptions(m)
-    genStmts(m.initProc, n)
+  if moduleHasChanged(graph, m.module):
+    # if the module is cached, we don't regenerate the main proc
+    # nor the dispatchers? But if the dispatchers changed?
+    # XXX emit the dispatchers into its own .c file?
+    if n != nil:
+      m.initProc.options = initProcOptions(m)
+      genStmts(m.initProc, n)
 
-  if m.hcrOn:
-    # make sure this is pulled in (meaning hcrGetGlobal() is called for it during init)
-    discard cgsym(m, "programResult")
-    if m.inHcrInitGuard:
-      endBlock(m.initProc)
-
-  if sfMainModule in m.module.flags:
     if m.hcrOn:
-      # pull ("define" since they are inline when HCR is on) these functions in the main file
-      # so it can load the HCR runtime and later pass the library handle to the HCR runtime which
-      # will in turn pass it to the other modules it initializes so they can initialize the
-      # register/get procs so they don't have to have the definitions of these functions as well
-      discard cgsym(m, "nimLoadLibrary")
-      discard cgsym(m, "nimLoadLibraryError")
-      discard cgsym(m, "nimGetProcAddr")
-      discard cgsym(m, "procAddrError")
-      discard cgsym(m, "rawWrite")
-
-    # raise dependencies on behalf of genMainProc
-    if m.config.target.targetOS != osStandalone and m.config.selectedGC != gcNone:
-      discard cgsym(m, "initStackBottomWith")
-    if emulatedThreadVars(m.config) and m.config.target.targetOS != osStandalone:
-      discard cgsym(m, "initThreadVarsEmulation")
+      # make sure this is pulled in (meaning hcrGetGlobal() is called for it during init)
+      discard cgsym(m, "programResult")
+      if m.inHcrInitGuard:
+        endBlock(m.initProc)
 
-    if m.g.breakpoints != nil:
-      discard cgsym(m, "dbgRegisterBreakpoint")
-    if optEndb in m.config.options:
-      discard cgsym(m, "dbgRegisterFilename")
-
-    if m.g.forwardedProcs.len == 0:
-      incl m.flags, objHasKidsValid
-    let disp = generateMethodDispatchers(graph)
-    for x in disp: genProcAux(m, x.sym)
+    if sfMainModule in m.module.flags:
+      if m.hcrOn:
+        # pull ("define" since they are inline when HCR is on) these functions in the main file
+        # so it can load the HCR runtime and later pass the library handle to the HCR runtime which
+        # will in turn pass it to the other modules it initializes so they can initialize the
+        # register/get procs so they don't have to have the definitions of these functions as well
+        discard cgsym(m, "nimLoadLibrary")
+        discard cgsym(m, "nimLoadLibraryError")
+        discard cgsym(m, "nimGetProcAddr")
+        discard cgsym(m, "procAddrError")
+        discard cgsym(m, "rawWrite")
+
+      # raise dependencies on behalf of genMainProc
+      if m.config.target.targetOS != osStandalone and m.config.selectedGC != gcNone:
+        discard cgsym(m, "initStackBottomWith")
+      if emulatedThreadVars(m.config) and m.config.target.targetOS != osStandalone:
+        discard cgsym(m, "initThreadVarsEmulation")
+
+      if m.g.breakpoints != nil:
+        discard cgsym(m, "dbgRegisterBreakpoint")
+      if optEndb in m.config.options:
+        discard cgsym(m, "dbgRegisterFilename")
+
+      if m.g.forwardedProcs.len == 0:
+        incl m.flags, objHasKidsValid
+      let disp = generateMethodDispatchers(graph)
+      for x in disp: genProcAux(m, x.sym)
 
   m.g.modulesClosed.add m
 
diff --git a/compiler/commands.nim b/compiler/commands.nim
index ed0320a7c..87ded61db 100644
--- a/compiler/commands.nim
+++ b/compiler/commands.nim
@@ -30,6 +30,8 @@ import
   wordrecg, parseutils, nimblecmd, parseopt, sequtils, lineinfos,
   pathutils, strtabs
 
+from incremental import nimIncremental
+
 # but some have deps to imported modules. Yay.
 bootSwitch(usedTinyC, hasTinyCBackend, "-d:tinyc")
 bootSwitch(usedNativeStacktrace,
@@ -669,7 +671,7 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo;
     helpOnError(conf, pass)
   of "symbolfiles": discard "ignore for backwards compat"
   of "incremental":
-    when not defined(nimIncremental):
+    when not nimIncremental:
       localError(conf, info, "the compiler was not built with " &
         "incremental compilation features; bootstrap with " &
         "-d:nimIncremental to enable")
diff --git a/compiler/incremental.nim b/compiler/incremental.nim
index 29528bbd3..6cc085020 100644
--- a/compiler/incremental.nim
+++ b/compiler/incremental.nim
@@ -10,13 +10,14 @@
 ## Basic type definitions the module graph needs in order to support
 ## incremental compilations.
 
-const nimIncremental* = defined(nimIncremental)
+const nimIncremental* = true # defined(nimIncremental)
 
 import options, lineinfos
 
 when nimIncremental:
   import ast, msgs, intsets, btrees, db_sqlite, std / sha1, pathutils
   from strutils import parseInt
+  from os import isAbsolute
 
   type
     Writer* = object
@@ -47,7 +48,7 @@ when nimIncremental:
 
   proc hashFileCached*(conf: ConfigRef; fileIdx: FileIndex; fullpath: AbsoluteFile): string =
     result = msgs.getHash(conf, fileIdx)
-    if result.len == 0:
+    if result.len == 0 and isAbsolute(string fullpath):
       result = $secureHashFile(string fullpath)
       msgs.setHash(conf, fileIdx, result)
 
diff --git a/compiler/modules.nim b/compiler/modules.nim
index 40d9a904c..13845e6e9 100644
--- a/compiler/modules.nim
+++ b/compiler/modules.nim
@@ -77,8 +77,6 @@ proc compileModule*(graph: ModuleGraph; fileIdx: FileIndex; flags: TSymFlags): P
     if result == nil:
       result = newModule(graph, fileIdx)
       result.flags = result.flags + flags
-      if sfMainModule in result.flags:
-        graph.config.mainPackageId = result.owner.id
       result.id = id
       registerModule(graph, result)
     else:
diff --git a/compiler/passes.nim b/compiler/passes.nim
index e917c1ce4..d6bbd4d14 100644
--- a/compiler/passes.nim
+++ b/compiler/passes.nim
@@ -113,6 +113,8 @@ const
     nkExportStmt, nkExportExceptStmt, nkFromStmt, nkImportStmt, nkImportExceptStmt}
 
 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:
   if module.owner.id == graph.config.mainPackageId:
     graph.config.notes = graph.config.mainPackageNotes
@@ -121,7 +123,7 @@ proc prepareConfigNotes(graph: ModuleGraph; module: PSym) =
     graph.config.notes = graph.config.foreignPackageNotes
 
 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.} =
   if graph.stopCompile(): return true
@@ -131,7 +133,7 @@ proc processModule*(graph: ModuleGraph; module: PSym, stream: PLLStream): bool {
     s: PLLStream
     fileIdx = module.fileIdx
   prepareConfigNotes(graph, module)
-  if not moduleHasChanged(graph, module):
+  if module.id < 0:
     # new module caching mechanism:
     for i in 0 ..< graph.passes.len:
       if not isNil(graph.passes[i].open) and not graph.passes[i].isFrontend:
diff --git a/compiler/rodimpl.nim b/compiler/rodimpl.nim
index f5f689939..69b50c391 100644
--- a/compiler/rodimpl.nim
+++ b/compiler/rodimpl.nim
@@ -17,7 +17,6 @@ import strutils, os, intsets, tables, ropes, db_sqlite, msgs, options, types,
 ## - Dependency computation should use *signature* hashes in order to
 ##   avoid recompiling dependent modules.
 ## - 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
 
@@ -72,9 +71,12 @@ proc getModuleId(g: ModuleGraph; fileIdx: FileIndex; fullpath: AbsoluteFile): in
       # not changed, so use the cached AST:
       doAssert(result != 0)
       var cycleCheck = initIntSet()
-      if not needsRecompile(g, fileIdx, fullpath, cycleCheck) and not g.incr.configChanged:
-        echo "cached successfully! ", string fullpath
-        return -result
+      if not needsRecompile(g, fileIdx, fullpath, cycleCheck):
+        if not g.incr.configChanged or g.config.symbolFiles == readOnlySf:
+          #echo "cached successfully! ", string fullpath
+          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"delete from deps 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)
   if s.flags != {}:
     result.add('$')
-    encodeVInt(cast[int32](s.flags), result)
+    encodeVBiggestInt(cast[int64](s.flags), result)
   if s.magic != mNone:
     result.add('@')
     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)
   if b.s[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] == '@':
     inc(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':
       inc b.pos
       result.transformedBody = decodeNode(g, b, result.info)
+      #result.transformedBody = nil
   of skModule, skPackage:
     decodeInstantiations(g, b, result.info, result.usedGenerics)
   of skLet, skVar, skField, skForVar:
@@ -769,7 +772,7 @@ proc loadSymFromBlob(g; b; info: TLineInfo): PSym =
 
   if b.s[b.pos] == '(':
     #if result.kind in routineKinds:
-    #  result.ast = decodeNodeLazyBody(b, result.info, result)
+    #  result.ast = nil
     #else:
     result.ast = decodeNode(g, b, result.info)
   if sfCompilerProc in result.flags:
@@ -886,6 +889,10 @@ proc replay(g: ModuleGraph; module: PSym; n: PNode) =
       internalAssert g.config, imported.id < 0
   of nkStmtList, nkStmtListExpr:
     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"
 
 proc loadNode*(g: ModuleGraph; module: PSym): PNode =
diff --git a/compiler/rodutils.nim b/compiler/rodutils.nim
index 43fb42de9..17c4832bf 100644
--- a/compiler/rodutils.nim
+++ b/compiler/rodutils.nim
@@ -46,14 +46,9 @@ proc toStrMaxPrecision*(f: BiggestFloat, literalPostfix = ""): string =
   of fcNegInf:
     result = "-INF"
   else:
-    when defined(nimNoArrayToCstringConversion):
-      result = newString(81)
-      let n = c_snprintf(result.cstring, result.len.uint, "%#.16e%s", f, literalPostfix.cstring)
-      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
+    result = newString(81)
+    let n = c_snprintf(result.cstring, result.len.uint, "%#.16e%s", f, literalPostfix.cstring)
+    setLen(result, n)
 
 proc encodeStr*(s: string, result: var string) =
   for i in 0 ..< len(s):
diff --git a/doc/manual.rst b/doc/manual.rst
index 61388a9cb..61a73357d 100644
--- a/doc/manual.rst
+++ b/doc/manual.rst
@@ -95,8 +95,8 @@ and code execution in the executable.
 
 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
-compiling it into the executable, it transforms the AST through `semantic
-analysis`:idx:. This adds semantic information such as expression types,
+compiling it into the executable, it transforms the AST through
+`semantic analysis`:idx:. This adds semantic information such as expression types,
 identifier meanings, and in some cases expression values. An error detected
 during semantic analysis is called a `static error`:idx:. Errors described in
 this manual are static errors when not otherwise specified.
diff --git a/koch.nim b/koch.nim
index 8845ba85e..57c490449 100644
--- a/koch.nim
+++ b/koch.nim
@@ -10,7 +10,7 @@
 #
 
 const
-  NimbleStableCommit = "d15c8530cb7480ce39ffa85a2dd9819d2d4fc645" # 0.10.2
+  NimbleStableCommit = "da82e3111e662fc1b12f96b3cddd66c749c0f686" # master
 
 when defined(gcc) and defined(windows):
   when defined(x86):
diff --git a/testament/categories.nim b/testament/categories.nim
index 3db90d15f..745abebe3 100644
--- a/testament/categories.nim
+++ b/testament/categories.nim
@@ -10,8 +10,9 @@
 ## Include for the tester that contains test suites that test special features
 ## of the compiler.
 
+# included from tester.nim
+
 import important_packages
-import sequtils
 
 const
   specialCategories = [
@@ -24,6 +25,7 @@ const
     "gc",
     "io",
     "js",
+    "ic",
     "lib",
     "longgc",
     "manyloc",
@@ -41,58 +43,52 @@ const
     "dir with space"
   ]
 
-# included from tester.nim
-# ---------------- ROD file tests ---------------------------------------------
+proc isTestFile*(file: string): bool =
+  let (_, name, ext) = splitFile(file)
+  result = ext == ".nim" and name.startsWith("t")
 
-const
-  rodfilesDir = "tests/rodfiles"
-
-proc delNimCache(filename, options: string) =
-  for target in low(TTarget)..high(TTarget):
-    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 test(filename: string, clearCacheFirst=false) =
-    if clearCacheFirst: delNimCache(filename, options)
-    testSpec r, makeTest(rodfilesDir / filename, options, cat)
-
-
-  # test basic recompilation scheme:
-  test "hallo", true
-  test "hallo"
-  when false:
-    # test incremental type information:
-    test "hallo2"
-
-  # test type converters:
-  test "aconv", true
-  test "bconv"
-
-  # test G, A, B example from the documentation; test init sections:
-  test "deada", true
-  test "deada2"
-
-  when false:
-    # test method generation:
-    test "bmethods", true
-    test "bmethods2"
-
-    # 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"
+# ---------------- IC tests ---------------------------------------------
+
+proc icTests(r: var TResults; testsDir: string, cat: Category, options: string) =
+  const
+    tooltests = ["compiler/nim.nim", "tools/nimgrep.nim"]
+    writeOnly = " --incremental:writeonly "
+    readOnly = " --incremental:readonly "
+    incrementalOn = " --incremental:on "
+
+  template test(x: untyped) =
+    testSpecWithNimcache(r, makeRawTest(file, x & options, cat), nimcache)
+
+  template editedTest(x: untyped) =
+    var test = makeTest(file, x & options, cat)
+    test.spec.targets = {getTestSpecTarget()}
+    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)
+
+      let content = readFile(it)
+      for fragment in content.split("#!EDIT!#"):
+        let file = it.replace(".nim", tempExt)
+        writeFile(file, fragment)
+        let oldPassed = r.passed
+        editedTest incrementalOn
+        if r.passed != oldPassed+1: break
+
+  for file in tooltests:
+    let nimcache = nimcacheDir(file, options, getTestSpecTarget())
+    removeDir(nimcache)
+
+    let oldPassed = r.passed
+    test writeOnly
+
+    if r.passed == oldPassed+1:
+      test readOnly
+      if r.passed == oldPassed+2:
+        test readOnly & "-d:nimBackendAssumesChange "
 
 # --------------------- flags tests -------------------------------------------
 
@@ -116,12 +112,6 @@ proc flagTests(r: var TResults, cat: Category, options: string) =
 
 # --------------------- 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) =
   const rpath = when defined(macosx):
       " --passL:-rpath --passL:@loader_path"
@@ -153,11 +143,12 @@ proc runBasicDLLTest(c, r: var TResults, cat: Category, options: string) =
 
   if "boehm" notin options:
     # 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)
-    hcr_integration.args = prepareTestArgs(hcr_integration.spec.getCmd, hcr_integration.name,
-                                           hcr_integration.options, getTestSpecTarget())
-    testSpec r, hcr_integration
+    let nimcache = nimcacheDir(hcri.name, hcri.options, getTestSpecTarget())
+    hcri.args = prepareTestArgs(hcri.spec.getCmd, hcri.name,
+                                hcri.options, nimcache, getTestSpecTarget())
+    testSpec r, hcri
 
 proc dllTests(r: var TResults, cat: Category, options: string) =
   # dummy compile result:
@@ -363,7 +354,8 @@ proc testNimInAction(r: var TResults, cat: Category, options: string) =
   for i, test in tests:
     let filename = testsDir / test.addFileExt("nim")
     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.
   for testfile in tests:
     test "tests/" & testfile & ".nim"
@@ -449,25 +441,6 @@ let
   nimbleExe = findExe("nimble")
   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] =
   let defaultCmd = "nimble test"
   let packageList = parseFile(packageIndex)
@@ -481,8 +454,8 @@ iterator listPackages(): tuple[name, url, cmd: string, hasDeps: bool] =
         let name = package["name"].str
         if name == n:
           found = true
-          let p_url = package["url"].str
-          yield (name, p_url, cmd, hasDeps)
+          let pUrl = package["url"].str
+          yield (name, pUrl, cmd, hasDeps)
           break
       if not found:
         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"
 
 proc `&.?`(a, b: string): string =
@@ -584,17 +557,12 @@ proc isJoinableSpec(spec: TSpec): bool =
     (spec.targets == {} or spec.targets == {targetC})
 
 proc norm(s: var string) =
-  # equivalent of s/\n+/\n/g (could use a single pass over input if needed)
   while true:
     let tmp = s.replace("\n\n", "\n")
     if tmp == s: break
     s = tmp
   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 =
   # todo: consider moving to system.nim
   result.addQuoted(a)
@@ -607,10 +575,10 @@ proc runJoinedTest(r: var TResults, cat: Category, testsDir: string) =
     let cat = dir[testsDir.len .. ^1]
     if kind == pcDir and cat notin specialCategories:
       for file in walkDirRec(testsDir / cat):
-        if not isTestFile(file): continue
-        let spec = parseSpec(file)
-        if isJoinableSpec(spec):
-          specs.add spec
+        if isTestFile(file):
+          let spec = parseSpec(file)
+          if isJoinableSpec(spec):
+            specs.add spec
 
   proc cmp(a: TSpec, b:TSpec): auto = cmp(a.file, b.file)
   sort(specs, cmp=cmp) # reproducible order
@@ -646,14 +614,13 @@ proc runJoinedTest(r: var TResults, cat: Category, testsDir: string) =
 
   let args = ["c", "--nimCache:" & outDir, "-d:testing", "--listCmd",
               "--listFullPaths:off", "--excessiveStackTrace:off", "megatest.nim"]
-  proc onStdout(line: string) = echo line
+
   var (cmdLine, buf, exitCode) = execCmdEx2(command = compilerPrefix, args = args, input = "")
   if exitCode != 0:
     echo "$ ", cmdLine
     echo buf.string
     quit("megatest compilation failed")
 
-  # Could also use onStdout here.
   (buf, exitCode) = execCmdEx("./megatest")
   if exitCode != 0:
     echo buf.string
@@ -690,6 +657,8 @@ proc processCategory(r: var TResults, cat: Category,
     when false:
       compileRodFiles(r, cat, options)
       runRodFiles(r, cat, options)
+  of "ic":
+    icTests(r, testsDir, cat, options)
   of "js":
     # only run the JS tests on Windows or Linux because Travis is bad
     # and other OSes like Haiku might lack nodejs:
diff --git a/testament/htmlgen.nim b/testament/htmlgen.nim
index 641c1c32c..67e44a2f8 100644
--- a/testament/htmlgen.nim
+++ b/testament/htmlgen.nim
@@ -9,7 +9,7 @@
 
 ## HTML generator for the tester.
 
-import cgi, backend, strutils, json, os, tables, times
+import strutils, json, os, times
 
 import "testamenthtml.nimf"
 
diff --git a/testament/important_packages.nim b/testament/important_packages.nim
index defd748ab..93aeb1bbc 100644
--- a/testament/important_packages.nim
+++ b/testament/important_packages.nim
@@ -1,4 +1,3 @@
-import strutils
 
 template pkg(name: string; cmd = "nimble test"; hasDeps = false; url = ""): untyped =
   packages.add((name, cmd, hasDeps, url))
diff --git a/testament/specs.nim b/testament/specs.nim
index 4d09f438f..cf3455027 100644
--- a/testament/specs.nim
+++ b/testament/specs.nim
@@ -7,7 +7,7 @@
 #    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")
 
@@ -122,6 +122,9 @@ proc addLine*(self: var string; a,b: string) =
   self.add b
   self.add "\n"
 
+proc initSpec*(filename: string): TSpec =
+  result.file = filename
+
 proc parseSpec*(filename: string): TSpec =
   result.file = filename
   let specStr = extractSpec(filename)
diff --git a/testament/tester.nim b/testament/tester.nim
index 1e5bc875a..c32228ca8 100644
--- a/testament/tester.nim
+++ b/testament/tester.nim
@@ -10,9 +10,9 @@
 ## This program verifies Nim against the testcases.
 
 import
-  parseutils, strutils, pegs, os, osproc, streams, parsecfg, json,
-  marshal, backend, parseopt, specs, htmlgen, browsers, terminal,
-  algorithm, times, sets, md5, sequtils
+  strutils, pegs, os, osproc, streams, json,
+  backend, parseopt, specs, htmlgen, browsers, terminal,
+  algorithm, times, md5, sequtils
 
 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 =
   ## Give each test a private nimcache dir so they don't clobber each other's.
   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] =
-  let nimcache = nimcacheDir(filename, options, target)
   let options = options & " " & quoteShell("--nimCache:" & nimcache) & extraOptions
-  return parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
+  result = parseCmdLine(cmdTemplate % ["target", targetToCmd[target],
                       "options", options, "file", filename.quoteShell,
                       "filedir", filename.getFileDir()])
 
-proc callCompiler(cmdTemplate, filename, options: string,
-                  target: TTarget, extraOptions=""): TSpec =
-  let c = prepareTestArgs(cmdTemplate, filename, options, target, extraOptions)
+proc callCompiler(cmdTemplate, filename, options, nimcache: string,
+                  target: TTarget,
+                  extraOptions=""): TSpec =
+  let c = prepareTestArgs(cmdTemplate, filename, options, nimcache, target,
+                          extraOptions)
   result.cmd = quoteShellCommand(c)
   var p = startProcess(command=c[0], args=c[1 .. ^1],
                        options={poStdErrToStdOut, poUsePath})
@@ -370,7 +371,7 @@ proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) =
     currentPos = giv.find(line.strip, currentPos)
     if currentPos < 0:
       given.err = reMsgsDiffer
-      return
+      break
 
 proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec,
                          expected: TSpec; r: var TResults) =
@@ -391,9 +392,9 @@ proc compilerOutputTests(test: TTest, target: TTarget, given: var TSpec,
 
 proc getTestSpecTarget(): TTarget =
   if getEnv("NIM_COMPILE_TO_CPP", "false").string == "true":
-    return targetCpp
+    result = targetCpp
   else:
-    return targetC
+    result = targetC
 
 proc checkDisabled(r: var TResults, test: TTest): bool =
   if test.spec.err in {reDisabled, reJoined}:
@@ -401,13 +402,72 @@ proc checkDisabled(r: var TResults, test: TTest): bool =
     r.addResult(test, targetC, "", "", test.spec.err)
     inc(r.skipped)
     inc(r.total)
-    return
-  true
+    result = false
+  else:
+    result = true
+
+var count = 0
+
+proc testSpecHelper(r: var TResults, test: TTest, expected: TSpec, target: TTarget, nimcache: string) =
+  case expected.action
+  of actionCompile:
+    var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target,
+          extraOptions = " --stdout --hint[Path]:off --hint[Processing]:off")
+    compilerOutputTests(test, target, given, expected, r)
+  of actionRun:
+    var given = callCompiler(expected.getCmd, test.name, test.options, nimcache, target)
+    if given.err != reSuccess:
+      r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err)
+    else:
+      let isJsTarget = target == targetJS
+      var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt)
+      if not existsFile(exeFile):
+        r.addResult(test, target, expected.output,
+                    "executable not found: " & exeFile, reExeNotFound)
+      else:
+        let nodejs = if isJsTarget: findNodeJs() else: ""
+        if isJsTarget and nodejs == "":
+          r.addResult(test, target, expected.output, "nodejs binary not in PATH",
+                      reExeNotFound)
+        else:
+          var exeCmd: string
+          var args = test.args
+          if isJsTarget:
+            exeCmd = nodejs
+            args = concat(@[exeFile], args)
+          else:
+            exeCmd = exeFile
+          var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
+          # 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.
+          if exitCode != 0: exitCode = 1
+          let bufB =
+            if expected.sortoutput:
+              var x = splitLines(strip(buf.string))
+              sort(x, system.cmp)
+              join(x, "\n")
+            else:
+              strip(buf.string)
+          if exitCode != expected.exitCode:
+            r.addResult(test, target, "exitcode: " & $expected.exitCode,
+                              "exitcode: " & $exitCode & "\n\nOutput:\n" &
+                              bufB, reExitCodesDiffer)
+          elif (expected.outputCheck == ocEqual and expected.output != bufB) or
+              (expected.outputCheck == ocSubstr and expected.output notin bufB):
+            given.err = reOutputsDiffer
+            r.addResult(test, target, expected.output, bufB, reOutputsDiffer)
+          else:
+            compilerOutputTests(test, target, given, expected, r)
+  of actionReject:
+    var given = callCompiler(expected.getCmd, test.name, test.options,
+                              nimcache, target)
+    cmpMsgs(r, expected, given, test, target)
+
 
 proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
   var expected = test.spec
   if expected.parseErrors.len > 0:
-    # targetC is a lie, but parameter is required
+    # targetC is a lie, but a parameter is required
     r.addResult(test, targetC, "", expected.parseErrors, reInvalidSpec)
     inc(r.total)
     return
@@ -422,73 +482,18 @@ proc testSpec(r: var TResults, test: TTest, targets: set[TTarget] = {}) =
     if target notin gTargets:
       r.addResult(test, target, "", "", reDisabled)
       inc(r.skipped)
-      continue
-
-    if simulate:
-      var count {.global.} = 0
-      count.inc
+    elif simulate:
+      inc count
       echo "testSpec count: ", count, " expected: ", expected
-      continue
-
-    case expected.action
-    of actionCompile:
-      var given = callCompiler(expected.getCmd, test.name, test.options, target,
-        extraOptions=" --stdout --hint[Path]:off --hint[Processing]:off")
-      compilerOutputTests(test, target, given, expected, r)
-    of actionRun:
-      # In this branch of code "early return" pattern is clearer than deep
-      # 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:
-        r.addResult(test, target, "", "$ " & given.cmd & "\n" & given.nimout, given.err)
-        continue
-      let isJsTarget = target == targetJS
-      var exeFile = changeFileExt(test.name, if isJsTarget: "js" else: ExeExt)
-      if not existsFile(exeFile):
-        r.addResult(test, target, expected.output,
-                    "executable not found: " & exeFile, reExeNotFound)
-        continue
-
-      let nodejs = if isJsTarget: findNodeJs() else: ""
-      if isJsTarget and nodejs == "":
-        r.addResult(test, target, expected.output, "nodejs binary not in PATH",
-                    reExeNotFound)
-        continue
-      var exeCmd: string
-      var args = test.args
-      if isJsTarget:
-        exeCmd = nodejs
-        args = concat(@[exeFile], args)
-      else:
-        exeCmd = exeFile
-      var (cmdLine, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
-      # 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.
-      if exitCode != 0: exitCode = 1
-      let bufB =
-        if expected.sortoutput:
-          var x = splitLines(strip(buf.string))
-          sort(x, system.cmp)
-          join(x, "\n")
-        else:
-          strip(buf.string)
-      if exitCode != expected.exitCode:
-        r.addResult(test, target, "exitcode: " & $expected.exitCode,
-                          "exitcode: " & $exitCode & "\n\nOutput:\n" &
-                          bufB, reExitCodesDiffer)
-        continue
-      if (expected.outputCheck == ocEqual and expected.output != bufB) or
-         (expected.outputCheck == ocSubstr and expected.output notin bufB):
-        given.err = reOutputsDiffer
-        r.addResult(test, target, expected.output, bufB, reOutputsDiffer)
-        continue
-      compilerOutputTests(test, target, given, expected, r)
-      continue
-    of actionReject:
-      var given = callCompiler(expected.getCmd, test.name, test.options,
-                               target)
-      cmpMsgs(r, expected, given, test, target)
-      continue
+    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) =
   # 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.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
 const disabledFilesDefault = @[
   "LockFreeHash.nim",
@@ -553,23 +567,18 @@ when defined(windows):
 else:
   const
     # 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
 
 proc loadSkipFrom(name: string): seq[string] =
-  if name.len() == 0: return
-
+  if name.len == 0: return
   # One skip per line, comments start with #
   # used by `nlvm` (at least)
-  try:
-    for line in lines(name):
-      let sline = line.strip()
-      if sline.len > 0 and not sline.startsWith("#"):
-        result.add sline
-  except:
-    echo "Could not load " & name & ", ignoring"
+  for line in lines(name):
+    let sline = line.strip()
+    if sline.len > 0 and not sline.startsWith("#"):
+      result.add sline
 
 proc main() =
   os.putenv "NIMTEST_COLOR", "never"
diff --git a/tests/ic/tgenerics.nim b/tests/ic/tgenerics.nim
new file mode 100644
index 000000000..bc5c05f4f
--- /dev/null
+++ b/tests/ic/tgenerics.nim
@@ -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"]
diff --git a/tests/ic/thallo.nim b/tests/ic/thallo.nim
new file mode 100644
index 000000000..7ead7c8ba
--- /dev/null
+++ b/tests/ic/thallo.nim
@@ -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"