diff --git a/compiler/ast.nim b/compiler/ast.nim index 78c2a6087c..49ca1c5e03 100644 --- a/compiler/ast.nim +++ b/compiler/ast.nim @@ -252,6 +252,7 @@ type sfProcvar, # proc can be passed to a proc var sfDiscriminant, # field is a discriminant in a record/object sfDeprecated, # symbol is deprecated + sfExplain, # provide more diagnostics when this symbol is used sfError, # usage of symbol should trigger a compile-time error sfShadowed, # a symbol that was shadowed in some inner scope sfThread, # proc will run as a thread diff --git a/compiler/msgs.nim b/compiler/msgs.nim index eb99861145..8f7c433121 100644 --- a/compiler/msgs.nim +++ b/compiler/msgs.nim @@ -499,7 +499,6 @@ type TErrorOutput* = enum eStdOut eStdErr - eInMemory TErrorOutputs* = set[TErrorOutput] @@ -653,6 +652,15 @@ var writelnHook*: proc (output: string) {.closure.} structuredErrorHook*: proc (info: TLineInfo; msg: string; severity: Severity) {.closure.} +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 + +template writeBufferedMsg(args: varargs[string, `$`]) = + bufferedMsgs.safeAdd concat(args) + proc suggestWriteln*(s: string) = if eStdOut in errorOutputs: if isNil(writelnHook): @@ -806,10 +814,7 @@ macro callStyledWriteLineStderr(args: varargs[typed]): untyped = result.add(arg) template callWritelnHook(args: varargs[string, `$`]) = - var s = "" - for arg in args: - s.add arg - writelnHook s + writelnHook concat(args) template styledMsgWriteln*(args: varargs[typed]) = if not isNil(writelnHook): diff --git a/compiler/parser.nim b/compiler/parser.nim index b63bab7817..8457adac7d 100644 --- a/compiler/parser.nim +++ b/compiler/parser.nim @@ -66,6 +66,7 @@ proc parseSymbol*(p: var TParser, allowNil = false): PNode proc parseTry(p: var TParser; isExpr: bool): PNode proc parseCase(p: var TParser): PNode proc parseStmtPragma(p: var TParser): PNode +proc parsePragma(p: var TParser): PNode # implementation proc getTok(p: var TParser) = @@ -770,6 +771,13 @@ proc parseOperators(p: var TParser, headNode: PNode, proc simpleExprAux(p: var TParser, limit: int, mode: TPrimaryMode): PNode = result = primary(p, mode) + if p.tok.tokType == tkCurlyDotLe and + p.lex.lineNumber == result.info.line and + mode == pmNormal: + var pragmaExp = newNodeP(nkPragmaExpr, p) + pragmaExp.addSon result + pragmaExp.addSon p.parsePragma + result = pragmaExp result = parseOperators(p, result, limit, mode) proc simpleExpr(p: var TParser, mode = pmNormal): PNode = diff --git a/compiler/pragmas.nim b/compiler/pragmas.nim index 387738f6d4..b30b94b5d3 100644 --- a/compiler/pragmas.nim +++ b/compiler/pragmas.nim @@ -55,7 +55,7 @@ const wPure, wHeader, wCompilerproc, wFinal, wSize, wExtern, wShallow, wImportCpp, wImportObjC, wError, wIncompleteStruct, wByCopy, wByRef, wInheritable, wGensym, wInject, wRequiresInit, wUnchecked, wUnion, wPacked, - wBorrow, wGcSafe, wExportNims, wPartial, wUsed} + wBorrow, wGcSafe, wExportNims, wPartial, wUsed, wExplain} fieldPragmas* = {wImportc, wExportc, wDeprecated, wExtern, wImportCpp, wImportObjC, wError, wGuard, wBitsize, wUsed} varPragmas* = {wImportc, wExportc, wVolatile, wRegister, wThreadVar, wNodecl, @@ -73,7 +73,7 @@ const proc pragma*(c: PContext, sym: PSym, n: PNode, validPragmas: TSpecialWords) # implementation -proc invalidPragma(n: PNode) = +proc invalidPragma*(n: PNode) = localError(n.info, errInvalidPragmaX, renderTree(n, {renderNoComments})) proc pragmaAsm*(c: PContext, n: PNode): char = @@ -773,6 +773,8 @@ proc singlePragma(c: PContext, sym: PSym, n: PNode, i: int, of wProcVar: noVal(it) incl(sym.flags, sfProcvar) + of wExplain: + sym.flags.incl sfExplain of wDeprecated: if it.kind == nkExprColonExpr: deprecatedStmt(c, it) elif sym != nil: incl(sym.flags, sfDeprecated) diff --git a/compiler/semcall.nim b/compiler/semcall.nim index b440d1e754..291cf0c6de 100644 --- a/compiler/semcall.nim +++ b/compiler/semcall.nim @@ -55,76 +55,60 @@ proc pickBestCandidate(c: PContext, headSymbol: PNode, initialBinding: PNode, filter: TSymKinds, best, alt: var TCandidate, - errors: var CandidateErrors) = + errors: var CandidateErrors, + diagnostics = false) = var o: TOverloadIter - var sym = initOverloadIter(o, c, headSymbol) - var scope = o.lastOverloadScope - # Thanks to the lazy semchecking for operands, we need to check whether - # 'initCandidate' modifies the symbol table (via semExpr). - # This can occur in cases like 'init(a, 1, (var b = new(Type2); b))' - let counterInitial = c.currentScope.symbols.counter - var syms: seq[tuple[s: PSym, scope: int]] - var nextSymIndex = 0 - while sym != nil: - if sym.kind in filter: - # Initialise 'best' and 'alt' with the first available symbol - initCandidate(c, best, sym, initialBinding, scope) - initCandidate(c, alt, sym, initialBinding, scope) - best.state = csNoMatch - break - else: - sym = nextOverloadIter(o, c, headSymbol) - scope = o.lastOverloadScope + # thanks to the lazy semchecking for operands, we need to iterate over the + # symbol table *before* any call to 'initCandidate' which might invoke + # semExpr which might modify the symbol table in cases like + # 'init(a, 1, (var b = new(Type2); b))'. + var symx = initOverloadIter(o, c, headSymbol) + let symScope = o.lastOverloadScope + + var syms: seq[tuple[a: PSym, b: int]] = @[] + while symx != nil: + if symx.kind in filter: + syms.add((symx, o.lastOverloadScope)) + symx = nextOverloadIter(o, c, headSymbol) + if syms.len == 0: return var z: TCandidate - while sym != nil: - if sym.kind notin filter: - sym = nextOverloadIter(o, c, headSymbol) - scope = o.lastOverloadScope - continue + initCandidate(c, best, syms[0][0], initialBinding, + symScope, diagnostics = diagnostics) + initCandidate(c, alt, syms[0][0], initialBinding, + symScope, diagnostics = diagnostics) + best.state = csNoMatch + + for i in 0 .. 0: - # fail fast: - globalError(n.info, errTypeMismatch, "") - if errors.isNil or errors.len == 0: - localError(n.info, errExprXCannotBeCalled, n[0].renderTree) - return + initCandidate(c, z, sym, initialBinding, + syms[i][1], diagnostics = diagnostics) + #if sym.name.s == "*" and (n.info ?? "temp5.nim") and n.info.line == 140: + # gDebug = true + matches(c, n, orig, z) + if z.state == csMatch: + # little hack so that iterators are preferred over everything else: + if sym.kind == skIterator: inc(z.exactMatches, 200) + case best.state + of csEmpty, csNoMatch: best = z + of csMatch: + var cmp = cmpCandidates(best, z) + if cmp < 0: best = z # x is better than the best so far + elif cmp == 0: alt = z # x is as good as the best so far + else: discard + #if sym.name.s == "cmp" and (n.info ?? "rstgen.nim") and n.info.line == 516: + # echo "Matches ", n.info, " ", typeToString(sym.typ) + # debug sym + # writeMatches(z) + # for i in 1 .. 0: + # fail fast: + globalError(n.info, errTypeMismatch, "") + if errors.isNil or errors.len == 0: + localError(n.info, errExprXCannotBeCalled, n[0].renderTree) + return + + let (prefer, candidates) = presentFailedCandidates(c, n, errors) + var result = msgKindToString(errTypeMismatch) + add(result, describeArgs(c, n, 1, prefer)) + add(result, ')') if candidates != "": add(result, "\n" & msgKindToString(errButExpected) & "\n" & candidates) localError(n.info, errGenerated, result) @@ -172,7 +170,7 @@ proc bracketNotFoundError(c: PContext; n: PNode) = var symx = initOverloadIter(o, c, headSymbol) while symx != nil: if symx.kind in routineKinds: - errors.add((symx, 0)) + errors.add((symx, 0, nil)) symx = nextOverloadIter(o, c, headSymbol) if errors.len == 0: localError(n.info, "could not resolve: " & $n) @@ -180,7 +178,7 @@ proc bracketNotFoundError(c: PContext; n: PNode) = notFoundError(c, n, errors) proc resolveOverloads(c: PContext, n, orig: PNode, - filter: TSymKinds; + filter: TSymKinds, flags: TExprFlags, errors: var CandidateErrors): TCandidate = var initialBinding: PNode var alt: TCandidate @@ -194,7 +192,7 @@ proc resolveOverloads(c: PContext, n, orig: PNode, template pickBest(headSymbol) = pickBestCandidate(c, headSymbol, n, orig, initialBinding, - filter, result, alt, errors) + filter, result, alt, errors, efExplain in flags) pickBest(f) let overloadsState = result.state @@ -212,7 +210,6 @@ proc resolveOverloads(c: PContext, n, orig: PNode, if result.state != csMatch: n.sons.delete(1) orig.sons.delete(1) - excl n.flags, nfExprCall else: return if nfDotField in n.flags: @@ -260,11 +257,6 @@ proc resolveOverloads(c: PContext, n, orig: PNode, # clean up the inserted ops n.sons.delete(2) n.sons[0] = f - - errors = @[] - pickBest(f) - #notFoundError(c, n, errors) - return if alt.state == csMatch and cmpCandidates(result, alt) == 0 and not sameMethodDispatcher(result.calleeSym, alt.calleeSym): @@ -286,7 +278,6 @@ proc resolveOverloads(c: PContext, n, orig: PNode, getProcHeader(result.calleeSym), getProcHeader(alt.calleeSym), args]) - proc instGenericConvertersArg*(c: PContext, a: PNode, x: TCandidate) = if a.kind == nkHiddenCallConv and a.sons[0].kind == nkSym: let s = a.sons[0].sym @@ -367,7 +358,7 @@ proc semResolvedCall(c: PContext, n: PNode, x: TCandidate): PNode = proc canDeref(n: PNode): bool {.inline.} = result = n.len >= 2 and (let t = n[1].typ; - t != nil and t.skipTypes({tyGenericInst, tyAlias}).kind in {tyPtr, tyRef}) + t != nil and t.skipTypes({tyGenericInst}).kind in {tyPtr, tyRef}) proc tryDeref(n: PNode): PNode = result = newNodeI(nkHiddenDeref, n.info) @@ -375,23 +366,40 @@ proc tryDeref(n: PNode): PNode = result.addSon(n) proc semOverloadedCall(c: PContext, n, nOrig: PNode, - filter: TSymKinds): PNode = - var errors: CandidateErrors - - var r = resolveOverloads(c, n, nOrig, filter, errors) - if r.state == csMatch: result = semResolvedCall(c, n, r) + filter: TSymKinds, flags: TExprFlags): PNode = + var errors: CandidateErrors = if efExplain in flags: @[] + else: nil + var r = resolveOverloads(c, n, nOrig, filter, flags, errors) + if r.state == csMatch: + # this may be triggered, when the explain pragma is used + if errors.len > 0: + let (_, candidates) = presentFailedCandidates(c, n, errors) + message(n.info, hintUser, + "Non-matching candidates for " & renderTree(n) & "\n" & + candidates) + result = semResolvedCall(c, n, r) elif experimentalMode(c) and canDeref(n): # try to deref the first argument and then try overloading resolution again: + # + # XXX: why is this here? + # it could be added to the long list of alternatives tried + # inside `resolveOverloads` or it could be moved all the way + # into sigmatch with hidden conversion produced there + # n.sons[1] = n.sons[1].tryDeref - var r = resolveOverloads(c, n, nOrig, filter, errors) + var r = resolveOverloads(c, n, nOrig, filter, flags, errors) if r.state == csMatch: result = semResolvedCall(c, n, r) else: # get rid of the deref again for a better error message: n.sons[1] = n.sons[1].sons[0] notFoundError(c, n, errors) else: - notFoundError(c, n, errors) - # else: result = errorNode(c, n) + if efExplain notin flags: + # repeat the overload resolution, + # this time enabling all the diagnostic output (this should fail again) + discard semOverloadedCall(c, n, nOrig, filter, flags + {efExplain}) + else: + notFoundError(c, n, errors) proc explicitGenericInstError(n: PNode): PNode = localError(n.info, errCannotInstantiateX, renderTree(n)) @@ -406,7 +414,12 @@ proc explicitGenericSym(c: PContext, n: PNode, s: PSym): PNode = let formal = s.ast.sons[genericParamsPos].sons[i-1].typ let arg = n[i].typ let tm = typeRel(m, formal, arg, true) - if tm in {isNone, isConvertible}: return nil + if tm in {isNone, isConvertible}: + if formal.sonsLen > 0 and formal.sons[0].kind != tyNone: + typeMismatch(n, formal.sons[0], arg) + else: + typeMismatch(n, formal, arg) + break var newInst = generateInstance(c, s, m.bindings, n.info) newInst.typ.flags.excl tfUnresolved markUsed(n.info, s, c.graph.usageSym) @@ -428,7 +441,6 @@ proc explicitGenericInstantiation(c: PContext, n: PNode, s: PSym): PNode = "; got " & $(n.len-1) & " type(s) but expected " & $expected) return n result = explicitGenericSym(c, n, s) - if result == nil: result = explicitGenericInstError(n) elif a.kind in {nkClosedSymChoice, nkOpenSymChoice}: # choose the generic proc with the proper number of type parameters. # XXX I think this could be improved by reusing sigmatch.paramTypesMatch. @@ -441,12 +453,11 @@ proc explicitGenericInstantiation(c: PContext, n: PNode, s: PSym): PNode = # it suffices that the candidate has the proper number of generic # type parameters: if safeLen(candidate.ast.sons[genericParamsPos]) == n.len-1: - let x = explicitGenericSym(c, n, candidate) - if x != nil: result.add(x) + result.add(explicitGenericSym(c, n, candidate)) # get rid of nkClosedSymChoice if not ambiguous: if result.len == 1 and a.kind == nkClosedSymChoice: result = result[0] - elif result.len == 0: result = explicitGenericInstError(n) + # candidateCount != 1: return explicitGenericInstError(n) else: result = explicitGenericInstError(n) @@ -460,7 +471,7 @@ proc searchForBorrowProc(c: PContext, startScope: PScope, fn: PSym): PSym = call.add(newIdentNode(fn.name, fn.info)) for i in 1.. 4: - localError(body.n[3].info, $body.n[3] & " too nested for type matching") + localError(body.info, $body & " too nested for type matching") return nil openScope(c) inc c.inTypeClass - defer: dec c.inTypeClass closeScope(c) @@ -599,7 +611,7 @@ proc matchUserTypeClass*(c: PContext, m: var TCandidate, param: PSym template paramSym(kind): untyped = - newSym(kind, typeParamName, body.sym, body.sym.info) + newSym(kind, typeParamName, Concept.sym, Concept.sym.info) case typ.kind of tyStatic: @@ -622,7 +634,7 @@ proc matchUserTypeClass*(c: PContext, m: var TCandidate, addDecl(c, param) typeParams.safeAdd((param, typ)) - for param in body.n[0]: + for param in Concept.n[0]: var dummyName: PNode dummyType: PType @@ -645,11 +657,31 @@ proc matchUserTypeClass*(c: PContext, m: var TCandidate, internalAssert dummyName.kind == nkIdent var dummyParam = newSym(if modifier == tyTypeDesc: skType else: skVar, - dummyName.ident, body.sym, body.sym.info) + dummyName.ident, Concept.sym, Concept.sym.info) dummyParam.typ = dummyType addDecl(c, dummyParam) - var checkedBody = c.semTryExpr(c, body.n[3].copyTree) + var + oldWriteHook: type(writelnHook) + diagnostics: seq[string] + flags: TExprFlags = {} + collectDiagnostics = m.diagnostics != nil or + sfExplain in Concept.sym.flags + + if collectDiagnostics: + oldWriteHook = writelnHook + # XXX: we can't write to m.diagnostics directly, because + # Nim doesn't support capturing var params in closures + diagnostics = @[] + writelnHook = proc (s: string) = diagnostics.add(s) + flags = {efExplain} + + var checkedBody = c.semTryExpr(c, body.copyTree, flags) + + if collectDiagnostics: + writelnHook = oldWriteHook + for msg in diagnostics: m.diagnostics.safeAdd msg + if checkedBody == nil: return nil # The inferrable type params have been identified during the semTryExpr above. diff --git a/compiler/wordrecg.nim b/compiler/wordrecg.nim index 6072bd64c9..98fd912d87 100644 --- a/compiler/wordrecg.nim +++ b/compiler/wordrecg.nim @@ -66,7 +66,7 @@ type wWrite, wGensym, wInject, wDirty, wInheritable, wThreadVar, wEmit, wAsmNoStackFrame, wImplicitStatic, wGlobal, wCodegenDecl, wUnchecked, wGuard, wLocks, - wPartial, + wPartial, wExplain, wAuto, wBool, wCatch, wChar, wClass, wConst_cast, wDefault, wDelete, wDouble, wDynamic_cast, @@ -152,7 +152,7 @@ const "computedgoto", "injectstmt", "experimental", "write", "gensym", "inject", "dirty", "inheritable", "threadvar", "emit", "asmnostackframe", "implicitstatic", "global", "codegendecl", "unchecked", - "guard", "locks", "partial", + "guard", "locks", "partial", "explain", "auto", "bool", "catch", "char", "class", "const_cast", "default", "delete", "double", diff --git a/tests/concepts/texplain.nim b/tests/concepts/texplain.nim new file mode 100644 index 0000000000..9b2b1f70d9 --- /dev/null +++ b/tests/concepts/texplain.nim @@ -0,0 +1,92 @@ +discard """ + cmd: "nim c --verbosity:0 --colors:off $file" + nimout: ''' +tests/concepts/texplain.nim(71, 10) Hint: Non-matching candidates for e(y) +proc e(i: int): int + [User] +tests/concepts/texplain.nim(74, 7) Hint: Non-matching candidates for e(10) +proc e[ExplainedConcept](o: ExplainedConcept): int +tests/concepts/texplain.nim(38, 6) Error: undeclared field: 'foo' +tests/concepts/texplain.nim(38, 6) Error: undeclared field: '.' +tests/concepts/texplain.nim(38, 6) Error: type mismatch: got ( + [User] +tests/concepts/texplain.nim(77, 10) Hint: Non-matching candidates for e(10) +proc e[ExplainedConcept](o: ExplainedConcept): int +tests/concepts/texplain.nim(38, 6) Error: undeclared field: 'foo' +tests/concepts/texplain.nim(38, 6) Error: undeclared field: '.' +tests/concepts/texplain.nim(38, 6) Error: type mismatch: got ( + [User] +tests/concepts/texplain.nim(81, 20) Error: type mismatch: got ( +tests/concepts/texplain.nim(82, 20) Error: type mismatch: got ( +tests/concepts/texplain.nim(83, 20) Hint: Non-matching candidates for r(y) +proc r(i: string): int + [User] +tests/concepts/texplain.nim(91, 2) Error: type mismatch: got (MatchingType) +but expected one of: +proc f[NestedConcept](o: NestedConcept) +tests/concepts/texplain.nim(42, 6) Error: undeclared field: 'foo' +tests/concepts/texplain.nim(42, 6) Error: undeclared field: '.' +tests/concepts/texplain.nim(42, 6) Error: type mismatch: got ( +tests/concepts/texplain.nim(46, 5) Error: type class predicate failed +''' + line: 46 + errormsg: "type class predicate failed" +""" + +type + ExplainedConcept {.explain.} = concept o + o.foo is int + o.bar is string + + RegularConcept = concept o + o.foo is int + o.bar is string + + NestedConcept = concept o + o.foo is RegularConcept + + NonMatchingType = object + foo: int + bar: int + + MatchingType = object + foo: int + bar: string + +proc e(o: ExplainedConcept): int = 1 +proc e(i: int): int = i + +proc r(o: RegularConcept): int = 1 +proc r(i: string): int = 1 + +proc f(o: NestedConcept) = discard + +var n = NonMatchingType(foo: 10, bar: 20) +var y = MatchingType(foo: 10, bar: "bar") + +# no diagnostic here: +discard e(y) + +# explain that e(int) doesn't match +discard e(y) {.explain.} + +# explain that e(ExplainedConcept) doesn't match +echo(e(10) {.explain.}, 20) + +# explain that e(ExplainedConcept) doesn't again +discard e(10) + +static: + # provide diagnostics why the compile block failed + assert(compiles(e(n)) {.explain.} == false) + assert(compiles(r(n)) {.explain.} == false) + assert(compiles(r(y)) {.explain.} == true) + + # these should not produce any output + assert(compiles(r(10)) == false) + assert(compiles(e(10)) == true) + +# finally, provide multiple nested explanations for failed matching +# of regular concepts, even when the explain pragma is not used +f(y) + diff --git a/tests/testament/tester.nim b/tests/testament/tester.nim index d4a161dabd..908eba962d 100644 --- a/tests/testament/tester.nim +++ b/tests/testament/tester.nim @@ -184,6 +184,8 @@ proc addResult(r: var TResults, test: TTest, proc cmpMsgs(r: var TResults, expected, given: TSpec, test: TTest) = if strip(expected.msg) notin strip(given.msg): r.addResult(test, expected.msg, given.msg, reMsgsDiffer) + elif expected.nimout.len > 0 and expected.nimout.normalize notin given.nimout.normalize: + r.addResult(test, expected.nimout, given.nimout, reMsgsDiffer) elif expected.tfile == "" and extractFilename(expected.file) != extractFilename(given.file) and "internal error:" notin expected.msg: r.addResult(test, expected.file, given.file, reFilesDiffer) @@ -233,6 +235,8 @@ proc nimoutCheck(test: TTest; expectedNimout: string; given: var TSpec) = if exp notin giv: given.err = reMsgsDiffer +proc normalize(s: string): string = s.strip.replace("\C\L", "\L") + proc makeDeterministic(s: string): string = var x = splitLines(s) sort(x, system.cmp)