diff --git a/Sources/AST/AST.swift b/Sources/AST/AST.swift index 6121b107..4b597132 100644 --- a/Sources/AST/AST.swift +++ b/Sources/AST/AST.swift @@ -118,6 +118,11 @@ public struct StructDeclaration: SourceEntity { } private var shouldInitializerBeSynthesized: Bool { + // Don't synthesize an initializer for the special stdlib Flint$Global struct. + guard identifier.name != Environment.globalFunctionStructName else { + return false + } + let containsInitializer = members.contains { member in if case .initializerDeclaration(_) = member { return true } return false diff --git a/Sources/AST/ASTPassContext.swift b/Sources/AST/ASTPassContext.swift index abb33897..8d2e2beb 100644 --- a/Sources/AST/ASTPassContext.swift +++ b/Sources/AST/ASTPassContext.swift @@ -109,7 +109,7 @@ extension ASTPassContext { /// Whether we are visiting a node in a function declaration or initializer. public var inFunctionOrInitializer: Bool { - return functionDeclarationContext != nil || functionDeclarationContext != nil + return functionDeclarationContext != nil || initializerDeclarationContext != nil } /// Whether we are visiting a property's default assignment. diff --git a/Sources/AST/Environment.swift b/Sources/AST/Environment.swift index fd93298d..2b4768ec 100644 --- a/Sources/AST/Environment.swift +++ b/Sources/AST/Environment.swift @@ -20,6 +20,17 @@ public struct Environment { /// A list of the names of the structs which have been declared in the program. var declaredStructs = [Identifier]() + /// The name of the stdlib struct which contains all global functions. + public static let globalFunctionStructName = "Flint$Global" + + /// The prefix for Flint runtime functions. + public static var runtimeFunctionPrefix = "flint$" + + /// Whether the given function call is a runtime function. + public static func isRuntimeFunctionCall(_ functionCall: FunctionCall) -> Bool { + return functionCall.identifier.name.starts(with: runtimeFunctionPrefix) + } + public init() {} /// Add a contract declaration to the environment. @@ -346,6 +357,7 @@ public struct Environment { public enum FunctionCallMatchResult { case matchedFunction(FunctionInformation) case matchedInitializer(InitializerInformation) + case matchedGlobalFunction(FunctionInformation) case failure(candidates: [FunctionInformation]) } @@ -396,6 +408,21 @@ public struct Environment { } } + // Check if it's a global function. + + if let functions = types[Environment.globalFunctionStructName]?.functions[functionCall.identifier.name] { + for candidate in functions { + + guard candidate.parameterTypes == argumentTypes, + areCallerCapabilitiesCompatible(source: callerCapabilities, target: candidate.callerCapabilities) else { + candidates.append(candidate) + continue + } + + match = .matchedGlobalFunction(candidate) + } + } + return match ?? .failure(candidates: candidates) } diff --git a/Sources/AST/SourceLocation.swift b/Sources/AST/SourceLocation.swift index 5e45e6f5..8fa0c080 100644 --- a/Sources/AST/SourceLocation.swift +++ b/Sources/AST/SourceLocation.swift @@ -13,12 +13,14 @@ public struct SourceLocation: Equatable { public var column: Int public var length: Int public var file: URL + public var isFromStdlib: Bool - public init(line: Int, column: Int, length: Int, file: URL) { + public init(line: Int, column: Int, length: Int, file: URL, isFromStdlib: Bool = false) { self.line = line self.column = column self.length = length self.file = file + self.isFromStdlib = isFromStdlib } public static func spanning(_ lowerBoundEntity: S1, to upperBoundEntity: S2) -> SourceLocation { diff --git a/Sources/IRGen/Function/IULIAContractInitializer.swift b/Sources/IRGen/Function/IULIAContractInitializer.swift index fb45391c..aab3bcbd 100644 --- a/Sources/IRGen/Function/IULIAContractInitializer.swift +++ b/Sources/IRGen/Function/IULIAContractInitializer.swift @@ -59,8 +59,6 @@ struct IULIAContractInitializer { let body = IULIAFunctionBody(functionDeclaration: initializerDeclaration.asFunctionDeclaration, typeIdentifier: typeIdentifier, capabilityBinding: capabilityBinding, callerCapabilities: callerCapabilities, environment: environment, isContractFunction: isContractFunction).rendered() - // TODO: Remove IULIARuntimeFunctionDeclaration.store once constructor code and function code is unified. - return """ init() function init() { diff --git a/Sources/IRGen/IULIAPreprocessor.swift b/Sources/IRGen/IULIAPreprocessor.swift index 7f201c93..e98b22e1 100644 --- a/Sources/IRGen/IULIAPreprocessor.swift +++ b/Sources/IRGen/IULIAPreprocessor.swift @@ -79,6 +79,7 @@ public struct IULIAPreprocessor: ASTPass { public func process(functionDeclaration: FunctionDeclaration, passContext: ASTPassContext) -> ASTPassResult { var functionDeclaration = functionDeclaration + // Bind the implicit Wei value of the transaction to a variable. if functionDeclaration.isPayable, let payableParameterIdentifier = functionDeclaration.firstPayableValueParameter?.identifier { let weiType = Identifier(identifierToken: Token(kind: .identifier("Wei"), sourceLocation: payableParameterIdentifier.sourceLocation)) let variableDeclaration = VariableDeclaration(declarationToken: nil, identifier: payableParameterIdentifier, type: Type(identifier: weiType)) @@ -92,12 +93,13 @@ public struct IULIAPreprocessor: ASTPass { if let structDeclarationContext = passContext.structDeclarationContext { let parameters = functionDeclaration.parameters.map { $0.type.rawType } let name = Mangler.mangleFunctionName(functionDeclaration.identifier.name, parameterTypes: parameters, enclosingType: passContext.enclosingTypeIdentifier!.name) - - // For struct functions, add `flintSelf` to the beginning of the parameters list. - let parameter = constructParameter(name: "flintSelf", type: .inoutType(.userDefinedType(structDeclarationContext.structIdentifier.name)), sourceLocation: functionDeclaration.sourceLocation) - functionDeclaration.parameters.insert(parameter, at: 0) - functionDeclaration.identifier = Identifier(identifierToken: Token(kind: .identifier(name), sourceLocation: functionDeclaration.identifier.sourceLocation)) + + if Environment.globalFunctionStructName != passContext.enclosingTypeIdentifier?.name { + // For struct functions, add `flintSelf` to the beginning of the parameters list. + let parameter = constructParameter(name: "flintSelf", type: .inoutType(.userDefinedType(structDeclarationContext.structIdentifier.name)), sourceLocation: functionDeclaration.sourceLocation) + functionDeclaration.parameters.insert(parameter, at: 0) + } } // Add an isMem parameter for each struct parameter. @@ -232,6 +234,14 @@ public struct IULIAPreprocessor: ASTPass { var receiverTrail = passContext.functionCallReceiverTrail ?? [] let enclosingType = passContext.enclosingTypeIdentifier!.name let callerCapabilities = passContext.contractBehaviorDeclarationContext?.callerCapabilities ?? [] + let isGlobalFunctionCall = self.isGlobalFunctionCall(functionCall, in: passContext) + + let scopeContext = passContext.scopeContext! + + guard !Environment.isRuntimeFunctionCall(functionCall) else { + // Don't mangle runtime functions. + return ASTPassResult(element: functionCall, diagnostics: [], passContext: passContext) + } if receiverTrail.isEmpty { receiverTrail = [.self(Token(kind: .self, sourceLocation: functionCall.sourceLocation))] @@ -248,25 +258,31 @@ public struct IULIAPreprocessor: ASTPass { let mangledName = mangledFunctionName(for: initializerWithoutReceiver, in: passContext) functionCall.identifier = Identifier(identifierToken: Token(kind: .identifier(mangledName), sourceLocation: functionCall.sourceLocation)) } else { - let scopeContext = passContext.scopeContext! // Get the result type of the call. - let type = passContext.environment!.type(of: receiverTrail.last!, enclosingType: enclosingType, callerCapabilities: callerCapabilities, scopeContext: scopeContext) + let declarationEnclosingType: RawTypeIdentifier + + if isGlobalFunctionCall { + declarationEnclosingType = Environment.globalFunctionStructName + } else { + declarationEnclosingType = passContext.environment!.type(of: receiverTrail.last!, enclosingType: enclosingType, callerCapabilities: callerCapabilities, scopeContext: scopeContext).name + } // If it returns a dynamic type, pass the receiver as the first parameter. - if passContext.environment!.isStructDeclared(type.name) { + if passContext.environment!.isStructDeclared(declarationEnclosingType) { let mangledName = mangledFunctionName(for: functionCall, in: passContext) - let receiver = constructExpression(from: receiverTrail) - let inoutExpression = InoutExpression(ampersandToken: Token(kind: .punctuation(.ampersand), sourceLocation: receiver.sourceLocation), expression: receiver) - functionCall.arguments.insert(.inoutExpression(inoutExpression), at: 0) + + if !isGlobalFunctionCall { + let receiver = constructExpression(from: receiverTrail) + let inoutExpression = InoutExpression(ampersandToken: Token(kind: .punctuation(.ampersand), sourceLocation: receiver.sourceLocation), expression: receiver) + functionCall.arguments.insert(.inoutExpression(inoutExpression), at: 0) + } // Replace the name of a function call by its mangled name. functionCall.identifier = Identifier(identifierToken: Token(kind: .identifier(mangledName), sourceLocation: functionCall.sourceLocation)) } } - let scopeContext = passContext.scopeContext! - // For each non-implicit dynamic type, add an isMem parameter. var offset = 0 for (index, argument) in functionCall.arguments.enumerated() { @@ -304,6 +320,11 @@ public struct IULIAPreprocessor: ASTPass { } func mangledFunctionName(for functionCall: FunctionCall, in passContext: ASTPassContext) -> String { + // Don't mangle runtime functions. + guard !Environment.isRuntimeFunctionCall(functionCall) else { + return functionCall.identifier.name + } + let environment = passContext.environment! let enclosingType: String = functionCall.identifier.enclosingType ?? passContext.enclosingTypeIdentifier!.name @@ -319,11 +340,30 @@ public struct IULIAPreprocessor: ASTPass { let declaration = initializerInformation.declaration let parameterTypes = declaration.parameters.map { $0.type.rawType } return Mangler.mangleInitializerName(functionCall.identifier.name, parameterTypes: parameterTypes) + case .matchedGlobalFunction(let functionInformation): + let parameterTypes = functionInformation.declaration.parameters.map { $0.type.rawType } + return Mangler.mangleFunctionName(functionCall.identifier.name, parameterTypes: parameterTypes, enclosingType: Environment.globalFunctionStructName) case .failure(_): fatalError("Unable to find declaration of \(functionCall)") } } + func isGlobalFunctionCall(_ functionCall: FunctionCall, in passContext: ASTPassContext) -> Bool { + let enclosingType = passContext.enclosingTypeIdentifier!.name + let callerCapabilities = passContext.contractBehaviorDeclarationContext?.callerCapabilities ?? [] + let scopeContext = passContext.scopeContext! + let environment = passContext.environment! + + let match = environment.matchFunctionCall(functionCall, enclosingType: enclosingType, callerCapabilities: callerCapabilities, scopeContext: scopeContext) + + // Mangle global function + if case .matchedGlobalFunction(_) = match { + return true + } + + return false + } + public func process(arrayLiteral: ArrayLiteral, passContext: ASTPassContext) -> ASTPassResult { return ASTPassResult(element: arrayLiteral, diagnostics: [], passContext: passContext) } diff --git a/Sources/IRGen/Runtime/IULIAFunctionSelector.swift b/Sources/IRGen/Runtime/IULIAFunctionSelector.swift index e7e980e7..2a552df4 100644 --- a/Sources/IRGen/Runtime/IULIAFunctionSelector.swift +++ b/Sources/IRGen/Runtime/IULIAFunctionSelector.swift @@ -15,7 +15,7 @@ struct IULIAFunctionSelector { let cases = renderCases() return """ - switch selector() + switch \(IULIARuntimeFunction.selector()) \(cases) default { revert(0, 0) diff --git a/Sources/IRGen/Runtime/IULIARuntimeFunction.swift b/Sources/IRGen/Runtime/IULIARuntimeFunction.swift index 3558109d..163b3b2c 100644 --- a/Sources/IRGen/Runtime/IULIARuntimeFunction.swift +++ b/Sources/IRGen/Runtime/IULIARuntimeFunction.swift @@ -9,7 +9,7 @@ import AST /// The runtime functions used by Flint. enum IULIARuntimeFunction { - enum Identifiers: String { + enum Identifiers { case selector case decodeAsAddress case decodeAsUInt @@ -25,18 +25,23 @@ enum IULIARuntimeFunction { case storageFixedSizeArrayOffset case storageDictionaryOffsetForKey case callvalue + case send + + var mangled: String { + return "\(Environment.runtimeFunctionPrefix)\(self)" + } } static func selector() -> String { - return "\(Identifiers.selector)()" + return "\(Identifiers.selector.mangled)()" } static func decodeAsAddress(offset: Int) -> String { - return "\(Identifiers.decodeAsAddress)(\(offset))" + return "\(Identifiers.decodeAsAddress.mangled)(\(offset))" } static func decodeAsUInt(offset: Int) -> String { - return "\(Identifiers.decodeAsUInt)(\(offset))" + return "\(Identifiers.decodeAsUInt.mangled)(\(offset))" } static func store(address: String, value: String, inMemory: Bool) -> String { @@ -44,7 +49,7 @@ enum IULIARuntimeFunction { } static func store(address: String, value: String, inMemory: String) -> String { - return "\(Identifiers.store)(\(address), \(value), \(inMemory))" + return "\(Identifiers.store.mangled)(\(address), \(value), \(inMemory))" } static func addOffset(base: String, offset: String, inMemory: Bool) -> String { @@ -52,7 +57,7 @@ enum IULIARuntimeFunction { } static func addOffset(base: String, offset: String, inMemory: String) -> String { - return "\(Identifiers.computeOffset)(\(base), \(offset), \(inMemory))" + return "\(Identifiers.computeOffset.mangled)(\(base), \(offset), \(inMemory))" } static func load(address: String, inMemory: Bool) -> String { @@ -60,74 +65,74 @@ enum IULIARuntimeFunction { } static func load(address: String, inMemory: String) -> String { - return "\(Identifiers.load)(\(address), \(inMemory))" + return "\(Identifiers.load.mangled)(\(address), \(inMemory))" } static func allocateMemory(size: Int) -> String { - return "\(Identifiers.allocateMemory)(\(size))" + return "\(Identifiers.allocateMemory.mangled)(\(size))" } static func isValidCallerCapability(address: String) -> String { - return "\(Identifiers.isValidCallerCapability)(\(address))" + return "\(Identifiers.isValidCallerCapability.mangled)(\(address))" } static func isCallerCapabilityInArray(arrayOffset: Int) -> String { - return "\(Identifiers.isCallerCapabilityInArray)(\(arrayOffset))" + return "\(Identifiers.isCallerCapabilityInArray.mangled)(\(arrayOffset))" } static func return32Bytes(value: String) -> String { - return "\(Identifiers.return32Bytes)(\(value))" + return "\(Identifiers.return32Bytes.mangled)(\(value))" } static func isInvalidSubscriptExpression(index: Int, arraySize: Int) -> String { - return "\(Identifiers.isInvalidSubscriptExpression)(\(index), \(arraySize))" + return "\(Identifiers.isInvalidSubscriptExpression.mangled)(\(index), \(arraySize))" } static func storageFixedSizeArrayOffset(arrayOffset: Int, index: String, arraySize: Int) -> String { - return "\(Identifiers.storageFixedSizeArrayOffset)(\(arrayOffset), \(index), \(arraySize))" + return "\(Identifiers.storageFixedSizeArrayOffset.mangled)(\(arrayOffset), \(index), \(arraySize))" } static func storageArrayOffset(arrayOffset: Int, index: String) -> String { - return "\(Identifiers.storageArrayOffset)(\(arrayOffset), \(index))" + return "\(Identifiers.storageArrayOffset.mangled)(\(arrayOffset), \(index))" } static func storageDictionaryOffsetForKey(dictionaryOffset: Int, key: String) -> String { - return "\(Identifiers.storageDictionaryOffsetForKey)(\(dictionaryOffset), \(key))" + return "\(Identifiers.storageDictionaryOffsetForKey.mangled)(\(dictionaryOffset), \(key))" } static func callvalue() -> String { return "\(Identifiers.callvalue)()" } - static let allDeclarations: [String] = [IULIARuntimeFunctionDeclaration.selector, IULIARuntimeFunctionDeclaration.decodeAsAddress, IULIARuntimeFunctionDeclaration.decodeAsUInt, IULIARuntimeFunctionDeclaration.store, IULIARuntimeFunctionDeclaration.load, IULIARuntimeFunctionDeclaration.computeOffset, IULIARuntimeFunctionDeclaration.allocateMemory, IULIARuntimeFunctionDeclaration.isValidCallerCapability, IULIARuntimeFunctionDeclaration.isCallerCapabilityInArray, IULIARuntimeFunctionDeclaration.return32Bytes, IULIARuntimeFunctionDeclaration.isInvalidSubscriptExpression, IULIARuntimeFunctionDeclaration.storageArrayOffset, IULIARuntimeFunctionDeclaration.storageFixedSizeArrayOffset, IULIARuntimeFunctionDeclaration.storageDictionaryOffsetForKey] + static let allDeclarations: [String] = [IULIARuntimeFunctionDeclaration.selector, IULIARuntimeFunctionDeclaration.decodeAsAddress, IULIARuntimeFunctionDeclaration.decodeAsUInt, IULIARuntimeFunctionDeclaration.store, IULIARuntimeFunctionDeclaration.load, IULIARuntimeFunctionDeclaration.computeOffset, IULIARuntimeFunctionDeclaration.allocateMemory, IULIARuntimeFunctionDeclaration.isValidCallerCapability, IULIARuntimeFunctionDeclaration.isCallerCapabilityInArray, IULIARuntimeFunctionDeclaration.return32Bytes, IULIARuntimeFunctionDeclaration.isInvalidSubscriptExpression, IULIARuntimeFunctionDeclaration.storageArrayOffset, IULIARuntimeFunctionDeclaration.storageFixedSizeArrayOffset, IULIARuntimeFunctionDeclaration.storageDictionaryOffsetForKey, IULIARuntimeFunctionDeclaration.send] } struct IULIARuntimeFunctionDeclaration { static let selector = """ - function selector() -> ret { + function flint$selector() -> ret { ret := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000) } """ static let decodeAsAddress = """ - function decodeAsAddress(offset) -> ret { - ret := decodeAsUInt(offset) + function flint$decodeAsAddress(offset) -> ret { + ret := flint$decodeAsUInt(offset) } """ static let decodeAsUInt = """ - function decodeAsUInt(offset) -> ret { + function flint$decodeAsUInt(offset) -> ret { ret := calldataload(add(4, mul(offset, 0x20))) } """ static let store = """ - function store(ptr, val, mem) { + function flint$store(ptr, val, mem) { switch iszero(mem) case 0 { mstore(ptr, val) @@ -140,7 +145,7 @@ struct IULIARuntimeFunctionDeclaration { static let load = """ - function load(ptr, mem) -> ret { + function flint$load(ptr, mem) -> ret { switch iszero(mem) case 0 { ret := mload(ptr) @@ -153,7 +158,7 @@ struct IULIARuntimeFunctionDeclaration { static let computeOffset = """ - function computeOffset(base, offset, mem) -> ret { + function flint$computeOffset(base, offset, mem) -> ret { switch iszero(mem) case 0 { ret := add(base, mul(offset, \(EVM.wordSize))) @@ -166,7 +171,7 @@ struct IULIARuntimeFunctionDeclaration { static let allocateMemory = """ - function allocateMemory(size) -> ret { + function flint$allocateMemory(size) -> ret { ret := mload(0x40) mstore(0x40, add(ret, size)) } @@ -174,20 +179,20 @@ struct IULIARuntimeFunctionDeclaration { static let isValidCallerCapability = """ - function isValidCallerCapability(_address) -> ret { + function flint$isValidCallerCapability(_address) -> ret { ret := eq(_address, caller()) } """ static let isCallerCapabilityInArray = """ - function isCallerCapabilityInArray(arrayOffset) -> ret { + function flint$isCallerCapabilityInArray(arrayOffset) -> ret { let size := sload(arrayOffset) let found := 0 let _caller := caller() let arrayStart := add(arrayOffset, 1) for { let i := 0 } and(lt(i, size), iszero(found)) { i := add(i, 1) } { - if eq(sload(storageArrayOffset(arrayOffset, i)), _caller) { + if eq(sload(flint$storageArrayOffset(arrayOffset, i)), _caller) { found := 1 } } @@ -197,7 +202,7 @@ struct IULIARuntimeFunctionDeclaration { static let return32Bytes = """ - function return32Bytes(v) { + function flint$return32Bytes(v) { mstore(0, v) return(0, 0x20) } @@ -205,43 +210,54 @@ struct IULIARuntimeFunctionDeclaration { static let isInvalidSubscriptExpression = """ - function isInvalidSubscriptExpression(index, arraySize) -> ret { + function flint$isInvalidSubscriptExpression(index, arraySize) -> ret { ret := or(iszero(arraySize), or(lt(index, 0), gt(index, sub(arraySize, 1)))) } """ static let storageFixedSizeArrayOffset = """ - function storageFixedSizeArrayOffset(arrayOffset, index, arraySize) -> ret { - if isInvalidSubscriptExpression(index, arraySize) { revert(0, 0) } + function flint$storageFixedSizeArrayOffset(arrayOffset, index, arraySize) -> ret { + if flint$isInvalidSubscriptExpression(index, arraySize) { revert(0, 0) } ret := add(arrayOffset, index) } """ static let storageArrayOffset = """ - function storageArrayOffset(arrayOffset, index) -> ret { + function flint$storageArrayOffset(arrayOffset, index) -> ret { let arraySize := sload(arrayOffset) switch eq(arraySize, index) case 0 { - if isInvalidSubscriptExpression(index, arraySize) { revert(0, 0) } + if flint$isInvalidSubscriptExpression(index, arraySize) { revert(0, 0) } } default { sstore(arrayOffset, add(arraySize, 1)) } - ret := storageDictionaryOffsetForKey(arrayOffset, index) + ret := flint$storageDictionaryOffsetForKey(arrayOffset, index) } """ static let storageDictionaryOffsetForKey = """ - function storageDictionaryOffsetForKey(dictionaryOffset, key) -> ret { + function flint$storageDictionaryOffsetForKey(dictionaryOffset, key) -> ret { mstore(0, key) mstore(32, dictionaryOffset) ret := sha3(0, 64) } """ + + static let send = + """ + function flint$send(_value, _address) { + let ret := call(gas(), _address, _value, 0, 0, 0, 0) + + if iszero(ret) { + revert(0, 0) + } + } + """ } diff --git a/Sources/Parser/Tokenizer.swift b/Sources/Parser/Tokenizer.swift index 23be79ff..031d21e4 100644 --- a/Sources/Parser/Tokenizer.swift +++ b/Sources/Parser/Tokenizer.swift @@ -15,10 +15,13 @@ public struct Tokenizer { /// The original source code of the Flint program. var sourceCode: String + + var isFromStdlib: Bool - public init(sourceFile: URL) { + public init(sourceFile: URL, isFromStdlib: Bool = false) { self.sourceFile = sourceFile self.sourceCode = try! String(contentsOf: sourceFile) + self.isFromStdlib = isFromStdlib } /// Converts the source code into a list of tokens. @@ -152,7 +155,7 @@ public struct Tokenizer { acc += String(char) } else if inStringLiteral { acc += String(char) - } else if CharacterSet.alphanumerics.contains(char.unicodeScalars.first!) || char == "@" { + } else if identifierChars.contains(char.unicodeScalars.first!) || char == "@" { acc += String(char) } else { if !acc.isEmpty { @@ -194,6 +197,11 @@ public struct Tokenizer { return components.filter { !$0.0.isEmpty } } + /// The set of characters which can be used in identifiers. + var identifierChars: CharacterSet { + return CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "$")) + } + /// Indicates whether two string components can be merged to form a single component. /// For example, `/` and `/` can be merged to form `//`. /// @@ -205,6 +213,6 @@ public struct Tokenizer { /// Creates a source location for the current file. func sourceLocation(line: Int, column: Int, length: Int) -> SourceLocation { - return SourceLocation(line: line, column: column, length: length, file: sourceFile) + return SourceLocation(line: line, column: column, length: length, file: sourceFile, isFromStdlib: isFromStdlib) } } diff --git a/Sources/SemanticAnalyzer/SemanticAnalyzer.swift b/Sources/SemanticAnalyzer/SemanticAnalyzer.swift index 73e637b9..9c180ecc 100644 --- a/Sources/SemanticAnalyzer/SemanticAnalyzer.swift +++ b/Sources/SemanticAnalyzer/SemanticAnalyzer.swift @@ -6,6 +6,7 @@ // import AST +import Foundation /// The `ASTPass` performing semantic analysis. public struct SemanticAnalyzer: ASTPass { @@ -190,11 +191,22 @@ public struct SemanticAnalyzer: ASTPass { return ASTPassResult(element: typeAnnotation, diagnostics: [], passContext: passContext) } + /// The set of characters for identifiers which can only be used in the stdlib. + var stdlibReservedCharacters: CharacterSet { + return CharacterSet(charactersIn: "$") + } + public func process(identifier: Identifier, passContext: ASTPassContext) -> ASTPassResult { var identifier = identifier var passContext = passContext var diagnostics = [Diagnostic]() + // Only allow stdlib files to include special characters, such as '$'. + if !identifier.sourceLocation.isFromStdlib, + let char = identifier.name.first(where: { return stdlibReservedCharacters.contains($0.unicodeScalars.first!) }) { + diagnostics.append(.invalidCharacter(identifier, character: char)) + } + let inFunctionDeclaration = passContext.functionDeclarationContext != nil let inInitializerDeclaration = passContext.initializerDeclarationContext != nil let inFunctionOrInitializer = inFunctionDeclaration || inInitializerDeclaration @@ -479,8 +491,11 @@ public struct SemanticAnalyzer: ASTPass { } public func postProcess(functionCall: FunctionCall, passContext: ASTPassContext) -> ASTPassResult { - // Called once we've visited the function call's arguments. + guard !Environment.isRuntimeFunctionCall(functionCall) else { + return ASTPassResult(element: functionCall, diagnostics: [], passContext: passContext) + } + // Called once we've visited the function call's arguments. var passContext = passContext let environment = passContext.environment! let enclosingType = passContext.enclosingTypeIdentifier!.name @@ -516,7 +531,7 @@ public struct SemanticAnalyzer: ASTPass { } } - case .matchedInitializer(_): + case .matchedInitializer(_), .matchedGlobalFunction(_): break case .failure(let candidates): diff --git a/Sources/SemanticAnalyzer/SemanticError.swift b/Sources/SemanticAnalyzer/SemanticError.swift index 1ce1b512..4393d7c9 100644 --- a/Sources/SemanticAnalyzer/SemanticError.swift +++ b/Sources/SemanticAnalyzer/SemanticError.swift @@ -15,6 +15,10 @@ extension Diagnostic { return Diagnostic(severity: .error, sourceLocation: identifier.sourceLocation, message: "Invalid redeclaration of '\(identifier.name)'", notes: [note]) } + static func invalidCharacter(_ identifier: Identifier, character: Character) -> Diagnostic { + return Diagnostic(severity: .error, sourceLocation: identifier.sourceLocation, message: "Use of invalid character '\(character)' in '\(identifier.name)'") + } + static func noMatchingFunctionForFunctionCall(_ functionCall: FunctionCall, contextCallerCapabilities: [CallerCapability], candidates: [FunctionInformation]) -> Diagnostic { let candidateNotes = candidates.map { candidate -> Diagnostic in diff --git a/Sources/flintc/Compiler.swift b/Sources/flintc/Compiler.swift index 85ecdc4e..d1929d83 100644 --- a/Sources/flintc/Compiler.swift +++ b/Sources/flintc/Compiler.swift @@ -15,12 +15,16 @@ import IRGen /// Runs the different stages of the compiler. struct Compiler { var inputFiles: [URL] + var stdlibFiles: [URL] var outputDirectory: URL var emitBytecode: Bool var shouldVerify: Bool func tokenizeFiles() -> [Token] { - return inputFiles.flatMap { Tokenizer(sourceFile: $0).tokenize() } + let stdlibTokens = StandardLibrary.default.files.flatMap { Tokenizer(sourceFile: $0, isFromStdlib: true).tokenize() } + let userTokens = inputFiles.flatMap { Tokenizer(sourceFile: $0).tokenize() } + + return stdlibTokens + userTokens } func compile() -> CompilationOutcome { diff --git a/Sources/flintc/main.swift b/Sources/flintc/main.swift index 19d56391..2da25644 100644 --- a/Sources/flintc/main.swift +++ b/Sources/flintc/main.swift @@ -15,7 +15,7 @@ func main() { let inputFiles = inputFilePaths.map(URL.init(fileURLWithPath:)) for inputFile in inputFiles { - guard FileManager.default.fileExists(atPath: inputFile.path) else { + guard FileManager.default.fileExists(atPath: inputFile.path), inputFile.pathExtension == "flint" else { exitWithFileNotFoundDiagnostic(file: inputFile) } } @@ -24,7 +24,8 @@ func main() { try! FileManager.default.createDirectory(atPath: outputDirectory.path, withIntermediateDirectories: true, attributes: nil) let compilationOutcome = Compiler( - inputFiles: inputFiles + StandardLibrary.default.files, + inputFiles: inputFiles, + stdlibFiles: StandardLibrary.default.files, outputDirectory: outputDirectory, emitBytecode: emitBytecode, shouldVerify: shouldVerify @@ -48,7 +49,7 @@ func main() { } func exitWithFileNotFoundDiagnostic(file: URL) -> Never { - let diagnostic = Diagnostic(severity: .error, sourceLocation: nil, message: "No such file: '\(file.path)'.") + let diagnostic = Diagnostic(severity: .error, sourceLocation: nil, message: "Invalid file: '\(file.path)'.") print(DiagnosticsFormatter(diagnostics: [diagnostic], compilationContext: nil).rendered()) exit(1) } diff --git a/Tests/BehaviorTests/tests/bank/bank.flint b/Tests/BehaviorTests/tests/bank/bank.flint index 51c03c01..274733eb 100644 --- a/Tests/BehaviorTests/tests/bank/bank.flint +++ b/Tests/BehaviorTests/tests/bank/bank.flint @@ -55,4 +55,9 @@ Bank :: account <- (accounts) { public mutating func transfer(amount: Int, destination: Address) { balances[destination].transfer(&balances[account], amount) } + + public mutating func withdraw(amount: Int) { + let w: Wei = Wei(&balances[account], amount) + send(account, &w) + } } diff --git a/Tests/BehaviorTests/tests/bank/test/test/test.js b/Tests/BehaviorTests/tests/bank/test/test/test.js index 35ab73b0..3253d91c 100644 --- a/Tests/BehaviorTests/tests/bank/test/test/test.js +++ b/Tests/BehaviorTests/tests/bank/test/test/test.js @@ -91,4 +91,63 @@ contract(config.contractName, function(accounts) { t = await instance.getBalance.call({from: accounts[0]}); assert.equal(t.valueOf(), 0); }); + + it("should be possible to withdraw money", async function() { + const instance = await Contract.deployed(); + let t; + + const oldBalance = web3.eth.getBalance(accounts[3]); + + const registerTxInfo = await instance.register({from: accounts[3]}); + const registerTx = await web3.eth.getTransaction(registerTxInfo.tx); + const registerGasCost = registerTx.gasPrice.mul(registerTxInfo.receipt.gasUsed); + + const depositTxInfo = await instance.deposit({from: accounts[3], value: 10000000000000000000}); + const depositTx = await web3.eth.getTransaction(depositTxInfo.tx); + const depositGasCost = depositTx.gasPrice.mul(depositTxInfo.receipt.gasUsed); + + t = await instance.getBalance.call({from: accounts[3]}); + assert.equal(t.valueOf(), 10000000000000000000); + + const withdrawTxInfo = await instance.withdraw(4000000000000000000, {from: accounts[3]}); + const withdrawTx = await web3.eth.getTransaction(withdrawTxInfo.tx); + const withdrawGasCost = withdrawTx.gasPrice.mul(withdrawTxInfo.receipt.gasUsed); + + t = await instance.getBalance.call({from: accounts[3]}); + assert.equal(t.valueOf(), 6000000000000000000); + + const newBalance = web3.eth.getBalance(accounts[3]); + + assert.equal(newBalance.valueOf(), oldBalance.sub(6000000000000000000).sub(registerGasCost).sub(depositGasCost).sub(withdrawGasCost).valueOf()); + }) + + it("should not be possible to withdraw too much money", async function() { + const instance = await Contract.deployed(); + let t; + + const oldBalance = web3.eth.getBalance(accounts[4]); + + const registerTxInfo = await instance.register({from: accounts[4]}); + const registerTx = await web3.eth.getTransaction(registerTxInfo.tx); + const registerGasCost = registerTx.gasPrice.mul(registerTxInfo.receipt.gasUsed); + + const depositTxInfo = await instance.deposit({from: accounts[4], value: 10000000000000000000}); + const depositTx = await web3.eth.getTransaction(depositTxInfo.tx); + const depositGasCost = depositTx.gasPrice.mul(depositTxInfo.receipt.gasUsed); + + t = await instance.getBalance.call({from: accounts[4]}); + assert.equal(t.valueOf(), 10000000000000000000); + + let withdrawGasCost = 0 + + const withdrawTxInfo = await instance.withdraw(20000000000000000000, {from: accounts[4]}); + const withdrawTx = await web3.eth.getTransaction(withdrawTxInfo.tx); + withdrawGasCost = withdrawTx.gasPrice.mul(withdrawTxInfo.receipt.gasUsed); + t = await instance.getBalance.call({from: accounts[4]}); + assert.equal(t.valueOf(), 10000000000000000000); + + const newBalance = web3.eth.getBalance(accounts[4]); + + assert.equal(newBalance.valueOf(), oldBalance.sub(10000000000000000000).sub(registerGasCost).sub(depositGasCost).sub(withdrawGasCost).valueOf()); + }) }); diff --git a/Tests/SemanticTests/invalid_identifiers.flint b/Tests/SemanticTests/invalid_identifiers.flint new file mode 100644 index 00000000..a907c549 --- /dev/null +++ b/Tests/SemanticTests/invalid_identifiers.flint @@ -0,0 +1,7 @@ +// RUN: %flintc %s --verify + +struct $ { // expected-error {{Use of invalid character '$' in '$'}} + func foo$() { // expected-error {{Use of invalid character '$' in 'foo$'}} + let a$ap: Int = 0 // expected-error {{Use of invalid character '$' in 'a$ap'}} + } +} diff --git a/docs/grammar.txt b/docs/grammar.txt index cd29a047..f7e95f3c 100644 --- a/docs/grammar.txt +++ b/docs/grammar.txt @@ -44,7 +44,7 @@ caller-capability-binding -> identifier [<-] // Identifier -identifier -> [a-zA-Z] . [a-zA-Z0-9]* +identifier -> [a-zA-Z] . [a-zA-Z0-9$]* // Function and initializer declarations diff --git a/examples/valid/bank.flint b/examples/valid/bank.flint index ef13d506..9a3d3d14 100644 --- a/examples/valid/bank.flint +++ b/examples/valid/bank.flint @@ -1,3 +1,4 @@ +// Contract declarations contain only their state properties. contract Bank { var manager: Address var balances: [Address: Wei] = [:] @@ -8,12 +9,13 @@ contract Bank { var didCompleteTransfer: Event } +// The functions in this block can be called by any user. Bank :: account <- (any) { - public init(manager: Address) { self.manager = manager } + // Returns the manager's address. public mutating func register() { accounts[lastIndex] = account lastIndex += 1 @@ -25,11 +27,16 @@ Bank :: account <- (any) { @payable public mutating func donate(implicit value: Wei) { + // This will transfer the funds into totalDonations. totalDonations.transfer(&value) } } +// Only the manager can call these functions. Bank :: (manager) { + + // This function needs to be declared "mutating" as its body mutates + // the contract's state. public mutating func freeDeposit(account: Address, amount: Int) { var w: Wei = Wei(amount) balances[account].transfer(&w) @@ -39,17 +46,34 @@ Bank :: (manager) { balances[account] = Wei(0) } + // This function is non-mutating. public func getDonations() -> Int { return totalDonations.getRawValue() } } +// Any user in accounts can call these functions. +// The matching user's address is bound to the variable account. Bank :: account <- (accounts) { public func getBalance() -> Int { return balances[account].getRawValue() } public mutating func transfer(amount: Int, destination: Address) { + // Transfer Wei from one account to another. The balances of the + // originator and the destination are updated atomically. + // Crashes if balances[account] doesn't have enough Wei. balances[destination].transfer(&balances[account], amount) + + // Emit the Ethereum event. + didCompleteTransfer(account, destination, amount) + } + + public mutating func withdraw(amount: Int) { + // Transfer some Wei from balances[account] into a local variable. + let w: Wei = Wei(&balances[account], amount) + + // Send the amount back to the Ethereum user. + send(account, &w) } } diff --git a/examples/valid/wallet.flint b/examples/valid/wallet.flint index bce567bd..35434d69 100644 --- a/examples/valid/wallet.flint +++ b/examples/valid/wallet.flint @@ -10,16 +10,19 @@ Wallet :: caller <- (any) { @payable public mutating func deposit(implicit value: Wei) { + // Record the Wei received into the contents state property. contents.transfer(&value) } } Wallet :: (owner) { public mutating func withdraw(value: Int) { - if value <= contents.getRawValue() { - var w: Wei = Wei(0) - w.transfer(&contents, value) - } + // Transfer an amount of Wei into a local variable. This + // removes Wei from the contents state property. + var w: Wei = Wei(&contents, value) + + // Send Wei to the owner's Ethereum address. + send(owner, &w) } public func getContents() -> Int { diff --git a/stdlib/Global.flint b/stdlib/Global.flint new file mode 100644 index 00000000..1ebed424 --- /dev/null +++ b/stdlib/Global.flint @@ -0,0 +1,6 @@ +struct Flint$Global { + func send(address: Address, value: inout Wei) { + let w: Wei = Wei(&value) + flint$send(w.getRawValue(), address) + } +} diff --git a/stdlib/Wei.flint b/stdlib/Wei.flint index 12bf907c..28058012 100644 --- a/stdlib/Wei.flint +++ b/stdlib/Wei.flint @@ -8,10 +8,16 @@ struct Wei { init(source: inout Wei, amount: Int) { if source.getRawValue() >= amount { source.rawValue -= amount - rawValue += amount + rawValue = amount } } + init(source: inout Wei) { + let value: Int = source.getRawValue() + source.rawValue -= value + rawValue = value + } + mutating func transfer(source: inout Wei, amount: Int) { if source.getRawValue() >= amount { source.rawValue -= amount