summary refs log tree commit diff stats
path: root/compiler/msgs.nim
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/msgs.nim')
-rw-r--r--compiler/msgs.nim723
1 files changed, 723 insertions, 0 deletions
diff --git a/compiler/msgs.nim b/compiler/msgs.nim
new file mode 100644
index 000000000..c49ca8c9b
--- /dev/null
+++ b/compiler/msgs.nim
@@ -0,0 +1,723 @@
+#
+#
+#           The Nim Compiler
+#        (c) Copyright 2013 Andreas Rumpf
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+import
+  std/[strutils, os, tables, terminal, macros, times],
+  std/private/miscdollars,
+  options, lineinfos, pathutils
+
+import ropes except `%`
+
+when defined(nimPreviewSlimSystem):
+  import std/[syncio, assertions]
+
+
+type InstantiationInfo* = typeof(instantiationInfo())
+template instLoc*(): InstantiationInfo = instantiationInfo(-2, fullPaths = true)
+
+template toStdOrrKind(stdOrr): untyped =
+  if stdOrr == stdout: stdOrrStdout else: stdOrrStderr
+
+proc toLowerAscii(a: var string) {.inline.} =
+  for c in mitems(a):
+    if isUpperAscii(c): c = char(uint8(c) xor 0b0010_0000'u8)
+
+proc flushDot*(conf: ConfigRef) =
+  ## safe to call multiple times
+  # xxx one edge case not yet handled is when `printf` is called at CT with `compiletimeFFI`.
+  let stdOrr = if optStdout in conf.globalOptions: stdout else: stderr
+  let stdOrrKind = toStdOrrKind(stdOrr)
+  if stdOrrKind in conf.lastMsgWasDot:
+    conf.lastMsgWasDot.excl stdOrrKind
+    write(stdOrr, "\n")
+
+proc toCChar*(c: char; result: var string) {.inline.} =
+  case c
+  of '\0'..'\x1F', '\x7F'..'\xFF':
+    result.add '\\'
+    result.add toOctal(c)
+  of '\'', '\"', '\\', '?':
+    result.add '\\'
+    result.add c
+  else:
+    result.add c
+
+proc makeCString*(s: string): Rope =
+  result = newStringOfCap(int(s.len.toFloat * 1.1) + 1)
+  result.add("\"")
+  for i in 0..<s.len:
+    # line wrapping of string litterals in cgen'd code was a bad idea, e.g. causes: bug #16265
+    # It also makes reading c sources or grepping harder, for zero benefit.
+    # const MaxLineLength = 64
+    # if (i + 1) mod MaxLineLength == 0:
+    #   res.add("\"\L\"")
+    toCChar(s[i], result)
+  result.add('\"')
+
+proc newFileInfo(fullPath: AbsoluteFile, projPath: RelativeFile): TFileInfo =
+  result = TFileInfo(fullPath: fullPath, projPath: projPath,
+                    shortName: fullPath.extractFilename,
+                    quotedFullName: fullPath.string.makeCString,
+                    lines: @[]
+  )
+  result.quotedName = result.shortName.makeCString
+  when defined(nimpretty):
+    if not result.fullPath.isEmpty:
+      try:
+        result.fullContent = readFile(result.fullPath.string)
+      except IOError:
+        #rawMessage(errCannotOpenFile, result.fullPath)
+        # XXX fixme
+        result.fullContent = ""
+
+when defined(nimpretty):
+  proc fileSection*(conf: ConfigRef; fid: FileIndex; a, b: int): string =
+    substr(conf.m.fileInfos[fid.int].fullContent, a, b)
+
+proc canonicalCase(path: var string) {.inline.} =
+  ## the idea is to only use this for checking whether a path is already in
+  ## the table but otherwise keep the original case
+  when FileSystemCaseSensitive: discard
+  else: toLowerAscii(path)
+
+proc fileInfoKnown*(conf: ConfigRef; filename: AbsoluteFile): bool =
+  var
+    canon: AbsoluteFile
+  try:
+    canon = canonicalizePath(conf, filename)
+  except OSError:
+    canon = filename
+  canon.string.canonicalCase
+  result = conf.m.filenameToIndexTbl.hasKey(canon.string)
+
+proc fileInfoIdx*(conf: ConfigRef; filename: AbsoluteFile; isKnownFile: var bool): FileIndex =
+  var
+    canon: AbsoluteFile
+    pseudoPath = false
+
+  try:
+    canon = canonicalizePath(conf, filename)
+  except OSError:
+    canon = filename
+    # The compiler uses "filenames" such as `command line` or `stdin`
+    # This flag indicates that we are working with such a path here
+    pseudoPath = true
+
+  var canon2 = canon.string
+  canon2.canonicalCase
+
+  if conf.m.filenameToIndexTbl.hasKey(canon2):
+    isKnownFile = true
+    result = conf.m.filenameToIndexTbl[canon2]
+  else:
+    isKnownFile = false
+    result = conf.m.fileInfos.len.FileIndex
+    conf.m.fileInfos.add(newFileInfo(canon, if pseudoPath: RelativeFile filename
+                                            else: relativeTo(canon, conf.projectPath)))
+    conf.m.filenameToIndexTbl[canon2] = result
+
+proc fileInfoIdx*(conf: ConfigRef; filename: AbsoluteFile): FileIndex =
+  var dummy: bool = false
+  result = fileInfoIdx(conf, filename, dummy)
+
+proc fileInfoIdx*(conf: ConfigRef; filename: RelativeFile; isKnownFile: var bool): FileIndex =
+  fileInfoIdx(conf, AbsoluteFile expandFilename(filename.string), isKnownFile)
+
+proc fileInfoIdx*(conf: ConfigRef; filename: RelativeFile): FileIndex =
+  var dummy: bool = false
+  fileInfoIdx(conf, AbsoluteFile expandFilename(filename.string), dummy)
+
+proc newLineInfo*(fileInfoIdx: FileIndex, line, col: int): TLineInfo =
+  result = TLineInfo(fileIndex: fileInfoIdx)
+  if line < int high(uint16):
+    result.line = uint16(line)
+  else:
+    result.line = high(uint16)
+  if col < int high(int16):
+    result.col = int16(col)
+  else:
+    result.col = -1
+
+proc newLineInfo*(conf: ConfigRef; filename: AbsoluteFile, line, col: int): TLineInfo {.inline.} =
+  result = newLineInfo(fileInfoIdx(conf, filename), line, col)
+
+const gCmdLineInfo* = newLineInfo(commandLineIdx, 1, 1)
+
+proc concat(strings: openArray[string]): string =
+  var totalLen = 0
+  for s in strings: totalLen += s.len
+  result = newStringOfCap totalLen
+  for s in strings: result.add s
+
+proc suggestWriteln*(conf: ConfigRef; s: string) =
+  if eStdOut in conf.m.errorOutputs:
+    if isNil(conf.writelnHook):
+      writeLine(stdout, s)
+      flushFile(stdout)
+    else:
+      conf.writelnHook(s)
+
+proc msgQuit*(x: int8) = quit x
+proc msgQuit*(x: string) = quit x
+
+proc suggestQuit*() =
+  raise newException(ESuggestDone, "suggest done")
+
+# this format is understood by many text editors: it is the same that
+# Borland and Freepascal use
+const
+  KindFormat   = " [$1]"
+  KindColor    = fgCyan
+  ErrorTitle   = "Error: "
+  ErrorColor   = fgRed
+  WarningTitle = "Warning: "
+  WarningColor = fgYellow
+  HintTitle    = "Hint: "
+  HintColor    = fgGreen
+  # NOTE: currently line info line numbers start with 1,
+  # but column numbers start with 0, however most editors expect
+  # first column to be 1, so we need to +1 here
+  ColOffset*   = 1
+  commandLineDesc* = "command line"
+
+proc getInfoContextLen*(conf: ConfigRef): int = return conf.m.msgContext.len
+proc setInfoContextLen*(conf: ConfigRef; L: int) = setLen(conf.m.msgContext, L)
+
+proc pushInfoContext*(conf: ConfigRef; info: TLineInfo; detail: string = "") =
+  conf.m.msgContext.add((info, detail))
+
+proc popInfoContext*(conf: ConfigRef) =
+  setLen(conf.m.msgContext, conf.m.msgContext.len - 1)
+
+proc getInfoContext*(conf: ConfigRef; index: int): TLineInfo =
+  let i = if index < 0: conf.m.msgContext.len + index else: index
+  if i >=% conf.m.msgContext.len: result = unknownLineInfo
+  else: result = conf.m.msgContext[i].info
+
+template toFilename*(conf: ConfigRef; fileIdx: FileIndex): string =
+  if fileIdx.int32 < 0 or conf == nil:
+    (if fileIdx == commandLineIdx: commandLineDesc else: "???")
+  else:
+    conf.m.fileInfos[fileIdx.int32].shortName
+
+proc toProjPath*(conf: ConfigRef; fileIdx: FileIndex): string =
+  if fileIdx.int32 < 0 or conf == nil:
+    (if fileIdx == commandLineIdx: commandLineDesc else: "???")
+  else: conf.m.fileInfos[fileIdx.int32].projPath.string
+
+proc toFullPath*(conf: ConfigRef; fileIdx: FileIndex): string =
+  if fileIdx.int32 < 0 or conf == nil:
+    result = (if fileIdx == commandLineIdx: commandLineDesc else: "???")
+  else:
+    result = conf.m.fileInfos[fileIdx.int32].fullPath.string
+
+proc setDirtyFile*(conf: ConfigRef; fileIdx: FileIndex; filename: AbsoluteFile) =
+  assert fileIdx.int32 >= 0
+  conf.m.fileInfos[fileIdx.int32].dirtyFile = filename
+  setLen conf.m.fileInfos[fileIdx.int32].lines, 0
+
+proc setHash*(conf: ConfigRef; fileIdx: FileIndex; hash: string) =
+  assert fileIdx.int32 >= 0
+  when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
+    conf.m.fileInfos[fileIdx.int32].hash = hash
+  else:
+    shallowCopy(conf.m.fileInfos[fileIdx.int32].hash, hash)
+
+
+proc getHash*(conf: ConfigRef; fileIdx: FileIndex): string =
+  assert fileIdx.int32 >= 0
+  when defined(gcArc) or defined(gcOrc) or defined(gcAtomicArc):
+    result = conf.m.fileInfos[fileIdx.int32].hash
+  else:
+    shallowCopy(result, conf.m.fileInfos[fileIdx.int32].hash)
+
+proc toFullPathConsiderDirty*(conf: ConfigRef; fileIdx: FileIndex): AbsoluteFile =
+  if fileIdx.int32 < 0:
+    result = AbsoluteFile(if fileIdx == commandLineIdx: commandLineDesc else: "???")
+  elif not conf.m.fileInfos[fileIdx.int32].dirtyFile.isEmpty:
+    result = conf.m.fileInfos[fileIdx.int32].dirtyFile
+  else:
+    result = conf.m.fileInfos[fileIdx.int32].fullPath
+
+template toFilename*(conf: ConfigRef; info: TLineInfo): string =
+  toFilename(conf, info.fileIndex)
+
+template toProjPath*(conf: ConfigRef; info: TLineInfo): string =
+  toProjPath(conf, info.fileIndex)
+
+template toFullPath*(conf: ConfigRef; info: TLineInfo): string =
+  toFullPath(conf, info.fileIndex)
+
+template toFullPathConsiderDirty*(conf: ConfigRef; info: TLineInfo): string =
+  string toFullPathConsiderDirty(conf, info.fileIndex)
+
+proc toFilenameOption*(conf: ConfigRef, fileIdx: FileIndex, opt: FilenameOption): string =
+  case opt
+  of foAbs: result = toFullPath(conf, fileIdx)
+  of foRelProject: result = toProjPath(conf, fileIdx)
+  of foCanonical:
+    let absPath = toFullPath(conf, fileIdx)
+    result = canonicalImportAux(conf, absPath.AbsoluteFile)
+  of foName: result = toProjPath(conf, fileIdx).lastPathPart
+  of foLegacyRelProj:
+    let
+      absPath = toFullPath(conf, fileIdx)
+      relPath = toProjPath(conf, fileIdx)
+    result = if (relPath.len > absPath.len) or (relPath.count("..") > 2):
+               absPath
+             else:
+               relPath
+  of foStacktrace:
+    if optExcessiveStackTrace in conf.globalOptions:
+      result = toFilenameOption(conf, fileIdx, foAbs)
+    else:
+      result = toFilenameOption(conf, fileIdx, foName)
+
+proc toMsgFilename*(conf: ConfigRef; fileIdx: FileIndex): string =
+  toFilenameOption(conf, fileIdx, conf.filenameOption)
+
+template toMsgFilename*(conf: ConfigRef; info: TLineInfo): string =
+  toMsgFilename(conf, info.fileIndex)
+
+proc toLinenumber*(info: TLineInfo): int {.inline.} =
+  result = int info.line
+
+proc toColumn*(info: TLineInfo): int {.inline.} =
+  result = info.col
+
+proc toFileLineCol(info: InstantiationInfo): string {.inline.} =
+  result = ""
+  result.toLocation(info.filename, info.line, info.column + ColOffset)
+
+proc toFileLineCol*(conf: ConfigRef; info: TLineInfo): string {.inline.} =
+  result = ""
+  result.toLocation(toMsgFilename(conf, info), info.line.int, info.col.int + ColOffset)
+
+proc `$`*(conf: ConfigRef; info: TLineInfo): string = toFileLineCol(conf, info)
+
+proc `$`*(info: TLineInfo): string {.error.} = discard
+
+proc `??`* (conf: ConfigRef; info: TLineInfo, filename: string): bool =
+  # only for debugging purposes
+  result = filename in toFilename(conf, info)
+
+type
+  MsgFlag* = enum  ## flags altering msgWriteln behavior
+    msgStdout,     ## force writing to stdout, even stderr is default
+    msgSkipHook    ## skip message hook even if it is present
+    msgNoUnitSep  ## the message is a complete "paragraph".
+  MsgFlags* = set[MsgFlag]
+
+proc msgWriteln*(conf: ConfigRef; s: string, flags: MsgFlags = {}) =
+  ## Writes given message string to stderr by default.
+  ## If ``--stdout`` option is given, writes to stdout instead. If message hook
+  ## is present, then it is used to output message rather than stderr/stdout.
+  ## This behavior can be altered by given optional flags.
+
+  ## This is used for 'nim dump' etc. where we don't have nimsuggest
+  ## support.
+  #if conf.cmd == cmdIdeTools and optCDebug notin gGlobalOptions: return
+  let sep = if msgNoUnitSep notin flags: conf.unitSep else: ""
+  if not isNil(conf.writelnHook) and msgSkipHook notin flags:
+    conf.writelnHook(s & sep)
+  elif optStdout in conf.globalOptions or msgStdout in flags:
+    if eStdOut in conf.m.errorOutputs:
+      flushDot(conf)
+      write stdout, s
+      writeLine(stdout, sep)
+      flushFile(stdout)
+  else:
+    if eStdErr in conf.m.errorOutputs:
+      flushDot(conf)
+      write stderr, s
+      writeLine(stderr, sep)
+      # On Windows stderr is fully-buffered when piped, regardless of C std.
+      when defined(windows):
+        flushFile(stderr)
+
+macro callIgnoringStyle(theProc: typed, first: typed,
+                        args: varargs[typed]): untyped =
+  let typForegroundColor = bindSym"ForegroundColor".getType
+  let typBackgroundColor = bindSym"BackgroundColor".getType
+  let typStyle = bindSym"Style".getType
+  let typTerminalCmd = bindSym"TerminalCmd".getType
+  result = newCall(theProc)
+  if first.kind != nnkNilLit: result.add(first)
+  for arg in children(args[0][1]):
+    if arg.kind == nnkNilLit: continue
+    let typ = arg.getType
+    if typ.kind != nnkEnumTy or
+       typ != typForegroundColor and
+       typ != typBackgroundColor and
+       typ != typStyle and
+       typ != typTerminalCmd:
+      result.add(arg)
+
+macro callStyledWriteLineStderr(args: varargs[typed]): untyped =
+  result = newCall(bindSym"styledWriteLine")
+  result.add(bindSym"stderr")
+  for arg in children(args[0][1]):
+    result.add(arg)
+  when false:
+    # not needed because styledWriteLine already ends with resetAttributes
+    result = newStmtList(result, newCall(bindSym"resetAttributes", bindSym"stderr"))
+
+template callWritelnHook(args: varargs[string, `$`]) =
+  conf.writelnHook concat(args)
+
+proc msgWrite(conf: ConfigRef; s: string) =
+  if conf.m.errorOutputs != {}:
+    let stdOrr =
+      if optStdout in conf.globalOptions:
+        stdout
+      else:
+        stderr
+    write(stdOrr, s)
+    flushFile(stdOrr)
+    conf.lastMsgWasDot.incl stdOrr.toStdOrrKind() # subsequent writes need `flushDot`
+
+template styledMsgWriteln(args: varargs[typed]) =
+  if not isNil(conf.writelnHook):
+    callIgnoringStyle(callWritelnHook, nil, args)
+  elif optStdout in conf.globalOptions:
+    if eStdOut in conf.m.errorOutputs:
+      flushDot(conf)
+      callIgnoringStyle(writeLine, stdout, args)
+      flushFile(stdout)
+  elif eStdErr in conf.m.errorOutputs:
+    flushDot(conf)
+    if optUseColors in conf.globalOptions:
+      callStyledWriteLineStderr(args)
+    else:
+      callIgnoringStyle(writeLine, stderr, args)
+    # On Windows stderr is fully-buffered when piped, regardless of C std.
+    when defined(windows):
+      flushFile(stderr)
+
+proc msgKindToString*(kind: TMsgKind): string = MsgKindToStr[kind]
+  # later versions may provide translated error messages
+
+proc getMessageStr(msg: TMsgKind, arg: string): string = msgKindToString(msg) % [arg]
+
+type TErrorHandling* = enum doNothing, doAbort, doRaise
+
+proc log*(s: string) =
+  var f: File = default(File)
+  if open(f, getHomeDir() / "nimsuggest.log", fmAppend):
+    f.writeLine(s)
+    close(f)
+
+proc quit(conf: ConfigRef; msg: TMsgKind) {.gcsafe.} =
+  if conf.isDefined("nimDebug"): quitOrRaise(conf, $msg)
+  elif defined(debug) or msg == errInternal or conf.hasHint(hintStackTrace):
+    {.gcsafe.}:
+      if stackTraceAvailable() and isNil(conf.writelnHook):
+        writeStackTrace()
+      else:
+        styledMsgWriteln(fgRed, """
+No stack traceback available
+To create a stacktrace, rerun compilation with './koch temp $1 <file>', see $2 for details""" %
+          [conf.command, "intern.html#debugging-the-compiler".createDocLink], conf.unitSep)
+  quit 1
+
+proc handleError(conf: ConfigRef; msg: TMsgKind, eh: TErrorHandling, s: string, ignoreMsg: bool) =
+  if msg in fatalMsgs:
+    if conf.cmd == cmdIdeTools: log(s)
+    if conf.cmd != cmdIdeTools or msg != errFatal:
+      quit(conf, msg)
+  if msg >= errMin and msg <= errMax or
+      (msg in warnMin..hintMax and msg in conf.warningAsErrors and not ignoreMsg):
+    inc(conf.errorCounter)
+    conf.exitcode = 1'i8
+    if conf.errorCounter >= conf.errorMax:
+      # only really quit when we're not in the new 'nim check --def' mode:
+      if conf.ideCmd == ideNone:
+        when defined(nimsuggest):
+          #we need to inform the user that something went wrong when initializing NimSuggest
+          raiseRecoverableError(s)
+        else:
+          quit(conf, msg)
+    elif eh == doAbort and conf.cmd != cmdIdeTools:
+      quit(conf, msg)
+    elif eh == doRaise:
+      raiseRecoverableError(s)
+
+proc `==`*(a, b: TLineInfo): bool =
+  result = a.line == b.line and a.fileIndex == b.fileIndex
+
+proc exactEquals*(a, b: TLineInfo): bool =
+  result = a.fileIndex == b.fileIndex and a.line == b.line and a.col == b.col
+
+proc writeContext(conf: ConfigRef; lastinfo: TLineInfo) =
+  const instantiationFrom = "template/generic instantiation from here"
+  const instantiationOfFrom = "template/generic instantiation of `$1` from here"
+  var info = lastinfo
+  for i in 0..<conf.m.msgContext.len:
+    let context = conf.m.msgContext[i]
+    if context.info != lastinfo and context.info != info:
+      if conf.structuredErrorHook != nil:
+        conf.structuredErrorHook(conf, context.info, instantiationFrom,
+                                 Severity.Hint)
+      else:
+        let message =
+          if context.detail == "":
+            instantiationFrom
+          else:
+            instantiationOfFrom.format(context.detail)
+        styledMsgWriteln(styleBright, conf.toFileLineCol(context.info), " ", resetStyle, message)
+    info = context.info
+
+proc ignoreMsgBecauseOfIdeTools(conf: ConfigRef; msg: TMsgKind): bool =
+  msg >= errGenerated and conf.cmd == cmdIdeTools and optIdeDebug notin conf.globalOptions
+
+proc addSourceLine(conf: ConfigRef; fileIdx: FileIndex, line: string) =
+  conf.m.fileInfos[fileIdx.int32].lines.add line
+
+proc numLines*(conf: ConfigRef, fileIdx: FileIndex): int =
+  ## xxx there's an off by 1 error that should be fixed; if a file ends with "foo" or "foo\n"
+  ## it will return same number of lines (ie, a trailing empty line is discounted)
+  result = conf.m.fileInfos[fileIdx.int32].lines.len
+  if result == 0:
+    try:
+      for line in lines(toFullPathConsiderDirty(conf, fileIdx).string):
+        addSourceLine conf, fileIdx, line
+    except IOError:
+      discard
+    result = conf.m.fileInfos[fileIdx.int32].lines.len
+
+proc sourceLine*(conf: ConfigRef; i: TLineInfo): string =
+  ## 1-based index (matches editor line numbers); 1st line is for i.line = 1
+  ## last valid line is `numLines` inclusive
+  if i.fileIndex.int32 < 0: return ""
+  let num = numLines(conf, i.fileIndex)
+  # can happen if the error points to EOF:
+  if i.line.int > num: return ""
+
+  result = conf.m.fileInfos[i.fileIndex.int32].lines[i.line.int-1]
+
+proc getSurroundingSrc(conf: ConfigRef; info: TLineInfo): string =
+  if conf.hasHint(hintSource) and info != unknownLineInfo:
+    const indent = "  "
+    result = "\n" & indent & $sourceLine(conf, info)
+    if info.col >= 0:
+      result.add "\n" & indent & spaces(info.col) & '^'
+  else:
+    result = ""
+
+proc formatMsg*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg: string): string =
+  let title = case msg
+              of warnMin..warnMax: WarningTitle
+              of hintMin..hintMax: HintTitle
+              else: ErrorTitle
+  conf.toFileLineCol(info) & " " & title & getMessageStr(msg, arg)
+
+proc liMessage*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg: string,
+               eh: TErrorHandling, info2: InstantiationInfo, isRaw = false,
+               ignoreError = false) {.gcsafe, noinline.} =
+  var
+    title: string
+    color: ForegroundColor
+    ignoreMsg = false
+    sev: Severity
+  let errorOutputsOld = conf.m.errorOutputs
+  if msg in fatalMsgs:
+    # don't gag, refs bug #7080, bug #18278; this can happen with `{.fatal.}`
+    # or inside a `tryConstExpr`.
+    conf.m.errorOutputs = {eStdOut, eStdErr}
+
+  let kind = if msg in warnMin..hintMax and msg != hintUserRaw: $msg else: "" # xxx not sure why hintUserRaw is special
+  case msg
+  of errMin..errMax:
+    sev = Severity.Error
+    writeContext(conf, info)
+    title = ErrorTitle
+    color = ErrorColor
+    when false:
+      # we try to filter error messages so that not two error message
+      # in the same file and line are produced:
+      # xxx `lastError` is only used in this disabled code; but could be useful to revive
+      ignoreMsg = conf.m.lastError == info and info != unknownLineInfo and eh != doAbort
+    if info != unknownLineInfo: conf.m.lastError = info
+  of warnMin..warnMax:
+    sev = Severity.Warning
+    ignoreMsg = not conf.hasWarn(msg)
+    if not ignoreMsg and msg in conf.warningAsErrors:
+      title = ErrorTitle
+      color = ErrorColor
+    else:
+      title = WarningTitle
+      color = WarningColor
+    if not ignoreMsg: writeContext(conf, info)
+    inc(conf.warnCounter)
+  of hintMin..hintMax:
+    sev = Severity.Hint
+    ignoreMsg = not conf.hasHint(msg)
+    if not ignoreMsg and msg in conf.warningAsErrors:
+      title = ErrorTitle
+      color = ErrorColor
+    else:
+      title = HintTitle
+      color = HintColor
+    inc(conf.hintCounter)
+
+  let s = if isRaw: arg else: getMessageStr(msg, arg)
+  if not ignoreMsg:
+    let loc = if info != unknownLineInfo: conf.toFileLineCol(info) & " " else: ""
+    # we could also show `conf.cmdInput` here for `projectIsCmd`
+    var kindmsg = if kind.len > 0: KindFormat % kind else: ""
+    if conf.structuredErrorHook != nil:
+      conf.structuredErrorHook(conf, info, s & kindmsg, sev)
+    if not ignoreMsgBecauseOfIdeTools(conf, msg):
+      if msg == hintProcessing and conf.hintProcessingDots:
+        msgWrite(conf, ".")
+      else:
+        styledMsgWriteln(styleBright, loc, resetStyle, color, title, resetStyle, s, KindColor, kindmsg,
+                         resetStyle, conf.getSurroundingSrc(info), conf.unitSep)
+        if hintMsgOrigin in conf.mainPackageNotes:
+          # xxx needs a bit of refactoring to honor `conf.filenameOption`
+          styledMsgWriteln(styleBright, toFileLineCol(info2), resetStyle,
+            " compiler msg initiated here", KindColor,
+            KindFormat % $hintMsgOrigin,
+            resetStyle, conf.unitSep)
+  if not ignoreError:
+    handleError(conf, msg, eh, s, ignoreMsg)
+  if msg in fatalMsgs:
+    # most likely would have died here but just in case, we restore state
+    conf.m.errorOutputs = errorOutputsOld
+
+template rawMessage*(conf: ConfigRef; msg: TMsgKind, args: openArray[string]) =
+  let arg = msgKindToString(msg) % args
+  liMessage(conf, unknownLineInfo, msg, arg, eh = doAbort, instLoc(), isRaw = true)
+
+template rawMessage*(conf: ConfigRef; msg: TMsgKind, arg: string) =
+  liMessage(conf, unknownLineInfo, msg, arg, eh = doAbort, instLoc())
+
+template fatal*(conf: ConfigRef; info: TLineInfo, arg = "", msg = errFatal) =
+  liMessage(conf, info, msg, arg, doAbort, instLoc())
+
+template globalAssert*(conf: ConfigRef; cond: untyped, info: TLineInfo = unknownLineInfo, arg = "") =
+  ## avoids boilerplate
+  if not cond:
+    var arg2 = "'$1' failed" % [astToStr(cond)]
+    if arg.len > 0: arg2.add "; " & astToStr(arg) & ": " & arg
+    liMessage(conf, info, errGenerated, arg2, doRaise, instLoc())
+
+template globalError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
+  ## `local` means compilation keeps going until errorMax is reached (via `doNothing`),
+  ## `global` means it stops.
+  liMessage(conf, info, msg, arg, doRaise, instLoc())
+
+template globalError*(conf: ConfigRef; info: TLineInfo, arg: string) =
+  liMessage(conf, info, errGenerated, arg, doRaise, instLoc())
+
+template localError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
+  liMessage(conf, info, msg, arg, doNothing, instLoc())
+
+template localError*(conf: ConfigRef; info: TLineInfo, arg: string) =
+  liMessage(conf, info, errGenerated, arg, doNothing, instLoc())
+
+template message*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
+  liMessage(conf, info, msg, arg, doNothing, instLoc())
+
+proc warningDeprecated*(conf: ConfigRef, info: TLineInfo = gCmdLineInfo, msg = "") {.inline.} =
+  message(conf, info, warnDeprecated, msg)
+
+proc internalErrorImpl(conf: ConfigRef; info: TLineInfo, errMsg: string, info2: InstantiationInfo) =
+  if conf.cmd in {cmdIdeTools, cmdCheck} and conf.structuredErrorHook.isNil: return
+  writeContext(conf, info)
+  liMessage(conf, info, errInternal, errMsg, doAbort, info2)
+
+template internalError*(conf: ConfigRef; info: TLineInfo, errMsg: string) =
+  internalErrorImpl(conf, info, errMsg, instLoc())
+
+template internalError*(conf: ConfigRef; errMsg: string) =
+  internalErrorImpl(conf, unknownLineInfo, errMsg, instLoc())
+
+template internalAssert*(conf: ConfigRef, e: bool) =
+  # xxx merge with `globalAssert`
+  if not e:
+    const info2 = instLoc()
+    let arg = info2.toFileLineCol
+    internalErrorImpl(conf, unknownLineInfo, arg, info2)
+
+template lintReport*(conf: ConfigRef; info: TLineInfo, beau, got: string, extraMsg = "") =
+  let m = "'$1' should be: '$2'$3" % [got, beau, extraMsg]
+  let msg = if optStyleError in conf.globalOptions: errGenerated else: hintName
+  liMessage(conf, info, msg, m, doNothing, instLoc())
+
+proc quotedFilename*(conf: ConfigRef; fi: FileIndex): Rope =
+  if fi.int32 < 0:
+    result = makeCString "???"
+  elif optExcessiveStackTrace in conf.globalOptions:
+    result = conf.m.fileInfos[fi.int32].quotedFullName
+  else:
+    result = conf.m.fileInfos[fi.int32].quotedName
+
+proc quotedFilename*(conf: ConfigRef; i: TLineInfo): Rope =
+  quotedFilename(conf, i.fileIndex)
+
+template listMsg(title, r) =
+  msgWriteln(conf, title, {msgNoUnitSep})
+  for a in r: msgWriteln(conf, "  [$1] $2" % [if a in conf.notes: "x" else: " ", $a], {msgNoUnitSep})
+
+proc listWarnings*(conf: ConfigRef) = listMsg("Warnings:", warnMin..warnMax)
+proc listHints*(conf: ConfigRef) = listMsg("Hints:", hintMin..hintMax)
+
+proc genSuccessX*(conf: ConfigRef) =
+  let mem =
+    when declared(system.getMaxMem): formatSize(getMaxMem()) & " peakmem"
+    else: formatSize(getTotalMem()) & " totmem"
+  let loc = $conf.linesCompiled
+  var build = ""
+  var flags = ""
+  const debugModeHints = "none (DEBUG BUILD, `-d:release` generates faster code)"
+  if conf.cmd in cmdBackends:
+    if conf.backend != backendJs:
+      build.add "mm: $#; " % $conf.selectedGC
+      if optThreads in conf.globalOptions: build.add "threads: on; "
+      build.add "opt: "
+      if optOptimizeSpeed in conf.options: build.add "speed"
+      elif optOptimizeSize in conf.options: build.add "size"
+      else: build.add debugModeHints
+        # pending https://github.com/timotheecour/Nim/issues/752, point to optimization.html
+      if isDefined(conf, "danger"): flags.add " -d:danger"
+      elif isDefined(conf, "release"): flags.add " -d:release"
+    else:
+      build.add "opt: "
+      if isDefined(conf, "danger"):
+        build.add "speed"
+        flags.add " -d:danger"
+      elif isDefined(conf, "release"):
+        build.add "speed"
+        flags.add " -d:release"
+      else: build.add debugModeHints
+    if flags.len > 0: build.add "; options:" & flags
+  let sec = formatFloat(epochTime() - conf.lastCmdTime, ffDecimal, 3)
+  let project = if conf.filenameOption == foAbs: $conf.projectFull else: $conf.projectName
+    # xxx honor conf.filenameOption more accurately
+  var output: string
+  if optCompileOnly in conf.globalOptions and conf.cmd != cmdJsonscript:
+    output = $conf.jsonBuildFile
+  elif conf.outFile.isEmpty and conf.cmd notin {cmdJsonscript} + cmdDocLike + cmdBackends:
+    # for some cmd we expect a valid absOutFile
+    output = "unknownOutput"
+  elif optStdout in conf.globalOptions:
+    output = "stdout"
+  else:
+    output = $conf.absOutFile
+  if conf.filenameOption != foAbs: output = output.AbsoluteFile.extractFilename
+    # xxx honor filenameOption more accurately
+  rawMessage(conf, hintSuccessX, [
+    "build", build,
+    "loc", loc,
+    "sec", sec,
+    "mem", mem,
+    "project", project,
+    "output", output,
+    ])