Skip to content

Commit

Permalink
Merge pull request #98 from franklinsch/better-suited-candidates
Browse files Browse the repository at this point in the history
Provide candidates when function is called using incompatible caller capabilities
  • Loading branch information
franklinsch authored Jan 25, 2018
2 parents f29977e + 2877333 commit 20c2887
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 48 deletions.
19 changes: 15 additions & 4 deletions Sources/AST/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand All @@ -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? {
Expand Down
6 changes: 5 additions & 1 deletion Sources/AST/Diagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
35 changes: 25 additions & 10 deletions Sources/AST/MangledFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)"
Expand Down
10 changes: 6 additions & 4 deletions Sources/SemanticAnalyzer/SemanticAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,17 +154,19 @@ 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)

if !functionDeclarationContext.isMutating {
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)
Expand Down
13 changes: 11 additions & 2 deletions Sources/SemanticAnalyzer/SemanticError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
64 changes: 39 additions & 25 deletions Sources/flintc/DiagnosticsFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,57 +14,71 @@ 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 {
guard let sourceLocation = sourceLocation else { return "" }
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..<sourceLocation.column + sourceLocation.length), highlightColor: highlightColor, style: style)
let indicator = String(repeating: " ", count: spaceOffset) + String(repeating: "^", count: sourceLocation.length).applyingCodes(highlightColor, style)

return """
\(renderSourceLine(sourceLines[sourceLocation.line - 1], rangeOfInterest: (sourceLocation.column..<sourceLocation.column + sourceLocation.length)))
\(String(repeating: " ", count: spaceOffset) + String(repeating: "^", count: sourceLocation.length).lightRed.bold)
\(sourceLine)
\(indicator)
"""
}

func renderSourceLine(_ sourceLine: String, rangeOfInterest: Range<Int>) -> String {
func renderSourceLine(_ sourceLine: String, rangeOfInterest: Range<Int>, 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..<lowerBoundIndex]) + String(sourceLine[lowerBoundIndex..<upperBoundIndex]).red.bold + String(sourceLine[upperBoundIndex..<sourceLine.endIndex])
return String(sourceLine[sourceLine.startIndex..<lowerBoundIndex]) + String(sourceLine[lowerBoundIndex..<upperBoundIndex]).applyingCodes(highlightColor, style) + String(sourceLine[upperBoundIndex..<sourceLine.endIndex])
}
}

Expand Down
2 changes: 1 addition & 1 deletion Tests/SemanticTests/events.flint
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ EventTest :: (any) {
// expected-error@18 {{Cannot convert expression of type Int to expected argument type Bool}}
eventA(true, 3, c)

// expected-error@21 {{Function eventA is not in scope or cannot be called using the caller capabilities ["any"]}}
// expected-error@21 {{Function eventA is not in scope or cannot be called using the caller capabilities (any)}}
eventA(true)
}
}
2 changes: 1 addition & 1 deletion Tests/SemanticTests/invalid_function_call.flint
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ contract Test {
Test :: (any) {
public func foo() {

// expected-error@11 {{Function bar is not in scope or cannot be called using the caller capabilities ["any"]}}
// expected-error@11 {{Function bar is not in scope or cannot be called using the caller capabilities (any)}}
bar()
}
}
Expand Down

0 comments on commit 20c2887

Please sign in to comment.