├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── .vscode ├── settings.json └── tasks.json ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── TypologyCLI │ └── main.swift └── TypologyCore │ ├── AST │ ├── AST.swift │ ├── ASTError.swift │ ├── Declaration │ │ ├── Accessor.swift │ │ ├── Attribute.swift │ │ ├── Binding.swift │ │ ├── Function.swift │ │ ├── Modifier.swift │ │ └── PatternBinding.swift │ ├── Expr.swift │ ├── Literal.swift │ ├── Pattern │ │ ├── IdentifierPattern.swift │ │ ├── Pattern.swift │ │ └── TuplePattern.swift │ ├── Statement │ │ └── Return.swift │ ├── Type.swift │ └── TypeAnnotation.swift │ ├── ConstraintSystem.swift │ ├── Diagnostic │ ├── Diagnose.swift │ ├── Diagnostic.swift │ ├── DiagnosticConsumer.swift │ ├── DiagnosticConsumers │ │ └── ConsoleDiagnosticConsumer.swift │ ├── DiagnosticEngine.swift │ └── Helpers.swift │ ├── DiagnosticError.swift │ ├── Protocol.swift │ ├── Scheme.swift │ ├── Solver.swift │ ├── Substitution.swift │ └── TypeError.swift ├── Tests ├── LinuxMain.swift └── TypologyTests │ ├── ASTTests.swift │ ├── Benchmarks.swift │ ├── DiagnosticTests.swift │ └── InferenceTests.swift ├── ValidationTests └── AST │ ├── Negative.swift │ └── Positive.swift ├── ci.sh └── codecov.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MaxDesiatov # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | macos-bigsur: 16 | runs-on: macos-11 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Build with latest Xcode 22 | run: ./ci.sh 23 | env: 24 | CODECOV_TOKEN: ${{ secrets.codecovToken }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | coverage.txt 22 | 23 | # Bundler 24 | .bundle 25 | 26 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 27 | # Carthage/Checkouts 28 | 29 | Carthage/Build 30 | 31 | # We recommend against adding the Pods directory to your .gitignore. However 32 | # you should judge for yourself, the pros and cons are mentioned at: 33 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 34 | # 35 | # Note: if you ignore the Pods directory, make sure to uncomment 36 | # `pod install` in .travis.yml 37 | # 38 | Pods/ 39 | 40 | # SwiftPM 41 | .build 42 | .swiftpm 43 | 44 | # Jazzy 45 | docs -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --indent 2 2 | --indentcase false 3 | --trimwhitespace always 4 | --ranges nospace 5 | --empty tuple 6 | --operatorfunc nospace 7 | --ifdef noindent 8 | --stripunusedargs closure-only 9 | --disable andOperator 10 | --swiftversion 5.0 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | - identifier_name 4 | - void_return 5 | - operator_whitespace 6 | - nesting 7 | - cyclomatic_complexity 8 | 9 | line_length: 100 10 | 11 | 12 | function_body_length: 13 | - 50 14 | 15 | included: 16 | - Sources 17 | - Tests -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "swift test", 8 | "type": "shell", 9 | "command": "swift test" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Max Desiatov and Typology Contributors. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Rainbow", 6 | "repositoryURL": "https://github.com/onevcat/Rainbow", 7 | "state": { 8 | "branch": null, 9 | "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", 10 | "version": "3.1.5" 11 | } 12 | }, 13 | { 14 | "package": "SwiftSyntax", 15 | "repositoryURL": "https://github.com/apple/swift-syntax.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "2fff9fc25cdc059379b6bd309377cfab45d8520c", 19 | "version": "0.50400.0" 20 | } 21 | }, 22 | { 23 | "package": "SwiftCLI", 24 | "repositoryURL": "https://github.com/jakeheis/SwiftCLI", 25 | "state": { 26 | "branch": null, 27 | "revision": "df4d5b3ec2c1421a58d71010ff43528db5b19e04", 28 | "version": "5.3.3" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Typology", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "TypologyCore", 12 | targets: ["TypologyCore"] 13 | ), 14 | .executable(name: "typology", targets: ["TypologyCLI"]), 15 | ], 16 | dependencies: [ 17 | .package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git", .exact("0.50400.0")), 18 | .package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.0.0"), 19 | .package(url: "https://github.com/onevcat/Rainbow", from: "3.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "TypologyCore", 26 | dependencies: ["SwiftSyntax", "Rainbow", "SwiftCLI"] 27 | ), 28 | .target( 29 | name: "TypologyCLI", 30 | dependencies: ["SwiftCLI", "TypologyCore"] 31 | ), 32 | .testTarget( 33 | name: "TypologyTests", 34 | dependencies: ["TypologyCore"] 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/) 2 | 3 | # Typology 4 | 5 | ![CI status](https://github.com/MaxDesiatov/Typology/workflows/CI/badge.svg?branch=main) 6 | [![Coverage](https://img.shields.io/codecov/c/github/MaxDesiatov/Typology/main.svg?style=flat)](https://codecov.io/gh/maxdesiatov/Typology) 7 | 8 | Typology is a work in progress attempt to implement type checking of Swift in Swift itself. 9 | Currently it uses [SwiftSyntax](https://github.com/apple/swift-syntax) as a parser, but is ready 10 | to switch to other pure Swift parsers in the future when any are available. 11 | 12 | ## Goals 13 | 14 | - **Education**: understanding how type checking can be implemented in a Swift 15 | compiler 16 | - **User Experience**: finding the best way to report type errors and to improve 17 | related developer tools 18 | - **Research and Experimentation**: prototyping advanced features that could be 19 | fully developed within Swift's type system. 20 | 21 | ## How does it work? 22 | 23 | Same as [the type checker in Apple's Swift 24 | compiler](https://github.com/apple/swift/blob/master/docs/TypeChecker.rst), 25 | Typology relies on the fact that you can express [type 26 | systems](https://en.m.wikipedia.org/wiki/Hindley–Milner_type_system) with a set of constraints 27 | on types that are resolved through [unification](). 28 | 29 | ## See also 30 | 31 | ### Type systems and type checkers 32 | 33 | - [Apple's Swift Compiler Type Checker Design and Implementation](https://github.com/apple/swift/blob/master/docs/TypeChecker.rst) by multiple contributors 34 | - [A Type System From Scratch](https://www.youtube.com/watch?v=IbjoA5xVUq0) by [@CodaFi](https://github.com/CodaFi) 35 | - [Write You a Haskell: Hindley-Milner Inference](http://dev.stephendiehl.com/fun/006_hindley_milner.html) by [@sdiehl](https://github.com/sdiehl) 36 | - [Typing Haskell in Haskell](http://web.cecs.pdx.edu/~mpj/thih/TypingHaskellInHaskell.html) by [Mark P Jones](https://web.cecs.pdx.edu/~mpj/) 37 | - [“What part of Hindley-Milner do you not understand?”](https://stackoverflow.com/questions/12532552/what-part-of-hindley-milner-do-you-not-understand) question and answers on StackOverflow 38 | - [So you want to write a type checker...](http://languagengine.co/blog/so-you-want-to-write-a-type-checker/) by [@psygnisfive](https://github.com/psygnisfive) 39 | - [Exponential time complexity in the Swift type checker](https://www.cocoawithlove.com/blog/2016/07/12/type-checker-issues.html) by [@mattgallagher](https://github.com/mattgallagher) 40 | - [A Swift Playground containing Martin Grabmüller's "Algorithm W Step-by-Step"](https://gist.github.com/CodaFi/ca35a0c22fbd96eca505b5df45f2509e) by [@CodaFi](https://github.com/CodaFi) 41 | 42 | ### Error reporting 43 | 44 | - [Compiler Errors for Humans](https://elm-lang.org/blog/compiler-errors-for-humans) on [Elm blog](https://elm-lang.org/blog) 45 | - [Shape of errors to come in Rust compiler](https://blog.rust-lang.org/2016/08/10/Shape-of-errors-to-come.html) by [Jonathan Turner](https://github.com/jonathandturner) 46 | 47 | ### Optimizing type checker's performance for large projects 48 | 49 | - [Apple's Swift Compiler Dependency Analysis](https://github.com/apple/swift/blob/master/docs/DependencyAnalysis.rst) by multiple contributors 50 | -------------------------------------------------------------------------------- /Sources/TypologyCLI/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypologyCLI.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/8/19. 6 | // 7 | 8 | import Foundation 9 | import SwiftCLI 10 | import TypologyCore 11 | 12 | let diagnose = CLI(singleCommand: Diagnose()) 13 | diagnose.goAndExit() 14 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/AST.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AST.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 19/04/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | typealias Identifier = String 12 | typealias Operator = String 13 | 14 | struct File { 15 | let statements: [Statement] 16 | } 17 | 18 | protocol Location { 19 | var range: SourceRange { get } 20 | } 21 | 22 | protocol Statement: Location {} 23 | 24 | struct CaseDecl {} 25 | 26 | struct EnumDecl { 27 | let cases: [CaseDecl] 28 | } 29 | 30 | struct ImportDecl { 31 | let path: [String] 32 | } 33 | 34 | struct Module { 35 | let files: [File] 36 | } 37 | 38 | struct Target { 39 | let dependencies: [Module] 40 | let main: Module 41 | } 42 | 43 | extension Syntax { 44 | func toStatement(_ converter: SourceLocationConverter) throws -> [Statement] { 45 | if let syntax = VariableDeclSyntax(self) { 46 | return try [BindingDecl(syntax, converter)] 47 | } else if let syntax = SequenceExprSyntax(self) { 48 | return try syntax.elements.map { try ExprNode($0, converter) } 49 | } else if let syntax = FunctionDeclSyntax(self) { 50 | return try [FunctionDecl(syntax, converter)] 51 | } else if let syntax = ReturnStmtSyntax(self) { 52 | return try [ReturnStmt(syntax, converter)] 53 | } else if let syntax = CodeBlockItemSyntax(self) { 54 | return try syntax.item.toStatement(converter) 55 | } else if let syntax = FunctionCallExprSyntax(self) { 56 | return try [ExprNode( 57 | expr: Expr.application( 58 | Expr(syntax.calledExpression, converter), 59 | syntax.argumentList.map { try Expr($0.expression, converter) } 60 | ), 61 | range: syntax.sourceRange(converter: converter) 62 | )] 63 | } else { 64 | throw ASTError(_syntaxNode, .unknownSyntax, converter) 65 | } 66 | } 67 | } 68 | 69 | extension Array where Element == Statement { 70 | init(_ syntax: CodeBlockItemListSyntax, _ converter: SourceLocationConverter) throws { 71 | self = try syntax.flatMap { try $0.item.toStatement(converter) } 72 | } 73 | } 74 | 75 | extension File { 76 | init(_ syntax: SourceFileSyntax, _ converter: SourceLocationConverter) throws { 77 | statements = try .init(syntax.statements, converter) 78 | } 79 | } 80 | 81 | extension File { 82 | public init(path: String) throws { 83 | let url = URL(fileURLWithPath: path) 84 | let syntax = try SyntaxParser.parse(url) 85 | try self.init(syntax, SourceLocationConverter(file: path, tree: syntax)) 86 | } 87 | } 88 | 89 | extension String { 90 | func parseAST() throws -> File { 91 | let url = URL(fileURLWithPath: NSTemporaryDirectory()) 92 | .appendingPathComponent("typology.swift") 93 | 94 | try write(toFile: url.path, atomically: true, encoding: .utf8) 95 | 96 | let syntax = try SyntaxParser.parse(url) 97 | try FileManager.default.removeItem(at: url) 98 | return try File(syntax, SourceLocationConverter(file: url.path, tree: syntax)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/ASTError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASTError.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct ASTError: DiagnosticError { 12 | enum Value { 13 | case unknownSyntax 14 | } 15 | 16 | let range: SourceRange 17 | let value: Value 18 | } 19 | 20 | extension ASTError { 21 | init(_ syntax: Syntax, _ value: Value, _ converter: SourceLocationConverter) { 22 | self.init(range: syntax.sourceRange(converter: converter), value: value) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/Accessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Accessor.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct AccessorDecl: Location { 12 | enum Kind: String { 13 | case get 14 | case set 15 | } 16 | 17 | let body: [Statement] 18 | let kind: Kind 19 | 20 | let range: SourceRange 21 | } 22 | 23 | extension AccessorDecl { 24 | init(_ syntax: AccessorDeclSyntax, _ converter: SourceLocationConverter) throws { 25 | guard let kind = Kind(rawValue: syntax.accessorKind.text) else { 26 | throw ASTError(syntax.accessorKind._syntaxNode, .unknownSyntax, converter) 27 | } 28 | 29 | let statements = syntax.body?.statements 30 | 31 | try self.init( 32 | body: statements.map { try [Statement]($0, converter) } ?? [], 33 | kind: kind, 34 | range: syntax.sourceRange(converter: converter) 35 | ) 36 | } 37 | } 38 | 39 | extension Array where Element == AccessorDecl { 40 | init(_ syntax: SyntaxProtocol?, _ converter: SourceLocationConverter) throws { 41 | if let block = syntax.flatMap({ CodeBlockSyntax($0._syntaxNode) }) { 42 | self = try [AccessorDecl( 43 | body: [Statement](block.statements, converter), 44 | kind: .get, 45 | range: block.sourceRange(converter: converter) 46 | )] 47 | } else if let block = syntax.flatMap({ AccessorBlockSyntax($0._syntaxNode) }) { 48 | self = try block.accessors.map { try AccessorDecl($0, converter) } 49 | } else if let syntax = syntax { 50 | throw ASTError(syntax._syntaxNode, .unknownSyntax, converter) 51 | } else { 52 | self = [] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/Attribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attribute.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | /// Declaration attributes such as @objc 12 | struct Attribute: Location { 13 | let name: String 14 | let argument: String? 15 | 16 | let range: SourceRange 17 | } 18 | 19 | extension Attribute { 20 | init(_ syntax: AttributeSyntax, _ converter: SourceLocationConverter) { 21 | self.init( 22 | name: syntax.attributeName.text, 23 | argument: (syntax.argument as? ObjCSelectorSyntax)?.description, 24 | range: syntax.sourceRange(converter: converter) 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct BindingDecl: Statement { 12 | let attributes: [Attribute] 13 | let bindings: [PatternBinding] 14 | let isConstant: Bool 15 | let modifiers: [Modifier] 16 | 17 | let range: SourceRange 18 | } 19 | 20 | extension BindingDecl { 21 | init(_ syntax: VariableDeclSyntax, _ converter: SourceLocationConverter) throws { 22 | try self.init( 23 | attributes: syntax.attributes?.compactMap { $0 as? AttributeSyntax }.map { 24 | Attribute($0, converter) 25 | } ?? [], 26 | bindings: syntax.bindings.map { try PatternBinding($0, converter) }, 27 | isConstant: syntax.letOrVarKeyword.text == "let", 28 | modifiers: syntax.modifiers?.map { Modifier($0, converter) } ?? [], 29 | range: syntax.sourceRange(converter: converter) 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/Function.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Function.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct FunctionDecl: Statement { 12 | let genericParameters: [TypeVariable] 13 | let parameters: [(externalName: String?, internalName: String?, typeAnnotation: TypeAnnotation)] 14 | let statements: [Statement] 15 | let returns: TypeAnnotation? 16 | 17 | let range: SourceRange 18 | 19 | var scheme: Scheme { 20 | return Scheme( 21 | parameters.map { $0.typeAnnotation.type } --> (returns?.type ?? .tuple([])), 22 | variables: genericParameters 23 | ) 24 | } 25 | } 26 | 27 | extension FunctionDecl { 28 | init(_ syntax: FunctionDeclSyntax, _ converter: SourceLocationConverter) throws { 29 | let returns = syntax.signature.output?.returnType 30 | let body = syntax.body?.statements 31 | 32 | try self.init( 33 | genericParameters: syntax.genericParameterClause? 34 | .genericParameterList.map { 35 | TypeVariable(value: $0.name.text) 36 | } ?? [], 37 | parameters: syntax.signature.input.parameterList 38 | .compactMap { parameter -> (String?, String?, TypeAnnotation)? in 39 | guard let type = parameter.type else { return nil } 40 | return try ( 41 | parameter.firstName?.text, 42 | parameter.secondName?.text, 43 | TypeAnnotation(type, converter) 44 | ) 45 | }, 46 | statements: body.flatMap { try [Statement]($0, converter) } ?? [], 47 | returns: returns.map { 48 | try TypeAnnotation($0, converter) 49 | }, 50 | range: syntax.sourceRange(converter: converter) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/Modifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | /// Declaration modifiers such as `private`, `public`, `dynamic` etc. 12 | struct Modifier: Location { 13 | /// The main name of the modifier 14 | let name: String 15 | 16 | /// Used to specify granular access, such as `private(set)` 17 | let detail: String? 18 | 19 | let range: SourceRange 20 | } 21 | 22 | extension Modifier { 23 | init(_ syntax: DeclModifierSyntax, _ converter: SourceLocationConverter) { 24 | self.init( 25 | name: syntax.name.text, 26 | detail: syntax.detail?.text, 27 | range: syntax.sourceRange(converter: converter) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Declaration/PatternBinding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatternBinding.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct PatternBinding: Location { 12 | let accessors: [AccessorDecl] 13 | let pattern: Pattern 14 | let typeAnnotation: TypeAnnotation? 15 | 16 | let range: SourceRange 17 | } 18 | 19 | extension PatternBinding { 20 | init(_ syntax: PatternBindingSyntax, _ converter: SourceLocationConverter) throws { 21 | try self.init( 22 | accessors: .init(syntax.accessor, converter), 23 | pattern: syntax.pattern.toPattern(converter), 24 | typeAnnotation: syntax.typeAnnotation.map { 25 | try TypeAnnotation($0, converter) 26 | }, 27 | range: syntax.sourceRange(converter: converter) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Expr.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Expr.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 12/05/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct ExprNode: Statement { 12 | let expr: Expr 13 | let range: SourceRange 14 | } 15 | 16 | extension ExprNode { 17 | init(_ syntax: ExprSyntax, _ converter: SourceLocationConverter) throws { 18 | self.init( 19 | expr: try Expr(syntax, converter), 20 | range: syntax.sourceRange(converter: converter) 21 | ) 22 | } 23 | } 24 | 25 | indirect enum Expr { 26 | case identifier(Identifier) 27 | case application(Expr, [Expr]) 28 | case lambda([Identifier], Expr) 29 | case literal(Literal) 30 | case ternary(Expr, Expr, Expr) 31 | case member(Expr, Identifier) 32 | case namedTuple([(Identifier?, Expr)]) 33 | 34 | static func tuple(_ expressions: [Expr]) -> Expr { 35 | return .namedTuple(expressions.enumerated().map { 36 | (nil, $0.1) 37 | }) 38 | } 39 | 40 | func infer( 41 | environment: Environment = [:], 42 | members: Members = [:] 43 | ) throws -> Type { 44 | var system = ConstraintSystem( 45 | environment, 46 | members: members 47 | ) 48 | let type = try system.infer(self) 49 | 50 | let solver = Solver( 51 | substitution: [:], 52 | system: system 53 | ) 54 | return try type.apply(solver.solve()) 55 | } 56 | } 57 | 58 | extension Expr: ExpressibleByStringLiteral { 59 | init(stringLiteral value: String) { 60 | self = .identifier(value) 61 | } 62 | } 63 | 64 | extension Expr { 65 | init(_ expr: ExprSyntaxProtocol, _ converter: SourceLocationConverter) throws { 66 | if let identifier = IdentifierExprSyntax(expr._syntaxNode) { 67 | self = .identifier(identifier.identifier.text) 68 | 69 | } else if let ternary = TernaryExprSyntax(expr._syntaxNode) { 70 | self = try .ternary( 71 | Expr(ternary.conditionExpression, converter), 72 | Expr(ternary.firstChoice, converter), 73 | Expr(ternary.secondChoice, converter) 74 | ) 75 | 76 | } else if let literal = IntegerLiteralExprSyntax(expr._syntaxNode) { 77 | guard let int = Int(literal.digits.text) else { 78 | throw ASTError(expr._syntaxNode, .unknownSyntax, converter) 79 | } 80 | self = .literal(.integer(int)) 81 | 82 | } else if let literal = FloatLiteralExprSyntax(expr._syntaxNode) { 83 | guard let double = Double(literal.floatingDigits.text) else { 84 | throw ASTError(expr._syntaxNode, .unknownSyntax, converter) 85 | } 86 | self = .literal(.floating(double)) 87 | 88 | } else if let literal = BooleanLiteralExprSyntax(expr._syntaxNode) { 89 | guard let bool = Bool(literal.booleanLiteral.text) else { 90 | throw ASTError(expr._syntaxNode, .unknownSyntax, converter) 91 | } 92 | self = .literal(.bool(bool)) 93 | 94 | } else if let literal = StringLiteralExprSyntax(expr._syntaxNode) { 95 | self = .literal(.string(literal.segments.compactMap { 96 | ($0 as? StringSegmentSyntax)?.content.text 97 | }.joined())) 98 | 99 | } else { 100 | throw ASTError(expr._syntaxNode, .unknownSyntax, converter) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Literal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Literal.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 25/05/2019. 6 | // 7 | 8 | enum Literal { 9 | case integer(Int) 10 | case floating(Double) 11 | case bool(Bool) 12 | case string(String) 13 | 14 | var defaultType: Type { 15 | switch self { 16 | case .integer: 17 | return .int 18 | case .floating: 19 | return .double 20 | case .bool: 21 | return .bool 22 | case .string: 23 | return .string 24 | } 25 | } 26 | } 27 | 28 | extension Literal: ExpressibleByStringLiteral { 29 | init(stringLiteral value: String) { 30 | self = .string(value) 31 | } 32 | } 33 | 34 | extension Literal: ExpressibleByIntegerLiteral { 35 | init(integerLiteral value: IntegerLiteralType) { 36 | self = .integer(value) 37 | } 38 | } 39 | 40 | extension Literal: ExpressibleByBooleanLiteral { 41 | init(booleanLiteral value: BooleanLiteralType) { 42 | self = .bool(value) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Pattern/IdentifierPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifierPattern.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct IdentifierPattern: Pattern { 12 | let identifier: String 13 | 14 | let range: SourceRange 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Pattern/Pattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pattern.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | protocol Pattern: Location {} 12 | 13 | extension PatternSyntaxProtocol { 14 | func toPattern(_ converter: SourceLocationConverter) throws -> Pattern { 15 | if let syntax = TuplePatternSyntax(_syntaxNode) { 16 | return try TuplePattern( 17 | elements: syntax.elements.map { try TuplePatternElement($0, converter) }, 18 | range: syntax.sourceRange(converter: converter) 19 | ) 20 | } else if let syntax = IdentifierPatternSyntax(_syntaxNode) { 21 | return IdentifierPattern( 22 | identifier: syntax.identifier.text, 23 | range: syntax.sourceRange(converter: converter) 24 | ) 25 | } else { 26 | throw ASTError(_syntaxNode, .unknownSyntax, converter) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Pattern/TuplePattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TuplePattern.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct TuplePattern: Pattern { 12 | let elements: [TuplePatternElement] 13 | let range: SourceRange 14 | } 15 | 16 | struct TuplePatternElement { 17 | let name: TuplePatternName? 18 | let pattern: Pattern 19 | } 20 | 21 | extension TuplePatternElement { 22 | init(_ syntax: TuplePatternElementSyntax, _ converter: SourceLocationConverter) throws { 23 | try self.init( 24 | name: syntax.labelName.map { 25 | .init(value: $0.text, range: $0.sourceRange(converter: converter)) 26 | }, 27 | pattern: syntax.pattern.toPattern(converter) 28 | ) 29 | } 30 | } 31 | 32 | struct TuplePatternName: Location { 33 | let value: String 34 | let range: SourceRange 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Statement/Return.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Return.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct ReturnStmt: Statement { 12 | let expr: Expr? 13 | 14 | let range: SourceRange 15 | } 16 | 17 | extension ReturnStmt { 18 | init(_ syntax: ReturnStmtSyntax, _ converter: SourceLocationConverter) throws { 19 | try self.init( 20 | expr: syntax.expression.map { try Expr($0, converter) }, 21 | range: syntax.sourceRange(converter: converter) 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/Type.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Type.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/04/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct TypeVariable: Hashable { 12 | let value: String 13 | } 14 | 15 | extension TypeVariable: ExpressibleByStringLiteral { 16 | init(stringLiteral value: String) { 17 | self.value = value 18 | } 19 | } 20 | 21 | extension TypeVariable: ExpressibleByStringInterpolation { 22 | init(stringInterpolation: DefaultStringInterpolation) { 23 | value = stringInterpolation.description 24 | } 25 | } 26 | 27 | struct TypeIdentifier: Hashable { 28 | let value: String 29 | } 30 | 31 | extension TypeIdentifier: ExpressibleByStringLiteral { 32 | init(stringLiteral value: String) { 33 | self.value = value 34 | } 35 | } 36 | 37 | enum Type { 38 | /** A type constructor is an abstraction on which generics system is built. 39 | It is a "type function", which takes other types as arguments and returns 40 | a new type. `Type.constructor("Int", [])` represents an `Int` type, while 41 | `Type.constructor("Dictionary", ["String", "Int"])` represents a 42 | `[String: Int]` type (`Dictionary` when desugared). 43 | 44 | Examples: 45 | 46 | * `Int` and `String` are nullary type constructors, they 47 | don't take any type arguments and already represent a ready to use type. 48 | 49 | * `Array` is a unary type constructor, which takes a single type argument: 50 | a type of its element. `Array` is a type constructor applied to the 51 | `Int` argument and produces a type for an array of integers. 52 | 53 | * `Dictionary` is a binary type constructor with two type arguments 54 | for the key type and value type respectively. `Dictionary` is 55 | a binary type constructor applied to produce a dictionary from `String` keys 56 | to `Int` values. 57 | 58 | * `->` is a binary type constructor with two type arguments for the argument 59 | and for the return value of a function. It is written as a binary operator 60 | to produce a type like `ArgsType -> ReturnType`. Note that we use a separate 61 | enum `case arrow(Type, Type)` for this due to a special treatment of function 62 | types in the type checker. 63 | 64 | Since type constructors are expected to be applied to a correct number of 65 | type arguments, it's useful to introduce a notion of 66 | ["kinds"](https://en.wikipedia.org/wiki/Kind_(type_theory)). At compile time 67 | all values have types that help us verify correctness of expressions, types 68 | have kinds that allow us to verify type constructor applications. Note that 69 | this is different from metatypes in Swift. Metatypes are still types, 70 | and metatype values can be stored as constants/variables and operated on at 71 | run time. Kinds are completely separate from this, and are a purely 72 | compile time concept that helps us to reason about generic types. 73 | 74 | All nullary type constructors have a kind `*`, you can think of `*` as a 75 | "placeholder" for a type. If we use `::` to represent "has a kind" 76 | declarations, we could declare that `Int :: *` or `String :: *`. Unary type 77 | constructors have a kind `<*> ~> *`, where `~>` is a binary operator for a 78 | "type function", and so `Array :: <*> ~> *`, while `Array :: *`. A 79 | binary type constructor has a kind `<*, *> ~> *`, therefore 80 | `Dictionary :: <*, *> ~> *` and `Dictionary :: *`. 81 | 82 | In Typology's documentation we adopt a notation for kinds similar to the one 83 | used in the widely available content on the type theory, but slightly 84 | modified for Swift. Specifically, type constructors in Swift don't use 85 | [currying](https://en.wikipedia.org/wiki/Currying), and Typology uses `~>` 86 | for type functions on the level of kinds, compared to `->` for value 87 | functions used on the level of types. Compare this to the type theory papers, 88 | which commonly use `->` on both levels. We find the common approach confusing 89 | in the context of Swift type system. 90 | */ 91 | case constructor(TypeIdentifier, [Type]) 92 | 93 | /** A free type variable that can be used as a temporary placeholder type 94 | during type inference, or as a type variable in a generic declaration as a 95 | part of a `Scheme` value. 96 | */ 97 | case variable(TypeVariable) 98 | 99 | /** Binary type operator `->` representing function types. 100 | */ 101 | indirect case arrow([Type], Type) 102 | 103 | /** Tuple types, where each element of an associated array is a corresponding 104 | type of the tuple's element. 105 | 106 | ``` 107 | (Int, String, Bool) 108 | ``` 109 | 110 | is represented in Typology as 111 | 112 | ``` 113 | Type.tuple([.int, .string, .bool]) 114 | ``` 115 | */ 116 | case namedTuple([(Identifier?, Type)]) 117 | 118 | static func tuple(_ types: [Type]) -> Type { 119 | return .namedTuple(types.enumerated().map { 120 | (nil, $0.1) 121 | }) 122 | } 123 | 124 | static let bool = Type.constructor("Bool", []) 125 | static let string = Type.constructor("String", []) 126 | static let double = Type.constructor("Double", []) 127 | static let int = Type.constructor("Int", []) 128 | } 129 | 130 | infix operator --> 131 | 132 | /// A shorthand version of `Type.arrow` 133 | func -->(arguments: [Type], returned: Type) -> Type { 134 | return Type.arrow(arguments, returned) 135 | } 136 | 137 | /// A shorthand version of `Type.arrow` for single argument functions 138 | func -->(argument: Type, returned: Type) -> Type { 139 | return Type.arrow([argument], returned) 140 | } 141 | 142 | extension Type: Equatable { 143 | static func ==(lhs: Type, rhs: Type) -> Bool { 144 | switch (lhs, rhs) { 145 | case let (.constructor(id1, t1), .constructor(id2, t2)): 146 | return id1 == id2 && t1 == t2 147 | case let (.variable(v1), .variable(v2)): 148 | return v1 == v2 149 | case let (.arrow(i1, o1), .arrow(i2, o2)): 150 | return i1 == i2 && o1 == o2 151 | case let (.namedTuple(t1), .namedTuple(t2)): 152 | return zip(t1, t2).allSatisfy { 153 | $0.0 == $1.0 && $0.1 == $1.1 154 | } 155 | default: 156 | return false 157 | } 158 | } 159 | } 160 | 161 | extension Type { 162 | init(_ type: TypeSyntaxProtocol, _ converter: SourceLocationConverter) throws { 163 | if let tuple = TupleTypeSyntax(type._syntaxNode) { 164 | self = try .tuple(tuple.elements.map { try Type($0.type, converter) }) 165 | } else if let identifier = SimpleTypeIdentifierSyntax(type._syntaxNode) { 166 | self = .constructor(TypeIdentifier(value: identifier.name.text), []) 167 | } else if let array = ArrayTypeSyntax(type._syntaxNode) { 168 | self = try .constructor("Array", [Type(array.elementType, converter)]) 169 | } else { 170 | throw ASTError(type._syntaxNode, .unknownSyntax, converter) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/TypologyCore/AST/TypeAnnotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeAnnotation.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | struct TypeAnnotation: Location { 12 | let type: Type 13 | 14 | let range: SourceRange 15 | } 16 | 17 | extension TypeAnnotation { 18 | init(_ syntax: TypeAnnotationSyntax, _ converter: SourceLocationConverter) throws { 19 | self.init( 20 | type: try Type(syntax.type, converter), 21 | range: syntax.sourceRange(converter: converter) 22 | ) 23 | } 24 | 25 | init(_ syntax: TypeSyntax, _ converter: SourceLocationConverter) throws { 26 | self.init( 27 | type: try Type(syntax, converter), 28 | range: syntax.sourceRange(converter: converter) 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/TypologyCore/ConstraintSystem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstraintSystem.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/04/2019. 6 | // 7 | 8 | enum Constraint { 9 | /// Type equality constraint 10 | case equal(Type, Type) 11 | 12 | /** Constraint used to resolve function overloads. This constraint is valid 13 | if `assumption` can be unified with any of the types passed as 14 | `alternatives`. 15 | */ 16 | case disjunction(Identifier, assumption: Type, alternatives: [Type]) 17 | 18 | /** Member constraint representing members of type declarations: functions and 19 | properties. 20 | */ 21 | case member(Type, member: Identifier, memberType: Type) 22 | } 23 | 24 | /** Environment of possible overloads for `Identifier`. There's an assumption 25 | that `[Scheme]` array can't be empty, since an empty array of overloads is 26 | meaningless. If no overloads are available for `Identifier`, it shouldn't be 27 | in the `Environoment` dictionary as a key in the first place. 28 | */ 29 | typealias Environment = [Identifier: [Scheme]] 30 | typealias Members = [TypeIdentifier: Environment] 31 | 32 | struct ConstraintSystem { 33 | private var typeVariableCount = 0 34 | private(set) var constraints = [Constraint]() 35 | 36 | private(set) var environment: Environment 37 | let members: Members 38 | 39 | init(_ environment: Environment, members: Members) { 40 | self.environment = environment 41 | self.members = members 42 | } 43 | 44 | mutating func removeFirst() -> Constraint? { 45 | return constraints.count > 0 ? constraints.removeFirst() : nil 46 | } 47 | 48 | mutating func prepend(_ constraint: Constraint) { 49 | constraints.insert(constraint, at: 0) 50 | } 51 | 52 | func appending(_ constraints: [Constraint]) -> ConstraintSystem { 53 | var result = self 54 | result.constraints.append(contentsOf: constraints) 55 | return result 56 | } 57 | 58 | mutating func apply(_ sub: Substitution) { 59 | constraints = constraints.apply(sub) 60 | } 61 | 62 | /// Temporarily extends the current `self.environment` with `environment` to 63 | /// infer the type of `inferred` expression. Is used to infer 64 | /// type of an expression evaluated in a lambda. 65 | private mutating func infer( 66 | inExtended environment: T, 67 | _ inferred: Expr 68 | ) throws -> Type where T: Sequence, T.Element == (Identifier, Scheme) { 69 | // preserve old environment to be restored after inference in extended 70 | // environment has finished 71 | var old = self.environment 72 | 73 | defer { self.environment = old } 74 | 75 | for (id, scheme) in environment { 76 | self.environment[id] = [scheme] 77 | } 78 | return try infer(inferred) 79 | } 80 | 81 | /** Generate a new type variable that can be stored in `constraints`. If 82 | constraints are consistent and a single solution ca be found, this 83 | type variable will be resolved to a concrete type with a substitution created 84 | by a `Solver`. 85 | */ 86 | private mutating func fresh() -> Type { 87 | defer { typeVariableCount += 1 } 88 | 89 | return .variable("T\(typeVariableCount)") 90 | } 91 | 92 | mutating func lookup( 93 | _ member: Identifier, 94 | in typeID: TypeIdentifier 95 | ) throws -> Type { 96 | guard let environment = members[typeID] else { 97 | throw TypeError.unknownType(typeID) 98 | } 99 | 100 | return try lookup( 101 | member, 102 | in: environment, 103 | orThrow: .unknownMember(typeID, member) 104 | ) 105 | } 106 | 107 | private mutating func lookup( 108 | _ id: Identifier, 109 | in environment: Environment, 110 | orThrow error: TypeError 111 | ) throws -> Type { 112 | guard let schemes = environment[id] else { throw error } 113 | 114 | let results = schemes.map { instantiate($0) } 115 | 116 | assert(results.count > 0) 117 | guard results.count > 1 else { return results[0] } 118 | 119 | let typeVariable = fresh() 120 | 121 | constraints.append( 122 | .disjunction(id, assumption: typeVariable, alternatives: results) 123 | ) 124 | return typeVariable 125 | } 126 | 127 | /// Converting a σ type into a τ type by creating fresh names for each type 128 | /// variable that does not appear in the current typing environment. 129 | private mutating func instantiate(_ scheme: Scheme) -> Type { 130 | let substitution = scheme.variables.map { ($0, fresh()) } 131 | return scheme.type.apply(Dictionary(uniqueKeysWithValues: substitution)) 132 | } 133 | 134 | mutating func infer(_ expr: Expr) throws -> Type { 135 | switch expr { 136 | case let .literal(literal): 137 | return literal.defaultType 138 | 139 | case let .identifier(id): 140 | return try lookup(id, in: environment, orThrow: .unbound(id)) 141 | 142 | case let .lambda(ids, expr): 143 | let parameters = ids.map { _ in fresh() } 144 | return try .arrow( 145 | parameters, 146 | infer(inExtended: zip(ids, parameters.map { Scheme($0) }), expr) 147 | ) 148 | 149 | case let .application(callable, arguments): 150 | let callableType = try infer(callable) 151 | let typeVariable = fresh() 152 | constraints.append(.equal( 153 | callableType, 154 | .arrow(try arguments.map { try infer($0) }, typeVariable) 155 | )) 156 | return typeVariable 157 | 158 | case let .ternary(cond, expr1, expr2): 159 | let result = try infer(expr1) 160 | try constraints.append(contentsOf: [ 161 | .equal(infer(cond), .bool), 162 | .equal(result, infer(expr2)), 163 | ]) 164 | return result 165 | 166 | case let .member(expr, id): 167 | switch try infer(expr) { 168 | case .arrow: 169 | throw TypeError.arrowMember(id) 170 | 171 | case let .constructor(typeID, _): 172 | return try lookup(id, in: typeID) 173 | 174 | case let .variable(v): 175 | let memberType = fresh() 176 | constraints.append( 177 | .member(.variable(v), member: id, memberType: memberType) 178 | ) 179 | return memberType 180 | 181 | case let .namedTuple(elements): 182 | if let idx = Int(id) { 183 | guard (0.. Bool in 29 | print("Error while reading a list of files" + 30 | " in folder at \(url.path): ", error) 31 | return true 32 | })?.compactMap({ $0 as? URL }) 33 | .filter({ isSwiftFile($0.path) }) 34 | else { 35 | fatalError("Enumerator is nil") 36 | } 37 | 38 | for fileURL in enumerator { 39 | try parseFile(path: fileURL.path, consumers: [consoleConsumer]) 40 | } 41 | } 42 | } 43 | } 44 | 45 | public func parseFile( 46 | path: String, consumers: [TypologyDiagnosticConsumer] 47 | ) throws { 48 | let contents = try String(contentsOfFile: path) 49 | let lines = contents.components(separatedBy: .newlines) 50 | let engine = TypologyDiagnosticEngine(fileContent: lines) 51 | for consumer in consumers { 52 | engine.addConsumer(consumer) 53 | } 54 | do { 55 | _ = try File(path: path) 56 | } catch let error as ASTError { 57 | let diagnose = Diagnostic.Message(.error, "\(error.value)") 58 | 59 | let diagnostic = TypologyDiagnostic( 60 | message: diagnose, 61 | location: error.range.start, 62 | notes: [], 63 | highlights: [ 64 | SourceRange( 65 | start: error.range.start, 66 | end: error.range.end 67 | ), 68 | ], 69 | fixIts: [] 70 | ) 71 | 72 | engine.diagnose(diagnostic) 73 | } catch { 74 | let diagnose = Diagnostic.Message(.note, error.localizedDescription) 75 | engine.diagnose(diagnose) 76 | } 77 | } 78 | 79 | public func isSwiftFile(_ path: String) -> Bool { 80 | return path.contains(".swift") 81 | } 82 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Diagnostic/Diagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diagnostic.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/10/19. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | /// A TypologyDiagnostic message that can be emitted regarding some piece of code. 12 | public struct TypologyDiagnostic: Codable { 13 | // These values must match clang/Frontend/SerializedDiagnostics.h 14 | /// The severity of the diagnostic. 15 | public enum Severity: UInt8, Codable { 16 | case note = 1 17 | case warning = 2 18 | case error = 3 19 | } 20 | 21 | /// The diagnostic's message. 22 | public let message: Diagnostic.Message 23 | 24 | /// The location the diagnostic should point. 25 | public let location: SourceLocation? 26 | 27 | /// An array of notes providing more context for this diagnostic. 28 | public let notes: [Note] 29 | 30 | /// An array of source ranges to highlight. 31 | public let highlights: [SourceRange] 32 | 33 | /// An array of possible FixIts to apply to this diagnostic. 34 | public let fixIts: [FixIt] 35 | 36 | /// Creates a new Diagnostic with the provided message, pointing to the 37 | /// provided location (if any). 38 | /// This initializer also takes a closure that will be passed a Diagnostic 39 | /// Builder as an inout parameter. Use this closure to add notes, highlights, 40 | /// and FixIts to the diagnostic through the Builder's API. 41 | /// - parameters: 42 | /// - message: The diagnostic's message. 43 | /// - location: The location the diagnostic is attached to. 44 | /// - actions: A closure that's used to attach notes and highlights to 45 | /// diagnostics. 46 | init(message: Diagnostic.Message, location: SourceLocation?) { 47 | self.init( 48 | message: message, 49 | location: location, 50 | notes: [], 51 | highlights: [], 52 | fixIts: [] 53 | ) 54 | } 55 | 56 | /// Creates a new Diagnostic with the provided message, pointing to the 57 | /// provided location (if any). 58 | /// - parameters: 59 | /// - message: The diagnostic's message. 60 | /// - location: The location the diagnostic is attached to. 61 | /// - highlights: An array of SourceRanges which will be highlighted when 62 | /// the diagnostic is presented. 63 | public init( 64 | message: Diagnostic.Message, 65 | location: SourceLocation?, 66 | notes: [Note], 67 | highlights: [SourceRange], 68 | fixIts: [FixIt] 69 | ) { 70 | self.message = message 71 | self.location = location 72 | self.notes = notes 73 | self.highlights = highlights 74 | self.fixIts = fixIts 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Diagnostic/DiagnosticConsumer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiagnosticConsumer.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/10/19. 6 | // 7 | 8 | /// An object that intends to receive notifications when diagnostics are 9 | /// emitted. 10 | public protocol TypologyDiagnosticConsumer { 11 | /// Handle the provided diagnostic which has just been registered with the 12 | /// DiagnosticEngine. 13 | func handle(_ diagnostic: TypologyDiagnostic, _ fileContent: [String]) 14 | 15 | /// Finalize the consumption of diagnostics, flushing to disk if necessary. 16 | func finalize() 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Diagnostic/DiagnosticConsumers/ConsoleDiagnosticConsumer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Console.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/8/19. 6 | // 7 | 8 | import Foundation 9 | import Rainbow 10 | import SwiftSyntax 11 | 12 | let verticalSeparator = "|".applyingColor(.blue) 13 | 14 | /// ConsoleDiagnosticConsumer formats diagnostics and prints them to the 15 | /// console. 16 | public class ConsoleDiagnosticConsumer: TypologyDiagnosticConsumer { 17 | /// Creates a new ConsoleDiagnosticConsumer. 18 | public init() {} 19 | 20 | /// Writes the text of the diagnostic to stderr. 21 | func write(_ msg: T) { 22 | FileHandle.standardError.write("\(msg)".data(using: .utf8)!) 23 | } 24 | 25 | /// Prints the contents of a diagnostic to stderr. 26 | public func handle( 27 | _ diagnostic: TypologyDiagnostic, 28 | _ fileContent: [String] 29 | ) { 30 | write(diagnostic, fileContent) 31 | } 32 | 33 | /// Prints each of the fields in a diagnostic to stderr. 34 | public func write(_ diagnostic: TypologyDiagnostic, _ fileContent: [String]) { 35 | if 36 | let loc = diagnostic.location, 37 | let file = loc.file, 38 | let line = loc.line, 39 | let column = loc.column { 40 | write("\(file):\(line):\(column): ") 41 | } else { 42 | write(":0:0: ") 43 | } 44 | switch diagnostic.message.severity { 45 | case .note: write("note: ".applyingColor(.magenta)) 46 | case .warning: write("warning: ".applyingColor(.yellow)) 47 | case .error: write("error: ".applyingColor(.red)) 48 | } 49 | 50 | write(diagnostic.message.text) 51 | write("\n") 52 | 53 | guard !diagnostic.highlights.isEmpty else { return } 54 | 55 | for highlight in diagnostic.highlights { 56 | guard let startLine = highlight.start.line, let endLine = highlight.end.line else { continue } 57 | 58 | let maxOffset = String( 59 | repeating: " ", 60 | count: "\(endLine)".count + 1 61 | ) 62 | 63 | if highlight.end.line != highlight.start.line { 64 | // show multiline error 65 | let errorLines = startLine...endLine 66 | 67 | print(maxOffset, verticalSeparator) 68 | 69 | for line in errorLines { 70 | let lineOffset = offset(line, endLine) 71 | let errorString = fileContent[line] 72 | 73 | print("\(String(line).applyingColor(.blue))" + 74 | "\(lineOffset)\(">".applyingColor(.red))" + 75 | "\(verticalSeparator) \(errorString)") 76 | } 77 | 78 | print(maxOffset, verticalSeparator) 79 | } else { 80 | // show one line error 81 | let errorLine = startLine 82 | let errorOffset = String(repeating: " ", count: errorLine) 83 | let errorString = fileContent[errorLine] 84 | let errorUnderscore = String( 85 | repeating: "^", 86 | count: endLine - startLine 87 | ) 88 | .applyingColor(.red) 89 | 90 | print(maxOffset, verticalSeparator) 91 | print("\(String(errorLine).applyingColor(.blue))" + 92 | " \(verticalSeparator) \(errorString)") 93 | print(maxOffset, verticalSeparator, errorOffset, errorUnderscore) 94 | } 95 | } 96 | } 97 | 98 | public func finalize() { 99 | // Do nothing 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Diagnostic/DiagnosticEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiagnosticEngine.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/10/19. 6 | // 7 | 8 | import Foundation 9 | import SwiftSyntax 10 | 11 | /// The DiagnosticEngine allows Swift tools to emit diagnostics. 12 | public class TypologyDiagnosticEngine { 13 | /// Creates a new DiagnosticEngine with no diagnostics. 14 | public init(fileContent: [String]) { 15 | self.fileContent = fileContent 16 | } 17 | 18 | /// The list of consumers of the diagnostic passing through this engine. 19 | internal var consumers = [TypologyDiagnosticConsumer]() 20 | 21 | /// The file content 22 | public var fileContent: [String] 23 | 24 | public private(set) var diagnostics = [TypologyDiagnostic]() 25 | 26 | /// Adds the provided consumer to the consumers list. 27 | public func addConsumer(_ consumer: TypologyDiagnosticConsumer) { 28 | consumers.append(consumer) 29 | 30 | // Start the consumer with all previous diagnostics. 31 | for diagnostic in diagnostics { 32 | consumer.handle(diagnostic, fileContent) 33 | } 34 | } 35 | 36 | /// Registers a diagnostic with the diagnostic engine. 37 | /// - parameters: 38 | /// - message: The message for the diagnostic. This message includes 39 | /// a severity and text that will be conveyed when the diagnostic 40 | /// is serialized. 41 | public func diagnose( 42 | _ message: Diagnostic.Message, 43 | location: SourceLocation? = nil 44 | ) { 45 | let diagnostic = TypologyDiagnostic( 46 | message: message, 47 | location: location 48 | ) 49 | diagnostics.append(diagnostic) 50 | for consumer in consumers { 51 | consumer.handle(diagnostic, fileContent) 52 | } 53 | } 54 | 55 | public func diagnose(_ diagnostic: TypologyDiagnostic) { 56 | diagnostics.append(diagnostic) 57 | for consumer in consumers { 58 | consumer.handle(diagnostic, fileContent) 59 | } 60 | } 61 | 62 | /// Tells each consumer to finalize their diagnostic output. 63 | deinit { 64 | for consumer in consumers { 65 | consumer.finalize() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Diagnostic/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/11/19. 6 | // 7 | 8 | import Foundation 9 | 10 | // Function to generate string of spaces that will be equivalent to 11 | // difference between strings length + 1 12 | // 13 | // Example: 14 | // offset(0, 100) will return String with 3 spaces like this: " " 15 | // offset(50, 100) will return String with 2 spaces like this: " " 16 | public func offset(_ startIndex: Int, _ endIndex: Int) -> String { 17 | return String( 18 | repeating: " ", 19 | count: "\(endIndex)".count - "\(startIndex)".count + 1 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /Sources/TypologyCore/DiagnosticError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiagnosticError.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 07/06/2019. 6 | // 7 | 8 | import SwiftSyntax 9 | 10 | protocol DiagnosticError: Error { 11 | var range: SourceRange { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Protocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocol.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/05/2019. 6 | // 7 | 8 | struct ProtocolIdentifier: Hashable { 9 | let value: String 10 | } 11 | 12 | extension ProtocolIdentifier: ExpressibleByStringLiteral { 13 | init(stringLiteral value: String) { 14 | self.value = value 15 | } 16 | } 17 | 18 | struct Protocol { 19 | let inherited: [ProtocolIdentifier] 20 | let members: Environment 21 | } 22 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Scheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scheme.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/05/2019. 6 | // 7 | 8 | /** Schemes are types containing one or more generic variables. A scheme 9 | explicitly specifies variables bound in the current type, which allows those 10 | variables to be distinguished from those that were bound in an outer scope. 11 | */ 12 | struct Scheme: Equatable { 13 | /** Type containing variables bound in `variables` property. 14 | */ 15 | let type: Type 16 | 17 | /// Variables bound in the scheme 18 | let variables: [TypeVariable] 19 | 20 | init( 21 | _ type: Type, 22 | variables: [TypeVariable] = [] 23 | ) { 24 | self.type = type 25 | self.variables = variables 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Solver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Solver.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/04/2019. 6 | // 7 | 8 | /** `Solver` operates on a constraint system, which contains an array of 9 | `Constraint` values. These constraints are reduced one by one to find a 10 | suitable `Substitution` that make the constraints consistent with each other. 11 | `Solver` values are immutable, which allows separate solver iterations to 12 | operate independently. For example, backtracking is implemented as discarding 13 | failed `Solver` values and proceeding from the last known consistent iteration 14 | with new assumptions. 15 | */ 16 | struct Solver { 17 | private let substitution: Substitution 18 | private let system: ConstraintSystem 19 | 20 | init( 21 | substitution: Substitution, 22 | system: ConstraintSystem 23 | ) { 24 | self.substitution = substitution 25 | self.system = system 26 | } 27 | 28 | private var empty: Solver { 29 | return Solver( 30 | substitution: [:], 31 | system: ConstraintSystem(system.environment, members: system.members) 32 | ) 33 | } 34 | 35 | /** Return a `Substitution` value that satisfies `constraints` within 36 | the current solver. 37 | */ 38 | func solve() throws -> Substitution { 39 | var system = self.system 40 | guard let constraint = system.removeFirst() else { return substitution } 41 | 42 | switch constraint { 43 | case let .equal(t1, t2): 44 | let s = try unify(t1, t2) 45 | 46 | system.apply(s.substitution) 47 | 48 | return try Solver( 49 | substitution: s.substitution.compose(substitution), 50 | system: s.system.appending(system.constraints) 51 | ).solve() 52 | 53 | case let .member(type, member, memberType): 54 | guard case let .constructor(typeID, _) = type else { 55 | fatalError("unhandled member constraint") 56 | } 57 | 58 | // generate new constraints for member lookup 59 | let assumedType = try system.lookup(member, in: typeID) 60 | 61 | if case .constructor = assumedType { 62 | system.prepend(.equal(memberType, assumedType)) 63 | } 64 | 65 | return try Solver( 66 | substitution: substitution, 67 | system: system 68 | ).solve() 69 | 70 | case let .disjunction(id, type, alternatives): 71 | switch type { 72 | case .variable: 73 | // run multiple independent solvers with each `alternative` prepended as 74 | // a new `equal` constraint. Potentially, these solvers could run on 75 | // multiple threads in parallel? 76 | let result = alternatives.compactMap { alternative -> Substitution? in 77 | do { 78 | var localSystem = system 79 | localSystem.prepend(.equal(type, alternative)) 80 | return try Solver( 81 | substitution: substitution, 82 | system: localSystem 83 | ).solve() 84 | } catch { 85 | return nil 86 | } 87 | } 88 | 89 | switch result.count { 90 | case 0: 91 | throw TypeError.noOverloadFound(id, type) 92 | case 1: 93 | return result[0] 94 | default: 95 | throw TypeError.ambiguous(id) 96 | } 97 | 98 | default: 99 | guard alternatives.contains(type) else { 100 | throw TypeError.noOverloadFound(id, type) 101 | } 102 | return try Solver( 103 | substitution: substitution, 104 | system: system 105 | ).solve() 106 | } 107 | } 108 | } 109 | 110 | private func unify(_ t1: Type, _ t2: Type) throws -> Solver { 111 | switch (t1, t2) { 112 | case let (.arrow(i1, o1), .arrow(i2, o2)): 113 | let s1 = try unify(.tuple(i1), .tuple(i2)) 114 | let s2 = try unify(o1.apply(s1.substitution), o2.apply(s1.substitution)) 115 | return Solver( 116 | substitution: s2.substitution.compose(s1.substitution), 117 | system: s1.system.appending(s2.system.constraints) 118 | ) 119 | 120 | case let (.variable(v), t): 121 | return try bind(type: t, to: v) 122 | 123 | case let (t, .variable(v)): 124 | return try bind(type: t, to: v) 125 | 126 | case let (.constructor(a), .constructor(b)) where a == b: 127 | return empty 128 | 129 | case let (.namedTuple(t1), .namedTuple(t2)) where t1.count == t2.count: 130 | return try zip(t1, t2).map { 131 | // check that we are on the lowest level of the tuple, otherwise 132 | // call unify on children 133 | guard let name1 = $0.0, let name2 = $1.0 else { 134 | return try unify($0.1, $1.1) 135 | } 136 | 137 | // if corresponding elements of both tuples have names, 138 | // they need to be the same to unify the tuples 139 | guard name1 == name2 else { 140 | throw TypeError.tupleUnificationFailure(name1, name2) 141 | } 142 | 143 | return try unify($0.1, $1.1) 144 | }.reduce(empty) { (s1: Solver, s2: Solver) -> Solver in 145 | // merge new solver with a solver produced for previous tuple elements 146 | Solver( 147 | substitution: s1.substitution.compose(s2.substitution), 148 | system: s1.system.appending(s2.system.constraints) 149 | ) 150 | } 151 | 152 | case let (a, b): 153 | throw TypeError.unificationFailure(a, b) 154 | } 155 | } 156 | 157 | /** Bind `type` to a type `variable` with a substitution and return a new 158 | solver with this substitution. Return an empty solver if `type` is already 159 | equivalent to the type `variable`. 160 | */ 161 | private func bind(type: Type, to variable: TypeVariable) throws -> Solver { 162 | if type == .variable(variable) { 163 | return empty 164 | } else if type.occurs(variable) { 165 | throw TypeError.infiniteType(variable, type) 166 | } 167 | 168 | return Solver( 169 | substitution: [variable: type], 170 | system: empty.system 171 | ) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Sources/TypologyCore/Substitution.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Substitution.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 27/04/2019. 6 | // 7 | 8 | typealias Substitution = [TypeVariable: Type] 9 | 10 | extension Substitution { 11 | /// It is up to the implementation of the inference algorithm to ensure that 12 | /// clashes do not occur between substitutions. 13 | func compose(_ sub: Substitution) -> Substitution { 14 | return sub.mapValues { $0.apply(self) }.merging(self) { value, _ in value } 15 | } 16 | } 17 | 18 | protocol Substitutable { 19 | func apply(_ sub: Substitution) -> Self 20 | var freeTypeVariables: Set { get } 21 | } 22 | 23 | extension Substitutable { 24 | func occurs(_ typeVariable: TypeVariable) -> Bool { 25 | return freeTypeVariables.contains(typeVariable) 26 | } 27 | } 28 | 29 | extension TypeVariable: Substitutable { 30 | var freeTypeVariables: Set { 31 | return [self] 32 | } 33 | 34 | func apply(_ sub: Substitution) -> TypeVariable { 35 | if case let .variable(v)? = sub[self] { 36 | return v 37 | } else { 38 | return self 39 | } 40 | } 41 | } 42 | 43 | extension Type: Substitutable { 44 | func apply(_ sub: Substitution) -> Type { 45 | switch self { 46 | case let .variable(v): 47 | return sub[v] ?? .variable(v) 48 | case let .arrow(t1, t2): 49 | return t1.apply(sub) --> t2.apply(sub) 50 | case .constructor: 51 | return self 52 | case let .namedTuple(elements): 53 | return .namedTuple(elements.map { ($0.0, $0.1.apply(sub)) }) 54 | } 55 | } 56 | 57 | var freeTypeVariables: Set { 58 | switch self { 59 | case .constructor: 60 | return [] 61 | case let .variable(v): 62 | return [v] 63 | case let .arrow(t1, t2): 64 | return t1.freeTypeVariables.union(t2.freeTypeVariables) 65 | case let .namedTuple(elements): 66 | return elements.map { $0.1 }.freeTypeVariables 67 | } 68 | } 69 | } 70 | 71 | extension Scheme: Substitutable { 72 | func apply(_ sub: Substitution) -> Scheme { 73 | let type = self.type.apply(variables.reduce(sub) { 74 | var result = $0 75 | result[$1] = nil 76 | return result 77 | }) 78 | return Scheme(type, variables: variables) 79 | } 80 | 81 | var freeTypeVariables: Set { 82 | return type.freeTypeVariables.subtracting(variables) 83 | } 84 | } 85 | 86 | extension Array: Substitutable where Element: Substitutable { 87 | func apply(_ sub: Substitution) -> [Element] { 88 | return map { $0.apply(sub) } 89 | } 90 | 91 | var freeTypeVariables: Set { 92 | return reduce([]) { $0.union($1.freeTypeVariables) } 93 | } 94 | } 95 | 96 | extension Environment: Substitutable { 97 | func apply(_ sub: Substitution) -> Environment { 98 | return mapValues { $0.apply(sub) } 99 | } 100 | 101 | var freeTypeVariables: Set { 102 | return Array(values).freeTypeVariables 103 | } 104 | } 105 | 106 | extension Constraint: Substitutable { 107 | func apply(_ sub: Substitution) -> Constraint { 108 | switch self { 109 | case let .equal(t1, t2): 110 | return .equal(t1.apply(sub), t2.apply(sub)) 111 | case let .member(type, member, mt): 112 | return .member(type.apply(sub), member: member, memberType: mt.apply(sub)) 113 | case let .disjunction(id, type, alternatives): 114 | return .disjunction( 115 | id, 116 | assumption: type.apply(sub), 117 | alternatives: alternatives.apply(sub) 118 | ) 119 | } 120 | } 121 | 122 | var freeTypeVariables: Set { 123 | switch self { 124 | case let .equal(t1, t2): 125 | return t1.freeTypeVariables.union(t2.freeTypeVariables) 126 | case let .member(type, _, memberType): 127 | return type.freeTypeVariables.union(memberType.freeTypeVariables) 128 | case let .disjunction(_, type, alternatives): 129 | return type.freeTypeVariables.union(alternatives.freeTypeVariables) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/TypologyCore/TypeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeError.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 28/05/2019. 6 | // 7 | 8 | enum TypeError: Error { 9 | case ambiguous(Identifier) 10 | case arrowMember(Identifier) 11 | case infiniteType(TypeVariable, Type) 12 | case noOverloadFound(Identifier, Type) 13 | case tupleIndexOutOfRange(total: Int, addressed: Int) 14 | case unificationFailure(Type, Type) 15 | case unknownType(TypeIdentifier) 16 | case unknownMember(TypeIdentifier, Identifier) 17 | case unknownTupleMember(Identifier) 18 | case unbound(Identifier) 19 | case tupleUnificationFailure(Identifier, Identifier) 20 | } 21 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | @testable import TypologyTests 2 | import XCTest 3 | 4 | XCTMain([ 5 | testCase(TypologyTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/TypologyTests/ASTTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASTTests.swift 3 | // TypologyTests 4 | // 5 | // Created by Max Desiatov on 01/06/2019. 6 | // 7 | 8 | @testable import TypologyCore 9 | import XCTest 10 | 11 | let root = URL(fileURLWithPath: #file) 12 | .deletingLastPathComponent() 13 | .deletingLastPathComponent() 14 | .deletingLastPathComponent() 15 | .appendingPathComponent("ValidationTests/AST/") 16 | 17 | final class ASTTests: XCTestCase { 18 | func testTernary() throws { 19 | let string = try #"true ? "then" : "else""# .parseAST() 20 | .statements.first as? ExprNode 21 | let int = try "false ? 0 : 42".parseAST() 22 | .statements.first as? ExprNode 23 | let error = try #"true ? "then" : 42"# .parseAST() 24 | .statements.first as? ExprNode 25 | 26 | XCTAssertEqual(try string?.expr.infer(), .string) 27 | XCTAssertEqual(try int?.expr.infer(), .int) 28 | XCTAssertThrowsError(try error?.expr.infer()) 29 | } 30 | 31 | func testFunc() throws { 32 | let f = try "func x(_ x: String, y: [Int]) -> Int { return 42 }" 33 | .parseAST().statements.first as? FunctionDecl 34 | 35 | XCTAssertEqual(f?.scheme, Scheme( 36 | [.string, .constructor("Array", [.int])] --> .int 37 | )) 38 | } 39 | 40 | func testGenericFunc() throws { 41 | let f = try "func x(_ x: T, _ y: T) -> T { return x }" 42 | .parseAST().statements.first as? FunctionDecl 43 | 44 | let tVar = "T" 45 | let t = Type.constructor(TypeIdentifier(value: tVar), []) 46 | 47 | XCTAssertEqual(f?.scheme, Scheme( 48 | [t, t] --> t, 49 | variables: [TypeVariable(value: tVar)] 50 | )) 51 | } 52 | 53 | func testFuncPosition() throws { 54 | let functions = try 55 | """ 56 | // declare function #commentsForComments 57 | //This is also a comment 58 | // but is written over multiple lines. 59 | func first(_ x: String) -> String { 60 | var x: String { 61 | return "Hello" 62 | } 63 | var y: String { 64 | get { 65 | return "Hello, " 66 | } 67 | set { 68 | print("world!") 69 | } 70 | } 71 | dynamic private(set) let a: Double = 3.14, b: Int 72 | let z = 5 73 | let (x, y) = z 74 | 75 | return x 76 | } 77 | 78 | /* This is also a comment 79 | but is written over multiple lines. */ 80 | // declare another function with double offset #commentsForComments 81 | func second(_ x: String) -> String { 82 | return x 83 | } 84 | """.parseAST() 85 | 86 | let firstFunc = functions.statements.first 87 | let secondFunc = functions.statements.last 88 | 89 | XCTAssertEqual(firstFunc?.range.start.line, 4) 90 | XCTAssertEqual(firstFunc?.range.start.column, 5) 91 | XCTAssertEqual(firstFunc?.range.end.line, 21) 92 | XCTAssertEqual(firstFunc?.range.end.column, 6) 93 | 94 | XCTAssertEqual(secondFunc?.range.start.line, 26) 95 | XCTAssertEqual(secondFunc?.range.start.column, 9) 96 | XCTAssertEqual(secondFunc?.range.end.line, 28) 97 | XCTAssertEqual(secondFunc?.range.end.column, 10) 98 | } 99 | 100 | func testInitFromFilePositive() throws { 101 | let url = root.appendingPathComponent("Positive.swift") 102 | XCTAssertNoThrow(try File(path: url.path)) 103 | } 104 | 105 | func testInitFromFileNegative() throws { 106 | let url = root.appendingPathComponent("Negative.swift") 107 | XCTAssertThrowsError(try File(path: url.path)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/TypologyTests/Benchmarks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Benchmarks.swift 3 | // SwiftSyntax 4 | // 5 | // Created by Max Desiatov on 25/05/2019. 6 | // 7 | 8 | @testable import TypologyCore 9 | import XCTest 10 | 11 | class Benchmarks: XCTestCase { 12 | func delayErrors(_ closure: (() -> ()) -> ()) throws { 13 | var caughtError: Error? 14 | 15 | closure { 16 | do { 17 | let lambda = Expr.lambda( 18 | ["x"], 19 | .application( 20 | "decode", 21 | [.application( 22 | "stringify", 23 | [.application("increment", [.ternary( 24 | .literal(.bool(false)), 25 | "x", 26 | .literal(42) 27 | )])] 28 | )] 29 | ) 30 | ) 31 | 32 | _ = try lambda.infer(environment: [ 33 | "increment": [.init(.int --> .int)], 34 | "stringify": [.init(.int --> .string)], 35 | "decode": [.init(.string --> .int)], 36 | ]) 37 | } catch { 38 | caughtError = error 39 | } 40 | } 41 | 42 | if let error = caughtError { 43 | throw error 44 | } 45 | } 46 | 47 | func testInference() throws { 48 | try delayErrors { closure in 49 | self.measure { 50 | closure() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TypologyTests/DiagnosticTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiagnosticTests.swift 3 | // Typology 4 | // 5 | // Created by Matvii Hodovaniuk on 6/11/19. 6 | // 7 | 8 | import SwiftCLI 9 | import SwiftSyntax 10 | @testable import TypologyCore 11 | import XCTest 12 | 13 | final class DiagnosticTests: XCTestCase { 14 | func testIsSwiftFile() throws { 15 | XCTAssertTrue(isSwiftFile("/FileName.swift")) 16 | XCTAssertFalse(isSwiftFile("/FileName.sh")) 17 | XCTAssertFalse(isSwiftFile("/FileName.yml")) 18 | } 19 | 20 | func testParseFile() throws { 21 | let consoleConsumer = ConsoleDiagnosticConsumer() 22 | let urlPositive = root.appendingPathComponent("Positive.swift") 23 | let urlNegative = root.appendingPathComponent("Negative.swift") 24 | XCTAssertNoThrow( 25 | try parseFile( 26 | path: urlPositive.path, 27 | consumers: [consoleConsumer] 28 | ) 29 | ) 30 | XCTAssertNoThrow( 31 | try parseFile( 32 | path: urlNegative.path, 33 | consumers: [consoleConsumer] 34 | ) 35 | ) 36 | } 37 | 38 | func testOffsetGenerateFunction() throws { 39 | XCTAssertEqual("\(offset(1, 234))", " ") 40 | XCTAssertEqual("\(offset(56, 7890))", " ") 41 | XCTAssertEqual("\(offset(11, 123))", " ") 42 | } 43 | 44 | func testTypologyDiagnosticEngine() throws { 45 | // Test diagnose add consumer functional 46 | let filePath = root.appendingPathComponent("Positive.swift").path 47 | let contents = try String(contentsOfFile: filePath) 48 | let lines = contents.components(separatedBy: .newlines) 49 | let engine = TypologyDiagnosticEngine(fileContent: lines) 50 | let consoleConsumer = ConsoleDiagnosticConsumer() 51 | let consoleConsumer1 = ConsoleDiagnosticConsumer() 52 | engine.addConsumer(consoleConsumer) 53 | XCTAssertEqual(engine.consumers.count, 1) 54 | 55 | // Test to call TypologyDiagnosticEngine.deinit while test 56 | _ = TypologyDiagnosticEngine(fileContent: lines) 57 | 58 | // Test diagnose handle message functional 59 | let message = Diagnostic.Message(.note, "note message") 60 | XCTAssertNoThrow(engine.diagnose(message)) 61 | 62 | // Test diagnose on previous added diagnostic with new added comsumer 63 | engine.addConsumer(consoleConsumer1) 64 | 65 | // Test when diagnose handle diagnostic that takes one line 66 | let location = SourceLocation( 67 | line: 1, 68 | column: 1, 69 | offset: 1, 70 | file: filePath 71 | ) 72 | let diagnostic = TypologyDiagnostic( 73 | message: message, 74 | location: location, 75 | notes: [], 76 | highlights: [ 77 | SourceRange( 78 | start: location, 79 | end: location 80 | ), 81 | ], 82 | fixIts: [] 83 | ) 84 | XCTAssertNoThrow(engine.diagnose(diagnostic)) 85 | 86 | // Test when diagnose handle diagnostic that takes multiple lines 87 | let location2 = SourceLocation( 88 | line: 3, 89 | column: 1, 90 | offset: 1, 91 | file: filePath 92 | ) 93 | let location3 = SourceLocation( 94 | line: 4, 95 | column: 4, 96 | offset: 4, 97 | file: filePath 98 | ) 99 | 100 | let diagnostic2 = TypologyDiagnostic( 101 | message: message, 102 | location: location2, 103 | notes: [], 104 | highlights: [ 105 | SourceRange( 106 | start: location, 107 | end: location3 108 | ), 109 | ], 110 | fixIts: [] 111 | ) 112 | XCTAssertNoThrow(engine.diagnose(diagnostic2)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/TypologyTests/InferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InferenceTests.swift 3 | // Typology 4 | // 5 | // Created by Max Desiatov on 16/04/2019. 6 | // Copyright © 2019 Typology. All rights reserved. 7 | // 8 | 9 | @testable import TypologyCore 10 | import XCTest 11 | 12 | final class InferenceTests: XCTestCase { 13 | func testTernary() throws { 14 | let string = Expr.ternary( 15 | .literal(true), 16 | .literal("then"), 17 | .literal("else") 18 | ) 19 | let int = Expr.ternary( 20 | .literal(.bool(false)), 21 | .literal(0), 22 | .literal(42) 23 | ) 24 | let error = Expr.ternary( 25 | .literal(true), 26 | .literal("then"), 27 | .literal(42) 28 | ) 29 | 30 | XCTAssertEqual(try string.infer(), .string) 31 | XCTAssertEqual(try int.infer(), .int) 32 | XCTAssertThrowsError(try error.infer()) 33 | } 34 | 35 | func testApplication() throws { 36 | let increment = Expr.application("increment", [.literal(0)]) 37 | let stringify = Expr.application("stringify", [.literal(0)]) 38 | let error = Expr.application("increment", [.literal(false)]) 39 | 40 | let e: Environment = [ 41 | "increment": [.init(.int --> .int)], 42 | "stringify": [.init(.int --> .string)], 43 | ] 44 | 45 | XCTAssertEqual(try increment.infer(environment: e), .int) 46 | XCTAssertEqual(try stringify.infer(environment: e), .string) 47 | XCTAssertThrowsError(try error.infer()) 48 | } 49 | 50 | func testLambda() throws { 51 | let lambda = Expr.lambda( 52 | ["x"], 53 | .application( 54 | "decode", 55 | [.application( 56 | "stringify", 57 | [.application("increment", ["x"])] 58 | )] 59 | ) 60 | ) 61 | 62 | let error = Expr.lambda( 63 | ["x"], 64 | .application( 65 | "stringify", 66 | [.application( 67 | "decode", 68 | [.application("increment", ["x"])] 69 | )] 70 | ) 71 | ) 72 | 73 | let e: Environment = [ 74 | "increment": [.init(.int --> .int)], 75 | "stringify": [.init(.int --> .string)], 76 | "decode": [.init(.string --> .int)], 77 | ] 78 | 79 | XCTAssertEqual(try lambda.infer(environment: e), .int --> .int) 80 | XCTAssertThrowsError(try error.infer()) 81 | } 82 | 83 | func testLambdaWithMultipleArguments() throws { 84 | let lambda = Expr.lambda( 85 | ["x", "y"], 86 | .application( 87 | "decode", 88 | [ 89 | .application( 90 | "stringify", 91 | [ 92 | .application("sum", ["x", "y"]), 93 | .application("sum", ["x", "y"]), 94 | ] 95 | ), 96 | .application( 97 | "stringify", 98 | [ 99 | .application("sum", ["x", "y"]), 100 | .application("sum", ["x", "y"]), 101 | ] 102 | ), 103 | ] 104 | ) 105 | ) 106 | 107 | let e: Environment = [ 108 | "sum": [.init([.int, .int] --> .int)], 109 | "stringify": [.init([.int, .int] --> .string)], 110 | "decode": [.init([.string, .string] --> .int)], 111 | ] 112 | 113 | XCTAssertEqual(try lambda.infer(environment: e), [.int, .int] --> .int) 114 | } 115 | 116 | func testLambdaWithMultipleArgumentsDiffrentTypes() throws { 117 | let lambda = Expr.lambda( 118 | ["str", "int"], 119 | .application( 120 | "decode", 121 | [ 122 | .application("concatenate", ["int", "str"]), 123 | .application("sum", ["int", "int"]), 124 | ] 125 | ) 126 | ) 127 | 128 | let e: Environment = [ 129 | "concatenate": [.init([.int, .string] --> .string)], 130 | "sum": [.init([.int, .int] --> .int)], 131 | "decode": [.init([.string, .int] --> .int)], 132 | ] 133 | 134 | XCTAssertEqual( 135 | try lambda.infer(environment: e), 136 | [.string, .int] --> .int 137 | ) 138 | } 139 | 140 | func testLambdaApplication() throws { 141 | let lambda = Expr.application( 142 | .lambda(["x"], .ternary("x", .literal(1), .literal(0))), [.literal(true)] 143 | ) 144 | 145 | let error = Expr.application( 146 | .lambda(["x"], .ternary("x", .literal(1), .literal(0))), [.literal("blah")] 147 | ) 148 | 149 | XCTAssertEqual(try lambda.infer(), .int) 150 | XCTAssertThrowsError(try error.infer()) 151 | } 152 | 153 | func testMember() throws { 154 | let appending = Expr.application( 155 | .member(.literal("Hello, "), "appending"), 156 | [.literal(" World")] 157 | ) 158 | let count = Expr.member(.literal("Test"), "count") 159 | 160 | let m: Members = [ 161 | "String": [ 162 | "appending": [.init(.string --> .string)], 163 | "count": [.init(.int)], 164 | ], 165 | ] 166 | 167 | XCTAssertEqual(try appending.infer(members: m), .string) 168 | XCTAssertEqual(try count.infer(members: m), .int) 169 | } 170 | 171 | func testMemberOfMember() throws { 172 | let literal = Expr.literal("Test") 173 | let magnitude = Expr.member(.member(literal, "count"), "magnitude") 174 | let error = Expr.member(.member(literal, "magnitude"), "count") 175 | 176 | let m: Members = [ 177 | "String": [ 178 | "count": [.init(.int)], 179 | ], 180 | "Int": [ 181 | "magnitude": [.init(.int)], 182 | ], 183 | ] 184 | 185 | XCTAssertEqual(try magnitude.infer(members: m), .int) 186 | XCTAssertThrowsError(try error.infer(members: m)) 187 | } 188 | 189 | func testLambdaMember() throws { 190 | let lambda = Expr.application( 191 | .lambda(["x"], .ternary("x", .literal("one"), .literal("zero"))), 192 | [.literal(true)] 193 | ) 194 | let count = Expr.member(lambda, "count") 195 | let error = Expr.member(lambda, "magnitude") 196 | 197 | let m: Members = [ 198 | "String": [ 199 | "count": [.init(.int)], 200 | ], 201 | "Int": [ 202 | "magnitude": [.init(.int)], 203 | ], 204 | ] 205 | 206 | XCTAssertEqual(try count.infer(members: m), .int) 207 | XCTAssertThrowsError(try error.infer(members: m)) 208 | } 209 | 210 | func testTupleMember() throws { 211 | let tuple = Expr.tuple([.literal(42), .literal("forty two")]) 212 | 213 | XCTAssertEqual(try Expr.member(tuple, "0").infer(), .int) 214 | XCTAssertEqual(try Expr.member(tuple, "1").infer(), .string) 215 | XCTAssertThrowsError(try Expr.member(tuple, "2").infer()) 216 | } 217 | 218 | func testNamedTupleMember() throws { 219 | let namedTuple = Expr.namedTuple([ 220 | ("text", .literal("some text")), 221 | ("count", .literal(10)), 222 | ]) 223 | let tuple = Expr.tuple([.literal("some text"), .literal(10)]) 224 | let mixedTuple = Expr.namedTuple([ 225 | ("text", .literal("some text")), 226 | (nil, .literal(10)), 227 | ]) 228 | let mixedTuple2 = Expr.namedTuple([ 229 | (nil, .literal("some text")), 230 | ("count", .literal(10)), 231 | ]) 232 | let fewArguments = Expr.tuple([.literal("some text")]) 233 | let wrongOrder = Expr.tuple([.literal(10), .literal("some text")]) 234 | 235 | let e: Environment = [ 236 | "acceptNamedTuple": [.init(.namedTuple([ 237 | ("text", .string), 238 | ("count", .int), 239 | ]) --> .string)], 240 | "acceptTuple": [.init(.namedTuple([ 241 | (nil, .string), 242 | (nil, .int), 243 | ]) --> .string)], 244 | "acceptMixedTuple": [.init(.namedTuple([ 245 | ("text", .string), 246 | (nil, .int), 247 | ]) --> .string)], 248 | ] 249 | 250 | XCTAssertEqual(try Expr.member(namedTuple, "text").infer(), .string) 251 | XCTAssertEqual(try Expr.member(namedTuple, "count").infer(), .int) 252 | XCTAssertThrowsError(try Expr.member(namedTuple, "name").infer()) 253 | 254 | XCTAssertEqual(try Expr.application("acceptNamedTuple", [namedTuple]) 255 | .infer(environment: e), .string) 256 | XCTAssertEqual(try Expr.application("acceptNamedTuple", [tuple]) 257 | .infer(environment: e), .string) 258 | XCTAssertEqual(try Expr.application("acceptNamedTuple", [mixedTuple]) 259 | .infer(environment: e), .string) 260 | XCTAssertEqual(try Expr.application("acceptNamedTuple", [mixedTuple2]) 261 | .infer(environment: e), .string) 262 | 263 | XCTAssertEqual(try Expr.application("acceptTuple", [namedTuple]) 264 | .infer(environment: e), .string) 265 | XCTAssertEqual(try Expr.application("acceptTuple", [tuple]) 266 | .infer(environment: e), .string) 267 | XCTAssertEqual(try Expr.application("acceptTuple", [mixedTuple]) 268 | .infer(environment: e), .string) 269 | XCTAssertEqual(try Expr.application("acceptTuple", [mixedTuple2]) 270 | .infer(environment: e), .string) 271 | 272 | XCTAssertEqual(try Expr.application("acceptMixedTuple", [namedTuple]) 273 | .infer(environment: e), .string) 274 | XCTAssertEqual(try Expr.application("acceptMixedTuple", [tuple]) 275 | .infer(environment: e), .string) 276 | XCTAssertEqual(try Expr.application("acceptMixedTuple", [mixedTuple]) 277 | .infer(environment: e), .string) 278 | XCTAssertEqual(try Expr.application("acceptMixedTuple", [mixedTuple2]) 279 | .infer(environment: e), .string) 280 | 281 | XCTAssertThrowsError(try Expr.application("acceptNamedTuple", [fewArguments]) 282 | .infer(environment: e)) 283 | XCTAssertThrowsError(try Expr.application("acceptTuple", [fewArguments]) 284 | .infer(environment: e)) 285 | XCTAssertThrowsError(try Expr.application("acceptMixedTuple", [fewArguments]) 286 | .infer(environment: e)) 287 | 288 | XCTAssertThrowsError(try Expr.application("acceptNamedTuple", [wrongOrder]) 289 | .infer(environment: e)) 290 | XCTAssertThrowsError(try Expr.application("acceptTuple", [wrongOrder]) 291 | .infer(environment: e)) 292 | XCTAssertThrowsError(try Expr.application("acceptMixedTuple", [wrongOrder]) 293 | .infer(environment: e)) 294 | } 295 | 296 | func testOverload() throws { 297 | let uint = Type.constructor("UInt", []) 298 | 299 | let count = Expr.member(.application("f", []), "count") 300 | let magnitude = Expr.member(.application("f", []), "magnitude") 301 | let error = Expr.member( 302 | .application("f", 303 | [.tuple( 304 | [.literal("blah")] 305 | )]), "count" 306 | ) 307 | 308 | let m: Members = [ 309 | "String": [ 310 | "count": [.init(.int)], 311 | ], 312 | "Int": [ 313 | "magnitude": [.init(uint)], 314 | ], 315 | ] 316 | let e: Environment = [ 317 | "f": [ 318 | .init([] --> .int), 319 | .init([] --> .string), 320 | ], 321 | ] 322 | 323 | XCTAssertEqual(try count.infer(environment: e, members: m), .int) 324 | XCTAssertEqual(try magnitude.infer(environment: e, members: m), uint) 325 | XCTAssertThrowsError(try error.infer(environment: e, members: m)) 326 | } 327 | 328 | func testNestedOverload() throws { 329 | let uint = Type.constructor("UInt", []) 330 | let a = Type.constructor("A", []) 331 | let b = Type.constructor("B", []) 332 | 333 | let magnitude = Expr.member( 334 | .member(.application("f", []), "a"), 335 | "magnitude" 336 | ) 337 | let count = Expr.member( 338 | .member(.application("f", []), "b"), 339 | "count" 340 | ) 341 | let ambiguousCount = Expr.member( 342 | .member(.application("f", []), "ambiguous"), 343 | "count" 344 | ) 345 | let ambiguousMagnitude = Expr.member( 346 | .member(.application("f", []), "ambiguous"), 347 | "magnitude" 348 | ) 349 | let ambiguous = Expr.member(.application("f", []), "ambiguous") 350 | let error = Expr.member( 351 | .member(.application("f", []), "ambiguous"), 352 | "ambiguous" 353 | ) 354 | 355 | let m: Members = [ 356 | "A": [ 357 | "a": [.init(.int)], 358 | "ambiguous": [.init(.string)], 359 | ], 360 | "B": [ 361 | "b": [.init(.string)], 362 | "ambiguous": [.init(.int)], 363 | ], 364 | "String": [ 365 | "count": [.init(.int)], 366 | ], 367 | "Int": [ 368 | "magnitude": [.init(uint)], 369 | ], 370 | ] 371 | let e: Environment = [ 372 | "f": [ 373 | .init([] --> a), 374 | .init([] --> b), 375 | ], 376 | ] 377 | 378 | XCTAssertEqual(try count.infer(environment: e, members: m), .int) 379 | XCTAssertEqual(try magnitude.infer(environment: e, members: m), uint) 380 | XCTAssertEqual(try ambiguousCount.infer(environment: e, members: m), .int) 381 | XCTAssertEqual(try ambiguousMagnitude.infer(environment: e, members: m), 382 | uint) 383 | XCTAssertThrowsError(try ambiguous.infer(environment: e, members: m)) 384 | XCTAssertThrowsError(try error.infer(environment: e, members: m)) 385 | } 386 | 387 | static var allTests = [ 388 | ("testTernary", testTernary), 389 | ] 390 | } 391 | -------------------------------------------------------------------------------- /ValidationTests/AST/Negative.swift: -------------------------------------------------------------------------------- 1 | fuc first(_ x: String) -> String { 2 | x 3 | } 4 | -------------------------------------------------------------------------------- /ValidationTests/AST/Positive.swift: -------------------------------------------------------------------------------- 1 | // declare function #commentsForComments 2 | // This is also a comment 3 | // but is written over multiple lines. 4 | func first(_ xArg: String) -> String { 5 | var xStr = xArg 6 | var yStr: String { 7 | get { 8 | return xStr 9 | } 10 | set { 11 | print("world!") 12 | } 13 | } 14 | return xStr 15 | } 16 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | swift test --enable-code-coverage 7 | xcrun llvm-cov show \ 8 | .build/debug/TypologyPackageTests.xctest/Contents/MacOS/TypologyPackageTests \ 9 | -instr-profile=.build/debug/codecov/default.profdata > coverage.txt 10 | bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Example" # ignore folders and all its contents 3 | - "Tests" --------------------------------------------------------------------------------