summary refs log tree commit diff stats
path: root/compiler/msgs.nim
blob: ed65e034296461f5fefe49705c624adcf77a1611 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# Tokenize by whitespace.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

# (re)compute the bounds of the next word in the line
# return empty string on reaching end of file
next-word:  # line : (address stream byte), out : (address slice)
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # . save registers
    50/push-eax
    51/push-ecx
    56/push-esi
    57/push-edi
    # esi = line
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           6/r32/esi   8/disp8         .                 # copy *(ebp+8) to esi
    # edi = out
    8b/copy                         1/mod/*+disp8   5/rm32/ebp    .           .             .           7/r32/edi   0xc/disp8       .                 # copy *(ebp+12) to edi
    # skip-chars-matching(line, ' ')
    # . . push args
    68/push  0x20/imm32/space
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  skip-chars-matching/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
$next-word:check0:
    # if (line->read >= line->write) clear out and return
    # . eax = line->read
    8b/copy                         1/mod/*+disp8   6/rm32/esi    .           .             .           0/r32/eax   4/disp8         .                 # copy *(esi+4) to eax
    # . if (eax < line->write) goto next check
    3b/compare                      0/mod/indirect  6/rm32/esi    .           .             .           0/r32/eax   .               .                 # compare eax with *esi
    7c/jump-if-lesser  $next-word:check-for-comment/disp8
    # . return out = {0, 0}
    c7          0/subop/copy        0/mod/direct    7/rm32/edi    .           .             .           .           .               0/imm32           # copy to *edi
    c7          0/subop/copy        1/mod/*+disp8   7/rm32/edi    .           .             .           .           4/disp8         0/imm32           # copy to *(edi+4)
    eb/jump  $next-word:end/disp8
$next-word:check-for-comment:
    # out->start = &line->data[line->read]
    8b/copy                         1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   4/disp8         .                 # copy *(esi+4) to ecx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/esi  1/index/ecx   .           0/r32/eax   0xc/disp8       .                 # copy esi+ecx+12 to eax
    89/copy                         0/mod/indirect  7/rm32/edi    .           .             .           0/r32/eax   .               .                 # copy eax to *edi
    # if (line->data[line->read] == '#') out->end = &line->data[line->write]), skip rest of stream and return
    # . eax = line->data[line->read]
    31/xor                          3/mod/direct    0/rm32/eax    .           .             .           0/r32/eax   .               .                 # clear eax
    8a/copy-byte                    1/mod/*+disp8   4/rm32/sib    6/base/esi  1/index/ecx   .           0/r32/AL    0xc/disp8       .                 # copy byte at *(esi+ecx+12) to AL
    # . compare
    3d/compare-eax-and  0x23/imm32/pound
    75/jump-if-not-equal  $next-word:regular-word/disp8
$next-word:comment:
    # . out->end = &line->data[line->write]
    8b/copy                         0/mod/indirect  6/rm32/esi    .           .             .           0/r32/eax   .               .                 # copy *esi to eax
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/esi  0/index/eax   .           0/r32/eax   0xc/disp8       .                 # copy esi+eax+12 to eax
    89/copy                         1/mod/*+disp8   7/rm32/edi    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(edi+4)
    # . line->read = line->write
    89/copy                         1/mod/*+disp8   6/rm32/esi    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(esi+4)
    # . return
    eb/jump  $next-word:end/disp8
$next-word:regular-word:
    # otherwise skip-chars-not-matching-whitespace(line)  # including trailing newline
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/ebp    .           .             .           .           8/disp8         .                 # push *(ebp+8)
    # . . call
    e8/call  skip-chars-not-matching-whitespace/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # out->end = &line->data[line->read]
    8b/copy                         1/mod/*+disp8   6/rm32/esi    .           .             .           1/r32/ecx   4/disp8         .                 # copy *(esi+4) to ecx
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    6/base/esi  1/index/ecx   .           0/r32/eax   0xc/disp8       .                 # copy esi+ecx+12 to eax
    89/copy                         1/mod/*+disp8   7/rm32/edi    .           .             .           0/r32/eax   4/disp8         .                 # copy eax to *(edi+4)
$next-word:end:
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-next-word:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # setup
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # var slice/ecx = {0, 0}
    68/push  0/imm32/end
    68/push  0/imm32/start
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # write(_test-stream, "  ab")
    # . . push args
    68/push  "  ab"/imm32
    68/push  _test-stream/imm32
    # . . call
    e8/call  write/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # next-word(_test-stream, slice)
    # . . push args
    51/push-ecx
    68/push  _test-stream/imm32
    # . . call
    e8/call  next-word/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               8/imm32           # add to esp
    # check-ints-equal(slice->start - _test-stream->data, 2, msg)
    # . check-ints-equal(slice->start - _test-stream, 14, msg)
    # . . push args
    68/push  "F - test-next-word: start"/imm32
    68/push  0xe/imm32
    # . . push slice->start - _test-stream
    8b/copy                         0/mod/indirect  1/rm32/ecx    .           .             .           0/r32/eax   .               .                 # copy *ecx to eax
    81          5/subop/subtract    3/mod/direct    0/rm32/eax    .           .             .           .           .               _test-stream/imm32 # subtract from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # check-ints-equal(slice->end - _test-stream->data, 4, msg)
    # . check-ints-equal(slice->end - _test-stream, 16, msg)
    # . . push args
    68/push  "F - test-next-word: end"/imm32
    68/push  0x10/imm32
    # . . push slice->end - _test-stream
    8b/copy                         1/mod/*+disp8   1/rm32/ecx    .           .             .           0/r32/eax   4/disp8         .                 # copy *(ecx+4) to eax
    81          5/subop/subtract    3/mod/direct    0/rm32/eax    .           .             .           .           .               _test-stream/imm32 # subtract from eax
    50/push-eax
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               0xc/imm32         # add to esp
    # . epilogue
    89/copy                         3/mod/direct    4/rm32/esp    .           .             .           5/r32/ebp   .               .                 # copy ebp to esp
    5d/pop-to-ebp
    c3/return

test-next-word-returns-whole-comment:
    # . prologue
    55/push-ebp
    89/copy                         3/mod/direct    5/rm32/ebp    .           .             .           4/r32/esp   .               .                 # copy esp to ebp
    # setup
    # . clear-stream(_test-stream)
    # . . push args
    68/push  _test-stream/imm32
    # . . call
    e8/call  clear-stream/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/esp    .           .             .           .           .               4/imm32           # add to esp
    # var slice/ecx = {0, 0}
    68/push  0/imm32/end
    68/push  0/imm32/start
    89/copy                         3/mod/direct    1/rm32/ecx    .           .             .           4/r32/esp   .               .                 # copy esp to ecx
    # write(_test-stream, "  # a")
    # . . push args
    68/push  "  # a"/imm32
    68/push  _test-stream/i
#
#
#           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, ropes, lineinfos, pathutils, strutils2

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 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 = nil
  var res = newStringOfCap(int(s.len.toFloat * 1.1) + 1)
  res.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], res)
  res.add('\"')
  result.add(rope(res))

proc newFileInfo(fullPath: AbsoluteFile, projPath: RelativeFile): TFileInfo =
  result.fullPath = fullPath
  #shallow(result.fullPath)
  result.projPath = projPath
  #shallow(result.projPath)
  result.shortName = fullPath.extractFilename
  result.quotedName = result.shortName.makeCString
  result.quotedFullName = fullPath.string.makeCString
  result.lines = @[]
  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) =
  ## 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)
    shallow(canon.string)
  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: string
  forceCopy(canon2, canon.string) # because `canon` may be shallow
  canon2.canonicalCase

  if conf.m.filenameToIndexTbl.hasKey(canon2):
    isKnownFile = true
    result = conf.m.filenameToIndexTbl[canon2]
  else:
    isKnownFile = false
    result = conf.m.fileInfos.len.FileIndex
    #echo "ID ", result.int, " ", canon2
    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
  result = fileInfoIdx(conf, filename, dummy)

proc newLineInfo*(fileInfoIdx: FileIndex, line, col: int): TLineInfo =
  result.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):
    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):
    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.toLocation(info.filename, info.line, info.column + ColOffset)

proc toFileLineCol*(conf: ConfigRef; info: TLineInfo): string {.inline.} =
  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
  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) =
  if msg in fatalMsgs:
    if conf.cmd == cmdIdeTools: log(s)
    quit(conf, msg)
  if msg >= errMin and msg <= errMax or
      (msg in warnMin..hintMax and msg in conf.warningAsErrors):
    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:
        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) & '^'

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) {.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 msg in conf.warningAsErrors:
      ignoreMsg = false
      title = ErrorTitle
    else:
      title = WarningTitle
    if not ignoreMsg: writeContext(conf, info)
    color = WarningColor
    inc(conf.warnCounter)
  of hintMin..hintMax:
    sev = Severity.Hint
    ignoreMsg = not conf.hasHint(msg)
    if msg in conf.warningAsErrors:
      ignoreMsg = false
      title = ErrorTitle
    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)
  handleError(conf, msg, eh, s)
  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 == cmdIdeTools 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; i: TLineInfo): Rope =
  if i.fileIndex.int32 < 0:
    result = makeCString "???"
  elif optExcessiveStackTrace in conf.globalOptions:
    result = conf.m.fileInfos[i.fileIndex.int32].quotedFullName
  else:
    result = conf.m.fileInfos[i.fileIndex.int32].quotedName

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 uniqueModuleName*(conf: ConfigRef; fid: FileIndex): string =
  ## The unique module name is guaranteed to only contain {'A'..'Z', 'a'..'z', '0'..'9', '_'}
  ## so that it is useful as a C identifier snippet.
  let path = AbsoluteFile toFullPath(conf, fid)
  let rel =
    if path.string.startsWith(conf.libpath.string):
      relativeTo(path, conf.libpath).string
    else:
      relativeTo(path, conf.projectPath).string
  let trunc = if rel.endsWith(".nim"): rel.len - len(".nim") else: rel.len
  result = newStringOfCap(trunc)
  for i in 0..<trunc:
    let c = rel[i]
    case c
    of 'a'..'z':
      result.add c
    of {os.DirSep, os.AltSep}:
      result.add 'Z' # because it looks a bit like '/'
    of '.':
      result.add 'O' # a circle
    else:
      # We mangle upper letters and digits too so that there cannot
      # be clashes with our special meanings of 'Z' and 'O'
      result.addInt ord(c)

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"
  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,
    ])