# # # The Nim Compiler # (c) Copyright 2013 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # import options, strutils, os, tables, ropes, terminal, macros, lineinfos, pathutils proc toCChar*(c: char; result: var string) = 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 = const MaxLineLength = 64 result = nil var res = newStringOfCap(int(s.len.toFloat * 1.1) + 1) add(res, "\"") for i in 0 ..< len(s): if (i + 1) mod MaxLineLength == 0: add(res, "\"\L\"") toCChar(s[i], res) add(res, '\"') add(result, 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 fileInfoKnown*(conf: ConfigRef; filename: AbsoluteFile): bool = var canon: AbsoluteFile try: canon = canonicalizePath(conf, filename) except OSError: canon = filename 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 if conf.m.filenameToIndexTbl.hasKey(canon.string): result = conf.m.filenameToIndexTbl[canon.string] 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[canon.string] = 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) 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 PosFormat = "$1($2, $3) " 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 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, len(conf.m.msgContext) - 1) proc getInfoContext*(conf: ConfigRef; index: int): TLineInfo = let L = conf.m.msgContext.len let i = if index < 0: L + index else: index if i >=% L: result = unknownLineInfo() else: result = conf.m.msgContext[i].info const commandLineDesc = "command line" 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 shallowCopy(conf.m.fileInfos[fileIdx.int32].hash, hash) proc getHash*(conf: ConfigRef; fileIdx: FileIndex): string = assert fileIdx.int32 >= 0 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 toMsgFilename*(conf: ConfigRef; info: FileIndex): string = let absPath = toFullPath(conf, info) relPath = toProjPath(conf, info) result = if (optListFullPaths in conf.globalOptions) or (relPath.len > absPath.len) or (relPath.count("..") > 2): absPath else: relPath 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*(conf: ConfigRef; info: TLineInfo): string {.inline.} = # consider calling `helpers.lineInfoToString` instead result = toMsgFilename(conf, info) & "(" & $info.line & ", " & $(info.col + 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 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 if not isNil(conf.writelnHook) and msgSkipHook notin flags: conf.writelnHook(s) elif optStdout in conf.globalOptions or msgStdout in flags: if eStdOut in conf.m.errorOutputs: writeLine(stdout, s) flushFile(stdout) else: if eStdErr in conf.m.errorOutputs: writeLine(stderr, s) # 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) template callWritelnHook(args: varargs[string, `$`]) = conf.writelnHook concat(args) 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: callIgnoringStyle(writeLine, stdout, args) flushFile(stdout) else: if eStdErr in conf.m.errorOutputs: 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 coordToStr(coord: int): string = if coord == -1: result = "???" else: result = $coord proc msgKindToString*(kind: TMsgKind): string = # later versions may provide translated error messages result = MsgKindToStr[kind] proc getMessageStr(msg: TMsgKind, arg: string): string = result = msgKindToString(msg) % [arg] type TErrorHandling = enum doNothing, doAbort, doRaise proc log*(s: string) {.procvar.} = var f: File if open(f, getHomeDir() / "nimsuggest.log", fmAppend): f.writeLine(s) close(f) proc quit(conf: ConfigRef; msg: TMsgKind) {.gcsafe.} = if defined(debug) or msg == errInternal or hintStackTrace in conf.notes: {.gcsafe.}: if stackTraceAvailable() and isNil(conf.writelnHook): writeStackTrace() else: styledMsgWriteln(fgRed, "No stack traceback available\n" & "To create a stacktrace, rerun compilation with ./koch temp " & conf.command & " ") quit 1 proc handleError(conf: ConfigRef; msg: TMsgKind, eh: TErrorHandling, s: string) = if msg >= fatalMin and msg <= fatalMax: if conf.cmd == cmdIdeTools: log(s) quit(conf, msg) if msg >= errMin and msg <= errMax: inc(conf.errorCounter) conf.exitcode = 1'i8 if conf.errorCounter >= conf.errorMax: 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 ..< len(conf.m.msgContext): 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.Error) else: let message = if context.detail == "": instantiationFrom else: instantiationOfFrom.format(context.detail) styledMsgWriteln(styleBright, PosFormat % [toMsgFilename(conf, context.info), coordToStr(context.info.line.int), coordToStr(context.info.col+ColOffset)], resetStyle, message) info = context.info proc ignoreMsgBecauseOfIdeTools(conf: ConfigRef; msg: TMsgKind): bool = msg >= errGenerated and conf.cmd == cmdIdeTools and optIdeDebug notin conf.globalOptions proc rawMessage*(conf: ConfigRef; msg: TMsgKind, args: openArray[string]) = var title: string color: ForegroundColor kind: string sev: Severity case msg of errMin..errMax: sev = Severity.Error writeContext(conf, unknownLineInfo()) title = ErrorTitle color = ErrorColor of warnMin..warnMax: sev = Severity.Warning if optWarns notin conf.options: return if msg notin conf.notes: return writeContext(conf, unknownLineInfo()) title = WarningTitle color = WarningColor kind = WarningsToStr[ord(msg) - ord(warnMin)] inc(conf.warnCounter) of hintMin..hintMax: sev = Severity.Hint if optHints notin conf.options: return if msg notin conf.notes: return title = HintTitle color = HintColor if msg != hintUserRaw: kind = HintsToStr[ord(msg) - ord(hintMin)] inc(conf.hintCounter) let s = msgKindToString(msg) % args if conf.structuredErrorHook != nil: conf.structuredErrorHook(conf, unknownLineInfo(), s & (if kind.len > 0: KindFormat % kind else: ""), sev) if not ignoreMsgBecauseOfIdeTools(conf, msg): if kind.len > 0: styledMsgWriteln(color, title, resetStyle, s, KindColor, `%`(KindFormat, kind)) else: styledMsgWriteln(color, title, resetStyle, s) handleError(conf, msg, doAbort, s) proc rawMessage*(conf: ConfigRef; msg: TMsgKind, arg: string) = rawMessage(conf, msg, [arg]) proc resetAttributes*(conf: ConfigRef) = if {optUseColors, optStdout} * conf.globalOptions == {optUseColors}: terminal.resetAttributes(stderr) proc addSourceLine(conf: ConfigRef; fileIdx: FileIndex, line: string) = conf.m.fileInfos[fileIdx.int32].lines.add line proc sourceLine*(conf: ConfigRef; i: TLineInfo): string = if i.fileIndex.int32 < 0: return "" if conf.m.fileInfos[i.fileIndex.int32].lines.len == 0: try: for line in lines(toFullPathConsiderDirty(conf, i)): addSourceLine conf, i.fileIndex, line.string except IOError: discard assert i.fileIndex.int32 < conf.m.fileInfos.len # can happen if the error points to EOF: if i.line.int > conf.m.fileInfos[i.fileIndex.int32].lines.len: return "" result = conf.m.fileInfos[i.fileIndex.int32].lines[i.line.int-1] proc writeSurroundingSrc(conf: ConfigRef; info: TLineInfo) = const indent = " " msgWriteln(conf, indent & $sourceLine(conf, info)) if info.col >= 0: msgWriteln(conf, 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 result = PosFormat % [toMsgFilename(conf, info), coordToStr(info.line.int), coordToStr(info.col+ColOffset)] & title & getMessageStr(msg, arg) proc liMessage(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg: string, eh: TErrorHandling) = var title: string color: ForegroundColor kind: string ignoreMsg = false sev: Severity case msg of errMin..errMax: sev = Severity.Error writeContext(conf, info) title = ErrorTitle color = ErrorColor # we try to filter error messages so that not two error message # in the same file and line are produced: #ignoreMsg = lastError == info and eh != doAbort conf.m.lastError = info of warnMin..warnMax: sev = Severity.Warning ignoreMsg = optWarns notin conf.options or msg notin conf.notes if not ignoreMsg: writeContext(conf, info) title = WarningTitle color = WarningColor kind = WarningsToStr[ord(msg) - ord(warnMin)] inc(conf.warnCounter) of hintMin..hintMax: sev = Severity.Hint ignoreMsg = optHints notin conf.options or msg notin conf.notes title = HintTitle color = HintColor if msg != hintUserRaw: kind = HintsToStr[ord(msg) - ord(hintMin)] inc(conf.hintCounter) let x = PosFormat % [toMsgFilename(conf, info), coordToStr(info.line.int), coordToStr(info.col+ColOffset)] let s = getMessageStr(msg, arg) if not ignoreMsg: if conf.structuredErrorHook != nil: conf.structuredErrorHook(conf, info, s & (if kind.len > 0: KindFormat % kind else: ""), sev) if not ignoreMsgBecauseOfIdeTools(conf, msg): if kind.len > 0: styledMsgWriteln(styleBright, x, resetStyle, color, title, resetStyle, s, KindColor, `%`(KindFormat, kind)) else: styledMsgWriteln(styleBright, x, resetStyle, color, title, resetStyle, s) if hintSource in conf.notes: conf.writeSurroundingSrc(info) handleError(conf, msg, eh, s) proc fatal*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") = # this fixes bug #7080 so that it is at least obvious 'fatal' # was executed. conf.m.errorOutputs = {eStdOut, eStdErr} liMessage(conf, info, msg, arg, doAbort) proc globalError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") = liMessage(conf, info, msg, arg, doRaise) proc globalError*(conf: ConfigRef; info: TLineInfo, arg: string) = liMessage(conf, info, errGenerated, arg, doRaise) proc localError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") = liMessage(conf, info, msg, arg, doNothing) proc localError*(conf: ConfigRef; info: TLineInfo, arg: string) = liMessage(conf, info, errGenerated, arg, doNothing) proc localError*(conf: ConfigRef; info: TLineInfo, format: string, params: openArray[string]) = localError(conf, info, format % params) proc message*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") = liMessage(conf, info, msg, arg, doNothing) proc internalError*(conf: ConfigRef; info: TLineInfo, errMsg: string) = if conf.cmd == cmdIdeTools and conf.structuredErrorHook.isNil: return writeContext(conf, info) liMessage(conf, info, errInternal, errMsg, doAbort) proc internalError*(conf: ConfigRef; errMsg: string) = if conf.cmd == cmdIdeTools and conf.structuredErrorHook.isNil: return writeContext(conf, unknownLineInfo()) rawMessage(conf, errInternal, errMsg) template assertNotNil*(conf: ConfigRef; e): untyped = if e == nil: internalError(conf, $instantiationInfo()) e template internalAssert*(conf: ConfigRef, e: bool) = if not e: internalError(conf, $instantiationInfo()) 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 proc listWarnings*(conf: ConfigRef) = msgWriteln(conf, "Warnings:") for warn in warnMin..warnMax: msgWriteln(conf, " [$1] $2" % [ if warn in conf.notes: "x" else: " ", lineinfos.WarningsToStr[ord(warn) - ord(warnMin)] ]) proc listHints*(conf: ConfigRef) = msgWriteln(conf, "Hints:") for hint in hintMin..hintMax: msgWriteln(conf, " [$1] $2" % [ if hint in conf.notes: "x" else: " ", lineinfos.HintsToStr[ord(hint) - ord(hintMin)] ]) proc lintReport*(conf: ConfigRef; info: TLineInfo, beau, got: string) = let m = "'$2' should be: '$1'" % [beau, got] if optStyleError in conf.globalOptions: localError(conf, info, m) else: message(conf, info, hintName, m)