diff --git a/Sources/AST/Context.swift b/Sources/AST/Context.swift index 604742e7..a64ad856 100644 --- a/Sources/AST/Context.swift +++ b/Sources/AST/Context.swift @@ -72,7 +72,7 @@ public struct Environment { } public func type(of functionCall: FunctionCall, contractIdentifier: Identifier, callerCapabilities: [CallerCapability]) -> Type.RawType? { - guard let matchingFunction = matchFunctionCall(functionCall, contractIdentifier: contractIdentifier, callerCapabilities: callerCapabilities) else { return .errorType } + guard case .success(let matchingFunction) = matchFunctionCall(functionCall, contractIdentifier: contractIdentifier, callerCapabilities: callerCapabilities) else { return .errorType } return typeMap[matchingFunction] } @@ -86,14 +86,25 @@ public struct Environment { typeMap[mangledFunction] = type.rawType } - public func matchFunctionCall(_ functionCall: FunctionCall, contractIdentifier: Identifier, callerCapabilities: [CallerCapability]) -> MangledFunction? { + public enum FunctionCallMatchResult { + case success(MangledFunction) + case failure(candidates: [MangledFunction]) + } + + public func matchFunctionCall(_ functionCall: FunctionCall, contractIdentifier: Identifier, callerCapabilities: [CallerCapability]) -> FunctionCallMatchResult { + var candidates = [MangledFunction]() + for function in functions { if function.canBeCalledBy(functionCall: functionCall, contractIdentifier: contractIdentifier, callerCapabilities: callerCapabilities) { - return function + return .success(function) + } + + if function.hasSameSignatureAs(functionCall) { + candidates.append(function) } } - return nil + return .failure(candidates: candidates) } public func matchEventCall(_ functionCall: FunctionCall, contractIdentifier: Identifier) -> VariableDeclaration? { diff --git a/Sources/AST/Diagnostic.swift b/Sources/AST/Diagnostic.swift index 3b5262b2..6573adff 100644 --- a/Sources/AST/Diagnostic.swift +++ b/Sources/AST/Diagnostic.swift @@ -9,19 +9,23 @@ public struct Diagnostic { public enum Severity: String { case warning case error + case note } public var severity: Severity public var sourceLocation: SourceLocation? public var message: String + public var notes: [Diagnostic] + public var isError: Bool { return severity == .error } - public init(severity: Severity, sourceLocation: SourceLocation?, message: String) { + public init(severity: Severity, sourceLocation: SourceLocation?, message: String, notes: [Diagnostic] = []) { self.severity = severity self.sourceLocation = sourceLocation self.message = message + self.notes = notes } } diff --git a/Sources/AST/MangledFunction.swift b/Sources/AST/MangledFunction.swift index bc4e0d94..a3ebddba 100644 --- a/Sources/AST/MangledFunction.swift +++ b/Sources/AST/MangledFunction.swift @@ -9,21 +9,32 @@ public struct MangledFunction: CustomStringConvertible { public var contractIdentifier: Identifier public var callerCapabilities: [CallerCapability] - public var identifier: Identifier - public var numParameters: Int - public var isMutating: Bool + public var functionDeclaration: FunctionDeclaration - public var resultType: Type? - public var rawType: Type.RawType + public var identifier: Identifier { + return functionDeclaration.identifier + } + + public var numParameters: Int { + return functionDeclaration.parameters.count + } + + public var isMutating: Bool { + return functionDeclaration.isMutating + } + + public var resultType: Type? { + return functionDeclaration.resultType + } + + public var rawType: Type.RawType { + return functionDeclaration.rawType + } init(functionDeclaration: FunctionDeclaration, contractIdentifier: Identifier, callerCapabilities: [CallerCapability]) { - self.identifier = functionDeclaration.identifier + self.functionDeclaration = functionDeclaration self.contractIdentifier = contractIdentifier self.callerCapabilities = callerCapabilities - self.numParameters = functionDeclaration.parameters.count - self.isMutating = functionDeclaration.isMutating - self.resultType = functionDeclaration.resultType - self.rawType = functionDeclaration.rawType } func canBeCalledBy(functionCall: FunctionCall, contractIdentifier: Identifier, callerCapabilities callCallerCapabilities: [CallerCapability]) -> Bool { @@ -42,6 +53,10 @@ public struct MangledFunction: CustomStringConvertible { return true } + func hasSameSignatureAs(_ functionCall: FunctionCall) -> Bool { + return identifier == functionCall.identifier && numParameters == functionCall.arguments.count + } + public var description: String { let callerCapabilitiesDescription = "(\(callerCapabilities.map { $0.identifier.name }.joined(separator: ","))" return "\(identifier.name)_\(numParameters)_\(contractIdentifier.name)_\(callerCapabilitiesDescription)" diff --git a/Sources/SemanticAnalyzer/SemanticAnalyzer.swift b/Sources/SemanticAnalyzer/SemanticAnalyzer.swift index ae5be2c5..3753c667 100644 --- a/Sources/SemanticAnalyzer/SemanticAnalyzer.swift +++ b/Sources/SemanticAnalyzer/SemanticAnalyzer.swift @@ -154,7 +154,8 @@ public struct SemanticAnalyzer: ASTPass { let contractIdentifier = functionDeclarationContext.contractContext.contractIdentifier var diagnostics = [Diagnostic]() - if let matchingFunction = environment.matchFunctionCall(functionCall, contractIdentifier: functionDeclarationContext.contractContext.contractIdentifier, callerCapabilities: functionDeclarationContext.contractContext.callerCapabilities) { + switch environment.matchFunctionCall(functionCall, contractIdentifier: functionDeclarationContext.contractContext.contractIdentifier, callerCapabilities: functionDeclarationContext.contractContext.callerCapabilities) { + case .success(let matchingFunction): if matchingFunction.isMutating { addMutatingExpression(.functionCall(functionCall), passContext: &passContext) @@ -162,9 +163,10 @@ public struct SemanticAnalyzer: ASTPass { diagnostics.append(.useOfMutatingExpressionInNonMutatingFunction(.functionCall(functionCall), functionDeclaration: functionDeclarationContext.declaration)) } } - } else if let _ = environment.matchEventCall(functionCall, contractIdentifier: contractIdentifier) { - } else { - diagnostics.append(.noMatchingFunctionForFunctionCall(functionCall, contextCallerCapabilities: functionDeclarationContext.contractContext.callerCapabilities)) + case .failure(candidates: let candidates): + if environment.matchEventCall(functionCall, contractIdentifier: contractIdentifier) == nil { + diagnostics.append(.noMatchingFunctionForFunctionCall(functionCall, contextCallerCapabilities: functionDeclarationContext.contractContext.callerCapabilities, candidates: candidates)) + } } return ASTPassResult(element: functionCall, diagnostics: diagnostics, passContext: passContext) diff --git a/Sources/SemanticAnalyzer/SemanticError.swift b/Sources/SemanticAnalyzer/SemanticError.swift index 12f4c3cf..8b854b5e 100644 --- a/Sources/SemanticAnalyzer/SemanticError.swift +++ b/Sources/SemanticAnalyzer/SemanticError.swift @@ -10,8 +10,13 @@ import AST // MARK: Errors extension Diagnostic { - static func noMatchingFunctionForFunctionCall(_ functionCall: FunctionCall, contextCallerCapabilities: [CallerCapability]) -> Diagnostic { - return Diagnostic(severity: .error, sourceLocation: functionCall.sourceLocation, message: "Function \(functionCall.identifier.name) is not in scope or cannot be called using the caller capabilities \(contextCallerCapabilities.map { $0.name })") + static func noMatchingFunctionForFunctionCall(_ functionCall: FunctionCall, contextCallerCapabilities: [CallerCapability], candidates: [MangledFunction]) -> Diagnostic { + + let candidateNotes = candidates.map { candidate in + return Diagnostic(severity: .note, sourceLocation: candidate.functionDeclaration.sourceLocation, message: "Perhaps you meant this function, which requires one of the caller capabilities in \(renderCapabilityGroup(candidate.callerCapabilities))") + } + + return Diagnostic(severity: .error, sourceLocation: functionCall.sourceLocation, message: "Function \(functionCall.identifier.name) is not in scope or cannot be called using the caller capabilities \(renderCapabilityGroup(contextCallerCapabilities))", notes: candidateNotes) } static func contractBehaviorDeclarationNoMatchingContract(_ contractBehaviorDeclaration: ContractBehaviorDeclaration) -> Diagnostic { @@ -41,6 +46,10 @@ extension Diagnostic { static func missingReturnInNonVoidFunction(closeBraceToken: Token, resultType: Type) -> Diagnostic { return Diagnostic(severity: .error, sourceLocation: closeBraceToken.sourceLocation, message: "Missing return in function expected to return \(resultType.name)") } + + static func renderCapabilityGroup(_ capabilities: [CallerCapability]) -> String { + return "(\(capabilities.map({ $0.name }).joined(separator: ", ")))" + } } // MARK: Warnings diff --git a/Sources/flintc/DiagnosticsFormatter.swift b/Sources/flintc/DiagnosticsFormatter.swift index c0e051ba..1dab9718 100644 --- a/Sources/flintc/DiagnosticsFormatter.swift +++ b/Sources/flintc/DiagnosticsFormatter.swift @@ -14,31 +14,41 @@ public struct DiagnosticsFormatter { var compilationContext: CompilationContext? public func rendered() -> String { - let sourceFileText: String + return diagnostics.map({ renderDiagnostic($0) }).joined(separator: "\n") + } + + func renderDiagnostic(_ diagnostic: Diagnostic, highlightColor: Color = .lightRed, style: Style = .bold) -> String { + var sourceFileText = "" if let compilationContext = compilationContext { sourceFileText = " in \(compilationContext.fileName.bold)" - } else { - sourceFileText = "" } - return diagnostics.map { diagnostic in - let infoLine = "\(diagnostic.severity == .error ? "Error".lightRed.bold : "Warning")\(sourceFileText):" - let body: String - - if let compilationContext = compilationContext { - body = """ - \(diagnostic.message.indented(by: 2).bold)\(render(diagnostic.sourceLocation).bold): - \(renderSourcePreview(at: diagnostic.sourceLocation, sourceCode: compilationContext.sourceCode).indented(by: 2)) - """ - } else { - body = " \(diagnostic.message.indented(by: 2).bold)" - } - - return """ - \(infoLine) - \(body) + let infoTopic: String + + switch diagnostic.severity { + case .error: infoTopic = "Error".lightRed.bold + case .warning: infoTopic = "Warning".bold + case .note: infoTopic = "Note".lightBlack.bold + } + + let infoLine = "\(infoTopic)\(sourceFileText):" + let body: String + + if let compilationContext = compilationContext { + body = """ + \(diagnostic.message.indented(by: 2).bold)\(render(diagnostic.sourceLocation).bold): + \(renderSourcePreview(at: diagnostic.sourceLocation, sourceCode: compilationContext.sourceCode, highlightColor: highlightColor, style: style)) """ - }.joined(separator: "\n") + } else { + body = " \(diagnostic.message.indented(by: 2).bold)" + } + + let notes = diagnostic.notes.map({ renderDiagnostic($0, highlightColor: .white, style: .default) }).joined(separator: "\n") + + return """ + \(infoLine) + \(body)\(notes.isEmpty ? "" : "\n \(notes.indented(by: 2))") + """ } func render(_ sourceLocation: SourceLocation?) -> String { @@ -46,25 +56,29 @@ public struct DiagnosticsFormatter { return " at line \(sourceLocation.line), column \(sourceLocation.column)" } - func renderSourcePreview(at sourceLocation: SourceLocation?, sourceCode: String) -> String { + func renderSourcePreview(at sourceLocation: SourceLocation?, sourceCode: String, highlightColor: Color, style: Style) -> String { let sourceLines = sourceCode.components(separatedBy: "\n") guard let sourceLocation = sourceLocation else { return "" } let spaceOffset = sourceLocation.column != 0 ? sourceLocation.column - 1 : 0 + + let sourceLine = renderSourceLine(sourceLines[sourceLocation.line - 1], rangeOfInterest: (sourceLocation.column..) -> String { + func renderSourceLine(_ sourceLine: String, rangeOfInterest: Range, highlightColor: Color, style: Style) -> String { let lowerBound = rangeOfInterest.lowerBound != 0 ? rangeOfInterest.lowerBound - 1 : 0 let upperBound = rangeOfInterest.upperBound != 0 ? rangeOfInterest.upperBound - 1 : sourceLine.count - 1 let lowerBoundIndex = sourceLine.index(sourceLine.startIndex, offsetBy: lowerBound) let upperBoundIndex = sourceLine.index(sourceLine.startIndex, offsetBy: upperBound) - return String(sourceLine[sourceLine.startIndex..