# # # 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 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..=% 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 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 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 ', 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..= 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, forceHint = false, extraMsg = "") = let m = "'$1' should be: '$2'$3" % [got, beau, extraMsg] let msg = if optStyleError in conf.globalOptions and not forceHint: 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.. 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, ])