summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--compiler/main.nim4
-rw-r--r--compiler/nimconf.nim106
-rw-r--r--compiler/options.nim11
-rw-r--r--compiler/scriptconfig.nim13
-rw-r--r--compiler/writetracking.nim288
-rw-r--r--lib/pure/os.nim526
-rw-r--r--lib/pure/ospaths.nim560
-rw-r--r--lib/pure/times.nim32
-rw-r--r--lib/system.nim29
-rw-r--r--lib/system/nimscript.nim31
-rw-r--r--todo.txt2
11 files changed, 852 insertions, 750 deletions
diff --git a/compiler/main.nim b/compiler/main.nim
index 7c043eb72..3bb6fc343 100644
--- a/compiler/main.nim
+++ b/compiler/main.nim
@@ -355,7 +355,9 @@ proc mainCommand* =
     gGlobalOptions.incl(optCaasEnabled)
     msgs.gErrorMax = high(int)  # do not stop after first error
     serve(mainCommand)
-  of "nop", "help": discard
+  of "nop", "help":
+    # prevent the "success" message:
+    gCmd = cmdDump
   else:
     rawMessage(errInvalidCommandX, command)
 
diff --git a/compiler/nimconf.nim b/compiler/nimconf.nim
index b9c78cecc..496bd0123 100644
--- a/compiler/nimconf.nim
+++ b/compiler/nimconf.nim
@@ -9,106 +9,106 @@
 
 # This module handles the reading of the config file.
 
-import 
-  llstream, nversion, commands, os, strutils, msgs, platform, condsyms, lexer, 
+import
+  llstream, nversion, commands, os, strutils, msgs, platform, condsyms, lexer,
   options, idents, wordrecg, strtabs
 
 # ---------------- configuration file parser -----------------------------
 # we use Nim's scanner here to save space and work
 
-proc ppGetTok(L: var TLexer, tok: var TToken) = 
+proc ppGetTok(L: var TLexer, tok: var TToken) =
   # simple filter
   rawGetTok(L, tok)
   while tok.tokType in {tkComment}: rawGetTok(L, tok)
-  
+
 proc parseExpr(L: var TLexer, tok: var TToken): bool
-proc parseAtom(L: var TLexer, tok: var TToken): bool = 
-  if tok.tokType == tkParLe: 
+proc parseAtom(L: var TLexer, tok: var TToken): bool =
+  if tok.tokType == tkParLe:
     ppGetTok(L, tok)
     result = parseExpr(L, tok)
     if tok.tokType == tkParRi: ppGetTok(L, tok)
     else: lexMessage(L, errTokenExpected, "\')\'")
-  elif tok.ident.id == ord(wNot): 
+  elif tok.ident.id == ord(wNot):
     ppGetTok(L, tok)
     result = not parseAtom(L, tok)
   else:
     result = isDefined(tok.ident)
     ppGetTok(L, tok)
 
-proc parseAndExpr(L: var TLexer, tok: var TToken): bool = 
+proc parseAndExpr(L: var TLexer, tok: var TToken): bool =
   result = parseAtom(L, tok)
-  while tok.ident.id == ord(wAnd): 
+  while tok.ident.id == ord(wAnd):
     ppGetTok(L, tok)          # skip "and"
     var b = parseAtom(L, tok)
     result = result and b
 
-proc parseExpr(L: var TLexer, tok: var TToken): bool = 
+proc parseExpr(L: var TLexer, tok: var TToken): bool =
   result = parseAndExpr(L, tok)
-  while tok.ident.id == ord(wOr): 
+  while tok.ident.id == ord(wOr):
     ppGetTok(L, tok)          # skip "or"
     var b = parseAndExpr(L, tok)
     result = result or b
 
-proc evalppIf(L: var TLexer, tok: var TToken): bool = 
+proc evalppIf(L: var TLexer, tok: var TToken): bool =
   ppGetTok(L, tok)            # skip 'if' or 'elif'
   result = parseExpr(L, tok)
   if tok.tokType == tkColon: ppGetTok(L, tok)
   else: lexMessage(L, errTokenExpected, "\':\'")
-  
+
 var condStack: seq[bool] = @[]
 
-proc doEnd(L: var TLexer, tok: var TToken) = 
+proc doEnd(L: var TLexer, tok: var TToken) =
   if high(condStack) < 0: lexMessage(L, errTokenExpected, "@if")
   ppGetTok(L, tok)            # skip 'end'
   setLen(condStack, high(condStack))
 
-type 
-  TJumpDest = enum 
+type
+  TJumpDest = enum
     jdEndif, jdElseEndif
 
 proc jumpToDirective(L: var TLexer, tok: var TToken, dest: TJumpDest)
-proc doElse(L: var TLexer, tok: var TToken) = 
+proc doElse(L: var TLexer, tok: var TToken) =
   if high(condStack) < 0: lexMessage(L, errTokenExpected, "@if")
   ppGetTok(L, tok)
   if tok.tokType == tkColon: ppGetTok(L, tok)
   if condStack[high(condStack)]: jumpToDirective(L, tok, jdEndif)
-  
-proc doElif(L: var TLexer, tok: var TToken) = 
+
+proc doElif(L: var TLexer, tok: var TToken) =
   if high(condStack) < 0: lexMessage(L, errTokenExpected, "@if")
   var res = evalppIf(L, tok)
   if condStack[high(condStack)] or not res: jumpToDirective(L, tok, jdElseEndif)
   else: condStack[high(condStack)] = true
-  
-proc jumpToDirective(L: var TLexer, tok: var TToken, dest: TJumpDest) = 
+
+proc jumpToDirective(L: var TLexer, tok: var TToken, dest: TJumpDest) =
   var nestedIfs = 0
-  while true: 
+  while true:
     if tok.ident != nil and tok.ident.s == "@":
       ppGetTok(L, tok)
       case whichKeyword(tok.ident)
-      of wIf: 
+      of wIf:
         inc(nestedIfs)
-      of wElse: 
+      of wElse:
         if dest == jdElseEndif and nestedIfs == 0:
           doElse(L, tok)
-          break 
-      of wElif: 
+          break
+      of wElif:
         if dest == jdElseEndif and nestedIfs == 0:
           doElif(L, tok)
-          break 
-      of wEnd: 
-        if nestedIfs == 0: 
+          break
+      of wEnd:
+        if nestedIfs == 0:
           doEnd(L, tok)
-          break 
+          break
         if nestedIfs > 0: dec(nestedIfs)
-      else: 
+      else:
         discard
       ppGetTok(L, tok)
     elif tok.tokType == tkEof:
       lexMessage(L, errTokenExpected, "@end")
     else:
       ppGetTok(L, tok)
-  
-proc parseDirective(L: var TLexer, tok: var TToken) = 
+
+proc parseDirective(L: var TLexer, tok: var TToken) =
   ppGetTok(L, tok)            # skip @
   case whichKeyword(tok.ident)
   of wIf:
@@ -126,13 +126,13 @@ proc parseDirective(L: var TLexer, tok: var TToken) =
     ppGetTok(L, tok)
   else:
     case tok.ident.s.normalize
-    of "putenv": 
+    of "putenv":
       ppGetTok(L, tok)
       var key = tokToStr(tok)
       ppGetTok(L, tok)
       os.putEnv(key, tokToStr(tok))
       ppGetTok(L, tok)
-    of "prependenv": 
+    of "prependenv":
       ppGetTok(L, tok)
       var key = tokToStr(tok)
       ppGetTok(L, tok)
@@ -145,17 +145,17 @@ proc parseDirective(L: var TLexer, tok: var TToken) =
       os.putEnv(key, os.getEnv(key) & tokToStr(tok))
       ppGetTok(L, tok)
     else: lexMessage(L, errInvalidDirectiveX, tokToStr(tok))
-  
-proc confTok(L: var TLexer, tok: var TToken) = 
+
+proc confTok(L: var TLexer, tok: var TToken) =
   ppGetTok(L, tok)
-  while tok.ident != nil and tok.ident.s == "@": 
+  while tok.ident != nil and tok.ident.s == "@":
     parseDirective(L, tok)    # else: give the token to the parser
-  
-proc checkSymbol(L: TLexer, tok: TToken) = 
-  if tok.tokType notin {tkSymbol..pred(tkIntLit), tkStrLit..tkTripleStrLit}: 
+
+proc checkSymbol(L: TLexer, tok: TToken) =
+  if tok.tokType notin {tkSymbol..pred(tkIntLit), tkStrLit..tkTripleStrLit}:
     lexMessage(L, errIdentifierExpected, tokToStr(tok))
-  
-proc parseAssignment(L: var TLexer, tok: var TToken) = 
+
+proc parseAssignment(L: var TLexer, tok: var TToken) =
   if tok.ident.id == getIdent("-").id or tok.ident.id == getIdent("--").id:
     confTok(L, tok)           # skip unnecessary prefix
   var info = getLineInfo(L, tok) # save for later in case of an error
@@ -163,13 +163,13 @@ proc parseAssignment(L: var TLexer, tok: var TToken) =
   var s = tokToStr(tok)
   confTok(L, tok)             # skip symbol
   var val = ""
-  while tok.tokType == tkDot: 
+  while tok.tokType == tkDot:
     add(s, '.')
     confTok(L, tok)
     checkSymbol(L, tok)
     add(s, tokToStr(tok))
     confTok(L, tok)
-  if tok.tokType == tkBracketLe: 
+  if tok.tokType == tkBracketLe:
     # BUGFIX: val, not s!
     # BUGFIX: do not copy '['!
     confTok(L, tok)
@@ -180,7 +180,7 @@ proc parseAssignment(L: var TLexer, tok: var TToken) =
     else: lexMessage(L, errTokenExpected, "']'")
     add(val, ']')
   let percent = tok.ident.id == getIdent("%=").id
-  if tok.tokType in {tkColon, tkEquals} or percent: 
+  if tok.tokType in {tkColon, tkEquals} or percent:
     if len(val) > 0: add(val, ':')
     confTok(L, tok)           # skip ':' or '=' or '%'
     checkSymbol(L, tok)
@@ -226,15 +226,7 @@ proc getSystemConfigPath(filename: string): string =
     if not existsFile(result): result = "/etc/" & filename
 
 proc loadConfigs*(cfg: string) =
-  # set default value (can be overwritten):
-  if libpath == "": 
-    # choose default libpath:
-    var prefix = getPrefixDir()
-    when defined(posix):
-      if prefix == "/usr": libpath = "/usr/lib/nim"
-      elif prefix == "/usr/local": libpath = "/usr/local/lib/nim"
-      else: libpath = joinPath(prefix, "lib")
-    else: libpath = joinPath(prefix, "lib")
+  setDefaultLibpath()
 
   if optSkipConfigFile notin gGlobalOptions:
     readConfigFile(getSystemConfigPath(cfg))
@@ -246,10 +238,10 @@ proc loadConfigs*(cfg: string) =
   if optSkipParentConfigFiles notin gGlobalOptions:
     for dir in parentDirs(pd, fromRoot=true, inclusive=false):
       readConfigFile(dir / cfg)
-  
+
   if optSkipProjConfigFile notin gGlobalOptions:
     readConfigFile(pd / cfg)
-    
+
     if gProjectName.len != 0:
       # new project wide config file:
       var projectConfig = changeFileExt(gProjectFull, "nimcfg")
diff --git a/compiler/options.nim b/compiler/options.nim
index 1f167e2a6..adb340fea 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -191,6 +191,17 @@ proc getPrefixDir*(): string =
   else:
     result = splitPath(getAppDir()).head
 
+proc setDefaultLibpath*() =
+  # set default value (can be overwritten):
+  if libpath == "":
+    # choose default libpath:
+    var prefix = getPrefixDir()
+    when defined(posix):
+      if prefix == "/usr": libpath = "/usr/lib/nim"
+      elif prefix == "/usr/local": libpath = "/usr/local/lib/nim"
+      else: libpath = joinPath(prefix, "lib")
+    else: libpath = joinPath(prefix, "lib")
+
 proc canonicalizePath*(path: string): string =
   when not FileSystemCaseSensitive: result = path.expandFilename.toLower
   else: result = path.expandFilename
diff --git a/compiler/scriptconfig.nim b/compiler/scriptconfig.nim
index 148a382c2..115ce940d 100644
--- a/compiler/scriptconfig.nim
+++ b/compiler/scriptconfig.nim
@@ -68,9 +68,20 @@ proc setupVM*(module: PSym; scriptName: string): PEvalContext =
     setResult(a, os.getCurrentDir())
   cbos moveFile:
     os.moveFile(getString(a, 0), getString(a, 1))
+  cbos copyFile:
+    os.copyFile(getString(a, 0), getString(a, 1))
   cbos getLastModificationTime:
     setResult(a, toSeconds(getLastModificationTime(getString(a, 0))))
 
+  cbconf getEnv:
+    setResult(a, os.getEnv(a.getString 0))
+  cbconf existsEnv:
+    setResult(a, os.existsEnv(a.getString 0))
+  cbconf dirExists:
+    setResult(a, os.dirExists(a.getString 0))
+  cbconf fileExists:
+    setResult(a, os.fileExists(a.getString 0))
+
   cbconf thisDir:
     setResult(a, vthisDir)
   cbconf put:
@@ -87,6 +98,8 @@ proc setupVM*(module: PSym; scriptName: string): PEvalContext =
     setResult(a, os.paramCount())
   cbconf cmpIgnoreStyle:
     setResult(a, strutils.cmpIgnoreStyle(a.getString 0, a.getString 1))
+  cbconf cmpIgnoreCase:
+    setResult(a, strutils.cmpIgnoreCase(a.getString 0, a.getString 1))
   cbconf setCommand:
     options.command = a.getString 0
   cbconf getCommand:
diff --git a/compiler/writetracking.nim b/compiler/writetracking.nim
index db3e6c53a..38258b07a 100644
--- a/compiler/writetracking.nim
+++ b/compiler/writetracking.nim
@@ -9,8 +9,13 @@
 
 ## This module implements the write tracking analysis. Read my block post for
 ## a basic description of the algorithm and ideas.
+## The algorithm operates in 2 phases:
+##
+##   * Collecting information about assignments (and pass-by-var calls).
+##   * Computing an aliasing relation based on the assignments. This relation
+##     is then used to compute the 'writes' and 'escapes' effects.
 
-import idents, ast, astalgo, trees, renderer, msgs, types
+import intsets, idents, ast, astalgo, trees, renderer, msgs, types
 
 const
   debug = false
@@ -24,11 +29,87 @@ type
     newNone,
     newLit,
     newCall
+  RootInfo = enum
+    rootIsResultOrParam,
+    rootIsHeapAccess,
+    rootIsSym,
+    markAsWrittenTo,
+    markAsEscaping
+
+  Assignment = object # \
+    # Note that the transitive closures MUST be computed in
+    # phase 2 of the algorithm.
+    dest, src: seq[ptr TSym] # we use 'ptr' here to save RC ops and GC cycles
+    destNoTc, srcNoTc: int # length of 'dest', 'src' without the
+                           # transitive closure
+    destInfo: set[RootInfo]
+    info: TLineInfo
+
   W = object # WriteTrackContext
     owner: PSym
     returnsNew: AssignToResult # assignments to 'result'
-    markAsWrittenTo, markAsEscaping: PNode
-    assignments: seq[(PNode, PNode)] # list of all assignments in this proc
+    assignments: seq[Assignment] # list of all assignments in this proc
+
+proc allRoots(n: PNode; result: var seq[ptr TSym]; info: var set[RootInfo]) =
+  case n.kind
+  of nkSym:
+    if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}:
+      if n.sym.kind in {skResult, skParam}: incl(info, rootIsResultOrParam)
+      result.add(cast[ptr TSym](n.sym))
+  of nkHiddenDeref, nkDerefExpr:
+    incl(info, rootIsHeapAccess)
+    allRoots(n.sons[0], result, info)
+  of nkDotExpr, nkBracketExpr, nkCheckedFieldExpr,
+      nkHiddenAddr, nkObjUpConv, nkObjDownConv:
+    allRoots(n.sons[0], result, info)
+  of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkConv,
+      nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkOfBranch,
+      nkElifBranch, nkElse, nkExceptBranch, nkFinally, nkCast:
+    allRoots(n.lastSon, result, info)
+  of nkCallKinds:
+    if getMagic(n) == mSlice:
+      allRoots(n.sons[1], result, info)
+    else:
+      # we do significantly better here by using the available escape
+      # information:
+      if n.sons[0].typ.isNil: return
+      var typ = n.sons[0].typ
+      if typ != nil:
+        typ = skipTypes(typ, abstractInst)
+        if typ.kind != tyProc: typ = nil
+        else: assert(sonsLen(typ) == sonsLen(typ.n))
+
+      for i in 1 ..< n.len:
+        let it = n.sons[i]
+        if typ != nil and i < sonsLen(typ):
+          assert(typ.n.sons[i].kind == nkSym)
+          let paramType = typ.n.sons[i]
+          if paramType.typ.isCompileTimeOnly: continue
+          if sfEscapes in paramType.sym.flags or paramType.typ.kind == tyVar:
+            allRoots(it, result, info)
+        else:
+          allRoots(it, result, info)
+  else:
+    for i in 0..<n.safeLen:
+      allRoots(n.sons[i], result, info)
+
+proc addAsgn(a: var Assignment; dest, src: PNode; destInfo: set[RootInfo]) =
+  a.dest = @[]
+  a.src = @[]
+  a.destInfo = destInfo
+  allRoots(dest, a.dest, a.destInfo)
+  if dest.kind == nkSym: incl(a.destInfo, rootIsSym)
+  if src != nil:
+    var dummy: set[RootInfo]
+    allRoots(src, a.src, dummy)
+  a.destNoTc = a.dest.len
+  a.srcNoTc = a.src.len
+  a.info = dest.info
+  #echo "ADDING ", dest.info, " ", a.destInfo
+
+proc srcHasSym(a: Assignment; x: ptr TSym): bool =
+  for i in 0 ..< a.srcNoTc:
+    if a.src[i] == x: return true
 
 proc returnsNewExpr*(n: PNode): NewLocation =
   case n.kind
@@ -54,10 +135,10 @@ proc returnsNewExpr*(n: PNode): NewLocation =
   else:
     result = newNone
 
-proc deps(w: var W; dest, src: PNode) =
+proc deps(w: var W; dest, src: PNode; destInfo: set[RootInfo]) =
   # let x = (localA, localB)
   # compute 'returnsNew' property:
-  let retNew = returnsNewExpr(src)
+  let retNew = if src.isNil: newNone else: returnsNewExpr(src)
   if dest.kind == nkSym and dest.sym.kind == skResult:
     if retNew != newNone:
       if w.returnsNew != asgnOther: w.returnsNew = asgnNew
@@ -66,7 +147,10 @@ proc deps(w: var W; dest, src: PNode) =
   # mark the dependency, but
   # rule out obviously innocent assignments like 'somebool = true'
   if dest.kind == nkSym and retNew == newLit: discard
-  else: w.assignments.add((dest, src))
+  else:
+    let L = w.assignments.len
+    w.assignments.setLen(L+1)
+    addAsgn(w.assignments[L], dest, src, destInfo)
 
 proc depsArgs(w: var W; n: PNode) =
   if n.sons[0].typ.isNil: return
@@ -80,11 +164,14 @@ proc depsArgs(w: var W; n: PNode) =
       assert(typ.n.sons[i].kind == nkSym)
       let paramType = typ.n.sons[i]
       if paramType.typ.isCompileTimeOnly: continue
+      var destInfo: set[RootInfo] = {}
       if sfWrittenTo in paramType.sym.flags or paramType.typ.kind == tyVar:
         # p(f(x, y), X, g(h, z))
-        deps(w, it, w.markAsWrittenTo)
+        destInfo.incl markAsWrittenTo
       if sfEscapes in paramType.sym.flags:
-        deps(w, it, w.markAsEscaping)
+        destInfo.incl markAsEscaping
+      if destInfo != {}:
+        deps(w, it, nil, destInfo)
 
 proc deps(w: var W; n: PNode) =
   case n.kind
@@ -95,168 +182,91 @@ proc deps(w: var W; n: PNode) =
       if child.kind == nkVarTuple and last.kind == nkPar:
         internalAssert child.len-2 == last.len
         for i in 0 .. child.len-3:
-          deps(w, child.sons[i], last.sons[i])
+          deps(w, child.sons[i], last.sons[i], {})
       else:
         for i in 0 .. child.len-3:
-          deps(w, child.sons[i], last)
+          deps(w, child.sons[i], last, {})
   of nkAsgn, nkFastAsgn:
-    deps(w, n.sons[0], n.sons[1])
+    deps(w, n.sons[0], n.sons[1], {})
   else:
     for i in 0 ..< n.safeLen:
       deps(w, n.sons[i])
     if n.kind in nkCallKinds:
       if getMagic(n) in {mNew, mNewFinalize, mNewSeq}:
         # may not look like an assignment, but it is:
-        deps(w, n.sons[1], newNodeIT(nkObjConstr, n.info, n.sons[1].typ))
+        deps(w, n.sons[1], newNodeIT(nkObjConstr, n.info, n.sons[1].typ), {})
       else:
         depsArgs(w, n)
 
-type
-  RootInfo = enum
-    rootIsResultOrParam,
-    rootIsHeapAccess
-
-proc allRoots(n: PNode; result: var seq[PSym]; info: var set[RootInfo]) =
-  case n.kind
-  of nkSym:
-    if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}:
-      if result.isNil: result = @[]
-      if n.sym notin result:
-        if n.sym.kind in {skResult, skParam}: incl(info, rootIsResultOrParam)
-        result.add n.sym
-  of nkHiddenDeref, nkDerefExpr:
-    incl(info, rootIsHeapAccess)
-    allRoots(n.sons[0], result, info)
-  of nkDotExpr, nkBracketExpr, nkCheckedFieldExpr,
-      nkHiddenAddr, nkObjUpConv, nkObjDownConv:
-    allRoots(n.sons[0], result, info)
-  of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv, nkConv,
-      nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkOfBranch,
-      nkElifBranch, nkElse, nkExceptBranch, nkFinally, nkCast:
-    allRoots(n.lastSon, result, info)
-  of nkCallKinds:
-    if getMagic(n) == mSlice:
-      allRoots(n.sons[1], result, info)
-    else:
-      # we do significantly better here by using the available escape
-      # information:
-      if n.sons[0].typ.isNil: return
-      var typ = n.sons[0].typ
-      if typ != nil:
-        typ = skipTypes(typ, abstractInst)
-        if typ.kind != tyProc: typ = nil
-        else: assert(sonsLen(typ) == sonsLen(typ.n))
-
-      for i in 1 ..< n.len:
-        let it = n.sons[i]
-        if typ != nil and i < sonsLen(typ):
-          assert(typ.n.sons[i].kind == nkSym)
-          let paramType = typ.n.sons[i]
-          if paramType.typ.isCompileTimeOnly: continue
-          if sfEscapes in paramType.sym.flags or paramType.typ.kind == tyVar:
-            allRoots(it, result, info)
-        else:
-          allRoots(it, result, info)
-  else:
-    for i in 0..<n.safeLen:
-      allRoots(n.sons[i], result, info)
-
-proc allRoots(n: PNode; result: var seq[PSym]) =
-  var dummy: set[RootInfo]
-  allRoots(n, result, dummy)
-
-proc hasSym(n: PNode; x: PSym): bool =
-  when false:
-    if n.kind == nkSym:
-      result = n.sym == x
-    else:
-      for i in 0..safeLen(n)-1:
-        if hasSym(n.sons[i], x): return true
-  else:
-    var tmp: seq[PSym]
-    allRoots(n, tmp)
-    result = not tmp.isNil and x in tmp
-
-when debug:
-  proc `$`*(x: PSym): string = x.name.s
-
-proc possibleAliases(w: W; result: var seq[PSym]) =
-  var todo = 0
+proc possibleAliases(w: var W; result: var seq[ptr TSym]) =
   # this is an expensive fixpoint iteration. We could speed up this analysis
   # by a smarter data-structure but we wait until prolifing shows us it's
   # expensive. Usually 'w.assignments' is small enough.
+  var alreadySeen = initIntSet()
+  template addNoDup(x) =
+    if not alreadySeen.containsOrIncl(x.id): result.add x
+  for x in result: alreadySeen.incl x.id
+
+  var todo = 0
   while todo < result.len:
     let x = result[todo]
     inc todo
-    when debug:
-      if w.owner.name.s == "m3": echo "select ", x, " ", todo, " ", result.len
-    for dest, src in items(w.assignments):
-      if src.hasSym(x):
-        # dest = f(..., s, ...)
-        allRoots(dest, result)
-        when debug:
-          if w.owner.name.s == "m3": echo "A ", result
-      elif dest.kind == nkSym and dest.sym == x:
-        # s = f(..., x, ....)
-        allRoots(src, result)
-        when debug:
-          if w.owner.name.s == "m3": echo "B ", result
-      else:
-        when debug:
-          if w.owner.name.s == "m3": echo "C ", x, " ", todo, " ", result.len
+    for a in mitems(w.assignments):
+      #if a.srcHasSym(x):
+      #  # y = f(..., x, ...)
+      #  for i in 0 ..< a.destNoTc: addNoDup a.dest[i]
+      if a.destNoTc > 0 and a.dest[0] == x and rootIsSym in a.destInfo:
+        # x = f(..., y, ....)
+        for i in 0 ..< a.srcNoTc: addNoDup a.src[i]
 
-proc markDirty(w: W) =
-  for dest, src in items(w.assignments):
-    var r: seq[PSym] = nil
-    var info: set[RootInfo]
-    allRoots(dest, r, info)
-    when debug:
-      if w.owner.info ?? "temp18":
-        echo "ASGN ", dest,  " = ", src, " |", heapAccess, " ", r.name.s
-    if rootIsHeapAccess in info or src == w.markAsWrittenTo:
-      # we have an assignment like:
-      # local.foo = bar
-      # --> check which parameter it may alias and mark these parameters
-      # as dirty:
-      possibleAliases(w, r)
-      for a in r:
-        if a.kind == skParam and a.owner == w.owner:
-          incl(a.flags, sfWrittenTo)
+proc markWriteOrEscape(w: var W) =
+  ## Both 'writes' and 'escapes' effects ultimately only care
+  ## about *parameters*.
+  ## However, due to aliasing, even locals that might not look as parameters
+  ## have to count as parameters if they can alias a parameter:
+  ##
+  ## .. code-block:: nim
+  ##   proc modifies(n: Node) {.writes: [n].} =
+  ##     let x = n
+  ##     x.data = "abc"
+  ##
+  ## We call a symbol *parameter-like* if it is a parameter or can alias a
+  ## parameter.
+  ## Let ``p``, ``q`` be *parameter-like* and ``x``, ``y`` be general
+  ## expressions.
+  ##
+  ## A write then looks like ``p[] = x``.
+  ## An escape looks like ``p[] = q`` or more generally
+  ## like ``p[] = f(q)`` where ``f`` can forward ``q``.
+  for a in mitems(w.assignments):
+    if a.destInfo != {}:
+      possibleAliases(w, a.dest)
 
-proc markEscaping(w: W) =
-  # let p1 = p
-  # let p2 = q
-  # p2.x = call(..., p1, ...)
-  for dest, src in items(w.assignments):
-    var r: seq[PSym] = nil
-    var info: set[RootInfo]
-    allRoots(dest, r, info)
+    if {rootIsHeapAccess, markAsWrittenTo} * a.destInfo != {}:
+      for p in a.dest:
+        if p.kind == skParam and p.owner == w.owner:
+          incl(p.flags, sfWrittenTo)
 
-    if (r.len > 0) and (info != {} or src == w.markAsEscaping):
-      possibleAliases(w, r)
+    if {rootIsResultOrParam, rootIsHeapAccess, markAsEscaping}*a.destInfo != {}:
       var destIsParam = false
-      for a in r:
-        if a.kind in {skResult, skParam} and a.owner == w.owner:
+      for p in a.dest:
+        if p.kind in {skResult, skParam} and p.owner == w.owner:
           destIsParam = true
           break
       if destIsParam:
-        var victims: seq[PSym] = @[]
-        allRoots(src, victims)
-        possibleAliases(w, victims)
-        for v in victims:
-          if v.kind == skParam and v.owner == w.owner:
-            incl(v.flags, sfEscapes)
+        possibleAliases(w, a.src)
+        for p in a.src:
+          if p.kind == skParam and p.owner == w.owner:
+            incl(p.flags, sfEscapes)
 
 proc trackWrites*(owner: PSym; body: PNode) =
   var w: W
   w.owner = owner
-  w.markAsWrittenTo = newNodeI(nkArgList, body.info)
-  w.markAsEscaping = newNodeI(nkArgList, body.info)
   w.assignments = @[]
+  # Phase 1: Collect and preprocess any assignments in the proc body:
   deps(w, body)
-  markDirty(w)
-  markEscaping(w)
+  # Phase 2: Compute the 'writes' and 'escapes' effects:
+  markWriteOrEscape(w)
   if w.returnsNew != asgnOther and not isEmptyType(owner.typ.sons[0]) and
       containsGarbageCollectedRef(owner.typ.sons[0]):
     incl(owner.typ.flags, tfReturnsNew)
-
diff --git a/lib/pure/os.nim b/lib/pure/os.nim
index 3d592a526..f413371cb 100644
--- a/lib/pure/os.nim
+++ b/lib/pure/os.nim
@@ -27,147 +27,7 @@ else:
   {.error: "OS module not ported to your operating system!".}
 
 include "system/ansi_c"
-
-type
-  ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
-                                            ## from an environment variable
-  WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
-                                            ## to an environment variable
-
-  ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a write
-                                            ## operation to the directory structure
-  WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write operation to
-                                            ## the directory structure
-
-  OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
-
-{.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
-    FReadDir: ReadDirEffect,
-    FWriteDir: WriteDirEffect,
-    TOSErrorCode: OSErrorCode
-].}
-const
-  doslike = defined(windows) or defined(OS2) or defined(DOS)
-    # DOS-like filesystem
-
-when defined(Nimdoc): # only for proper documentation:
-  const
-    CurDir* = '.'
-      ## The constant string used by the operating system to refer to the
-      ## current directory.
-      ##
-      ## For example: '.' for POSIX or ':' for the classic Macintosh.
-
-    ParDir* = ".."
-      ## The constant string used by the operating system to refer to the
-      ## parent directory.
-      ##
-      ## For example: ".." for POSIX or "::" for the classic Macintosh.
-
-    DirSep* = '/'
-      ## The character used by the operating system to separate pathname
-      ## components, for example, '/' for POSIX or ':' for the classic
-      ## Macintosh.
-
-    AltSep* = '/'
-      ## An alternative character used by the operating system to separate
-      ## pathname components, or the same as `DirSep` if only one separator
-      ## character exists. This is set to '/' on Windows systems where `DirSep`
-      ## is a backslash.
-
-    PathSep* = ':'
-      ## The character conventionally used by the operating system to separate
-      ## search patch components (as in PATH), such as ':' for POSIX or ';' for
-      ## Windows.
-
-    FileSystemCaseSensitive* = true
-      ## true if the file system is case sensitive, false otherwise. Used by
-      ## `cmpPaths` to compare filenames properly.
-
-    ExeExt* = ""
-      ## The file extension of native executables. For example:
-      ## "" for POSIX, "exe" on Windows.
-
-    ScriptExt* = ""
-      ## The file extension of a script file. For example: "" for POSIX,
-      ## "bat" on Windows.
-
-    DynlibFormat* = "lib$1.so"
-      ## The format string to turn a filename into a `DLL`:idx: file (also
-      ## called `shared object`:idx: on some operating systems).
-
-elif defined(macos):
-  const
-    CurDir* = ':'
-    ParDir* = "::"
-    DirSep* = ':'
-    AltSep* = Dirsep
-    PathSep* = ','
-    FileSystemCaseSensitive* = false
-    ExeExt* = ""
-    ScriptExt* = ""
-    DynlibFormat* = "$1.dylib"
-
-  #  MacOS paths
-  #  ===========
-  #  MacOS directory separator is a colon ":" which is the only character not
-  #  allowed in filenames.
-  #
-  #  A path containing no colon or which begins with a colon is a partial path.
-  #  E.g. ":kalle:petter" ":kalle" "kalle"
-  #
-  #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
-  #  When generating paths, one is safe if one ensures that all partial paths
-  #  begin with a colon, and all full paths end with a colon.
-  #  In full paths the first name (e g HD above) is the name of a mounted
-  #  volume.
-  #  These names are not unique, because, for instance, two diskettes with the
-  #  same names could be inserted. This means that paths on MacOS are not
-  #  waterproof. In case of equal names the first volume found will do.
-  #  Two colons "::" are the relative path to the parent. Three is to the
-  #  grandparent etc.
-elif doslike:
-  const
-    CurDir* = '.'
-    ParDir* = ".."
-    DirSep* = '\\' # seperator within paths
-    AltSep* = '/'
-    PathSep* = ';' # seperator between paths
-    FileSystemCaseSensitive* = false
-    ExeExt* = "exe"
-    ScriptExt* = "bat"
-    DynlibFormat* = "$1.dll"
-elif defined(PalmOS) or defined(MorphOS):
-  const
-    DirSep* = '/'
-    AltSep* = Dirsep
-    PathSep* = ';'
-    ParDir* = ".."
-    FileSystemCaseSensitive* = false
-    ExeExt* = ""
-    ScriptExt* = ""
-    DynlibFormat* = "$1.prc"
-elif defined(RISCOS):
-  const
-    DirSep* = '.'
-    AltSep* = '.'
-    ParDir* = ".." # is this correct?
-    PathSep* = ','
-    FileSystemCaseSensitive* = true
-    ExeExt* = ""
-    ScriptExt* = ""
-    DynlibFormat* = "lib$1.so"
-else: # UNIX-like operating system
-  const
-    CurDir* = '.'
-    ParDir* = ".."
-    DirSep* = '/'
-    AltSep* = DirSep
-    PathSep* = ':'
-    FileSystemCaseSensitive* = true
-    ExeExt* = ""
-    ScriptExt* = ""
-    DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
+include ospaths
 
 when defined(posix):
   when NoFakeVars:
@@ -177,11 +37,6 @@ when defined(posix):
     var
       pathMax {.importc: "PATH_MAX", header: "<stdlib.h>".}: cint
 
-const
-  ExtSep* = '.'
-    ## The character which separates the base filename from the extension;
-    ## for example, the '.' in ``os.nim``.
-
 proc osErrorMsg*(): string {.rtl, extern: "nos$1", deprecated.} =
   ## Retrieves the operating system's error flag, ``errno``.
   ## On Windows ``GetLastError`` is checked before ``errno``.
@@ -300,61 +155,6 @@ proc osLastError*(): OSErrorCode =
     result = OSErrorCode(errno)
 {.pop.}
 
-proc unixToNativePath*(path: string, drive=""): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Converts an UNIX-like path to a native one.
-  ##
-  ## On an UNIX system this does nothing. Else it converts
-  ## '/', '.', '..' to the appropriate things.
-  ##
-  ## On systems with a concept of "drives", `drive` is used to determine
-  ## which drive label to use during absolute path conversion.
-  ## `drive` defaults to the drive of the current working directory, and is
-  ## ignored on systems that do not have a concept of "drives".
-
-  when defined(unix):
-    result = path
-  else:
-    var start: int
-    if path[0] == '/':
-      # an absolute path
-      when doslike:
-        if drive != "":
-          result = drive & ":" & DirSep
-        else:
-          result = $DirSep
-      elif defined(macos):
-        result = "" # must not start with ':'
-      else:
-        result = $DirSep
-      start = 1
-    elif path[0] == '.' and path[1] == '/':
-      # current directory
-      result = $CurDir
-      start = 2
-    else:
-      result = ""
-      start = 0
-
-    var i = start
-    while i < len(path): # ../../../ --> ::::
-      if path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
-        # parent directory
-        when defined(macos):
-          if result[high(result)] == ':':
-            add result, ':'
-          else:
-            add result, ParDir
-        else:
-          add result, ParDir & DirSep
-        inc(i, 3)
-      elif path[i] == '/':
-        add result, DirSep
-        inc(i)
-      else:
-        add result, path[i]
-        inc(i)
-
 when defined(windows):
   when useWinUnicode:
     template wrapUnary(varname, winApiProc, arg: expr) {.immediate.} =
@@ -518,220 +318,6 @@ proc setCurrentDir*(newDir: string) {.inline, tags: [].} =
   else:
     if chdir(newDir) != 0'i32: raiseOSError(osLastError())
 
-proc joinPath*(head, tail: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Joins two directory names to one.
-  ##
-  ## For example on Unix:
-  ##
-  ## .. code-block:: nim
-  ##   joinPath("usr", "lib")
-  ##
-  ## results in:
-  ##
-  ## .. code-block:: nim
-  ##   "usr/lib"
-  ##
-  ## If head is the empty string, tail is returned. If tail is the empty
-  ## string, head is returned with a trailing path separator. If tail starts
-  ## with a path separator it will be removed when concatenated to head. Other
-  ## path separators not located on boundaries won't be modified. More
-  ## examples on Unix:
-  ##
-  ## .. code-block:: nim
-  ##   assert joinPath("usr", "") == "usr/"
-  ##   assert joinPath("", "lib") == "lib"
-  ##   assert joinPath("", "/lib") == "/lib"
-  ##   assert joinPath("usr/", "/lib") == "usr/lib"
-  if len(head) == 0:
-    result = tail
-  elif head[len(head)-1] in {DirSep, AltSep}:
-    if tail[0] in {DirSep, AltSep}:
-      result = head & substr(tail, 1)
-    else:
-      result = head & tail
-  else:
-    if tail[0] in {DirSep, AltSep}:
-      result = head & tail
-    else:
-      result = head & DirSep & tail
-
-proc joinPath*(parts: varargs[string]): string {.noSideEffect,
-  rtl, extern: "nos$1OpenArray".} =
-  ## The same as `joinPath(head, tail)`, but works with any number of directory
-  ## parts. You need to pass at least one element or the proc will assert in
-  ## debug builds and crash on release builds.
-  result = parts[0]
-  for i in 1..high(parts):
-    result = joinPath(result, parts[i])
-
-proc `/` * (head, tail: string): string {.noSideEffect.} =
-  ## The same as ``joinPath(head, tail)``
-  ##
-  ## Here are some examples for Unix:
-  ##
-  ## .. code-block:: nim
-  ##   assert "usr" / "" == "usr/"
-  ##   assert "" / "lib" == "lib"
-  ##   assert "" / "/lib" == "/lib"
-  ##   assert "usr/" / "/lib" == "usr/lib"
-  return joinPath(head, tail)
-
-proc splitPath*(path: string): tuple[head, tail: string] {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Splits a directory into (head, tail), so that
-  ## ``head / tail == path`` (except for edge cases like "/usr").
-  ##
-  ## Examples:
-  ##
-  ## .. code-block:: nim
-  ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
-  ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
-  ##   splitPath("bin") -> ("", "bin")
-  ##   splitPath("/bin") -> ("", "bin")
-  ##   splitPath("") -> ("", "")
-  var sepPos = -1
-  for i in countdown(len(path)-1, 0):
-    if path[i] in {DirSep, AltSep}:
-      sepPos = i
-      break
-  if sepPos >= 0:
-    result.head = substr(path, 0, sepPos-1)
-    result.tail = substr(path, sepPos+1)
-  else:
-    result.head = ""
-    result.tail = path
-
-proc parentDirPos(path: string): int =
-  var q = 1
-  if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
-  for i in countdown(len(path)-q, 0):
-    if path[i] in {DirSep, AltSep}: return i
-  result = -1
-
-proc parentDir*(path: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Returns the parent directory of `path`.
-  ##
-  ## This is often the same as the ``head`` result of ``splitPath``.
-  ## If there is no parent, "" is returned.
-  ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
-  ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
-  let sepPos = parentDirPos(path)
-  if sepPos >= 0:
-    result = substr(path, 0, sepPos-1)
-  else:
-    result = ""
-
-proc tailDir*(path: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Returns the tail part of `path`..
-  ##
-  ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
-  ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
-  ## | Example: ``tailDir("bin") == ""``.
-  var q = 1
-  if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
-  for i in 0..len(path)-q:
-    if path[i] in {DirSep, AltSep}:
-      return substr(path, i+1)
-  result = ""
-
-proc isRootDir*(path: string): bool {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Checks whether a given `path` is a root directory
-  result = parentDirPos(path) < 0
-
-iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
-  ## Walks over all parent directories of a given `path`
-  ##
-  ## If `fromRoot` is set, the traversal will start from the file system root
-  ## diretory. If `inclusive` is set, the original argument will be included
-  ## in the traversal.
-  ##
-  ## Relative paths won't be expanded by this proc. Instead, it will traverse
-  ## only the directories appearing in the relative path.
-  if not fromRoot:
-    var current = path
-    if inclusive: yield path
-    while true:
-      if current.isRootDir: break
-      current = current.parentDir
-      yield current
-  else:
-    for i in countup(0, path.len - 2): # ignore the last /
-      # deal with non-normalized paths such as /foo//bar//baz
-      if path[i] in {DirSep, AltSep} and
-          (i == 0 or path[i-1] notin {DirSep, AltSep}):
-        yield path.substr(0, i)
-
-    if inclusive: yield path
-
-proc `/../` * (head, tail: string): string {.noSideEffect.} =
-  ## The same as ``parentDir(head) / tail`` unless there is no parent directory.
-  ## Then ``head / tail`` is performed instead.
-  let sepPos = parentDirPos(head)
-  if sepPos >= 0:
-    result = substr(head, 0, sepPos-1) / tail
-  else:
-    result = head / tail
-
-proc normExt(ext: string): string =
-  if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
-  else: result = ExtSep & ext
-
-proc searchExtPos(s: string): int =
-  # BUGFIX: do not search until 0! .DS_Store is no file extension!
-  result = -1
-  for i in countdown(len(s)-1, 1):
-    if s[i] == ExtSep:
-      result = i
-      break
-    elif s[i] in {DirSep, AltSep}:
-      break # do not skip over path
-
-proc splitFile*(path: string): tuple[dir, name, ext: string] {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Splits a filename into (dir, filename, extension).
-  ## `dir` does not end in `DirSep`.
-  ## `extension` includes the leading dot.
-  ##
-  ## Example:
-  ##
-  ## .. code-block:: nim
-  ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
-  ##   assert dir == "usr/local"
-  ##   assert name == "nimc"
-  ##   assert ext == ".html"
-  ##
-  ## If `path` has no extension, `ext` is the empty string.
-  ## If `path` has no directory component, `dir` is the empty string.
-  ## If `path` has no filename component, `name` and `ext` are empty strings.
-  if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
-    result = (path, "", "")
-  else:
-    var sepPos = -1
-    var dotPos = path.len
-    for i in countdown(len(path)-1, 0):
-      if path[i] == ExtSep:
-        if dotPos == path.len and i > 0 and
-            path[i-1] notin {DirSep, AltSep}: dotPos = i
-      elif path[i] in {DirSep, AltSep}:
-        sepPos = i
-        break
-    result.dir = substr(path, 0, sepPos-1)
-    result.name = substr(path, sepPos+1, dotPos-1)
-    result.ext = substr(path, dotPos)
-
-proc extractFilename*(path: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Extracts the filename of a given `path`. This is the same as
-  ## ``name & ext`` from ``splitFile(path)``.
-  if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
-    result = ""
-  else:
-    result = splitPath(path).tail
-
 proc expandFilename*(filename: string): string {.rtl, extern: "nos$1",
   tags: [ReadDirEffect].} =
   ## Returns the full path of `filename`, raises OSError in case of an error.
@@ -757,61 +343,6 @@ proc expandFilename*(filename: string): string {.rtl, extern: "nos$1",
     if r.isNil: raiseOSError(osLastError())
     setLen(result, c_strlen(result))
 
-proc changeFileExt*(filename, ext: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Changes the file extension to `ext`.
-  ##
-  ## If the `filename` has no extension, `ext` will be added.
-  ## If `ext` == "" then any extension is removed.
-  ## `Ext` should be given without the leading '.', because some
-  ## filesystems may use a different character. (Although I know
-  ## of none such beast.)
-  var extPos = searchExtPos(filename)
-  if extPos < 0: result = filename & normExt(ext)
-  else: result = substr(filename, 0, extPos-1) & normExt(ext)
-
-proc addFileExt*(filename, ext: string): string {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Adds the file extension `ext` to `filename`, unless
-  ## `filename` already has an extension.
-  ##
-  ## `Ext` should be given without the leading '.', because some
-  ## filesystems may use a different character.
-  ## (Although I know of none such beast.)
-  var extPos = searchExtPos(filename)
-  if extPos < 0: result = filename & normExt(ext)
-  else: result = filename
-
-proc cmpPaths*(pathA, pathB: string): int {.
-  noSideEffect, rtl, extern: "nos$1".} =
-  ## Compares two paths.
-  ##
-  ## On a case-sensitive filesystem this is done
-  ## case-sensitively otherwise case-insensitively. Returns:
-  ##
-  ## | 0 iff pathA == pathB
-  ## | < 0 iff pathA < pathB
-  ## | > 0 iff pathA > pathB
-  if FileSystemCaseSensitive:
-    result = cmp(pathA, pathB)
-  else:
-    result = cmpIgnoreCase(pathA, pathB)
-
-proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
-  ## Checks whether a given `path` is absolute.
-  ##
-  ## On Windows, network paths are considered absolute too.
-  when doslike:
-    var len = len(path)
-    result = (len > 1 and path[0] in {'/', '\\'}) or
-             (len > 2 and path[0] in Letters and path[1] == ':')
-  elif defined(macos):
-    result = path.len > 0 and path[0] != ':'
-  elif defined(RISCOS):
-    result = path[0] == '$'
-  elif defined(posix):
-    result = path[0] == '/'
-
 when defined(Windows):
   proc openHandle(path: string, followSymlink=true): Handle =
     var flags = FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL
@@ -1649,24 +1180,7 @@ proc exclFilePermissions*(filename: string,
   ##   setFilePermissions(filename, getFilePermissions(filename)-permissions)
   setFilePermissions(filename, getFilePermissions(filename)-permissions)
 
-proc getHomeDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
-  ## Returns the home directory of the current user.
-  ##
-  ## This proc is wrapped by the expandTilde proc for the convenience of
-  ## processing paths coming from user configuration files.
-  when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
-  else: return string(getEnv("HOME")) & "/"
-
-proc getConfigDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
-  ## Returns the config directory of the current user for applications.
-  when defined(windows): return string(getEnv("APPDATA")) & "\\"
-  else: return string(getEnv("HOME")) & "/.config/"
-
-proc getTempDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
-  ## Returns the temporary directory of the current user for applications to
-  ## save temporary files in.
-  when defined(windows): return string(getEnv("TEMP")) & "\\"
-  else: return "/tmp/"
+include ospaths
 
 proc expandSymlink*(symlinkPath: string): string =
   ## Returns a string representing the path to which the symbolic link points.
@@ -1907,42 +1421,6 @@ proc getFileSize*(file: string): BiggestInt {.rtl, extern: "nos$1",
       close(f)
     else: raiseOSError(osLastError())
 
-proc expandTilde*(path: string): string {.tags: [ReadEnvEffect].} =
-  ## Expands a path starting with ``~/`` to a full path.
-  ##
-  ## If `path` starts with the tilde character and is followed by `/` or `\\`
-  ## this proc will return the reminder of the path appended to the result of
-  ## the getHomeDir() proc, otherwise the input path will be returned without
-  ## modification.
-  ##
-  ## The behaviour of this proc is the same on the Windows platform despite not
-  ## having this convention. Example:
-  ##
-  ## .. code-block:: nim
-  ##   let configFile = expandTilde("~" / "appname.cfg")
-  ##   echo configFile
-  ##   # --> C:\Users\amber\appname.cfg
-  if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
-    result = getHomeDir() / path[2..len(path)-1]
-  else:
-    result = path
-
-proc findExe*(exe: string): string {.tags: [ReadDirEffect, ReadEnvEffect].} =
-  ## Searches for `exe` in the current working directory and then
-  ## in directories listed in the ``PATH`` environment variable.
-  ## Returns "" if the `exe` cannot be found. On DOS-like platforms, `exe`
-  ## is added the `ExeExt <#ExeExt>`_ file extension if it has none.
-  result = addFileExt(exe, os.ExeExt)
-  if existsFile(result): return
-  var path = string(os.getEnv("PATH"))
-  for candidate in split(path, PathSep):
-    when defined(windows):
-      var x = candidate / result
-    else:
-      var x = expandTilde(candidate) / result
-    if existsFile(x): return x
-  result = ""
-
 when defined(Windows):
   type
     DeviceId* = int32
diff --git a/lib/pure/ospaths.nim b/lib/pure/ospaths.nim
new file mode 100644
index 000000000..55962a6a5
--- /dev/null
+++ b/lib/pure/ospaths.nim
@@ -0,0 +1,560 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2015 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+# Included by the ``os`` module but a module in its own right for NimScript
+# support.
+
+when defined(nimscript):
+  {.pragma: rtl.}
+  {.push hint[ConvFromXtoItselfNotNeeded]:off.}
+
+when not declared(getEnv) or defined(nimscript):
+  type
+    ReadEnvEffect* = object of ReadIOEffect   ## effect that denotes a read
+                                              ## from an environment variable
+    WriteEnvEffect* = object of WriteIOEffect ## effect that denotes a write
+                                              ## to an environment variable
+
+    ReadDirEffect* = object of ReadIOEffect   ## effect that denotes a write
+                                              ## operation to the directory structure
+    WriteDirEffect* = object of WriteIOEffect ## effect that denotes a write operation to
+                                              ## the directory structure
+
+    OSErrorCode* = distinct int32 ## Specifies an OS Error Code.
+
+  {.deprecated: [FReadEnv: ReadEnvEffect, FWriteEnv: WriteEnvEffect,
+      FReadDir: ReadDirEffect,
+      FWriteDir: WriteDirEffect,
+      TOSErrorCode: OSErrorCode
+  ].}
+  const
+    doslike = defined(windows) or defined(OS2) or defined(DOS)
+      # DOS-like filesystem
+
+  when defined(Nimdoc): # only for proper documentation:
+    const
+      CurDir* = '.'
+        ## The constant string used by the operating system to refer to the
+        ## current directory.
+        ##
+        ## For example: '.' for POSIX or ':' for the classic Macintosh.
+
+      ParDir* = ".."
+        ## The constant string used by the operating system to refer to the
+        ## parent directory.
+        ##
+        ## For example: ".." for POSIX or "::" for the classic Macintosh.
+
+      DirSep* = '/'
+        ## The character used by the operating system to separate pathname
+        ## components, for example, '/' for POSIX or ':' for the classic
+        ## Macintosh.
+
+      AltSep* = '/'
+        ## An alternative character used by the operating system to separate
+        ## pathname components, or the same as `DirSep` if only one separator
+        ## character exists. This is set to '/' on Windows systems where `DirSep`
+        ## is a backslash.
+
+      PathSep* = ':'
+        ## The character conventionally used by the operating system to separate
+        ## search patch components (as in PATH), such as ':' for POSIX or ';' for
+        ## Windows.
+
+      FileSystemCaseSensitive* = true
+        ## true if the file system is case sensitive, false otherwise. Used by
+        ## `cmpPaths` to compare filenames properly.
+
+      ExeExt* = ""
+        ## The file extension of native executables. For example:
+        ## "" for POSIX, "exe" on Windows.
+
+      ScriptExt* = ""
+        ## The file extension of a script file. For example: "" for POSIX,
+        ## "bat" on Windows.
+
+      DynlibFormat* = "lib$1.so"
+        ## The format string to turn a filename into a `DLL`:idx: file (also
+        ## called `shared object`:idx: on some operating systems).
+
+  elif defined(macos):
+    const
+      CurDir* = ':'
+      ParDir* = "::"
+      DirSep* = ':'
+      AltSep* = Dirsep
+      PathSep* = ','
+      FileSystemCaseSensitive* = false
+      ExeExt* = ""
+      ScriptExt* = ""
+      DynlibFormat* = "$1.dylib"
+
+    #  MacOS paths
+    #  ===========
+    #  MacOS directory separator is a colon ":" which is the only character not
+    #  allowed in filenames.
+    #
+    #  A path containing no colon or which begins with a colon is a partial path.
+    #  E.g. ":kalle:petter" ":kalle" "kalle"
+    #
+    #  All other paths are full (absolute) paths. E.g. "HD:kalle:" "HD:"
+    #  When generating paths, one is safe if one ensures that all partial paths
+    #  begin with a colon, and all full paths end with a colon.
+    #  In full paths the first name (e g HD above) is the name of a mounted
+    #  volume.
+    #  These names are not unique, because, for instance, two diskettes with the
+    #  same names could be inserted. This means that paths on MacOS are not
+    #  waterproof. In case of equal names the first volume found will do.
+    #  Two colons "::" are the relative path to the parent. Three is to the
+    #  grandparent etc.
+  elif doslike:
+    const
+      CurDir* = '.'
+      ParDir* = ".."
+      DirSep* = '\\' # seperator within paths
+      AltSep* = '/'
+      PathSep* = ';' # seperator between paths
+      FileSystemCaseSensitive* = false
+      ExeExt* = "exe"
+      ScriptExt* = "bat"
+      DynlibFormat* = "$1.dll"
+  elif defined(PalmOS) or defined(MorphOS):
+    const
+      DirSep* = '/'
+      AltSep* = Dirsep
+      PathSep* = ';'
+      ParDir* = ".."
+      FileSystemCaseSensitive* = false
+      ExeExt* = ""
+      ScriptExt* = ""
+      DynlibFormat* = "$1.prc"
+  elif defined(RISCOS):
+    const
+      DirSep* = '.'
+      AltSep* = '.'
+      ParDir* = ".." # is this correct?
+      PathSep* = ','
+      FileSystemCaseSensitive* = true
+      ExeExt* = ""
+      ScriptExt* = ""
+      DynlibFormat* = "lib$1.so"
+  else: # UNIX-like operating system
+    const
+      CurDir* = '.'
+      ParDir* = ".."
+      DirSep* = '/'
+      AltSep* = DirSep
+      PathSep* = ':'
+      FileSystemCaseSensitive* = true
+      ExeExt* = ""
+      ScriptExt* = ""
+      DynlibFormat* = when defined(macosx): "lib$1.dylib" else: "lib$1.so"
+
+  const
+    ExtSep* = '.'
+      ## The character which separates the base filename from the extension;
+      ## for example, the '.' in ``os.nim``.
+
+
+  proc joinPath*(head, tail: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Joins two directory names to one.
+    ##
+    ## For example on Unix:
+    ##
+    ## .. code-block:: nim
+    ##   joinPath("usr", "lib")
+    ##
+    ## results in:
+    ##
+    ## .. code-block:: nim
+    ##   "usr/lib"
+    ##
+    ## If head is the empty string, tail is returned. If tail is the empty
+    ## string, head is returned with a trailing path separator. If tail starts
+    ## with a path separator it will be removed when concatenated to head. Other
+    ## path separators not located on boundaries won't be modified. More
+    ## examples on Unix:
+    ##
+    ## .. code-block:: nim
+    ##   assert joinPath("usr", "") == "usr/"
+    ##   assert joinPath("", "lib") == "lib"
+    ##   assert joinPath("", "/lib") == "/lib"
+    ##   assert joinPath("usr/", "/lib") == "usr/lib"
+    if len(head) == 0:
+      result = tail
+    elif head[len(head)-1] in {DirSep, AltSep}:
+      if tail[0] in {DirSep, AltSep}:
+        result = head & substr(tail, 1)
+      else:
+        result = head & tail
+    else:
+      if tail[0] in {DirSep, AltSep}:
+        result = head & tail
+      else:
+        result = head & DirSep & tail
+
+  proc joinPath*(parts: varargs[string]): string {.noSideEffect,
+    rtl, extern: "nos$1OpenArray".} =
+    ## The same as `joinPath(head, tail)`, but works with any number of directory
+    ## parts. You need to pass at least one element or the proc will assert in
+    ## debug builds and crash on release builds.
+    result = parts[0]
+    for i in 1..high(parts):
+      result = joinPath(result, parts[i])
+
+  proc `/` * (head, tail: string): string {.noSideEffect.} =
+    ## The same as ``joinPath(head, tail)``
+    ##
+    ## Here are some examples for Unix:
+    ##
+    ## .. code-block:: nim
+    ##   assert "usr" / "" == "usr/"
+    ##   assert "" / "lib" == "lib"
+    ##   assert "" / "/lib" == "/lib"
+    ##   assert "usr/" / "/lib" == "usr/lib"
+    return joinPath(head, tail)
+
+  proc splitPath*(path: string): tuple[head, tail: string] {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Splits a directory into (head, tail), so that
+    ## ``head / tail == path`` (except for edge cases like "/usr").
+    ##
+    ## Examples:
+    ##
+    ## .. code-block:: nim
+    ##   splitPath("usr/local/bin") -> ("usr/local", "bin")
+    ##   splitPath("usr/local/bin/") -> ("usr/local/bin", "")
+    ##   splitPath("bin") -> ("", "bin")
+    ##   splitPath("/bin") -> ("", "bin")
+    ##   splitPath("") -> ("", "")
+    var sepPos = -1
+    for i in countdown(len(path)-1, 0):
+      if path[i] in {DirSep, AltSep}:
+        sepPos = i
+        break
+    if sepPos >= 0:
+      result.head = substr(path, 0, sepPos-1)
+      result.tail = substr(path, sepPos+1)
+    else:
+      result.head = ""
+      result.tail = path
+
+  proc parentDirPos(path: string): int =
+    var q = 1
+    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
+    for i in countdown(len(path)-q, 0):
+      if path[i] in {DirSep, AltSep}: return i
+    result = -1
+
+  proc parentDir*(path: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Returns the parent directory of `path`.
+    ##
+    ## This is often the same as the ``head`` result of ``splitPath``.
+    ## If there is no parent, "" is returned.
+    ## | Example: ``parentDir("/usr/local/bin") == "/usr/local"``.
+    ## | Example: ``parentDir("/usr/local/bin/") == "/usr/local"``.
+    let sepPos = parentDirPos(path)
+    if sepPos >= 0:
+      result = substr(path, 0, sepPos-1)
+    else:
+      result = ""
+
+  proc tailDir*(path: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Returns the tail part of `path`..
+    ##
+    ## | Example: ``tailDir("/usr/local/bin") == "local/bin"``.
+    ## | Example: ``tailDir("usr/local/bin/") == "local/bin"``.
+    ## | Example: ``tailDir("bin") == ""``.
+    var q = 1
+    if len(path) >= 1 and path[len(path)-1] in {DirSep, AltSep}: q = 2
+    for i in 0..len(path)-q:
+      if path[i] in {DirSep, AltSep}:
+        return substr(path, i+1)
+    result = ""
+
+  proc isRootDir*(path: string): bool {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Checks whether a given `path` is a root directory
+    result = parentDirPos(path) < 0
+
+  iterator parentDirs*(path: string, fromRoot=false, inclusive=true): string =
+    ## Walks over all parent directories of a given `path`
+    ##
+    ## If `fromRoot` is set, the traversal will start from the file system root
+    ## diretory. If `inclusive` is set, the original argument will be included
+    ## in the traversal.
+    ##
+    ## Relative paths won't be expanded by this proc. Instead, it will traverse
+    ## only the directories appearing in the relative path.
+    if not fromRoot:
+      var current = path
+      if inclusive: yield path
+      while true:
+        if current.isRootDir: break
+        current = current.parentDir
+        yield current
+    else:
+      for i in countup(0, path.len - 2): # ignore the last /
+        # deal with non-normalized paths such as /foo//bar//baz
+        if path[i] in {DirSep, AltSep} and
+            (i == 0 or path[i-1] notin {DirSep, AltSep}):
+          yield path.substr(0, i)
+
+      if inclusive: yield path
+
+  proc `/../` * (head, tail: string): string {.noSideEffect.} =
+    ## The same as ``parentDir(head) / tail`` unless there is no parent directory.
+    ## Then ``head / tail`` is performed instead.
+    let sepPos = parentDirPos(head)
+    if sepPos >= 0:
+      result = substr(head, 0, sepPos-1) / tail
+    else:
+      result = head / tail
+
+  proc normExt(ext: string): string =
+    if ext == "" or ext[0] == ExtSep: result = ext # no copy needed here
+    else: result = ExtSep & ext
+
+  proc searchExtPos(s: string): int =
+    # BUGFIX: do not search until 0! .DS_Store is no file extension!
+    result = -1
+    for i in countdown(len(s)-1, 1):
+      if s[i] == ExtSep:
+        result = i
+        break
+      elif s[i] in {DirSep, AltSep}:
+        break # do not skip over path
+
+  proc splitFile*(path: string): tuple[dir, name, ext: string] {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Splits a filename into (dir, filename, extension).
+    ## `dir` does not end in `DirSep`.
+    ## `extension` includes the leading dot.
+    ##
+    ## Example:
+    ##
+    ## .. code-block:: nim
+    ##   var (dir, name, ext) = splitFile("usr/local/nimc.html")
+    ##   assert dir == "usr/local"
+    ##   assert name == "nimc"
+    ##   assert ext == ".html"
+    ##
+    ## If `path` has no extension, `ext` is the empty string.
+    ## If `path` has no directory component, `dir` is the empty string.
+    ## If `path` has no filename component, `name` and `ext` are empty strings.
+    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
+      result = (path, "", "")
+    else:
+      var sepPos = -1
+      var dotPos = path.len
+      for i in countdown(len(path)-1, 0):
+        if path[i] == ExtSep:
+          if dotPos == path.len and i > 0 and
+              path[i-1] notin {DirSep, AltSep}: dotPos = i
+        elif path[i] in {DirSep, AltSep}:
+          sepPos = i
+          break
+      result.dir = substr(path, 0, sepPos-1)
+      result.name = substr(path, sepPos+1, dotPos-1)
+      result.ext = substr(path, dotPos)
+
+  proc extractFilename*(path: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Extracts the filename of a given `path`. This is the same as
+    ## ``name & ext`` from ``splitFile(path)``.
+    if path.len == 0 or path[path.len-1] in {DirSep, AltSep}:
+      result = ""
+    else:
+      result = splitPath(path).tail
+
+
+  proc changeFileExt*(filename, ext: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Changes the file extension to `ext`.
+    ##
+    ## If the `filename` has no extension, `ext` will be added.
+    ## If `ext` == "" then any extension is removed.
+    ## `Ext` should be given without the leading '.', because some
+    ## filesystems may use a different character. (Although I know
+    ## of none such beast.)
+    var extPos = searchExtPos(filename)
+    if extPos < 0: result = filename & normExt(ext)
+    else: result = substr(filename, 0, extPos-1) & normExt(ext)
+
+  proc addFileExt*(filename, ext: string): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Adds the file extension `ext` to `filename`, unless
+    ## `filename` already has an extension.
+    ##
+    ## `Ext` should be given without the leading '.', because some
+    ## filesystems may use a different character.
+    ## (Although I know of none such beast.)
+    var extPos = searchExtPos(filename)
+    if extPos < 0: result = filename & normExt(ext)
+    else: result = filename
+
+  proc cmpPaths*(pathA, pathB: string): int {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Compares two paths.
+    ##
+    ## On a case-sensitive filesystem this is done
+    ## case-sensitively otherwise case-insensitively. Returns:
+    ##
+    ## | 0 iff pathA == pathB
+    ## | < 0 iff pathA < pathB
+    ## | > 0 iff pathA > pathB
+    if FileSystemCaseSensitive:
+      result = cmp(pathA, pathB)
+    else:
+      when defined(nimscript):
+        result = cmpic(pathA, pathB)
+      else:
+        result = cmpIgnoreCase(pathA, pathB)
+
+  proc isAbsolute*(path: string): bool {.rtl, noSideEffect, extern: "nos$1".} =
+    ## Checks whether a given `path` is absolute.
+    ##
+    ## On Windows, network paths are considered absolute too.
+    when doslike:
+      var len = len(path)
+      result = (len > 1 and path[0] in {'/', '\\'}) or
+               (len > 2 and path[0] in {'a'..'z', 'A'..'Z'} and path[1] == ':')
+    elif defined(macos):
+      result = path.len > 0 and path[0] != ':'
+    elif defined(RISCOS):
+      result = path[0] == '$'
+    elif defined(posix):
+      result = path[0] == '/'
+
+  proc unixToNativePath*(path: string, drive=""): string {.
+    noSideEffect, rtl, extern: "nos$1".} =
+    ## Converts an UNIX-like path to a native one.
+    ##
+    ## On an UNIX system this does nothing. Else it converts
+    ## '/', '.', '..' to the appropriate things.
+    ##
+    ## On systems with a concept of "drives", `drive` is used to determine
+    ## which drive label to use during absolute path conversion.
+    ## `drive` defaults to the drive of the current working directory, and is
+    ## ignored on systems that do not have a concept of "drives".
+
+    when defined(unix):
+      result = path
+    else:
+      var start: int
+      if path[0] == '/':
+        # an absolute path
+        when doslike:
+          if drive != "":
+            result = drive & ":" & DirSep
+          else:
+            result = $DirSep
+        elif defined(macos):
+          result = "" # must not start with ':'
+        else:
+          result = $DirSep
+        start = 1
+      elif path[0] == '.' and path[1] == '/':
+        # current directory
+        result = $CurDir
+        start = 2
+      else:
+        result = ""
+        start = 0
+
+      var i = start
+      while i < len(path): # ../../../ --> ::::
+        if path[i] == '.' and path[i+1] == '.' and path[i+2] == '/':
+          # parent directory
+          when defined(macos):
+            if result[high(result)] == ':':
+              add result, ':'
+            else:
+              add result, ParDir
+          else:
+            add result, ParDir & DirSep
+          inc(i, 3)
+        elif path[i] == '/':
+          add result, DirSep
+          inc(i)
+        else:
+          add result, path[i]
+          inc(i)
+
+when declared(getEnv) or defined(nimscript):
+  proc getHomeDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
+    ## Returns the home directory of the current user.
+    ##
+    ## This proc is wrapped by the expandTilde proc for the convenience of
+    ## processing paths coming from user configuration files.
+    when defined(windows): return string(getEnv("USERPROFILE")) & "\\"
+    else: return string(getEnv("HOME")) & "/"
+
+  proc getConfigDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
+    ## Returns the config directory of the current user for applications.
+    when defined(windows): return string(getEnv("APPDATA")) & "\\"
+    else: return string(getEnv("HOME")) & "/.config/"
+
+  proc getTempDir*(): string {.rtl, extern: "nos$1", tags: [ReadEnvEffect].} =
+    ## Returns the temporary directory of the current user for applications to
+    ## save temporary files in.
+    when defined(windows): return string(getEnv("TEMP")) & "\\"
+    else: return "/tmp/"
+
+  proc expandTilde*(path: string): string {.tags: [ReadEnvEffect].} =
+    ## Expands a path starting with ``~/`` to a full path.
+    ##
+    ## If `path` starts with the tilde character and is followed by `/` or `\\`
+    ## this proc will return the reminder of the path appended to the result of
+    ## the getHomeDir() proc, otherwise the input path will be returned without
+    ## modification.
+    ##
+    ## The behaviour of this proc is the same on the Windows platform despite not
+    ## having this convention. Example:
+    ##
+    ## .. code-block:: nim
+    ##   let configFile = expandTilde("~" / "appname.cfg")
+    ##   echo configFile
+    ##   # --> C:\Users\amber\appname.cfg
+    if len(path) > 1 and path[0] == '~' and (path[1] == '/' or path[1] == '\\'):
+      result = getHomeDir() / path.substr(2)
+    else:
+      result = path
+
+  when not declared(split):
+    iterator split(s: string, sep: char): string =
+      var last = 0
+      if len(s) > 0:
+        while last <= len(s):
+          var first = last
+          while last < len(s) and s[last] != sep: inc(last)
+          yield substr(s, first, last-1)
+          inc(last)
+
+  proc findExe*(exe: string): string {.tags: [ReadDirEffect, ReadEnvEffect].} =
+    ## Searches for `exe` in the current working directory and then
+    ## in directories listed in the ``PATH`` environment variable.
+    ## Returns "" if the `exe` cannot be found. On DOS-like platforms, `exe`
+    ## is added the `ExeExt <#ExeExt>`_ file extension if it has none.
+    result = addFileExt(exe, ExeExt)
+    if existsFile(result): return
+    var path = string(getEnv("PATH"))
+    for candidate in split(path, PathSep):
+      when defined(windows):
+        var x = candidate / result
+      else:
+        var x = expandTilde(candidate) / result
+      if existsFile(x): return x
+    result = ""
+
+when defined(nimscript):
+  {.pop.} # hint[ConvFromXtoItselfNotNeeded]:off
diff --git a/lib/pure/times.nim b/lib/pure/times.nim
index 6d45dc7f1..aa4ae5ace 100644
--- a/lib/pure/times.nim
+++ b/lib/pure/times.nim
@@ -1051,26 +1051,30 @@ proc parse*(value, layout: string): TimeInfo =
   return info
 
 # Leap year calculations are adapted from:
-# from http://www.codeproject.com/Articles/7358/Ultra-fast-Algorithms-for-Working-with-Leap-Years
+# http://www.codeproject.com/Articles/7358/Ultra-fast-Algorithms-for-Working-with-Leap-Years
 # The dayOfTheWeek procs are adapated from:
-# from http://stason.org/TULARC/society/calendars/2-5-What-day-of-the-week-was-2-August-1953.html
+# http://stason.org/TULARC/society/calendars/2-5-What-day-of-the-week-was-2-August-1953.html
 
-# Note: for leap years, start date is assumed to be 1 AD.
-# counts the number of leap years up to January 1st of a given year.
-# Keep in mind that if specified year is a leap year, the leap day
-# has not happened before January 1st of that year.
-proc countLeapYears(yearSpan: int): int =
-  (((yearSpan - 1) / 4) - ((yearSpan - 1) / 100) + ((yearSpan - 1)/400)).int
-
-proc countDays(yearSpan: int): int =
+proc countLeapYears*(yearSpan: int): int =
+  ## Returns the number of leap years spanned by a given number of years.
+  ##
+  ## Note: for leap years, start date is assumed to be 1 AD.
+  ## counts the number of leap years up to January 1st of a given year.
+  ## Keep in mind that if specified year is a leap year, the leap day
+  ## has not happened before January 1st of that year.
+  (((yearSpan - 1) / 4) - ((yearSpan - 1) / 100) + ((yearSpan - 1) / 400)).int
+
+proc countDays*(yearSpan: int): int =
+  ## Returns the number of days spanned by a given number of years.
   (yearSpan - 1) * 365 + countLeapYears(yearSpan)
 
-proc countYears(daySpan: int): int =
-  # counts the number of years spanned by a given number of days.
+proc countYears*(daySpan: int): int =
+  ## Returns the number of years spanned by a given number of days.
   ((daySpan - countLeapYears(daySpan div 365)) div 365)
 
-proc countYearsAndDays(daySpan: int): tuple[years: int, days: int] =
-  # counts the number of years spanned by a given number of days and the remainder as days.
+proc countYearsAndDays*(daySpan: int): tuple[years: int, days: int] =
+  ## Returns the number of years spanned by a given number of days and the
+  ## remainder as days.
   let days = daySpan - countLeapYears(daySpan div 365)
   result.years = days div 365
   result.days = days mod 365
diff --git a/lib/system.nim b/lib/system.nim
index 2a3f72f3b..042813ae1 100644
--- a/lib/system.nim
+++ b/lib/system.nim
@@ -1375,7 +1375,8 @@ type # these work for most platforms:
   culonglong* {.importc: "unsigned long long", nodecl.} = uint64
     ## This is the same as the type ``unsigned long long`` in *C*.
 
-  cstringArray* {.importc: "char**", nodecl.} = ptr array [0..ArrayDummySize, cstring]
+  cstringArray* {.importc: "char**", nodecl.} = ptr
+    array [0..ArrayDummySize, cstring]
     ## This is binary compatible to the type ``char**`` in *C*. The array's
     ## high value is large enough to disable bounds checking in practice.
     ## Use `cstringArrayToSeq` to convert it into a ``seq[string]``.
@@ -1886,7 +1887,7 @@ iterator pairs*[T](a: openArray[T]): tuple[key: int, val: T] {.inline.} =
     yield (i, a[i])
     inc(i)
 
-iterator mpairs*[T](a: var openArray[T]): tuple[key: int, val: var T] {.inline.} =
+iterator mpairs*[T](a: var openArray[T]): tuple[key:int, val:var T]{.inline.} =
   ## iterates over each item of `a`. Yields ``(index, a[index])`` pairs.
   ## ``a[index]`` can be modified.
   var i = 0
@@ -1903,7 +1904,7 @@ iterator pairs*[IX, T](a: array[IX, T]): tuple[key: IX, val: T] {.inline.} =
       if i >= high(IX): break
       inc(i)
 
-iterator mpairs*[IX, T](a: var array[IX, T]): tuple[key: IX, val: var T] {.inline.} =
+iterator mpairs*[IX, T](a:var array[IX, T]):tuple[key:IX,val:var T] {.inline.} =
   ## iterates over each item of `a`. Yields ``(index, a[index])`` pairs.
   ## ``a[index]`` can be modified.
   var i = low(IX)
@@ -3149,25 +3150,31 @@ proc staticExec*(command: string, input = "", cache = ""): string {.
   ## .. code-block:: nim
   ##     const stateMachine = staticExec("dfaoptimizer", "input", "0.8.0")
 
-proc `+=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.magic: "Inc", noSideEffect.}
+proc `+=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.
+  magic: "Inc", noSideEffect.}
   ## Increments an ordinal
 
-proc `-=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.magic: "Dec", noSideEffect.}
+proc `-=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.
+  magic: "Dec", noSideEffect.}
   ## Decrements an ordinal
 
-proc `*=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.inline, noSideEffect.} =
+proc `*=`*[T: SomeOrdinal|uint|uint64](x: var T, y: T) {.
+  inline, noSideEffect.} =
   ## Binary `*=` operator for ordinals
   x = x * y
 
-proc `+=`*[T: float|float32|float64] (x: var T, y: T) {.inline, noSideEffect.} =
+proc `+=`*[T: float|float32|float64] (x: var T, y: T) {.
+  inline, noSideEffect.} =
   ## Increments in placee a floating point number
   x = x + y
 
-proc `-=`*[T: float|float32|float64] (x: var T, y: T) {.inline, noSideEffect.} =
+proc `-=`*[T: float|float32|float64] (x: var T, y: T) {.
+  inline, noSideEffect.} =
   ## Decrements in place a floating point number
   x = x - y
 
-proc `*=`*[T: float|float32|float64] (x: var T, y: T) {.inline, noSideEffect.} =
+proc `*=`*[T: float|float32|float64] (x: var T, y: T) {.
+  inline, noSideEffect.} =
   ## Multiplies in place a floating point number
   x = x * y
 
@@ -3380,8 +3387,8 @@ when hasAlloc:
     x.add(y)
 
   proc safeAdd*(x: var string, y: string) =
-    ## Adds ``y`` to ``x`` unless ``x`` is not yet initalized; in that case, ``x``
-    ## becomes ``y``
+    ## Adds ``y`` to ``x`` unless ``x`` is not yet initalized; in that
+    ## case, ``x`` becomes ``y``
     if x == nil: x = y
     else: x.add(y)
 
diff --git a/lib/system/nimscript.nim b/lib/system/nimscript.nim
index c714f88ee..38e1f81a7 100644
--- a/lib/system/nimscript.nim
+++ b/lib/system/nimscript.nim
@@ -19,6 +19,7 @@ proc listFiles*(dir: string): seq[string] = builtin
 proc removeDir(dir: string) = builtin
 proc removeFile(dir: string) = builtin
 proc moveFile(src, dest: string) = builtin
+proc copyFile(src, dest: string) = builtin
 proc createDir(dir: string) = builtin
 proc getOsError: string = builtin
 proc setCurrentDir(dir: string) = builtin
@@ -30,6 +31,25 @@ proc switch*(key: string, val="") = builtin
 proc getCommand*(): string = builtin
 proc setCommand*(cmd: string) = builtin
 proc cmpIgnoreStyle(a, b: string): int = builtin
+proc cmpIgnoreCase(a, b: string): int = builtin
+
+proc cmpic*(a, b: string): int = cmpIgnoreCase(a, b)
+
+proc getEnv*(key: string): string = builtin
+proc existsEnv*(key: string): bool = builtin
+proc fileExists*(filename: string): bool = builtin
+proc dirExists*(dir: string): bool = builtin
+
+proc existsFile*(filename: string): bool = fileExists(filename)
+proc existsDir*(dir: string): bool = dirExists(dir)
+
+proc toExe*(filename: string): string =
+  ## On Windows adds ".exe" to `filename`, else returns `filename` unmodified.
+  (when defined(windows): filename & ".exe" else: filename)
+
+proc toDll*(filename: string): string =
+  ## On Windows adds ".dll" to `filename`, on Posix produces "lib$filename.so".
+  (when defined(windows): filename & ".dll" else: "lib" & filename & ".so")
 
 proc strip(s: string): string =
   var i = 0
@@ -79,6 +99,11 @@ proc mvFile*(`from`, to: string) {.raises: [OSError].} =
     moveFile `from`, to
     checkOsError()
 
+proc cpFile*(`from`, to: string) {.raises: [OSError].} =
+  log "mvFile: " & `from` & ", " & to:
+    copyFile `from`, to
+    checkOsError()
+
 proc exec*(command: string, input = "", cache = "") =
   ## Executes an external process.
   log "exec: " & command:
@@ -120,7 +145,7 @@ template withDir*(dir: string; body: untyped): untyped =
   ##
   ## If you need a permanent change, use the `cd() <#cd>`_ proc. Usage example:
   ##
-  ## .. code-block:: nimrod
+  ## .. code-block:: nim
   ##   withDir "foo":
   ##     # inside foo
   ##   #back to last dir
@@ -157,8 +182,8 @@ var
     binDir*, backend*: string
 
   skipDirs*, skipFiles*, skipExt*, installDirs*, installFiles*,
-    installExt*, bin*: seq[string]
-  requiresData*: seq[string]
+    installExt*, bin*: seq[string] = @[]
+  requiresData*: seq[string] = @[]
 
 proc requires*(deps: varargs[string]) =
   for d in deps: requiresData.add(d)
diff --git a/todo.txt b/todo.txt
index 82d1f32db..463ffc3b1 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,7 +1,7 @@
 version 0.11.4
 ==============
 
-- make ``os`` and ``math`` work with NimScript
+- document NimScript
 - document special cased varargs[untyped] and varargs[typed]
 
 - The remaining bugs of the lambda lifting pass that is responsible to enable