├── .github └── workflows │ ├── gradle.yml │ └── publish-to-maven-central.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmarks ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ ├── JsonGrammarBenchmark.kt │ │ ├── JsonInput.kt │ │ ├── NaiveJsonGrammar.kt │ │ └── OptimizedJsonGrammar.kt │ └── commonTest │ └── kotlin │ └── Main.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── AndCodegen.kt │ ├── MultiplatformLibraryDependency.kt │ └── TupleCodegen.kt ├── demo ├── demo-js │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── kotlin │ │ └── Main.kt │ │ └── resources │ │ └── main.html ├── demo-jvm │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── example │ │ ├── ArithmeticsEvaluator.kt │ │ ├── BooleanExpression.kt │ │ └── SyntaxTreeDemo.kt └── demo-native │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ └── nativeMain │ └── kotlin │ └── commandLineParser.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── src ├── commonMain │ └── kotlin │ │ ├── com │ │ └── github │ │ │ └── h0tk3y │ │ │ └── betterParse │ │ │ ├── combinators │ │ │ ├── AndCombinator.kt │ │ │ ├── MapCombinator.kt │ │ │ ├── OptionalCombinator.kt │ │ │ ├── OrCombinator.kt │ │ │ ├── RepeatCombinator.kt │ │ │ ├── Separated.kt │ │ │ └── SkipParser.kt │ │ │ ├── grammar │ │ │ └── Grammar.kt │ │ │ ├── lexer │ │ │ ├── DefaultTokenizer.kt │ │ │ ├── LambdaToken.kt │ │ │ ├── LiteralToken.kt │ │ │ ├── RegexToken.kt │ │ │ ├── Token.kt │ │ │ ├── TokenMatch.kt │ │ │ ├── TokenMatchesSequence.kt │ │ │ └── Tokenizer.kt │ │ │ ├── parser │ │ │ └── Parser.kt │ │ │ ├── st │ │ │ ├── LiftToSyntaxTree.kt │ │ │ └── SyntaxTree.kt │ │ │ └── utils │ │ │ └── Tuple.kt │ │ └── generated │ │ ├── andFunctions.kt │ │ └── tuples.kt ├── commonTest │ └── kotlin │ │ ├── AndTest.kt │ │ ├── GrammarTest.kt │ │ ├── MapTest.kt │ │ ├── OptionalTest.kt │ │ ├── OrTest.kt │ │ ├── ParserTest.kt │ │ ├── RepeatTest.kt │ │ ├── SeparatedTest.kt │ │ ├── TestLiftToAst.kt │ │ ├── TokenTest.kt │ │ └── TokenizerTest.kt ├── jsMain │ └── kotlin │ │ └── com │ │ └── github │ │ └── h0tk3y │ │ └── betterParse │ │ └── lexer │ │ └── RegexToken.kt ├── jvmMain │ └── kotlin │ │ └── com │ │ └── github │ │ └── h0tk3y │ │ └── betterParse │ │ └── lexer │ │ ├── Language.kt │ │ └── RegexToken.kt ├── jvmTest │ └── kotlin │ │ └── FlagsCompatibilityTest.kt ├── linuxX64Test │ └── kotlin │ │ └── ConcurrentExecution.kt └── nativeMain │ └── kotlin │ └── com │ └── github │ └── h0tk3y │ └── betterParse │ └── lexer │ └── com │ └── github │ └── h0tk3y │ └── betterParse │ └── lexer │ └── RegexToken.kt └── versions.settings.gradle.kts /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Gradle build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | # .github/workflows/gradle-build-pr.yml 13 | jobs: 14 | gradle: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-java@v1 22 | with: 23 | java-version: 11 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build everything and run all tests 27 | uses: gradle/gradle-build-action@v2 28 | with: 29 | arguments: build -------------------------------------------------------------------------------- /.github/workflows/publish-to-maven-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | gradle: 10 | strategy: 11 | matrix: 12 | os: [ ubuntu-latest, macos-latest, windows-latest ] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: 11 19 | - name: Grant execute permission for gradlew 20 | run: chmod +x gradlew 21 | - name: Run Workflow 22 | uses: timheuer/base64-to-file@v1.1 23 | with: 24 | fileName: 'secring.gpg' 25 | fileDir: ${{github.workspace}}/keys 26 | encodedString: ${{secrets.SIGNING_SECRET_KEY_RING_FILE}} 27 | - name: Publish 28 | uses: gradle/gradle-build-action@v2 29 | with: 30 | arguments: | 31 | publish 32 | -PsonatypePassword=${{secrets.SONATYPE_PASSWORD}} 33 | -Psigning.keyId=${{secrets.SIGNING_KEYID}} 34 | -Psigning.secretKeyRingFile=${{github.workspace}}/keys/secring.gpg 35 | -Psigning.password=${{secrets.SIGNING_PASSWORD}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | **/build/** 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | 12 | # Cache of project 13 | .gradletasknamecache 14 | 15 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 16 | # gradle/wrapper/gradle-wrapper.properties 17 | ### JetBrains template 18 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 19 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 20 | 21 | # User-specific stuff: 22 | .idea/** 23 | .idea/**/tasks.xml 24 | .idea/dictionaries 25 | 26 | # Sensitive or high-churn files: 27 | .idea/**/dataSources/ 28 | .idea/**/dataSources.ids 29 | .idea/**/dataSources.xml 30 | .idea/**/dataSources.local.xml 31 | .idea/**/sqlDataSources.xml 32 | .idea/**/dynamic.xml 33 | 34 | # Gradle: 35 | 36 | # Mongo Explorer plugin: 37 | .idea/**/mongoSettings.xml 38 | 39 | ## File-based project format: 40 | *.iws 41 | 42 | ## Plugin-specific files: 43 | 44 | # IntelliJ 45 | /out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Crashlytics plugin (for Android Studio and IntelliJ) 54 | com_crashlytics_export_strings.xml 55 | crashlytics.properties 56 | crashlytics-build.properties 57 | fabric.properties 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 {yyyy} {name of copyright owner} 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # better-parse 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.h0tk3y.betterParse/better-parse/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.h0tk3y.betterParse/better-parse) 4 | [![Gradle build](https://github.com/h0tk3y/better-parse/workflows/Gradle%20build/badge.svg) ](https://github.com/h0tk3y/better-parse/actions?query=workflow%3A%22Gradle+build%22) 5 | 6 | A nice parser combinator library for Kotlin JVM, JS, and Multiplatform projects 7 | 8 | ```kotlin 9 | val booleanGrammar = object : Grammar() { 10 | val id by regexToken("\\w+") 11 | val not by literalToken("!") 12 | val and by literalToken("&") 13 | val or by literalToken("|") 14 | val ws by regexToken("\\s+", ignore = true) 15 | val lpar by literalToken("(") 16 | val rpar by literalToken(")") 17 | 18 | val term by 19 | (id use { Variable(text) }) or 20 | (-not * parser(this::term) map { Not(it) }) or 21 | (-lpar * parser(this::rootParser) * -rpar) 22 | 23 | val andChain by leftAssociative(term, and) { l, _, r -> And(l, r) } 24 | override val rootParser by leftAssociative(andChain, or) { l, _, r -> Or(l, r) } 25 | } 26 | 27 | val ast = booleanGrammar.parseToEnd("a & !b | b & (!a | c)") 28 | ``` 29 | 30 | ### Using with Gradle 31 | 32 | ```groovy 33 | dependencies { 34 | implementation("com.github.h0tk3y.betterParse:better-parse:0.4.4") 35 | } 36 | ``` 37 | 38 | With multiplatform projects, it's OK to add the dependency just to the `commonMain` source set, or some other source set if you want it for specific parts of the code. 39 | 40 | ## Tokens ## 41 | As many other language recognition tools, `better-parse` abstracts away from raw character input by 42 | pre-processing it with a `Tokenizer`, that can match `Token`s (with regular expressions, literal values or arbitrary 43 | against an input character sequence. 44 | 45 | There are several kinds of supported `Token`s: 46 | 47 | * a `regexToken("(?:my)?(?:regex))` is matched as a regular expression; 48 | * a `literalToken("foo")` is matched literally, character to character; 49 | * a `token { (charSequence, from) -> ... }` is matched using the passed function. 50 | 51 | A `Tokenizer` tokenizes an input sequence such as `InputStream` or a `String` into a `Sequence`, providing 52 | each with a position in the input. 53 | 54 | One way to create a `Tokenizer` is to first define the `Tokens` to be matched: 55 | 56 | ```kotlin 57 | val id = regexToken("\\w+") 58 | val cm = literalToken(",") 59 | val ws = regexToken("\\s+", ignore = true) 60 | ``` 61 | 62 | > A `Token` can be ignored by setting its `ignore = true`. An ignored token can still be matched explicitly, but if 63 | another token is expected, the ignored one is just dropped from the sequence. 64 | 65 | ```kotlin 66 | val tokenizer = DefaultTokenizer(listOf(id, cm, ws)) 67 | ``` 68 | 69 | > Note: the tokens order matters in some cases, because the tokenizer tries to match them in exactly this order. 70 | > For instance, if `literalToken("a")` 71 | > is listed before `literalToken("aa")`, the latter will never be matched. Be careful with keyword tokens! 72 | > If you match them with regexes, a word boundary `\b` in the end may help against ambiguity. 73 | 74 | ```kotlin 75 | val tokenMatches: Sequence = tokenizer.tokenize("hello, world") 76 | ``` 77 | 78 | > A more convenient way of defining tokens is described in the [**Grammar**](#grammar) section. 79 | 80 | It is possible to provide a custom implementation of a `Tokenizer`. 81 | 82 | ## Parser ## 83 | 84 | A `Parser` is an object that accepts an input sequence (`TokenMatchesSequence`) and 85 | tries to convert some (from none to all) of its items into a `T`. In `better-parse`, parsers are also 86 | the building blocks used to create new parsers by *combining* them. 87 | 88 | When a parser tries to process the input, there are two possible outcomes: 89 | 90 | * If it succeeds, it returns `Parsed` containing the `T` result and the `nextPosition: Int` that points to what 91 | it left unprocessed. The latter can then be, and often is, passed to another parser. 92 | 93 | * If it fails, it reports the failure returning an `ErrorResult`, which provides detailed information about the failure. 94 | 95 | A very basic parser to start with is a `Token` itself: given an input `TokenMatchesSequence` and a position in it, 96 | it succeeds if the sequence starts with the match of this token itself 97 | _(possibly, skipping some **ignored** tokens)_ and returns that `TokenMatch`, pointing at the next token 98 | with the `nextPosition`. 99 | 100 | ```kotlin 101 | val a = regexToken("a+") 102 | val b = regexToken("b+") 103 | val tokenMatches = DefaultTokenizer(listOf(a, b)).tokenize("aabbaaa") 104 | val result = a.tryParse(tokenMatches, 0) // contains the match for "aa" and the next index is 1 for the match of b 105 | ``` 106 | 107 | ## Combinators ## 108 | 109 | Simpler parsers can be combined to build a more complex parser, from tokens to terms and to the whole language. 110 | There are several kinds of combinators included in `better-parse`: 111 | 112 | * `map`, `use`, `asJust` 113 | 114 | The map combinator takes a successful input of another parser and applies a transforming function to it. 115 | The error results are returned unchanged. 116 | 117 | ```kotlin 118 | val id = regexToken("\\w+") 119 | val aText = a map { it.text } // Parser, returns the matched text from the input sequence 120 | ``` 121 | 122 | A parser for objects of a custom type can be created with `map`: 123 | 124 | ```kotlin 125 | val variable = a map { JavaVariable(name = it.text) } // Parser. 126 | ``` 127 | 128 | * `someParser use { ... }` is a `map` equivalent that takes a function with receiver instead. Example: `id use { text }`. 129 | 130 | * `foo asJust bar` can be used to map a parser to some constant value. 131 | 132 | * `optional(...)` 133 | 134 | Given a `Parser`, tries to parse the sequence with it, but returns a `null` result if the parser failed, and thus never fails itself: 135 | 136 | ```kotlin 137 | val p: Parser = ... 138 | val o = optional(p) // Parser 139 | ``` 140 | 141 | * `and`, `and skip(...)` 142 | 143 | The tuple combinator arranges the parsers in a sequence, so that the remainder of the first one goes to the second one and so on. 144 | If all the parsers succeed, their results are merged into a `Tuple`. If either parser failes, its `ErrorResult` is returned by the combinator. 145 | 146 | ```kotlin 147 | val a: Parser = ... 148 | val b: Parser = ... 149 | val aAndB = a and b // This is a `Parser>` 150 | val bAndBAndA = b and b and a // This is a `Parser>` 151 | ``` 152 | 153 | You can `skip(...)` components in a tuple combinator: the parsers will be called just as well, but their results won't be included in the 154 | resulting tuple: 155 | 156 | ```kotlin 157 | val bbWithoutA = skip(a) and b and skip(a) and b and skip(a) // Parser> 158 | ``` 159 | 160 | > If all the components in an `and` chain are skipped except for one `Parser`, the resulting parser 161 | is `Parser`, not `Parser>`. 162 | 163 | To process the resulting `Tuple`, use the aforementioned `map` and `use`. These parsers are equivalent: 164 | 165 | * ```val fCall = id and skip(lpar) and id and skip(rpar) map { (fName, arg) -> FunctionCall(fName, arg) }``` 166 | 167 | * ```val fCall = id and lpar and id and rpar map { (fName, _, arg, _) -> FunctionCall(fName, arg) }``` 168 | 169 | * ```val fCall = id and lpar and id and rpar use { FunctionCall(t1, t3) }``` 170 | 171 | * ```val fCall = id * -lpar * id * -rpar use { FunctionCall(t1, t2) }``` (see operators below) 172 | 173 | > There are `Tuple` classes up to `Tuple16` and the corresponding `and` overloads. 174 | 175 | ##### Operators 176 | 177 | There are operator overloads for more compact `and` chains definition: 178 | 179 | * `a * b` is equivalent to `a and b`. 180 | 181 | * `-a` is equivalent to `skip(a)`. 182 | 183 | With these operators, the parser `a and skip(b) and skip(c) and d` can also be defined as 184 | `a * -b * -c * d`. 185 | 186 | * `or` 187 | 188 | The alternative combinator tries to parse the sequence with the parsers it combines one by one until one succeeds. If all the parsers fail, 189 | the returned `ErrorResult` is an `AlternativesFailure` instance that contains all the failures from the parsers. 190 | 191 | The result type for the combined parsers is the least common supertype (which is possibly `Any`). 192 | 193 | ```kotlin 194 | val expr = const or variable or fCall 195 | ``` 196 | 197 | * `zeroOrMore(...)`, `oneOrMore(...)`, `N times`, `N timesOrMore`, `N..M times` 198 | 199 | These combinators transform a `Parser` into a `Parser>`, invokng the parser several times and failing if there was not 200 | enough matches. 201 | 202 | ```kotlin 203 | val modifiers = zeroOrMore(functionModifier) 204 | val rectangleParser = 4 times number map { (a, b, c, d) -> Rect(a, b, c, d) } 205 | ``` 206 | 207 | * `separated(term, separator)`, `separatedTerms(term, separator)`, `leftAssociative(...)`, `rightAssociative(...)` 208 | 209 | Combines the two parsers, invoking them in turn and thus parsing a sequence of `term` matches separated by `separator` matches. 210 | 211 | The result is a `Separated` which provides the matches of both parsers (note that terms are one more than separators) and 212 | can also be reduced in either direction. 213 | 214 | ```kotlin 215 | val number: Parser = ... 216 | val sumParser = separated(number, plus) use { reduce { a, _, b -> a + b } } 217 | ``` 218 | 219 | The `leftAssociative` and `rightAssociative` combinators do exactly this, but they take the reducing operation as they are built: 220 | 221 | ```kotlin 222 | val term: Parser 223 | val andChain = leftAssociative(term, andOperator) { l, _, r -> And(l, r) } 224 | ``` 225 | 226 | ## Grammar 227 | 228 | As a convenient way of defining a grammar of a language, there is an abstract class `Grammar`, that collects the `by`-delegated 229 | properties into a `Tokenizer` automatically, and also behaves as a composition of the `Tokenizer` and the `rootParser`. 230 | 231 | *Note:* a `Grammar` also collects `by`-delegated `Parser` properties so that they can be accessed as 232 | `declaredParsers` along with the tokens. As a good style, declare the parsers inside a `Grammar` by delegation as well. 233 | 234 | ```kotlin 235 | interface Item 236 | class Number(val value: Int) : Item 237 | class Variable(val name: String) : Item 238 | 239 | class ItemsParser : Grammar>() { 240 | val num by regexToken("\\d+") 241 | val word by regexToken("[A-Za-z]+") 242 | val comma by regexToken(",\\s+") 243 | 244 | val numParser by num use { Number(text.toInt()) } 245 | val varParser by word use { Variable(text) } 246 | 247 | override val rootParser by separatedTerms(numParser or varParser, comma) 248 | } 249 | 250 | val result: List = ItemsParser().parseToEnd("one, 2, three, 4, five") 251 | ``` 252 | 253 | To use a parser that has not been constructed yet, reference it with `parser { someParser }` or `parser(this::someParser)`: 254 | 255 | ```kotlin 256 | val term by 257 | constParser or 258 | variableParser or 259 | (-lpar and parser(this::term) and -rpar) 260 | ``` 261 | 262 | A `Grammar` implementation can override the `tokenizer` property to provide a custom implementation of `Tokenizer`. 263 | 264 | ## Syntax trees 265 | 266 | A `Parser` can be converted to another `Parser>`, where a `SyntaxTree`, along with the parsed `T` 267 | contains the children syntax trees, the reference to the parser and the positions in the input sequence. 268 | This can be done with `parser.liftToSyntaxTreeParser()`. 269 | 270 | This can be used for syntax highlighting and inspecting the resulting tree in case the parsed result 271 | does not contain the full syntactic structure. 272 | 273 | For convenience, a `Grammar` can also be lifted to that parsing a `SyntaxTree` with 274 | `grammar.liftToSyntaxTreeGrammar()`. 275 | 276 | ```kotlin 277 | val treeGrammar = booleanGrammar.liftToSyntaxTreeGrammar() 278 | val tree = treeGrammar.parseToEnd("a & !b | c -> d") 279 | assertTrue(tree.parser == booleanGrammar.implChain) 280 | val firstChild = tree.children.first() 281 | assertTrue(firstChild.parser == booleanGrammar.orChain) 282 | assertTrue(firstChild.range == 0..9) 283 | ``` 284 | 285 | There are optional arguments for customizing the transformation: 286 | 287 | * `LiftToSyntaxTreeOptions` 288 | * `retainSkipped` — whether the resulting syntax tree should include skipped `and` components; 289 | * `retainSeparators` — whether the `Separated` combinator parsed separators should be included; 290 | * `structureParsers` — defines the parsers that are retained in the syntax tree; the nodes with parsers that are 291 | not in this set are flattened so that their children are attached to their parents in their place. 292 | 293 | For `Parser`, the default is `null`, which means no nodes are flattened. 294 | 295 | In case of `Grammar`, `structureParsers` defaults to the grammar's `declaredParsers`. 296 | 297 | * `transformer` — a strategy to transform non-built-in parsers. If you define your own combinators and want them 298 | to be lifted to syntax tree parsers, pass a `LiftToSyntaxTreeTransformer` that will be called on the parsers. When 299 | a custom combinator nests another parser, a transformer implementation should call `default.transform(...)` on that parser. 300 | 301 | See [`SyntaxTreeDemo.kt`](https://github.com/h0tk3y/better-parse/blob/master/demo/demo-jvm/src/main/kotlin/com/example/SyntaxTreeDemo.kt) for an example of working with syntax trees. 302 | 303 | ## Examples 304 | 305 | * A boolean expressions parser that constructs a simple AST: [`BooleanExpression.kt`](https://github.com/h0tk3y/better-parse/blob/master/demo/demo-jvm/src/main/kotlin/com/example/BooleanExpression.kt) 306 | * An integer arithmetic expressions evaluator: [`ArithmeticsEvaluator.kt`](https://github.com/h0tk3y/better-parse/blob/master/demo/demo-jvm/src/main/kotlin/com/example/ArithmeticsEvaluator.kt) 307 | * A toy programming language parser: [(link)](https://github.com/h0tk3y/compilers-course/blob/master/src/main/kotlin/com/github/h0tk3y/compilersCourse/parsing/Parser.kt) 308 | * A sample JSON parser by [silmeth](https://github.com/silmeth): [(link)](https://github.com/silmeth/jsonParser) 309 | 310 | ## Benchmarks 311 | 312 | See the benchmarks repository [`h0tk3y/better-parse-benchmark`](https://github.com/h0tk3y/better-parse-benchmark) and feel free to contribute. 313 | -------------------------------------------------------------------------------- /benchmarks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | kotlin("plugin.allopen") 4 | id("org.jetbrains.kotlinx.benchmark") 5 | } 6 | 7 | kotlin { 8 | sourceSets { 9 | commonMain { 10 | dependencies { 11 | implementation(rootProject) 12 | implementation(benchmark) 13 | implementation(serialization) 14 | } 15 | } 16 | commonTest { 17 | dependencies { 18 | implementation(kotlin("test")) 19 | } 20 | } 21 | } 22 | 23 | jvm { 24 | compilations["main"].apply { 25 | kotlinOptions.jvmTarget = "1.8" 26 | } 27 | 28 | compilations["test"].defaultSourceSet.dependencies { 29 | implementation(kotlin("test-junit")) 30 | } 31 | } 32 | 33 | js { 34 | nodejs() 35 | 36 | compilations["main"].defaultSourceSet.dependencies { 37 | implementation(kotlin("stdlib-js")) 38 | } 39 | compilations["test"].defaultSourceSet.dependencies { 40 | implementation(kotlin("test-js")) 41 | } 42 | } 43 | 44 | macosX64 { } 45 | linuxX64 { } 46 | mingwX64 { } 47 | } 48 | 49 | allOpen.annotation("org.openjdk.jmh.annotations.State") 50 | 51 | benchmark { 52 | targets.register("jvm") 53 | targets.register("js") 54 | 55 | // TODO: enable Kotlin/Native benchmark once the issue 56 | // with kotlinx.benchmarks 0.3.1 and Kotlin 1.5.21+ compatibility is resolved 57 | // targets.register("macosX64") 58 | // targets.register("linuxX64") 59 | // targets.register("mingwX64") 60 | 61 | configurations["main"].apply { 62 | warmups = 5 63 | iterations = 10 64 | } 65 | } -------------------------------------------------------------------------------- /benchmarks/src/commonMain/kotlin/JsonGrammarBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.benchmark 2 | 3 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 4 | import kotlinx.benchmark.Benchmark 5 | import kotlinx.benchmark.Scope 6 | import kotlinx.benchmark.State 7 | import kotlinx.serialization.json.Json 8 | 9 | @State(Scope.Benchmark) 10 | open class JsonGrammar { 11 | @Benchmark 12 | open fun jsonBetterParseNaive() { 13 | NaiveJsonGrammar().parseToEnd(jsonSample1K) 14 | } 15 | 16 | @Benchmark 17 | open fun jsonBetterParse() { 18 | OptimizedJsonGrammar().parseToEnd(jsonSample1K) 19 | } 20 | 21 | @Benchmark 22 | open fun jsonKotlinxDeserializer() { 23 | Json {}.parseToJsonElement(jsonSample1K) 24 | } 25 | } -------------------------------------------------------------------------------- /benchmarks/src/commonMain/kotlin/NaiveJsonGrammar.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.benchmark 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.lexer.* 5 | import com.github.h0tk3y.betterParse.grammar.Grammar 6 | import com.github.h0tk3y.betterParse.parser.Parser 7 | 8 | class NaiveJsonGrammar : Grammar() { 9 | @Suppress("unused") 10 | private val whiteSpace by regexToken("[\r|\n]|\\s+", ignore = true) 11 | 12 | /* the regex "[^\\"]*(\\["nrtbf\\][^\\"]*)*" matches: 13 | * " – opening double quote, 14 | * [^\\"]* – any number of not escaped characters, nor double quotes 15 | * ( 16 | * \\["nrtbf\\] – backslash followed by special character (\", \n, \r, \\, etc.) 17 | * [^\\"]* – and any number of non-special characters 18 | * )* – repeating as a group any number of times 19 | * " – closing double quote 20 | */ 21 | private val stringLiteral by regexToken("\"[^\\\\\"]*(\\\\[\"nrtbf\\\\][^\\\\\"]*)*\"") 22 | 23 | private val comma by regexToken(",") 24 | private val colon by regexToken(":") 25 | 26 | private val openingBrace by regexToken("\\{") 27 | private val closingBrace by regexToken("}") 28 | 29 | private val openingBracket by regexToken("\\[") 30 | private val closingBracket by regexToken("]") 31 | 32 | private val nullToken by regexToken("\\bnull\\b") 33 | private val trueToken by regexToken("\\btrue\\b") 34 | private val falseToken by regexToken("\\bfalse\\b") 35 | 36 | private val num by regexToken("-?[0-9]*(?:\\.[0-9]*)?") 37 | 38 | private val jsonNull: Parser by nullToken asJust null 39 | private val jsonBool: Parser by (trueToken asJust true) or (falseToken asJust false) 40 | private val string: Parser by (stringLiteral use { text.substring(1, text.lastIndex) }) 41 | 42 | private val number: Parser by 43 | num use { text.toDouble() } 44 | 45 | private val jsonPrimitiveValue: Parser by 46 | jsonNull or jsonBool or string or number 47 | 48 | private val jsonObject: Parser> by 49 | (-openingBrace * separated(string * -colon * this, comma, true) * -closingBrace) 50 | .map { mutableMapOf().apply { it.terms.forEach { put(it.t1, it.t2) } } } 51 | 52 | private val jsonArray: Parser> by 53 | (-openingBracket * separated(this, comma, true) * -closingBracket) 54 | .map { it.terms } 55 | 56 | override val rootParser by jsonPrimitiveValue or jsonObject or jsonArray 57 | } -------------------------------------------------------------------------------- /benchmarks/src/commonMain/kotlin/OptimizedJsonGrammar.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.benchmark 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.grammar.* 5 | import com.github.h0tk3y.betterParse.lexer.* 6 | import com.github.h0tk3y.betterParse.parser.* 7 | 8 | class OptimizedJsonGrammar : Grammar() { 9 | private fun Char.isLetterOrDigit() = 10 | (this in 'a'..'z') || (this in 'A'..'Z') || (this in '0'..'9') 11 | 12 | private fun Char.isDigit() = this in '0'..'9' 13 | 14 | private fun tokenIdent(text: String) = token { it, at -> 15 | if (!it.startsWith(text, at)) return@token 0 16 | if (at + text.length > it.length && it[at + text.length].isLetterOrDigit()) return@token 0 17 | text.length 18 | } 19 | 20 | private fun tokenNumber() = token { it, at -> 21 | var index = at 22 | val maybeSign = it[index] 23 | val sign = if (maybeSign == '+' || maybeSign == '-') { 24 | index++ 25 | true 26 | } else 27 | false 28 | 29 | val length = it.length 30 | while (index < length && it[index].isDigit()) 31 | index++ 32 | 33 | if (index < length && it[index] == '.') { // decimal 34 | index++ 35 | while (index < length && it[index].isDigit()) 36 | index++ 37 | } 38 | if (index == at || (index == at + 1 && sign)) return@token 0 39 | index - at 40 | } 41 | 42 | @Suppress("unused") 43 | private val whiteSpace by token(ignored = true) { it, at -> 44 | var index = at 45 | val length = it.length 46 | while (index < length && it[index].isWhitespace()) 47 | index++ 48 | index - at 49 | } 50 | 51 | /* the regex "[^\\"]*(\\["nrtbf\\][^\\"]*)*" matches: 52 | * " – opening double quote, 53 | * [^\\"]* – any number of not escaped characters, nor double quotes 54 | * ( 55 | * \\["nrtbf\\] – backslash followed by special character (\", \n, \r, \\, etc.) 56 | * [^\\"]* – and any number of non-special characters 57 | * )* – repeating as a group any number of times 58 | * " – closing double quote 59 | */ 60 | private val stringLiteral by token { it, at -> 61 | var index = at 62 | if (it[index++] != '"') return@token 0 63 | val length = it.length 64 | while (index < length && it[index] != '"') { 65 | if (it[index] == '\\') { // quote 66 | index++ 67 | } 68 | index++ 69 | } 70 | if (index == length) return@token 0 // unclosed string 71 | index + 1 - at 72 | } 73 | 74 | private val comma by literalToken(",") 75 | private val colon by literalToken(":") 76 | 77 | private val openingBrace by literalToken("{") 78 | private val closingBrace by literalToken("}") 79 | 80 | private val openingBracket by literalToken("[") 81 | private val closingBracket by literalToken("]") 82 | 83 | private val nullToken by tokenIdent("null") 84 | private val trueToken by tokenIdent("true") 85 | private val falseToken by tokenIdent("false") 86 | 87 | private val num by tokenNumber() 88 | 89 | private val jsonNull: Parser by nullToken asJust null 90 | private val jsonBool: Parser by (trueToken asJust true) or (falseToken asJust false) 91 | private val string: Parser by (stringLiteral use { input.substring(offset + 1, offset + length - 1) }) 92 | 93 | private val number: Parser by num use { text.toDouble() } 94 | 95 | private val jsonPrimitiveValue: Parser by jsonNull or jsonBool or string or number 96 | 97 | private val jsonObject: Parser> by 98 | (-openingBrace * separated(string * -colon * this, comma, true) * -closingBrace) 99 | .map { mutableMapOf().apply { it.terms.forEach { put(it.t1, it.t2) } } } 100 | 101 | private val jsonArray: Parser> by 102 | (-openingBracket * separated(this, comma, true) * -closingBracket) 103 | .map { it.terms } 104 | 105 | override val rootParser by jsonPrimitiveValue or jsonObject or jsonArray 106 | } -------------------------------------------------------------------------------- /benchmarks/src/commonTest/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.benchmark 2 | 3 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 4 | import kotlin.test.Ignore 5 | import kotlin.test.Test 6 | 7 | @Ignore 8 | class Main { 9 | val repeat = 1000_000_000 10 | 11 | @Test 12 | fun testNaive() { 13 | NaiveJsonGrammar().parseToEnd(jsonSample1K) 14 | } 15 | 16 | @Test 17 | fun testOptimized() { 18 | repeat(repeat) { 19 | OptimizedJsonGrammar().parseToEnd(jsonSample1K) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.taskdefs.condition.Os 2 | import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeTargetPreset 3 | import java.net.URI 4 | 5 | plugins { 6 | kotlin("multiplatform") 7 | 8 | id("maven-publish") 9 | id("signing") 10 | } 11 | 12 | kotlin { 13 | explicitApi() 14 | 15 | sourceSets { 16 | all { 17 | languageSettings.optIn("kotlin.ExperimentalMultiplatform") 18 | } 19 | 20 | commonTest { 21 | dependencies { 22 | implementation(kotlin("test-common")) 23 | implementation(kotlin("test-annotations-common")) 24 | } 25 | } 26 | 27 | create("nativeMain") { 28 | dependsOn(commonMain.get()) 29 | } 30 | } 31 | 32 | jvm { 33 | compilations["test"].defaultSourceSet.dependencies { 34 | implementation(kotlin("test-junit")) 35 | } 36 | compilations.all { 37 | kotlinOptions.jvmTarget = "1.6" 38 | } 39 | } 40 | 41 | js(BOTH) { 42 | browser() 43 | nodejs() 44 | 45 | compilations["test"].defaultSourceSet.dependencies { 46 | implementation(kotlin("test-js")) 47 | } 48 | compilations.all { 49 | kotlinOptions.moduleKind = "umd" 50 | } 51 | } 52 | 53 | presets.withType>().forEach { 54 | targetFromPreset(it) { 55 | compilations.getByName("main") { 56 | defaultSourceSet.dependsOn(sourceSets["nativeMain"]) 57 | } 58 | } 59 | } 60 | } 61 | 62 | //region Code generation 63 | 64 | val codegen by tasks.registering { 65 | val maxTupleSize = 16 66 | 67 | andCodegen( 68 | maxTupleSize, 69 | kotlin.sourceSets.commonMain.get().kotlin.srcDirs.first().absolutePath + "/generated/andFunctions.kt" 70 | ) 71 | tupleCodegen( 72 | maxTupleSize, 73 | kotlin.sourceSets.commonMain.get().kotlin.srcDirs.first().absolutePath + "/generated/tuples.kt" 74 | ) 75 | } 76 | 77 | kotlin.sourceSets.commonMain { 78 | kotlin.srcDirs(files().builtBy(codegen)) 79 | } 80 | 81 | //endregion 82 | 83 | //region Publication 84 | 85 | val publicationsFromWindows = listOf("mingwX64", "mingwX86") 86 | 87 | val publicationsFromMacos = 88 | kotlin.targets.names.filter { 89 | it.startsWith("macos") || it.startsWith("ios") || it.startsWith("watchos") || it.startsWith("tvos") 90 | } 91 | 92 | val publicationsFromLinux = publishing.publications.names - publicationsFromWindows - publicationsFromMacos 93 | 94 | val publicationsFromThisPlatform = when { 95 | Os.isFamily(Os.FAMILY_WINDOWS) -> publicationsFromWindows 96 | Os.isFamily(Os.FAMILY_MAC) -> publicationsFromMacos 97 | Os.isFamily(Os.FAMILY_UNIX) -> publicationsFromLinux 98 | else -> error("Expected Windows, Mac, or Linux host") 99 | } 100 | 101 | tasks.withType(AbstractPublishToMaven::class).all { 102 | onlyIf { publication.name in publicationsFromThisPlatform } 103 | } 104 | 105 | publishing { 106 | repositories { 107 | maven { 108 | name = "central" 109 | val sonatypeUsername = "h0tk3y" 110 | url = URI("https://oss.sonatype.org/service/local/staging/deploy/maven2") 111 | 112 | credentials { 113 | username = sonatypeUsername 114 | password = findProperty("sonatypePassword") as? String 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Add a Javadoc JAR to each publication as required by Maven Central: 121 | 122 | val javadocJar by tasks.creating(Jar::class) { 123 | archiveClassifier.value("javadoc") 124 | // TODO: instead of a single empty Javadoc JAR, generate real documentation for each module 125 | } 126 | 127 | publishing { 128 | publications.withType().all { 129 | artifact(javadocJar) 130 | } 131 | } 132 | 133 | fun customizeForMavenCentral(pom: org.gradle.api.publish.maven.MavenPom) = pom.withXml { 134 | fun groovy.util.Node.add(key: String, value: String) { 135 | appendNode(key).setValue(value) 136 | } 137 | 138 | fun groovy.util.Node.node(key: String, content: groovy.util.Node.() -> Unit) { 139 | appendNode(key).also(content) 140 | } 141 | 142 | asNode().run { 143 | add("name", "better-parse") 144 | add( 145 | "description", 146 | "A library that provides a set of parser combinator tools for building parsers and translators in Kotlin." 147 | ) 148 | add("url", "https://github.com/h0tk3y/better-parse") 149 | node("organization") { 150 | add("name", "com.github.h0tk3y") 151 | add("url", "https://github.com/h0tk3y") 152 | } 153 | node("issueManagement") { 154 | add("system", "github") 155 | add("url", "https://github.com/h0tk3y/better-parse/issues") 156 | } 157 | node("licenses") { 158 | node("license") { 159 | add("name", "Apache License 2.0") 160 | add("url", "https://raw.githubusercontent.com/h0tk3y/better-parse/master/LICENSE") 161 | add("distribution", "repo") 162 | } 163 | } 164 | node("scm") { 165 | add("url", "https://github.com/h0tk3y/better-parse") 166 | add("connection", "scm:git:git://github.com/h0tk3y/better-parse") 167 | add("developerConnection", "scm:git:ssh://github.com/h0tk3y/better-parse.git") 168 | } 169 | node("developers") { 170 | node("developer") { 171 | add("name", "h0tk3y") 172 | } 173 | } 174 | } 175 | } 176 | 177 | publishing { 178 | publications.withType().all { 179 | customizeForMavenCentral(pom) 180 | 181 | // Signing requires that 182 | // `signing.keyId`, `signing.password`, and `signing.secretKeyRingFile` are provided as Gradle properties 183 | signing.sign(this@all) 184 | } 185 | } 186 | 187 | //endregion -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // TODO: setup the version in the same way as in the root project once IJ fixes importing of buildSrc 3 | kotlin("jvm").version("1.4.21") 4 | } 5 | 6 | repositories { 7 | // TODO: unify repository setup with the root project once IJ fixes importing of buildSrc 8 | mavenCentral() 9 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/AndCodegen.kt: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | 3 | fun andCodegen(maxN: Int, outputFile: String) { 4 | fun genericsStr(i: Int) = (1..i).joinToString(prefix = "<", postfix = ">") { "T$it" } 5 | 6 | val resultCode = buildString { 7 | appendLine("@file:Suppress(") 8 | appendLine(" \"NO_EXPLICIT_RETURN_TYPE_IN_API_MODE\", // fixme: bug in Kotlin 1.4.21, fixed in 1.4.30") 9 | appendLine(" \"MoveLambdaOutsideParentheses\", ") 10 | appendLine(" \"PackageDirectoryMismatch\"") 11 | appendLine(")") 12 | appendLine() 13 | 14 | appendLine("package com.github.h0tk3y.betterParse.combinators") 15 | appendLine("import com.github.h0tk3y.betterParse.utils.*") 16 | appendLine("import com.github.h0tk3y.betterParse.parser.*") 17 | appendLine("import kotlin.jvm.JvmName") 18 | appendLine() 19 | 20 | for (i in 2 until maxN) { 21 | val generics = genericsStr(i) 22 | 23 | val reifiedNext = (1..i + 1).joinToString { "reified T$it" } 24 | val casts = (1..i + 1).joinToString { "it[${it - 1}] as T$it" } 25 | 26 | appendLine( 27 | """ 28 | @JvmName("and$i") public inline infix fun <$reifiedNext> 29 | AndCombinator.and(p${i + 1}: Parser) 30 | // : AndCombinator = 31 | = AndCombinator(consumersImpl + p${i + 1}, { 32 | Tuple${i + 1}($casts) 33 | }) 34 | """.trimIndent() + "\n" 35 | ) 36 | 37 | appendLine( 38 | """ 39 | @JvmName("and$i${"Operator"}") public inline operator fun <$reifiedNext> 40 | AndCombinator.times(p${i + 1}: Parser) 41 | // : AndCombinator = 42 | = this and p${i + 1} 43 | """.trimIndent() + "\n\n" 44 | ) 45 | } 46 | } 47 | 48 | File(outputFile).apply { 49 | parentFile.mkdirs() 50 | writeText(resultCode) 51 | } 52 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/MultiplatformLibraryDependency.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | 3 | val Project.kotlinVersion: String 4 | get() = project.property("kotlinVersion").toString() 5 | 6 | val Project.benchmarkVersion: String 7 | get() = project.property("benchmarkVersion").toString() 8 | 9 | val Project.serializationVersion: String 10 | get() = project.property("serializationVersion").toString() 11 | 12 | private const val serializationJsonPrefix = "org.jetbrains.kotlinx:kotlinx-serialization-json" 13 | private const val benchmarksRuntimePrefix = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime" 14 | 15 | data class MultiplatformLibraryDependency( 16 | val rootModule: String, 17 | val version: String 18 | ) { 19 | val notation = "$rootModule:$version" 20 | } 21 | 22 | val Project.benchmark get() = 23 | MultiplatformLibraryDependency(benchmarksRuntimePrefix, benchmarkVersion).notation 24 | 25 | val Project.serialization get() = 26 | MultiplatformLibraryDependency(serializationJsonPrefix, serializationVersion).notation 27 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/TupleCodegen.kt: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | 3 | fun tupleCodegen(maxN: Int, outputFile: String) { 4 | fun genericsStr(i: Int) = (1..i).joinToString(prefix = "<", postfix = ">") { "T$it" } 5 | 6 | val resultCode = buildString { 7 | appendLine("package com.github.h0tk3y.betterParse.utils\n") 8 | 9 | for (i in 1..maxN) { 10 | val generics = genericsStr(i) 11 | val ctorParameters = (1..i).joinToString { "val t$it: T$it" } 12 | val components = (1..i).joinToString { "t$it" } 13 | val genericsBoundByT = (1..i).joinToString { "T$it : T" } 14 | 15 | appendLine( 16 | """ 17 | public data class Tuple$i$generics($ctorParameters) : Tuple 18 | public val Tuple$i$generics.components: List get() = listOf($components) 19 | """.trimIndent() 20 | ) 21 | 22 | appendLine() 23 | } 24 | } 25 | 26 | File(outputFile).apply { 27 | parentFile.mkdirs() 28 | writeText(resultCode) 29 | } 30 | } -------------------------------------------------------------------------------- /demo/demo-js/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("js") 3 | } 4 | 5 | dependencies { 6 | implementation(rootProject) 7 | } 8 | 9 | kotlin.js().nodejs() 10 | 11 | var assembleWeb = task("assembleWeb") { 12 | val main by kotlin.js().compilations.getting 13 | 14 | from(project.provider { 15 | main.compileDependencyFiles.map { it.absolutePath }.map(::zipTree).map { 16 | it.matching { 17 | include("*.js") 18 | exclude("**/META-INFΩ/**") 19 | } 20 | } 21 | }) 22 | 23 | from(main.compileKotlinTaskProvider.map { it.destinationDir }) 24 | from(kotlin.sourceSets.main.get().resources) { include("*.html") } 25 | into("${buildDir}/web") 26 | } 27 | 28 | tasks.assemble { 29 | dependsOn(assembleWeb) 30 | } -------------------------------------------------------------------------------- /demo/demo-js/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.Grammar 4 | import com.github.h0tk3y.betterParse.grammar.parser 5 | import com.github.h0tk3y.betterParse.grammar.tryParseToEnd 6 | import com.github.h0tk3y.betterParse.lexer.literalToken 7 | import com.github.h0tk3y.betterParse.lexer.regexToken 8 | import com.github.h0tk3y.betterParse.parser.ErrorResult 9 | import com.github.h0tk3y.betterParse.parser.Parsed 10 | import com.github.h0tk3y.betterParse.parser.Parser 11 | import org.w3c.dom.HTMLInputElement 12 | import kotlinx.browser.document 13 | import kotlinx.browser.window 14 | 15 | fun main() { 16 | window.onload = { 17 | val parseButton = document.getElementById("parse")!! 18 | val exprInput = document.getElementById("expr") as HTMLInputElement 19 | val result = document.getElementById("result")!! 20 | parseButton.addEventListener("click", { 21 | val expr = exprInput.value 22 | val parseResult = BooleanGrammar.tryParseToEnd(expr) 23 | 24 | val resultText = when (parseResult) { 25 | is Parsed -> parseResult.value.toString() 26 | is ErrorResult -> parseResult.toString() 27 | } 28 | 29 | result.textContent = resultText 30 | }) 31 | } 32 | } 33 | 34 | sealed class BooleanExpression 35 | 36 | object TRUE : BooleanExpression() { 37 | override fun toString() = "TRUE" 38 | } 39 | 40 | object FALSE : BooleanExpression() { 41 | override fun toString(): String = "FALSE" 42 | } 43 | 44 | data class Variable(val name: String) : BooleanExpression() 45 | data class Not(val body: BooleanExpression) : BooleanExpression() 46 | data class And(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 47 | data class Or(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 48 | data class Impl(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 49 | 50 | private object BooleanGrammar : Grammar() { 51 | val tru by literalToken("true") 52 | val fal by literalToken("false") 53 | val id by regexToken("\\w+") 54 | val lpar by literalToken("(") 55 | val rpar by literalToken(")") 56 | val not by literalToken("!") 57 | val and by literalToken("&") 58 | val or by literalToken("|") 59 | val impl by literalToken("->") 60 | val ws by regexToken("\\s+", ignore = true) 61 | 62 | val negation by -not * parser(this::term) map { Not(it) } 63 | val bracedExpression by -lpar * parser(this::implChain) * -rpar 64 | 65 | val term: Parser by 66 | (tru asJust TRUE) or 67 | (fal asJust FALSE) or 68 | (id map { Variable(it.text) }) or 69 | negation or 70 | bracedExpression 71 | 72 | val andChain by leftAssociative(term, and) { a, _, b -> And(a, b) } 73 | val orChain by leftAssociative(andChain, or) { a, _, b -> Or(a, b) } 74 | val implChain by rightAssociative(orChain, impl) { a, _, b -> Impl(a, b) } 75 | 76 | override val rootParser by implChain 77 | } -------------------------------------------------------------------------------- /demo/demo-js/src/main/resources/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | better-parse JS demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Boolean expression parser

15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/demo-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | implementation(rootProject) 7 | implementation(kotlin("stdlib")) 8 | } -------------------------------------------------------------------------------- /demo/demo-jvm/src/main/kotlin/com/example/ArithmeticsEvaluator.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 6 | import com.github.h0tk3y.betterParse.grammar.parser 7 | import com.github.h0tk3y.betterParse.lexer.literalToken 8 | import com.github.h0tk3y.betterParse.lexer.regexToken 9 | import com.github.h0tk3y.betterParse.parser.Parser 10 | import kotlin.math.pow 11 | 12 | class ArithmeticsEvaluator : Grammar() { 13 | val num by regexToken("-?\\d+") 14 | val lpar by literalToken("(") 15 | val rpar by literalToken(")") 16 | val mul by literalToken("*") 17 | val pow by literalToken("^") 18 | val div by literalToken("/") 19 | val minus by literalToken("-") 20 | val plus by literalToken("+") 21 | val ws by regexToken("\\s+", ignore = true) 22 | 23 | val number by num use { text.toInt() } 24 | val term: Parser by number or 25 | (skip(minus) and parser(::term) map { -it }) or 26 | (skip(lpar) and parser(::rootParser) and skip(rpar)) 27 | 28 | val powChain by leftAssociative(term, pow) { a, _, b -> a.toDouble().pow(b.toDouble()).toInt() } 29 | 30 | val divMulChain by leftAssociative(powChain, div or mul use { type }) { a, op, b -> 31 | if (op == div) a / b else a * b 32 | } 33 | 34 | val subSumChain by leftAssociative(divMulChain, plus or minus use { type }) { a, op, b -> 35 | if (op == plus) a + b else a - b 36 | } 37 | 38 | override val rootParser: Parser by subSumChain 39 | } 40 | 41 | fun main(args: Array) { 42 | val expr = "1 + 2 * (3 - 1^1) - 2^2^2 * (1 + 1)" 43 | val result = ArithmeticsEvaluator().parseToEnd(expr) 44 | println(result) 45 | } -------------------------------------------------------------------------------- /demo/demo-jvm/src/main/kotlin/com/example/BooleanExpression.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 6 | import com.github.h0tk3y.betterParse.grammar.parser 7 | import com.github.h0tk3y.betterParse.lexer.literalToken 8 | import com.github.h0tk3y.betterParse.lexer.regexToken 9 | import com.github.h0tk3y.betterParse.parser.Parser 10 | 11 | sealed class BooleanExpression 12 | 13 | object TRUE : BooleanExpression() 14 | object FALSE : BooleanExpression() 15 | data class Variable(val name: String) : BooleanExpression() 16 | data class Not(val body: BooleanExpression) : BooleanExpression() 17 | data class And(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 18 | data class Or(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 19 | data class Impl(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 20 | 21 | object BooleanGrammar : Grammar() { 22 | val tru by literalToken("true") 23 | val fal by literalToken("false") 24 | val id by regexToken("\\w+") 25 | val lpar by literalToken("(") 26 | val rpar by literalToken(")") 27 | val not by literalToken("!") 28 | val and by literalToken("&") 29 | val or by literalToken("|") 30 | val impl by literalToken("->") 31 | val ws by regexToken("\\s+", ignore = true) 32 | 33 | val negation by -not * parser(this::term) map { Not(it) } 34 | val bracedExpression by -lpar * parser(this::implChain) * -rpar 35 | 36 | val term: Parser by 37 | (tru asJust TRUE) or 38 | (fal asJust FALSE) or 39 | (id map { Variable(it.text) }) or 40 | negation or 41 | bracedExpression 42 | 43 | val andChain by leftAssociative(term, and) { a, _, b -> And(a, b) } 44 | val orChain by leftAssociative(andChain, or) { a, _, b -> Or(a, b) } 45 | val implChain by rightAssociative(orChain, impl) { a, _, b -> Impl(a, b) } 46 | 47 | override val rootParser by implChain 48 | } 49 | 50 | fun main(args: Array) { 51 | val expr = "a & (b1 -> c1) | a1 & !b | !(a1 -> a2) -> a" 52 | println(BooleanGrammar.parseToEnd(expr)) 53 | } -------------------------------------------------------------------------------- /demo/demo-jvm/src/main/kotlin/com/example/SyntaxTreeDemo.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.github.h0tk3y.betterParse.grammar.tryParseToEnd 4 | import com.github.h0tk3y.betterParse.parser.ErrorResult 5 | import com.github.h0tk3y.betterParse.parser.Parsed 6 | import com.github.h0tk3y.betterParse.st.SyntaxTree 7 | import com.github.h0tk3y.betterParse.st.liftToSyntaxTreeGrammar 8 | 9 | fun main() { 10 | val exprs = listOf("a -> b | !c", 11 | "a & !b | (a -> a & b) -> a | b | a & b", 12 | "a & !(b -> a | c) | (c -> d) & !(!c -> !d & a)") 13 | 14 | val readExprSequence = generateSequence { 15 | print("Enter a boolean expression:") 16 | val result = readLine() 17 | if (result.isNullOrBlank()) null else result 18 | } 19 | 20 | (exprs.asSequence() + readExprSequence).forEach { parseAndPrintTree(it); println("\n") } 21 | } 22 | 23 | val booleanSyntaxTreeGrammar = BooleanGrammar.liftToSyntaxTreeGrammar() 24 | 25 | fun parseAndPrintTree(expr: String) { 26 | println(expr) 27 | 28 | when (val result = booleanSyntaxTreeGrammar.tryParseToEnd(expr)) { 29 | is ErrorResult -> println("Could not parse expression: $result") 30 | is Parsed> -> printSyntaxTree(expr, result.value) 31 | } 32 | } 33 | 34 | fun printSyntaxTree(expr: String, syntaxTree: SyntaxTree<*>) { 35 | var currentLayer: List> = listOf(syntaxTree) 36 | while (currentLayer.isNotEmpty()) { 37 | val underscores = currentLayer.flatMap { t -> t.range.map { index -> index to charByTree(t) } }.toMap() 38 | val underscoreStr = expr.indices.map { underscores[it] ?: ' ' }.joinToString("") 39 | println(underscoreStr) 40 | 41 | currentLayer = currentLayer.flatMap { it.children } 42 | } 43 | } 44 | 45 | fun charByTree(tree: SyntaxTree<*>): Char = with(BooleanGrammar) { 46 | when (tree.parser) { 47 | id -> 'i' 48 | and, andChain -> '&' 49 | or, orChain -> '|' 50 | impl, implChain -> '>' 51 | not, negation -> '!' 52 | term -> 't' 53 | bracedExpression -> '(' 54 | else -> ' ' 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /demo/demo-native/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | } 6 | 7 | kotlin { 8 | macosX64("macos") 9 | linuxX64("linux") 10 | mingwX64("windows") 11 | 12 | sourceSets { 13 | val commonMain by getting { 14 | dependencies { 15 | api(rootProject) 16 | } 17 | } 18 | 19 | val nativeMain by creating { 20 | dependsOn(commonMain) 21 | } 22 | 23 | kotlin.targets.withType().all { 24 | compilations.getByName("main") { 25 | defaultSourceSet.dependsOn(nativeMain) 26 | binaries.executable { } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demo/demo-native/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.native.enableDependencyPropagation=false -------------------------------------------------------------------------------- /demo/demo-native/src/nativeMain/kotlin/commandLineParser.kt: -------------------------------------------------------------------------------- 1 | import com.github.h0tk3y.betterParse.combinators.* 2 | import com.github.h0tk3y.betterParse.grammar.Grammar 3 | import com.github.h0tk3y.betterParse.grammar.parser 4 | import com.github.h0tk3y.betterParse.grammar.tryParseToEnd 5 | import com.github.h0tk3y.betterParse.lexer.literalToken 6 | import com.github.h0tk3y.betterParse.lexer.regexToken 7 | import com.github.h0tk3y.betterParse.parser.ErrorResult 8 | import com.github.h0tk3y.betterParse.parser.Parsed 9 | import com.github.h0tk3y.betterParse.parser.Parser 10 | 11 | fun main() { 12 | println("Enter a boolean expression:") 13 | 14 | val expr = readLine().orEmpty() 15 | 16 | val resultText = when (val parseResult = BooleanGrammar().tryParseToEnd(expr)) { 17 | is Parsed -> parseResult.value.toString() 18 | is ErrorResult -> parseResult.toString() 19 | } 20 | 21 | println(resultText) 22 | } 23 | 24 | sealed class BooleanExpression 25 | 26 | object TRUE : BooleanExpression() 27 | object FALSE : BooleanExpression() 28 | data class Variable(val name: String) : BooleanExpression() 29 | data class Not(val body: BooleanExpression) : BooleanExpression() 30 | data class And(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 31 | data class Or(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 32 | data class Impl(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 33 | 34 | private class BooleanGrammar : Grammar() { 35 | val tru by literalToken("true") 36 | val fal by literalToken("false") 37 | val id by regexToken("\\w+") 38 | val lpar by literalToken("(") 39 | val rpar by literalToken(")") 40 | val not by literalToken("!") 41 | val and by literalToken("&") 42 | val or by literalToken("|") 43 | val impl by literalToken("->") 44 | val ws by regexToken("\\s+", ignore = true) 45 | 46 | val negation by -not * parser(this::term) map { Not(it) } 47 | val bracedExpression by -lpar * parser { implChain } * -rpar 48 | 49 | val term: Parser by 50 | (tru asJust TRUE) or 51 | (fal asJust FALSE) or 52 | (id map { Variable(it.text) }) or 53 | negation or 54 | bracedExpression 55 | 56 | val andChain by leftAssociative(term, and) { a, _, b -> And(a, b) } 57 | val orChain by leftAssociative(andChain, or) { a, _, b -> Or(a, b) } 58 | val implChain by rightAssociative(orChain, impl) { a, _, b -> Impl(a, b) } 59 | 60 | override val rootParser by implChain 61 | } 62 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.github.h0tk3y.betterParse 2 | 3 | kotlin.code.style=official 4 | 5 | kotlin.mpp.enableCompatibilityMetadataVariant=true 6 | kotlin.mpp.stability.nowarn=true 7 | 8 | # Workaround for Bintray treating .sha512 files as artifacts 9 | # https://github.com/gradle/gradle/issues/11412 10 | systemProp.org.gradle.internal.publish.checksums.insecure=true 11 | 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0tk3y/better-parse/af4599c04f84463a4b708e7e1385217b41ae7b9e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.gradle.api.artifacts.dsl.RepositoryHandler 4 | 5 | pluginManagement { 6 | abstract class RepositorySetup : 7 | BuildServiceParameters, (RepositoryHandler, Boolean) -> Unit, BuildService { 8 | override fun invoke(repositories: RepositoryHandler, isPlugins: Boolean): Unit = with(repositories) { 9 | mavenCentral() 10 | if (isPlugins) { 11 | gradlePluginPortal() 12 | } 13 | } 14 | } 15 | 16 | val configureRepositories = gradle.sharedServices.registerIfAbsent("repositories", RepositorySetup::class) { }.get() 17 | 18 | configureRepositories(repositories, true) 19 | gradle.allprojects { configureRepositories(repositories, false) } 20 | 21 | apply(from = "versions.settings.gradle.kts") 22 | val kotlinVersion: String by settings 23 | val benchmarkVersion: String by settings 24 | 25 | plugins { 26 | kotlin("multiplatform").version(kotlinVersion) 27 | kotlin("js").version(kotlinVersion) 28 | kotlin("plugin.allopen").version(kotlinVersion) 29 | id("org.jetbrains.kotlinx.benchmark").version(benchmarkVersion) 30 | } 31 | } 32 | 33 | rootProject.name = "better-parse" 34 | 35 | include(":benchmarks", ":demo:demo-jvm", ":demo:demo-js", ":demo:demo-native") 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/AndCombinator.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 4 | import com.github.h0tk3y.betterParse.parser.* 5 | import com.github.h0tk3y.betterParse.utils.Tuple2 6 | import kotlin.jvm.JvmName 7 | 8 | /** Parses the sequence with the receiver [Parser] and then with the [other] parser. If both succeed, returns a [Tuple2] 9 | * with the values from the [Parsed] results. Otherwise, returns the [ErrorResult] of the failed parser. */ 10 | public inline infix fun Parser
.and(other: Parser): AndCombinator> = 11 | AndCombinator(listOf(this, other)) { (a1, a2) -> Tuple2(a1 as A, a2 as B) } 12 | 13 | /** The same as `this `[and]` other`*/ 14 | public inline operator fun Parser.times(other: Parser): AndCombinator> = 15 | this and other 16 | 17 | /** Parses the sequence with the receiver [Parser] and then with the [other] parser. If both succeed, returns a [Tuple2] 18 | * with the values from the [Parsed] results. Otherwise, returns the [ErrorResult] of the failed parser. */ 19 | @JvmName("and0") 20 | public inline infix fun AndCombinator.and(other: Parser): AndCombinator> = 21 | AndCombinator(consumersImpl + listOf(other)) { (a1, a2) -> Tuple2(a1 as A, a2 as B) } 22 | 23 | /** The same as `this `[and]` other`*/ 24 | public inline operator fun AndCombinator.times(other: Parser): AndCombinator> = 25 | this and other 26 | 27 | public class AndCombinator @PublishedApi internal constructor( 28 | @PublishedApi internal val consumersImpl: List, 29 | internal val transform: (List) -> R 30 | ) : Parser { 31 | 32 | @Deprecated("Use parsers or skipParsers instead to get the type-safe results.") 33 | public val consumers: List 34 | get() = consumersImpl 35 | 36 | public val parsers: List?> 37 | get() = consumersImpl.map { it as? Parser<*> } 38 | 39 | public val skipParsers: List 40 | get() = consumersImpl.map { it as? SkipParser } 41 | 42 | internal val nonSkippedIndices = consumersImpl.indices.filter { consumersImpl[it] !is SkipParser } 43 | 44 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult { 45 | var nextPosition = fromPosition 46 | 47 | var results: ArrayList? = null 48 | loop@ for (index in 0 until consumersImpl.size) { 49 | val consumer = consumersImpl[index] 50 | when (consumer) { 51 | is Parser<*> -> { 52 | val result = consumer.tryParse(tokens, nextPosition) 53 | when (result) { 54 | is ErrorResult -> return result 55 | is Parsed<*> -> { 56 | (results ?: ArrayList().also { results = it }).add(result.value) 57 | nextPosition = result.nextPosition 58 | } 59 | } 60 | } 61 | is SkipParser -> { 62 | val result = consumer.innerParser.tryParse(tokens, nextPosition) 63 | when (result) { 64 | is ErrorResult -> return result 65 | is Parsed<*> -> nextPosition = result.nextPosition 66 | } 67 | } 68 | else -> throw IllegalArgumentException() 69 | } 70 | } 71 | 72 | return ParsedValue(transform(results ?: emptyList()), nextPosition) 73 | } 74 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/MapCombinator.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | 6 | /** Parses the sequence with [innerParser], and if that succeeds, maps its [Parsed] result with [transform]. 7 | * Returns the [ErrorResult] of the `innerParser` otherwise. 8 | * @sample MapTest*/ 9 | public class MapCombinator( 10 | public val innerParser: Parser, 11 | public val transform: (T) -> R 12 | ) : Parser { 13 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult { 14 | val innerResult = innerParser.tryParse(tokens, fromPosition) 15 | return when (innerResult) { 16 | is ErrorResult -> innerResult 17 | is Parsed -> ParsedValue(transform(innerResult.value), innerResult.nextPosition) 18 | } 19 | } 20 | } 21 | 22 | /** Applies the [transform] function to the successful results of the receiver parser. See [MapCombinator]. */ 23 | public infix fun Parser.map(transform: (A) -> T): Parser = MapCombinator(this, transform) 24 | 25 | /** Applies the [transform] extension to the successful results of the receiver parser. See [MapCombinator]. */ 26 | public infix fun Parser.use(transform: A.() -> T): Parser = MapCombinator(this, transform) 27 | 28 | /** Replaces the [Parsed] result of the receiver parser with the provided [value]. */ 29 | public infix fun Parser.asJust(value: T): MapCombinator = MapCombinator(this) { value } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/OptionalCombinator.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | 6 | /** Tries to parse the sequence with [parser], and if that fails, returns [Parsed] of null instead. */ 7 | public class OptionalCombinator(public val parser: Parser) : 8 | Parser { 9 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult { 10 | val result = parser.tryParse(tokens, fromPosition) 11 | return when (result) { 12 | is ErrorResult -> ParsedValue(null, fromPosition) 13 | is Parsed -> result 14 | } 15 | } 16 | } 17 | 18 | /** Uses [parser] and if that fails returns [Parsed] of null. */ 19 | public fun optional(parser: Parser): Parser = OptionalCombinator(parser) -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/OrCombinator.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | 6 | /** Tries to parse the sequence with the [parsers] until one succeeds. Returns its [Parsed] result in this case. 7 | * If none succeeds, returns the [AlternativesFailure] with all the [ErrorResult]s. */ 8 | public class OrCombinator(public val parsers: List>) : 9 | Parser { 10 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult { 11 | var failures: ArrayList? = null 12 | for (index in 0 until parsers.size) { 13 | val result = parsers[index].tryParse(tokens, fromPosition) 14 | when (result) { 15 | is Parsed -> return result 16 | is ErrorResult -> { 17 | if (failures == null) 18 | failures = ArrayList() 19 | failures.add(result) 20 | } 21 | } 22 | } 23 | return AlternativesFailure(failures.orEmpty()) 24 | } 25 | } 26 | 27 | /** Parse the sequence with either the receiver [Parser] or the [other] parser. See [OrCombinator] */ 28 | public infix fun Parser.or(other: Parser): Parser { 29 | val leftParsers = if (this is OrCombinator) parsers else listOf(this) 30 | val rightParsers = if (other is OrCombinator) other.parsers else listOf(other) 31 | return OrCombinator(leftParsers + rightParsers) 32 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/RepeatCombinator.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | 6 | /** Tries to parse the sequence with the [parser], as many times as it succeeds, but no more than [atMost]. 7 | * If the parser succeeded less than [atLeast] times, returns its [ErrorResult], otherwise returns the list of [Parsed] 8 | * results from the parser invocations.*/ 9 | public class RepeatCombinator internal constructor( 10 | public val parser: Parser, 11 | public val atLeast: Int = 0, 12 | public val atMost: Int = -1 13 | ) : Parser> { 14 | 15 | init { 16 | require(atLeast >= 0) { "atLeast = $atLeast, expected non-negative" } 17 | require(atMost == -1 || atMost >= atLeast) { "atMost = $atMost is invalid, should be greater or equal than atLeast = $atLeast" } 18 | } 19 | 20 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult> { 21 | val resultsList = arrayListOf() 22 | var nextPosition = fromPosition 23 | while (atMost == -1 || resultsList.size < atMost) { 24 | val result = parser.tryParse(tokens, nextPosition) 25 | when (result) { 26 | is ErrorResult -> { 27 | return if (resultsList.size >= atLeast) 28 | ParsedValue(resultsList, nextPosition) 29 | else 30 | result 31 | } 32 | is Parsed -> { 33 | resultsList.add(result.value) 34 | nextPosition = result.nextPosition 35 | } 36 | } 37 | } 38 | return ParsedValue(resultsList, nextPosition) 39 | } 40 | } 41 | 42 | public fun zeroOrMore(parser: Parser): Parser> = RepeatCombinator(parser) 43 | 44 | public fun oneOrMore(parser: Parser): Parser> = RepeatCombinator(parser, atLeast = 1) 45 | 46 | public infix fun Int.times(parser: Parser): Parser> = 47 | RepeatCombinator(parser, atLeast = this, atMost = this) 48 | 49 | public infix fun IntRange.times(parser: Parser): Parser> = 50 | RepeatCombinator(parser, atLeast = first, atMost = last) 51 | 52 | public infix fun Int.timesOrMore(parser: Parser): Parser> = RepeatCombinator(parser, atLeast = this) -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/Separated.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 4 | import com.github.h0tk3y.betterParse.parser.* 5 | 6 | @Suppress("UNCHECKED_CAST") 7 | public class SeparatedCombinator( 8 | public val termParser: Parser, 9 | public val separatorParser: Parser, 10 | public val acceptZero: Boolean 11 | ) : Parser> { 12 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult> { 13 | val termMatches = mutableListOf() 14 | val separatorMatches = mutableListOf() 15 | 16 | val first = termParser.tryParse(tokens, fromPosition) 17 | 18 | return when (first) { 19 | is ErrorResult -> if (acceptZero) 20 | ParsedValue(Separated(emptyList(), emptyList()), fromPosition) 21 | else 22 | first 23 | 24 | is Parsed -> { 25 | termMatches.add(first.value) 26 | var nextPosition = first.nextPosition 27 | loop@ while (true) { 28 | val separator = separatorParser.tryParse(tokens, nextPosition) 29 | when (separator) { 30 | is ErrorResult -> break@loop 31 | is Parsed -> { 32 | val nextTerm = termParser.tryParse(tokens, separator.nextPosition) 33 | when (nextTerm) { 34 | is ErrorResult -> break@loop 35 | is Parsed -> { 36 | separatorMatches.add(separator.value) 37 | termMatches.add(nextTerm.value) 38 | nextPosition = nextTerm.nextPosition 39 | } 40 | } 41 | } 42 | } 43 | } 44 | ParsedValue(Separated(termMatches, separatorMatches), nextPosition) 45 | } 46 | } 47 | } 48 | } 49 | 50 | /** A list of [terms] separated by [separators], which is either empty (both `terms` and `separators`) or contains one 51 | * more term than there are separators. */ 52 | public class Separated( 53 | public val terms: List, 54 | public val separators: List 55 | ) { 56 | init { 57 | require(terms.size == separators.size + 1 || terms.isEmpty() && separators.isEmpty()) 58 | } 59 | 60 | /** Returns the result of reducing [terms] and [separators] with [function], starting from the left and processing 61 | * current result (initially, `terms[0]`), `separators[i]` and `terms[i + 1]` at each step for `i` in `0 until 62 | * terms.size`. 63 | * @throws [NoSuchElementException] if [terms] are empty */ 64 | public fun reduce(function: (T, S, T) -> T): T { 65 | if (terms.isEmpty()) throw NoSuchElementException() 66 | var result = terms.first() 67 | for (i in separators.indices) 68 | result = function(result, separators[i], terms[i + 1]) 69 | return result 70 | } 71 | 72 | /** Returns the result of reducing [terms] and [separators] with [function], starting from the right and processing 73 | * `terms[i]`, `separators[i]` and current result (initially, `terms.last()`) at each step for `i` in `terms.size - 1 downTo 0`. 74 | * @throws [NoSuchElementException] if [terms] are empty */ 75 | public fun reduceRight(function: (T, S, T) -> T): T { 76 | if (terms.isEmpty()) throw NoSuchElementException() 77 | var result = terms.last() 78 | for (i in separators.indices.reversed()) 79 | result = function(terms[i], separators[i], result) 80 | return result 81 | } 82 | } 83 | 84 | /** Parses a chain of [term]s separated by [separator], also accepting no matches at all if [acceptZero] is true. */ 85 | public inline fun separated( 86 | term: Parser, 87 | separator: Parser, 88 | acceptZero: Boolean = false 89 | ): Parser> = SeparatedCombinator(term, separator, acceptZero) 90 | 91 | /** Parses a chain of [term]s separated by [separator], also accepting no matches at all if [acceptZero] is true, and returning 92 | * only matches of [term]. */ 93 | public inline fun separatedTerms( 94 | term: Parser, 95 | separator: Parser, 96 | acceptZero: Boolean = false 97 | ): Parser> = separated(term, separator, acceptZero) map { it.terms } 98 | 99 | /** Parses a chain of [term]s separated by [operator] and reduces the result with [Separated.reduce]. */ 100 | public inline fun leftAssociative( 101 | term: Parser, 102 | operator: Parser, 103 | noinline transform: (T, S, T) -> T 104 | ): Parser = separated(term, operator) map { it.reduce(transform) } 105 | 106 | /** Parses a chain of [term]s separated by [operator] and reduces the result with [Separated.reduceRight]. */ 107 | public inline fun rightAssociative( 108 | term: Parser, 109 | operator: Parser, 110 | noinline transform: (T, S, T) -> T 111 | ): Parser = 112 | separated(term, operator) map { it.reduceRight(transform) } 113 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/combinators/SkipParser.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.combinators 2 | 3 | import com.github.h0tk3y.betterParse.parser.Parsed 4 | import com.github.h0tk3y.betterParse.parser.Parser 5 | import com.github.h0tk3y.betterParse.utils.Tuple 6 | import com.github.h0tk3y.betterParse.utils.Tuple1 7 | 8 | /** Wraps a [Parser] to distinguish it from other parsers when it is used in [and] functions. */ 9 | public class SkipParser(public val innerParser: Parser<*>) 10 | 11 | /** Wraps a [Parser] to distinguish it from other parsers when it is used in [and] functions. */ 12 | public fun skip(parser: Parser): SkipParser = SkipParser(parser) 13 | 14 | /** The same as `[skip] of this parser. ` */ 15 | public operator fun Parser<*>.unaryMinus(): SkipParser = skip(this) 16 | 17 | /** Parses the sequence with the receiver [Parser] and the wrapped [other] parser, but returns the [Parsed] result 18 | * from the receiver parser. */ 19 | public infix fun AndCombinator.and(other: SkipParser): AndCombinator = 20 | AndCombinator(consumersImpl + other, transform) 21 | 22 | public operator fun AndCombinator.times(other: SkipParser): AndCombinator = this and other 23 | 24 | /** Parses the sequence with the receiver [Parser] and the wrapped [other] parser, but returns the [Parsed] result 25 | * with a value from the receiver parser in a [Tuple1]. */ 26 | public inline infix fun Parser.and(other: SkipParser): AndCombinator = 27 | AndCombinator(listOf(this, other), { (a) -> a as T } ) 28 | 29 | /** The same as `this `[and]` other`*/ 30 | public inline operator fun Parser.times(other: SkipParser): AndCombinator = this and other 31 | 32 | /** Parses the wrapped receiver [Parser] and the [other] parser and returns the [Parsed] result 33 | * with a value from the [other] parser in a [Tuple1]. */ 34 | public inline infix fun SkipParser.and(other: Parser): AndCombinator = 35 | AndCombinator(listOf(this, other)) { (b) -> b as T } 36 | 37 | /** The same as `this `[and]` other`*/ 38 | public inline operator fun SkipParser.times(other: Parser): AndCombinator = this and other 39 | 40 | public infix fun SkipParser.and(other: SkipParser): SkipParser { 41 | val parsersLeft = if (innerParser is AndCombinator) innerParser.consumersImpl else listOf(innerParser) 42 | return SkipParser(AndCombinator(parsersLeft + other.innerParser, { })) 43 | } 44 | 45 | /** The same as `this `[and]` other`*/ 46 | public operator fun SkipParser.times(other: SkipParser): SkipParser = this and other -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/grammar/Grammar.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.grammar 2 | 3 | import com.github.h0tk3y.betterParse.lexer.DefaultTokenizer 4 | import com.github.h0tk3y.betterParse.lexer.Token 5 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 6 | import com.github.h0tk3y.betterParse.lexer.Tokenizer 7 | import com.github.h0tk3y.betterParse.parser.ParseResult 8 | import com.github.h0tk3y.betterParse.parser.Parser 9 | import com.github.h0tk3y.betterParse.parser.parseToEnd 10 | import com.github.h0tk3y.betterParse.parser.tryParseToEnd 11 | import kotlin.reflect.KProperty 12 | 13 | /** 14 | * A language grammar represented by a list of [Token]s and one or more [Parser]s, with one 15 | * specific [rootParser] that accepts the words of this [Grammar]. 16 | */ 17 | public abstract class Grammar : Parser { 18 | 19 | private val _tokens = arrayListOf() 20 | 21 | private val _parsers = linkedSetOf>() 22 | 23 | /** List of tokens that is by default used for tokenizing a sequence before parsing this language. The tokens are 24 | * added to this list during an instance construction. */ 25 | public open val tokens: List get(): List = _tokens.distinctBy { it.name ?: it } 26 | 27 | /** Set of the tokens and parsers that were declared by delegation to the parser instances (`val p by someParser`), and [rootParser] */ 28 | public open val declaredParsers: Set> get() = (_parsers + _tokens + rootParser).toSet() 29 | 30 | /** A [Tokenizer] that is built with the [Token]s defined within this [Grammar], in their order of declaration */ 31 | public open val tokenizer: Tokenizer by lazy { DefaultTokenizer(tokens) } 32 | 33 | /** A [Parser] that represents the root rule of this [Grammar] and is used by default for parsing. */ 34 | public abstract val rootParser: Parser 35 | 36 | final override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult = rootParser.tryParse( 37 | tokens, fromPosition 38 | ) 39 | 40 | protected operator fun Parser.provideDelegate(thisRef: Grammar<*>, property: KProperty<*>): Parser = 41 | also { _parsers.add(it) } 42 | 43 | protected operator fun Parser.getValue(thisRef: Grammar<*>, property: KProperty<*>): Parser = this 44 | 45 | protected operator fun Token.provideDelegate(thisRef: Grammar<*>, property: KProperty<*>): Token = 46 | also { 47 | if (it.name == null) { 48 | it.name = property.name 49 | } 50 | _tokens.add(it) 51 | } 52 | 53 | protected operator fun Token.getValue(thisRef: Grammar<*>, property: KProperty<*>): Token = this 54 | } 55 | 56 | /** A convenience function to use for referencing a parser that is not initialized up to this moment. */ 57 | public fun parser(block: () -> Parser): Parser = ParserReference(block) 58 | 59 | public class ParserReference internal constructor(parserProvider: () -> Parser) : Parser { 60 | public val parser: Parser by lazy(parserProvider) 61 | 62 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult = 63 | parser.tryParse(tokens, fromPosition) 64 | } 65 | 66 | public fun Grammar.tryParseToEnd(input: String): ParseResult = 67 | rootParser.tryParseToEnd(tokenizer.tokenize(input), 0) 68 | 69 | public fun Grammar.parseToEnd(input: String): T = 70 | rootParser.parseToEnd(tokenizer.tokenize(input)) -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/DefaultTokenizer.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | private fun Regex.countGroups() = "(?:$pattern)?".toRegex().find("")!!.groups.size - 1 4 | 5 | /** Tokenizes input character sequences using the [tokens], prioritized by their order in the list, 6 | * first matched first. */ 7 | public class DefaultTokenizer(override val tokens: List) : Tokenizer { 8 | init { 9 | require(tokens.isNotEmpty()) { "The tokens list should not be empty" } 10 | } 11 | 12 | /** Tokenizes the [input] from a [String] into a [TokenMatchesSequence]. */ 13 | override fun tokenize(input: String): TokenMatchesSequence = tokenize(input as CharSequence) 14 | 15 | /** Tokenizes the [input] from a [CharSequence] into a [TokenMatchesSequence]. */ 16 | public fun tokenize(input: CharSequence): TokenMatchesSequence = 17 | TokenMatchesSequence(DefaultTokenProducer(tokens, input)) 18 | } 19 | 20 | private class DefaultTokenProducer(private val tokens: List, private val input: CharSequence) : TokenProducer { 21 | private val inputLength = input.length 22 | private var tokenIndex = 0 23 | private var pos = 0 24 | private var row = 1 25 | private var col = 1 26 | 27 | private var errorState = false 28 | 29 | override fun nextToken(): TokenMatch? { 30 | if (pos > input.lastIndex || errorState) { 31 | return null 32 | } 33 | 34 | @Suppress("ReplaceManualRangeWithIndicesCalls") 35 | for (index in 0 until tokens.size) { 36 | val token = tokens[index] 37 | val matchLength = token.match(input, pos) 38 | if (matchLength == 0) 39 | continue 40 | 41 | val result = TokenMatch(token, tokenIndex++, input, pos, matchLength, row, col) 42 | 43 | for (i in pos until pos + matchLength) { 44 | if (input[i] == '\n') { 45 | row++ 46 | col = 1 47 | } else { 48 | col++ 49 | } 50 | } 51 | 52 | pos += matchLength 53 | 54 | return result 55 | } 56 | 57 | errorState = true 58 | return TokenMatch(noneMatched, tokenIndex++, input, pos, inputLength - pos, row, col) 59 | } 60 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/LambdaToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | public inline fun token(ignored: Boolean = false, crossinline matcher: (CharSequence, Int) -> Int): Token { 4 | return object : Token(null, ignored) { 5 | override fun match(input: CharSequence, fromIndex: Int) = matcher(input, fromIndex) 6 | override fun toString() = "${name ?: ""} {lambda}" + if (ignored) " [ignorable]" else "" 7 | } 8 | } 9 | 10 | public inline fun token(name: String, ignored: Boolean = false, crossinline matcher: (CharSequence, Int) -> Int): Token { 11 | return object : Token(name, ignored) { 12 | override fun match(input: CharSequence, fromIndex: Int) = matcher(input, fromIndex) 13 | override fun toString() = "$name {lambda}" + if (ignored) " [ignorable]" else "" 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/LiteralToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | public class LiteralToken(name: String?, public val text: String, ignored: Boolean = false) : Token(name, ignored) { 4 | override fun match(input: CharSequence, fromIndex: Int): Int = if (input.startsWith(text, fromIndex)) text.length else 0 5 | override fun toString(): String = "${name ?: ""} ($text)" + if (ignored) " [ignorable]" else "" 6 | } 7 | 8 | public class CharToken(name: String?, public val text: Char, ignored: Boolean = false) : Token(name, ignored) { 9 | override fun match(input: CharSequence, fromIndex: Int): Int = if (input.isNotEmpty() && input[fromIndex] == text) 1 else 0 10 | override fun toString(): String = "${name ?: ""} ($text)" + if (ignored) " [ignorable]" else "" 11 | } 12 | 13 | public fun literalToken(name: String, text: String, ignore: Boolean = false): Token { 14 | if (text.length == 1) 15 | return CharToken(name, text[0], ignore) 16 | return LiteralToken(name, text, ignore) 17 | } 18 | 19 | public fun literalToken(text: String, ignore: Boolean = false): Token { 20 | if (text.length == 1) 21 | return CharToken(null, text[0], ignore) 22 | return LiteralToken(null, text, ignore) 23 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/RegexToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | public expect class RegexToken : Token { 4 | public constructor(name: String?, @Language("RegExp", "", "") patternString: String, ignored: Boolean = false) 5 | public constructor(name: String?, regex: Regex, ignored: Boolean = false) 6 | } 7 | 8 | public fun regexToken(name: String, @Language("RegExp", "", "") pattern: String, ignore: Boolean = false): RegexToken = 9 | RegexToken(name, pattern, ignore) 10 | 11 | public fun regexToken(name: String, pattern: Regex, ignore: Boolean = false): RegexToken = 12 | RegexToken(name, pattern, ignore) 13 | public fun regexToken(@Language("RegExp", "", "") pattern: String, ignore: Boolean = false): RegexToken = 14 | RegexToken(null, pattern, ignore) 15 | public fun regexToken(pattern: Regex, ignore: Boolean = false): RegexToken = RegexToken(null, pattern, ignore) 16 | 17 | @Deprecated( 18 | "Use either regexToken or literalToken. This function will be removed soon", 19 | ReplaceWith("regexToken(pattern, ignore)") 20 | ) 21 | public fun token(name: String, @Language("RegExp", "", "") pattern: String, ignore: Boolean = false): RegexToken = 22 | RegexToken(name, pattern, ignore) 23 | 24 | @Deprecated( 25 | "Use either regexToken or literalToken. This function will be removed soon", 26 | ReplaceWith("regexToken(pattern, ignore)") 27 | ) 28 | public fun token(name: String, pattern: Regex, ignore: Boolean = false): RegexToken = RegexToken(name, pattern, ignore) 29 | 30 | @Deprecated( 31 | "Use either regexToken or literalToken. This function will be removed soon", 32 | ReplaceWith("regexToken(pattern, ignore)") 33 | ) 34 | public fun token(@Language("RegExp", "", "") pattern: String, ignore: Boolean = false): RegexToken = 35 | RegexToken(null, pattern, ignore) 36 | 37 | @Deprecated( 38 | "Use either regexToken or literalToken. This function will be removed soon", 39 | ReplaceWith("regexToken(pattern, ignore)") 40 | ) 41 | public fun token(pattern: Regex, ignore: Boolean = false): RegexToken = RegexToken(null, pattern, ignore) -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/Token.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | import com.github.h0tk3y.betterParse.parser.* 4 | 5 | @OptionalExpectation 6 | public expect annotation class Language(val value: String, val prefix: String, val suffix: String) 7 | 8 | /** 9 | * Represents a basic detectable part of the input that may be [ignored] during parsing. 10 | * Parses to [TokenMatch]. 11 | * The [name] only provides additional information. 12 | */ 13 | public abstract class Token(public var name: String? = null, public val ignored: Boolean) : Parser { 14 | 15 | public abstract fun match(input: CharSequence, fromIndex: Int): Int 16 | 17 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult = 18 | tryParseImpl(tokens, fromPosition) 19 | 20 | private tailrec fun tryParseImpl(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult { 21 | val tokenMatch = tokens[fromPosition] ?: return UnexpectedEof(this) 22 | return when { 23 | tokenMatch.type == this -> tokenMatch 24 | tokenMatch.type == noneMatched -> NoMatchingToken(tokenMatch) 25 | tokenMatch.type.ignored -> tryParseImpl(tokens, fromPosition + 1) 26 | else -> MismatchedToken(this, tokenMatch) 27 | } 28 | } 29 | } 30 | 31 | /** Token type indicating that there was no [Token] found to be matched by a [Tokenizer]. */ 32 | public val noneMatched: Token = object : Token("no token matched", false) { 33 | override fun match(input: CharSequence, fromIndex: Int): Int = 0 34 | override fun toString(): String = "noneMatched!" 35 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/TokenMatch.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | import com.github.h0tk3y.betterParse.parser.Parsed 4 | 5 | /** 6 | * Represents a [Parsed] result of a [Token], with the token [type], the [text] that matched the token in the input 7 | * sequence, the [offset] in the sequence (starting from 0), and [row] and [column] (both starting from 1). 8 | */ 9 | public data class TokenMatch( 10 | val type: Token, 11 | val tokenIndex: Int, 12 | val input: CharSequence, 13 | val offset: Int, 14 | val length: Int, 15 | val row: Int, 16 | val column: Int 17 | ) : Parsed() { 18 | val text: String get() = input.substring(offset, offset + length) 19 | 20 | override val value: TokenMatch 21 | get() = this 22 | override val nextPosition: Int 23 | get() = tokenIndex + 1 24 | override fun toString(): String = "${type.name}@$nextPosition for \"$text\" at $offset ($row:$column)" 25 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/TokenMatchesSequence.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | /** Stateful producer of tokens that yields [Token]s from some inputs sequence that it is based upon, one by one */ 4 | public interface TokenProducer { 5 | public fun nextToken(): TokenMatch? 6 | } 7 | 8 | public class TokenMatchesSequence( 9 | private val tokenProducer: TokenProducer, 10 | private val matches: ArrayList = arrayListOf() 11 | ) : Sequence { 12 | 13 | private inline fun ensureReadPosition(position: Int): Boolean { 14 | while (position >= matches.size) { 15 | val next = tokenProducer.nextToken() 16 | ?: return false 17 | matches.add(next) 18 | } 19 | return true 20 | } 21 | 22 | public operator fun get(position: Int): TokenMatch? { 23 | if (!ensureReadPosition(position)) { 24 | return null 25 | } 26 | return matches[position] 27 | } 28 | 29 | public fun getNotIgnored(position: Int): TokenMatch? { 30 | if (!ensureReadPosition(position)) { 31 | return null 32 | } 33 | 34 | var pos = position 35 | while (true) { 36 | val value = if (pos < matches.size) 37 | matches[pos] 38 | else { 39 | val next = tokenProducer.nextToken() 40 | if (next == null) 41 | return null 42 | else { 43 | matches.add(next) 44 | next 45 | } 46 | } 47 | if (!value.type.ignored) 48 | return value 49 | pos++ 50 | } 51 | } 52 | 53 | override fun iterator(): Iterator = 54 | object : AbstractIterator() { 55 | var position = 0 56 | var noneMatchedAtThisPosition = false 57 | 58 | override fun computeNext() { 59 | if (noneMatchedAtThisPosition) { 60 | done() 61 | } 62 | 63 | val nextMatch = get(position) ?: run { done(); return } 64 | setNext(nextMatch) 65 | if (nextMatch.type == noneMatched) { 66 | noneMatchedAtThisPosition = true 67 | } 68 | ++position 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/lexer/Tokenizer.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | public interface Tokenizer { 4 | public val tokens: List 5 | 6 | /** Tokenizes the [input] from a [String] into a [TokenMatchesSequence]. */ 7 | public fun tokenize(input: String): TokenMatchesSequence 8 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/parser/Parser.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.parser 2 | 3 | import com.github.h0tk3y.betterParse.lexer.Token 4 | import com.github.h0tk3y.betterParse.lexer.TokenMatch 5 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 6 | 7 | /** A common interface for parsers that can try to consume a part or the whole [TokenMatch] sequence and return one of 8 | * possible [ParseResult], either [Parsed] or [ErrorResult] */ 9 | public interface Parser { 10 | public fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult 11 | } 12 | 13 | public object EmptyParser : Parser { 14 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult = ParsedValue(Unit, fromPosition) 15 | } 16 | 17 | public fun Parser.tryParseToEnd(tokens: TokenMatchesSequence, position: Int): ParseResult { 18 | val result = tryParse(tokens, position) 19 | return when (result) { 20 | is ErrorResult -> result 21 | is Parsed -> tokens.getNotIgnored(result.nextPosition)?.let { 22 | UnparsedRemainder(it) 23 | } ?: result 24 | } 25 | } 26 | 27 | public fun Parser.parse(tokens: TokenMatchesSequence): T = tryParse(tokens, 0).toParsedOrThrow().value 28 | 29 | public fun Parser.parseToEnd(tokens: TokenMatchesSequence): T = tryParseToEnd(tokens, 0).toParsedOrThrow().value 30 | 31 | 32 | /** Represents a result of input sequence parsing by a [Parser] that tried to parse [T]. */ 33 | public sealed class ParseResult 34 | 35 | /** Represents a successful parsing result of a [Parser] that produced [value] 36 | * and left input starting with [nextPosition] unprocessed. */ 37 | public abstract class Parsed : ParseResult() { 38 | public abstract val value: T 39 | public abstract val nextPosition: Int 40 | 41 | override fun toString(): String = "Parsed($value)" 42 | 43 | override fun equals(other: Any?): Boolean { 44 | if (this === other) return true 45 | if (other == null || this::class != other::class) return false 46 | 47 | other as Parsed<*> 48 | 49 | if (value != other.value) return false 50 | if (nextPosition != other.nextPosition) return false 51 | 52 | return true 53 | } 54 | 55 | override fun hashCode(): Int { 56 | var result = value?.hashCode() ?: 0 57 | result = 31 * result + nextPosition 58 | return result 59 | } 60 | } 61 | 62 | internal class ParsedValue(override val value: T, override val nextPosition : Int) : Parsed() 63 | 64 | /** Represents a parse error of a [Parser] that could not successfully parse an input sequence. */ 65 | public abstract class ErrorResult : ParseResult() { 66 | override fun toString(): String = "ErrorResult" 67 | } 68 | 69 | /** A [startsWith] token was found where the end of the input sequence was expected during 70 | * [tryParseToEnd] or [parseToEnd]. */ 71 | public data class UnparsedRemainder(val startsWith: TokenMatch) : ErrorResult() 72 | 73 | /** A token was [found] where another type of token was [expected]. */ 74 | public data class MismatchedToken(val expected: Token, val found: TokenMatch) : ErrorResult() 75 | 76 | /** A lexer could not match the input sequence with any token known to it. Contains [tokenMismatch] with special type */ 77 | public data class NoMatchingToken(val tokenMismatch: TokenMatch) : ErrorResult() 78 | 79 | /** An end of the input sequence was encountered where a token was [expected]. */ 80 | public data class UnexpectedEof(val expected: Token) : ErrorResult() 81 | 82 | /** A parser tried several alternatives but all resulted into [errors]. */ 83 | public data class AlternativesFailure(val errors: List) : ErrorResult() 84 | 85 | /** Thrown when a [Parser] is forced to parse a sequence with [parseToEnd] or [parse] and fails with an [ErrorResult]. */ 86 | public class ParseException(@Suppress("CanBeParameter") public val errorResult: ErrorResult) 87 | : Exception("Could not parse input: $errorResult") 88 | 89 | /** Throws [ParseException] if the receiver [ParseResult] is a [ErrorResult]. Returns the [Parsed] result otherwise. */ 90 | public fun ParseResult.toParsedOrThrow(): Parsed = when (this) { 91 | is Parsed -> this 92 | is ErrorResult -> throw ParseException(this) 93 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/st/LiftToSyntaxTree.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.st 2 | 3 | import com.github.h0tk3y.betterParse.combinators.* 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.ParserReference 6 | import com.github.h0tk3y.betterParse.lexer.Token 7 | import com.github.h0tk3y.betterParse.lexer.TokenMatch 8 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 9 | import com.github.h0tk3y.betterParse.parser.* 10 | import com.github.h0tk3y.betterParse.parser.ParsedValue 11 | 12 | /** Encloses custom logic for transforming a [Parser] to a parser of [SyntaxTree]. 13 | * A correct implementation overrides [liftToSyntaxTree] so that it calls `recurse` for the sub-parsers, if any, and 14 | * combines them into the parser that it returns. */ 15 | public interface LiftToSyntaxTreeTransformer { 16 | public interface DefaultTransformerReference { 17 | public fun transform(parser: Parser): Parser> 18 | } 19 | 20 | public fun liftToSyntaxTree(parser: Parser, default: DefaultTransformerReference): Parser> 21 | } 22 | 23 | /** Options for transforming a [Parser] to a parser of [SyntaxTree]. 24 | * @param retainSkipped - whether the [skip]ped parsers should be present in the syntax tree structure. 25 | * @param retainSeparators - whether the separators of [separated], [leftAssociative] and [rightAssociative] should be 26 | * present in the syntax tree structure. */ 27 | public data class LiftToSyntaxTreeOptions( 28 | val retainSkipped: Boolean = false, 29 | val retainSeparators: Boolean = true 30 | ) 31 | 32 | /** Converts a [Parser] of [T] to another that parses [SyntaxTree]. The resulting [SyntaxTree]s will have this parser 33 | * as [SyntaxTree.parser] and the result that this parser is stored in [SyntaxTree.item]. The [liftOptions] are 34 | * used to determine which parts of the syntax tree should be dropped. The [structureParsers] define the resulting 35 | * structure of the syntax tree: only the nodes having these parsers are retained (see: [SyntaxTree.flatten]), pass 36 | * empty set to retain all nodes. */ 37 | public fun Parser.liftToSyntaxTreeParser( 38 | liftOptions: LiftToSyntaxTreeOptions = LiftToSyntaxTreeOptions(), 39 | structureParsers: Set>? = null, 40 | transformer: LiftToSyntaxTreeTransformer? = null 41 | ): Parser> { 42 | val astParser = ParserToSyntaxTreeLifter(liftOptions, transformer).lift(this) 43 | return if (structureParsers == null) 44 | astParser else 45 | astParser.flattened(structureParsers) 46 | } 47 | 48 | /** Converts a [Grammar] so that its [Grammar.rootParser] parses a [SyntaxTree]. See: [liftToSyntaxTreeParser]. */ 49 | public fun Grammar.liftToSyntaxTreeGrammar( 50 | liftOptions: LiftToSyntaxTreeOptions = LiftToSyntaxTreeOptions(), 51 | structureParsers: Set>? = declaredParsers, 52 | transformer: LiftToSyntaxTreeTransformer? = null 53 | ): Grammar> = object : Grammar>() { 54 | override val rootParser: Parser> = this@liftToSyntaxTreeGrammar.rootParser 55 | .liftToSyntaxTreeParser(liftOptions, structureParsers, transformer) 56 | 57 | override val tokens: List get() = this@liftToSyntaxTreeGrammar.tokens 58 | override val declaredParsers: Set> = this@liftToSyntaxTreeGrammar.declaredParsers 59 | } 60 | 61 | private class ParserToSyntaxTreeLifter( 62 | val liftOptions: LiftToSyntaxTreeOptions, 63 | val transformer: LiftToSyntaxTreeTransformer? 64 | ) { 65 | @Suppress("UNCHECKED_CAST") 66 | fun lift(parser: Parser): Parser> { 67 | if (parser in parsersInStack) 68 | return referenceResultInStack(parser) as Parser> 69 | 70 | parsersInStack += parser 71 | 72 | val result = when (parser) { 73 | is EmptyParser -> emptyASTParser() 74 | is Token -> liftTokenToAST(parser) 75 | is MapCombinator<*, *> -> liftMapCombinatorToAST(parser) 76 | is AndCombinator -> liftAndCombinatorToAST(parser) 77 | is OrCombinator -> liftOrCombinatorToAST(parser) 78 | is OptionalCombinator<*> -> liftOptionalCombinatorToAST(parser) 79 | is RepeatCombinator<*> -> liftRepeatCombinatorToAST(parser) 80 | is ParserReference<*> -> liftParserReferenceToAST(parser) 81 | is SeparatedCombinator<*, *> -> liftSeparatedCombinatorToAST(parser) 82 | else -> { 83 | transformer?.liftToSyntaxTree(parser, default) ?: throw IllegalArgumentException("Unexpected parser $this. Provide a custom transformer that can lift it.") 84 | } 85 | } as Parser> 86 | 87 | resultMap[parser] = result 88 | 89 | parsersInStack -= parser 90 | 91 | return result 92 | } 93 | 94 | inner class DefaultTransformerReference : LiftToSyntaxTreeTransformer.DefaultTransformerReference { 95 | override fun transform(parser: Parser): Parser> = lift(parser) 96 | } 97 | 98 | private val default = DefaultTransformerReference() 99 | 100 | private fun liftTokenToAST(token: Token): Parser> { 101 | return token.map { SyntaxTree(it, listOf(), token, it.offset until (it.offset + it.length)) } 102 | } 103 | 104 | private fun liftMapCombinatorToAST(combinator: MapCombinator): Parser> { 105 | val liftedInner = lift(combinator.innerParser) 106 | return liftedInner.map { 107 | SyntaxTree(combinator.transform(it.item), listOf(it), combinator, it.range) 108 | } 109 | } 110 | 111 | private fun liftOptionalCombinatorToAST(combinator: OptionalCombinator): Parser> { 112 | return object: Parser> { 113 | override fun tryParse(tokens: TokenMatchesSequence, fromPosition: Int): ParseResult> { 114 | val result = optional(lift(combinator.parser)).tryParse(tokens, fromPosition) 115 | return when (result) { 116 | is ErrorResult -> result 117 | is Parsed -> { 118 | val inputPosition = tokens[fromPosition]?.offset ?: 0 119 | val ast = SyntaxTree(result.value?.item, 120 | listOfNotNull(result.value), 121 | combinator, 122 | result.value?.range ?: inputPosition..inputPosition) 123 | ParsedValue(ast, result.nextPosition) 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | private fun liftParserReferenceToAST(combinator: ParserReference): Parser> { 131 | return lift(combinator.parser) 132 | } 133 | 134 | private fun liftAndCombinatorToAST(combinator: AndCombinator): AndCombinator> { 135 | val liftedConsumers = combinator.consumersImpl.map { 136 | when (it) { 137 | is Parser<*> -> lift(it) 138 | is SkipParser -> lift(it.innerParser) 139 | else -> throw IllegalArgumentException() 140 | } 141 | } 142 | return AndCombinator(liftedConsumers) { parsedItems -> 143 | val nonSkippedResults = combinator.nonSkippedIndices.map { parsedItems[it] } 144 | val originalResult = combinator.transform(nonSkippedResults.map { (it as SyntaxTree<*>).item }) 145 | val start = (parsedItems.first() as SyntaxTree<*>).range.first 146 | val end = ((parsedItems.lastOrNull { (it as SyntaxTree<*>).range.last != 0 }) as? SyntaxTree<*>)?.range?.last ?: 0 147 | @Suppress("UNCHECKED_CAST") 148 | val children = if (liftOptions.retainSkipped) 149 | parsedItems as List> else 150 | combinator.nonSkippedIndices.map { parsedItems[it] } as List> 151 | return@AndCombinator SyntaxTree(originalResult, children, combinator, start..end) 152 | } 153 | } 154 | 155 | private fun liftOrCombinatorToAST(combinator: OrCombinator): Parser> { 156 | val liftedParsers = combinator.parsers.map { lift(it) } 157 | return OrCombinator(liftedParsers).map { SyntaxTree(it.item, listOf(it), combinator, it.range) } 158 | } 159 | 160 | private fun liftRepeatCombinatorToAST(combinator: RepeatCombinator): Parser>> { 161 | val liftedInner = lift(combinator.parser) 162 | return RepeatCombinator(liftedInner, combinator.atLeast, combinator.atMost).map { 163 | val start = it.firstOrNull()?.range?.start ?: 0 164 | val end = it.lastOrNull { it.range.endInclusive != 0 }?.range?.endInclusive ?: 0 165 | SyntaxTree(it.map { it.item }, it, combinator, start..end) 166 | } 167 | } 168 | 169 | private fun liftSeparatedCombinatorToAST(combinator: SeparatedCombinator): Parser>> { 170 | val liftedTerm = lift(combinator.termParser) 171 | val liftedSeparator = lift(combinator.separatorParser) 172 | return SeparatedCombinator(liftedTerm, liftedSeparator, combinator.acceptZero).map { separated -> 173 | val item = Separated(separated.terms.map { it.item }, separated.separators.map { it.item }) 174 | val children = when { 175 | separated.terms.isEmpty() -> emptyList>() 176 | liftOptions.retainSeparators -> listOf(separated.terms.first()) + 177 | (separated.separators zip separated.terms.drop(1)).flatMap { (s, t) -> listOf(s, t) } 178 | else -> separated.terms 179 | } 180 | val start = children.firstOrNull()?.range?.start ?: 0 181 | val end = children.lastOrNull { it.range.last != 0 }?.range?.last ?: 0 182 | SyntaxTree(item, children, combinator, start..end) 183 | } 184 | } 185 | 186 | private fun emptyASTParser() = EmptyParser.map { SyntaxTree(Unit, emptyList(), 187 | EmptyParser, 0..0) } 188 | 189 | private val resultMap = hashMapOf, Parser>>() 190 | 191 | private fun referenceResultInStack(parser: Parser<*>) = ParserReference { resultMap[parser]!! } 192 | 193 | private val parsersInStack = hashSetOf>() 194 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/st/SyntaxTree.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.st 2 | 3 | import com.github.h0tk3y.betterParse.combinators.map 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.ParserReference 6 | import com.github.h0tk3y.betterParse.parser.Parser 7 | 8 | /** Stores the syntactic structure of a [parser] parsing result, with [item] as the result value, 9 | * [children] storing the same structure of the referenced parsers and [range] displaying the positions 10 | * in the input sequence. */ 11 | public data class SyntaxTree( 12 | val item: T, 13 | val children: List>, 14 | val parser: Parser, 15 | val range: IntRange 16 | ) 17 | 18 | /** Returns a [SyntaxTree] that contains only parsers from [structureParsers] in its nodes. The nodes that have other parsers 19 | * are replaced in their parents by their children that are also flattened in the same way. If the root node is to be 20 | * replaced, another SyntaxTree is created that contains the resulting nodes as children and the same parser. */ 21 | public fun SyntaxTree.flatten(structureParsers: Set>): SyntaxTree { 22 | val list = flattenToList(this, structureParsers) 23 | @Suppress("UNCHECKED_CAST") 24 | return if (parser == list.singleOrNull()?.parser) 25 | list.single() as SyntaxTree else 26 | SyntaxTree(item, list, parser, range) 27 | } 28 | 29 | /** Creates another SyntaxTree parser that [flatten]s the result of this parser. */ 30 | public fun Parser>.flattened(structureParsers: Set>): Parser> = 31 | map { it.flatten(structureParsers) } 32 | 33 | /** Performs the same operation as [flatten], using the parsers defined in [grammar] as `structureParsers`. */ 34 | public fun SyntaxTree.flattenForGrammar(grammar: Grammar<*>): SyntaxTree = 35 | flatten(grammar.declaredParsers) 36 | 37 | /** Performs the same as [flattened], using the parsers defined in [grammar] as `structureParsers`. */ 38 | public fun Parser>.flattenedForGrammar(grammar: Grammar<*>): Parser> = 39 | map { it.flattenForGrammar(grammar) } 40 | 41 | private fun flattenToList(syntaxTree: SyntaxTree, structureParsers: Set>): List> { 42 | val flattenedChildren = syntaxTree.children.flatMap { flattenToList(it, structureParsers) } 43 | return if (syntaxTree.parser in structureParsers || syntaxTree.parser is ParserReference && syntaxTree.parser.parser in structureParsers) 44 | listOf(syntaxTree.copy(children = flattenedChildren)) else 45 | flattenedChildren 46 | } 47 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/github/h0tk3y/betterParse/utils/Tuple.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.utils 2 | 3 | public interface Tuple -------------------------------------------------------------------------------- /src/commonMain/kotlin/generated/andFunctions.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", // fixme: bug in Kotlin 1.4.21, fixed in 1.4.30 3 | "MoveLambdaOutsideParentheses", 4 | "PackageDirectoryMismatch" 5 | ) 6 | 7 | package com.github.h0tk3y.betterParse.combinators 8 | import com.github.h0tk3y.betterParse.utils.* 9 | import com.github.h0tk3y.betterParse.parser.* 10 | import kotlin.jvm.JvmName 11 | 12 | @JvmName("and2") public inline infix fun 13 | AndCombinator>.and(p3: Parser) 14 | // : AndCombinator> = 15 | = AndCombinator(consumersImpl + p3, { 16 | Tuple3(it[0] as T1, it[1] as T2, it[2] as T3) 17 | }) 18 | 19 | @JvmName("and2Operator") public inline operator fun 20 | AndCombinator>.times(p3: Parser) 21 | // : AndCombinator> = 22 | = this and p3 23 | 24 | 25 | @JvmName("and3") public inline infix fun 26 | AndCombinator>.and(p4: Parser) 27 | // : AndCombinator> = 28 | = AndCombinator(consumersImpl + p4, { 29 | Tuple4(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4) 30 | }) 31 | 32 | @JvmName("and3Operator") public inline operator fun 33 | AndCombinator>.times(p4: Parser) 34 | // : AndCombinator> = 35 | = this and p4 36 | 37 | 38 | @JvmName("and4") public inline infix fun 39 | AndCombinator>.and(p5: Parser) 40 | // : AndCombinator> = 41 | = AndCombinator(consumersImpl + p5, { 42 | Tuple5(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5) 43 | }) 44 | 45 | @JvmName("and4Operator") public inline operator fun 46 | AndCombinator>.times(p5: Parser) 47 | // : AndCombinator> = 48 | = this and p5 49 | 50 | 51 | @JvmName("and5") public inline infix fun 52 | AndCombinator>.and(p6: Parser) 53 | // : AndCombinator> = 54 | = AndCombinator(consumersImpl + p6, { 55 | Tuple6(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6) 56 | }) 57 | 58 | @JvmName("and5Operator") public inline operator fun 59 | AndCombinator>.times(p6: Parser) 60 | // : AndCombinator> = 61 | = this and p6 62 | 63 | 64 | @JvmName("and6") public inline infix fun 65 | AndCombinator>.and(p7: Parser) 66 | // : AndCombinator> = 67 | = AndCombinator(consumersImpl + p7, { 68 | Tuple7(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7) 69 | }) 70 | 71 | @JvmName("and6Operator") public inline operator fun 72 | AndCombinator>.times(p7: Parser) 73 | // : AndCombinator> = 74 | = this and p7 75 | 76 | 77 | @JvmName("and7") public inline infix fun 78 | AndCombinator>.and(p8: Parser) 79 | // : AndCombinator> = 80 | = AndCombinator(consumersImpl + p8, { 81 | Tuple8(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8) 82 | }) 83 | 84 | @JvmName("and7Operator") public inline operator fun 85 | AndCombinator>.times(p8: Parser) 86 | // : AndCombinator> = 87 | = this and p8 88 | 89 | 90 | @JvmName("and8") public inline infix fun 91 | AndCombinator>.and(p9: Parser) 92 | // : AndCombinator> = 93 | = AndCombinator(consumersImpl + p9, { 94 | Tuple9(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9) 95 | }) 96 | 97 | @JvmName("and8Operator") public inline operator fun 98 | AndCombinator>.times(p9: Parser) 99 | // : AndCombinator> = 100 | = this and p9 101 | 102 | 103 | @JvmName("and9") public inline infix fun 104 | AndCombinator>.and(p10: Parser) 105 | // : AndCombinator> = 106 | = AndCombinator(consumersImpl + p10, { 107 | Tuple10(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10) 108 | }) 109 | 110 | @JvmName("and9Operator") public inline operator fun 111 | AndCombinator>.times(p10: Parser) 112 | // : AndCombinator> = 113 | = this and p10 114 | 115 | 116 | @JvmName("and10") public inline infix fun 117 | AndCombinator>.and(p11: Parser) 118 | // : AndCombinator> = 119 | = AndCombinator(consumersImpl + p11, { 120 | Tuple11(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11) 121 | }) 122 | 123 | @JvmName("and10Operator") public inline operator fun 124 | AndCombinator>.times(p11: Parser) 125 | // : AndCombinator> = 126 | = this and p11 127 | 128 | 129 | @JvmName("and11") public inline infix fun 130 | AndCombinator>.and(p12: Parser) 131 | // : AndCombinator> = 132 | = AndCombinator(consumersImpl + p12, { 133 | Tuple12(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11, it[11] as T12) 134 | }) 135 | 136 | @JvmName("and11Operator") public inline operator fun 137 | AndCombinator>.times(p12: Parser) 138 | // : AndCombinator> = 139 | = this and p12 140 | 141 | 142 | @JvmName("and12") public inline infix fun 143 | AndCombinator>.and(p13: Parser) 144 | // : AndCombinator> = 145 | = AndCombinator(consumersImpl + p13, { 146 | Tuple13(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11, it[11] as T12, it[12] as T13) 147 | }) 148 | 149 | @JvmName("and12Operator") public inline operator fun 150 | AndCombinator>.times(p13: Parser) 151 | // : AndCombinator> = 152 | = this and p13 153 | 154 | 155 | @JvmName("and13") public inline infix fun 156 | AndCombinator>.and(p14: Parser) 157 | // : AndCombinator> = 158 | = AndCombinator(consumersImpl + p14, { 159 | Tuple14(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11, it[11] as T12, it[12] as T13, it[13] as T14) 160 | }) 161 | 162 | @JvmName("and13Operator") public inline operator fun 163 | AndCombinator>.times(p14: Parser) 164 | // : AndCombinator> = 165 | = this and p14 166 | 167 | 168 | @JvmName("and14") public inline infix fun 169 | AndCombinator>.and(p15: Parser) 170 | // : AndCombinator> = 171 | = AndCombinator(consumersImpl + p15, { 172 | Tuple15(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11, it[11] as T12, it[12] as T13, it[13] as T14, it[14] as T15) 173 | }) 174 | 175 | @JvmName("and14Operator") public inline operator fun 176 | AndCombinator>.times(p15: Parser) 177 | // : AndCombinator> = 178 | = this and p15 179 | 180 | 181 | @JvmName("and15") public inline infix fun 182 | AndCombinator>.and(p16: Parser) 183 | // : AndCombinator> = 184 | = AndCombinator(consumersImpl + p16, { 185 | Tuple16(it[0] as T1, it[1] as T2, it[2] as T3, it[3] as T4, it[4] as T5, it[5] as T6, it[6] as T7, it[7] as T8, it[8] as T9, it[9] as T10, it[10] as T11, it[11] as T12, it[12] as T13, it[13] as T14, it[14] as T15, it[15] as T16) 186 | }) 187 | 188 | @JvmName("and15Operator") public inline operator fun 189 | AndCombinator>.times(p16: Parser) 190 | // : AndCombinator> = 191 | = this and p16 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/generated/tuples.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.utils 2 | 3 | public data class Tuple1(val t1: T1) : Tuple 4 | public val Tuple1.components: List get() = listOf(t1) 5 | 6 | public data class Tuple2(val t1: T1, val t2: T2) : Tuple 7 | public val Tuple2.components: List get() = listOf(t1, t2) 8 | 9 | public data class Tuple3(val t1: T1, val t2: T2, val t3: T3) : Tuple 10 | public val Tuple3.components: List get() = listOf(t1, t2, t3) 11 | 12 | public data class Tuple4(val t1: T1, val t2: T2, val t3: T3, val t4: T4) : Tuple 13 | public val Tuple4.components: List get() = listOf(t1, t2, t3, t4) 14 | 15 | public data class Tuple5(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5) : Tuple 16 | public val Tuple5.components: List get() = listOf(t1, t2, t3, t4, t5) 17 | 18 | public data class Tuple6(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6) : Tuple 19 | public val Tuple6.components: List get() = listOf(t1, t2, t3, t4, t5, t6) 20 | 21 | public data class Tuple7(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7) : Tuple 22 | public val Tuple7.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7) 23 | 24 | public data class Tuple8(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8) : Tuple 25 | public val Tuple8.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8) 26 | 27 | public data class Tuple9(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9) : Tuple 28 | public val Tuple9.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9) 29 | 30 | public data class Tuple10(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10) : Tuple 31 | public val Tuple10.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10) 32 | 33 | public data class Tuple11(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11) : Tuple 34 | public val Tuple11.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11) 35 | 36 | public data class Tuple12(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11, val t12: T12) : Tuple 37 | public val Tuple12.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12) 38 | 39 | public data class Tuple13(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11, val t12: T12, val t13: T13) : Tuple 40 | public val Tuple13.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13) 41 | 42 | public data class Tuple14(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11, val t12: T12, val t13: T13, val t14: T14) : Tuple 43 | public val Tuple14.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14) 44 | 45 | public data class Tuple15(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11, val t12: T12, val t13: T13, val t14: T14, val t15: T15) : Tuple 46 | public val Tuple15.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15) 47 | 48 | public data class Tuple16(val t1: T1, val t2: T2, val t3: T3, val t4: T4, val t5: T5, val t6: T6, val t7: T7, val t8: T8, val t9: T9, val t10: T10, val t11: T11, val t12: T12, val t13: T13, val t14: T14, val t15: T15, val t16: T16) : Tuple 49 | public val Tuple16.components: List get() = listOf(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16) 50 | 51 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/AndTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.Grammar 4 | import com.github.h0tk3y.betterParse.lexer.regexToken 5 | import com.github.h0tk3y.betterParse.parser.Parser 6 | import com.github.h0tk3y.betterParse.parser.parseToEnd 7 | import com.github.h0tk3y.betterParse.utils.components 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | class AndTest : Grammar() { 12 | override val rootParser: Parser get() = throw NoSuchElementException() 13 | 14 | val a by regexToken("a") 15 | val b by regexToken("b") 16 | 17 | @Test fun simpleAnd() { 18 | val tokens = tokenizer.tokenize("aba") 19 | val parser = a and b and a use { components.map { it.type } } 20 | val result = parser.parseToEnd(tokens) 21 | 22 | assertEquals(listOf(a, b, a), result) 23 | } 24 | 25 | @Test fun skip() { 26 | val tokens = tokenizer.tokenize("abab") 27 | val parserA = a and skip(b) and a and skip(b) use { components.map { it.type } } 28 | val parserB = skip(a) and b and skip(a) and b use { components.map { it.type } } 29 | 30 | assertEquals(listOf(a, a), parserA.parseToEnd(tokens)) 31 | assertEquals(listOf(b, b), parserB.parseToEnd(tokens)) 32 | } 33 | 34 | @Test fun leftmostSeveralSkips() { 35 | val tokens = tokenizer.tokenize("ababab") 36 | val parser = -a * -b * a * -b * -a * b use { t1.type to t2.type } 37 | val result = parser.parseToEnd(tokens) 38 | 39 | assertEquals(a to b, result) 40 | } 41 | 42 | @Test fun singleParserInSkipChain() { 43 | val tokens = tokenizer.tokenize("ababa") 44 | val parser = -a * -b * a * -b * -a use { offset } 45 | val result = parser.parseToEnd(tokens) 46 | 47 | assertEquals(2, result) 48 | } 49 | 50 | @Test fun longAndOperatorChain() { 51 | val tokens = tokenizer.tokenize("aaabbb") 52 | val parser = a * a * a * b * b * b use { listOf(t6, t5, t4, t3, t2, t1).map { it.type } } 53 | val result = parser.parseToEnd(tokens) 54 | 55 | assertEquals(listOf(b, b, b, a, a, a), result) 56 | } 57 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/GrammarTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.map 3 | import com.github.h0tk3y.betterParse.combinators.separated 4 | import com.github.h0tk3y.betterParse.combinators.use 5 | import com.github.h0tk3y.betterParse.grammar.Grammar 6 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 7 | import com.github.h0tk3y.betterParse.lexer.regexToken 8 | import com.github.h0tk3y.betterParse.lexer.token 9 | import com.github.h0tk3y.betterParse.parser.Parser 10 | import kotlin.test.Test 11 | import kotlin.test.assertEquals 12 | 13 | class GrammarTest { 14 | @Test 15 | fun simpleParse() { 16 | val digits = "0123456789" 17 | val g = object : Grammar() { 18 | val n by token { input, from -> 19 | var length = 0 20 | while (from + length < input.length && input[from + length] in digits) 21 | length++ 22 | length 23 | } 24 | val s by regexToken("\\+|-") 25 | val ws by regexToken("\\s+", ignore = true) 26 | 27 | override val rootParser: Parser = separated(n use { text.toInt() }, s use { text }).map { 28 | it.reduce { a, s, b -> 29 | if (s == "+") a + b else a - b 30 | } 31 | } 32 | } 33 | 34 | val result = g.parseToEnd("1 + 2 + 3 + 4 - 11") 35 | assertEquals(-1, result) 36 | } 37 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/MapTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | import kotlin.test.* 6 | 7 | class MapTest { 8 | val aPlus = RegexToken("aPlus", "a+") 9 | val bPlus = RegexToken("aPlus", "b+") 10 | val lexer = DefaultTokenizer(listOf(aPlus, bPlus)) 11 | 12 | @Test fun testSuccessfulMap() { 13 | val tokens = lexer.tokenize("aaa") 14 | val result = aPlus.map { it.text }.tryParse(tokens,0) 15 | assertEquals("aaa", result.toParsedOrThrow().value) 16 | } 17 | 18 | @Test fun testSuccessfulUse() { 19 | val tokens = lexer.tokenize("abbaaa") 20 | val result = (aPlus and bPlus and aPlus use { t3.text + t2.text + t1.text}).tryParse(tokens,0) 21 | assertEquals("aaabba", result.toParsedOrThrow().value) 22 | } 23 | 24 | @Test fun testSuccessfulAsJust() { 25 | val tokens = lexer.tokenize("abbaaa") 26 | val result = (aPlus asJust 1).tryParse(tokens,0) 27 | assertEquals(1, result.toParsedOrThrow().value) 28 | } 29 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/OptionalTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.* 4 | import com.github.h0tk3y.betterParse.lexer.* 5 | import com.github.h0tk3y.betterParse.parser.* 6 | import com.github.h0tk3y.betterParse.utils.* 7 | import kotlin.test.* 8 | 9 | class OptionalTest : Grammar() { 10 | override val rootParser: Parser get() = throw NoSuchElementException() 11 | 12 | val a by regexToken("a") 13 | val b by regexToken("b") 14 | 15 | @Test fun successful() { 16 | val tokens = tokenizer.tokenize("abab") 17 | val result = optional(a and b and a and b).tryParse(tokens,0) 18 | assertTrue(result.toParsedOrThrow().value is Tuple4) 19 | } 20 | 21 | @Test fun unsuccessful() { 22 | val tokens = tokenizer.tokenize("abab") 23 | val result = optional(b and a and b and a).tryParse(tokens,0) 24 | assertNull(result.toParsedOrThrow().value) 25 | } 26 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/OrTest.kt: -------------------------------------------------------------------------------- 1 | import com.github.h0tk3y.betterParse.combinators.* 2 | import com.github.h0tk3y.betterParse.grammar.* 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import com.github.h0tk3y.betterParse.parser.* 5 | import kotlin.test.* 6 | 7 | class OrTest : Grammar() { 8 | override val rootParser: Parser get() = throw NoSuchElementException() 9 | 10 | val a by regexToken("a") 11 | val b by regexToken("b") 12 | 13 | @Test fun aOrB() { 14 | val tokens = tokenizer.tokenize("abababa") 15 | val abOrA = zeroOrMore((a and b use { t2 }) or a) use { map { it.type } } 16 | val result = abOrA.parseToEnd(tokens) 17 | 18 | assertEquals(listOf(b, b, b, a), result) 19 | } 20 | 21 | @Test fun alternativesError() { 22 | val tokens = tokenizer.tokenize("ab") 23 | val parser = (a and a) or (a and b and a) 24 | val result = parser.tryParse(tokens,0) as AlternativesFailure 25 | 26 | assertTrue(result.errors[0] is MismatchedToken) 27 | assertTrue(result.errors[1] is UnexpectedEof) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ParserTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.lexer.* 3 | import com.github.h0tk3y.betterParse.parser.* 4 | import kotlin.test.* 5 | 6 | class ParserTest { 7 | val a = literalToken("a", "a") 8 | val ignoredX = RegexToken("ignoredX", "x", ignored = true) 9 | 10 | @Test fun ignoredUnparsed() { 11 | val tokens = DefaultTokenizer(listOf(ignoredX, a)).tokenize("axxx") 12 | a.parseToEnd(tokens) // should not throw 13 | } 14 | 15 | @Test fun unparsedReportsNoIgnoredTokens() { 16 | val tokens = DefaultTokenizer(listOf(ignoredX, a)).tokenize("axxxa") 17 | val result = a.tryParseToEnd(tokens,0) 18 | assertTrue(result is UnparsedRemainder && result.startsWith.type == a) 19 | } 20 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/RepeatTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.* 4 | import com.github.h0tk3y.betterParse.lexer.* 5 | import com.github.h0tk3y.betterParse.parser.* 6 | import kotlin.test.* 7 | 8 | internal class RepeatTest : Grammar() { 9 | override val rootParser: Parser get() = throw NoSuchElementException() 10 | 11 | val a by regexToken("a") 12 | 13 | @Test fun repeat() { 14 | val minN = 0 15 | val maxN = 30 16 | 17 | val rangeParsers = 18 | listOf(0..maxN to zeroOrMore(a)) + 19 | (1..maxN to oneOrMore(a)) + 20 | (minN..maxN).map { (it..it) to (it times a) } + 21 | (minN..maxN).map { (it..maxN) to (it timesOrMore a) } + 22 | (minN..maxN).flatMap { i -> 23 | val timesLess = (minN until i).map { j -> (j..i) to (j..i times a) } 24 | val timesMore = (i + 1..maxN).map { j -> (i..j) to (i..j times a) } 25 | timesLess + timesMore 26 | } 27 | 28 | for (n in minN..maxN) { 29 | val input = CharArray(n) { 'a' }.concatToString() 30 | val tokens = tokenizer.tokenize(input) 31 | 32 | for ((range, parser) in rangeParsers) { 33 | val result = parser.tryParseToEnd(tokens,0) 34 | 35 | when { 36 | n in range -> assertTrue(result is Parsed) 37 | n > range.last -> assertTrue(result is UnparsedRemainder) 38 | n < range.first -> assertTrue(result is UnexpectedEof) 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/SeparatedTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.Grammar 4 | import com.github.h0tk3y.betterParse.lexer.regexToken 5 | import com.github.h0tk3y.betterParse.parser.* 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertTrue 9 | 10 | class SeparatedTest : Grammar() { 11 | override val rootParser: Parser get() = throw NoSuchElementException() 12 | 13 | val number by regexToken("\\d+") 14 | val comma by regexToken(",\\s+") 15 | val word by regexToken("\\w+") 16 | 17 | @Test fun separate() { 18 | val tokens = tokenizer.tokenize("one, two, three") 19 | val result = separated(word use { text }, comma).tryParse(tokens,0).toParsedOrThrow().value 20 | 21 | assertEquals(listOf("one", "two", "three"), result.terms) 22 | assertEquals(2, result.separators.size) 23 | } 24 | 25 | @Test fun singleSeparated() { 26 | val tokens = tokenizer.tokenize("one") 27 | val result = separated(word use { text }, comma).parseToEnd(tokens) 28 | 29 | assertEquals(listOf("one"), result.terms) 30 | assertTrue(result.separators.isEmpty()) 31 | } 32 | 33 | @Test fun acceptZero() { 34 | val tokens = tokenizer.tokenize("123") 35 | 36 | val resultRejectZero = separated(word asJust "x", comma).tryParse(tokens,0) 37 | assertTrue(resultRejectZero is MismatchedToken) 38 | 39 | val resultAcceptZero = separated(word asJust "x", comma, acceptZero = true).tryParse(tokens,0) 40 | assertTrue(resultAcceptZero is Parsed && resultAcceptZero.value.terms.isEmpty()) 41 | } 42 | 43 | @Test fun reduceLeftRight() { 44 | val tokens = tokenizer.tokenize("3, 4, 5, 6") 45 | val result = separated(number use { text.toInt() }, comma).parseToEnd(tokens) 46 | 47 | val minusLeft = result.reduce { a, _, b -> a - b } 48 | assertEquals(3 - 4 - 5 - 6, minusLeft) 49 | 50 | val minusRight = result.reduceRight { x, _, y -> y - x } 51 | assertEquals(6 - 5 - 4 - 3, minusRight) 52 | } 53 | 54 | @Test fun associative() { 55 | val tokens = tokenizer.tokenize("3, 4, 5, 6") 56 | val p = (number use { text.toInt() }) as Parser 57 | 58 | val resultLeft = leftAssociative(p, comma) { a, _, b -> a to b }.parseToEnd(tokens) 59 | assertEquals(((3 to 4) to 5) to 6, resultLeft) 60 | 61 | val resultRight = rightAssociative(p, comma) { a, _, b -> a to b }.parseToEnd(tokens) 62 | assertEquals(3 to (4 to (5 to 6)), resultRight) 63 | } 64 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/TestLiftToAst.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.* 3 | import com.github.h0tk3y.betterParse.grammar.Grammar 4 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 5 | import com.github.h0tk3y.betterParse.grammar.parser 6 | import com.github.h0tk3y.betterParse.lexer.TokenMatch 7 | import com.github.h0tk3y.betterParse.lexer.TokenMatchesSequence 8 | import com.github.h0tk3y.betterParse.lexer.regexToken 9 | import com.github.h0tk3y.betterParse.parser.* 10 | import com.github.h0tk3y.betterParse.st.* 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertTrue 14 | 15 | private sealed class BooleanExpression 16 | 17 | private fun Any.typeName() = when (this) { 18 | is Separated<*, *> -> "Separated" 19 | TRUE -> "TRUE" 20 | FALSE -> "FALSE" 21 | is Variable -> "Variable" 22 | is Not -> "Not" 23 | is And -> "And" 24 | is Or -> "Or" 25 | is Impl -> "Impl" 26 | else -> "???" 27 | } 28 | 29 | private object TRUE : BooleanExpression() 30 | private object FALSE : BooleanExpression() 31 | private data class Variable(val name: String) : BooleanExpression() 32 | private data class Not(val body: BooleanExpression) : BooleanExpression() 33 | private data class And(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 34 | private data class Or(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 35 | private data class Impl(val left: BooleanExpression, val right: BooleanExpression) : BooleanExpression() 36 | 37 | private val booleanGrammar = object : Grammar() { 38 | val tru by regexToken("true") 39 | val fal by regexToken("false") 40 | val id by regexToken("\\w+") 41 | val lpar by regexToken("\\(") 42 | val rpar by regexToken("\\)") 43 | val not by regexToken("!") 44 | val and by regexToken("&") 45 | val or by regexToken("\\|") 46 | val impl by regexToken("->") 47 | val ws by regexToken("\\s+", ignore = true) 48 | 49 | val term: Parser by 50 | (tru asJust TRUE) or 51 | (fal asJust FALSE) or 52 | (id map { Variable(it.text) }) or 53 | (-not * parser(this::term) map { Not(it) }) or 54 | (-lpar * parser(this::implChain) * -rpar) 55 | 56 | val andChain by leftAssociative(term, and) { a, _, b -> And(a, b) } 57 | val orChain by leftAssociative(andChain, or) { a, _, b -> Or(a, b) } 58 | val implChain by rightAssociative(orChain, impl) { a, _, b -> Impl(a, b) } 59 | 60 | override val rootParser by implChain 61 | } 62 | 63 | private fun SyntaxTree<*>.topDownNodesSequence(): Sequence> = sequence { 64 | yield(this@topDownNodesSequence) 65 | for (c in children) { 66 | yieldAll(c.topDownNodesSequence()) 67 | } 68 | } 69 | 70 | private fun SyntaxTree<*>.toTopDownStrings() = topDownNodesSequence() 71 | .filter { 72 | (it.children.isEmpty() || it.children.size > 1) 73 | } 74 | .map { 75 | val item = it.item 76 | if (item is TokenMatch) 77 | item.text else 78 | it.item?.typeName() 79 | }.toList() 80 | 81 | internal class TestLiftToAst { 82 | @Test 83 | fun continuousRange() { 84 | val astParser = booleanGrammar.liftToSyntaxTreeGrammar(LiftToSyntaxTreeOptions(retainSkipped = true), structureParsers = null) 85 | val ast = astParser.parseToEnd("a&(b1->c1)|a1&!b|!(a1->a2)->a") 86 | checkAstContinuousRange(ast) 87 | } 88 | 89 | @Test 90 | fun astStructure() { 91 | val astParser = booleanGrammar.liftToSyntaxTreeGrammar(LiftToSyntaxTreeOptions(retainSkipped = true), structureParsers = null) 92 | val ast = astParser.parseToEnd("(!(a)->((b)|(!c))->!(!a)&!(d))") 93 | val types = ast.toTopDownStrings() 94 | 95 | val expected = listOf("Impl", "Impl", "(", "Separated", "Variable", "!", "Variable", "Variable", "(", "a", ")", 96 | "->", "Or", "Or", "(", "Separated", "Variable", "Variable", "(", "b", ")", "|", "Not", "Not", "(", 97 | "Variable", "!", "c", ")", ")", "->", "Separated", "Not", "!", "Not", "Not", "(", "Variable", "!", "a", ")", 98 | "&", "Variable", "!", "Variable", "Variable", "(", "d", ")", ")") 99 | assertEquals(expected, types) 100 | } 101 | 102 | @Test 103 | fun testDropSkipped() { 104 | val astParser = booleanGrammar.liftToSyntaxTreeGrammar(LiftToSyntaxTreeOptions(retainSkipped = false), structureParsers = null) 105 | val ast = astParser.parseToEnd("(!(a)->((b)|(!c))->!(!a)&!(d))") 106 | val types = ast.toTopDownStrings() 107 | 108 | val expected = listOf("Separated", "a", "->", "Separated", "b", "|", "c", "->", "Separated", "a", "&", "d") 109 | assertEquals(expected, types) 110 | } 111 | 112 | fun checkAstContinuousRange(syntaxTree: SyntaxTree<*>) { 113 | val first = syntaxTree.range.first 114 | val last = syntaxTree.range.last 115 | if (syntaxTree.children.isNotEmpty()) { 116 | assertEquals(first, syntaxTree.children.first().range.first) 117 | assertEquals(last, syntaxTree.children.last().range.last) 118 | for ((i, j) in syntaxTree.children.zip(syntaxTree.children.drop(1))) { 119 | assertEquals(i.range.last, j.range.first - 1) 120 | } 121 | for (c in syntaxTree.children) { 122 | checkAstContinuousRange(c) 123 | } 124 | } 125 | } 126 | 127 | @Test 128 | fun testFlattening() { 129 | val expr = "a & (b1 -> c1) | a1 & !b | !(a1 -> a2) -> a" 130 | val booleanAstGrammar = booleanGrammar.liftToSyntaxTreeGrammar(LiftToSyntaxTreeOptions(retainSeparators = false)) 131 | val ast = booleanAstGrammar.parseToEnd(expr) 132 | assertTrue(ast.topDownNodesSequence().all { it.parser in booleanAstGrammar.declaredParsers }) 133 | assertEquals( 134 | listOf("Impl", "Or", "And", "a", "Impl", "b1", "c1", "And", "a1", "b", "Impl", "a1", "a2", "a"), 135 | ast.toTopDownStrings()) 136 | } 137 | 138 | @Test 139 | fun testDropSeparators() { 140 | val expr = "a & (b1 -> c1) | a1 & !b | !(a1 -> a2) -> a" 141 | val booleanAstGrammar = booleanGrammar.liftToSyntaxTreeGrammar(LiftToSyntaxTreeOptions(retainSkipped = false, retainSeparators = false)) 142 | val ast = booleanAstGrammar.parseToEnd(expr) 143 | val separatorStrings = setOf("&", "|", "->") 144 | assertTrue(ast.toTopDownStrings().none { it in separatorStrings }) 145 | } 146 | 147 | @Test 148 | fun testCustomTransformer() { 149 | class ForcedDuplicate(val alternatives: List>) : 150 | Parser> { 151 | override fun tryParse( 152 | tokens: TokenMatchesSequence, 153 | fromPosition: Int 154 | ): ParseResult> { 155 | val res = alternatives.asSequence().map { it to it.tryParse(tokens, fromPosition) }.firstOrNull { it.second is Parsed } 156 | ?: return object : ErrorResult() {} 157 | val (parser1, res1) = res 158 | val res2 = parser1.tryParse(tokens, res1.toParsedOrThrow().nextPosition) 159 | return when (res2) { 160 | is ErrorResult -> res2 161 | is Parsed -> ParsedValue( 162 | res1.toParsedOrThrow().value to res2.value, 163 | res2.nextPosition 164 | ) 165 | } 166 | } 167 | } 168 | 169 | val transformer = object : LiftToSyntaxTreeTransformer { 170 | @Suppress("UNCHECKED_CAST") 171 | override fun liftToSyntaxTree( 172 | parser: Parser, 173 | default: LiftToSyntaxTreeTransformer.DefaultTransformerReference 174 | ): Parser> { 175 | if (parser is ForcedDuplicate<*>) 176 | return object : Parser> { 177 | val parsers = parser.alternatives.map { default.transform(it) } 178 | 179 | override fun tryParse( 180 | tokens: TokenMatchesSequence, 181 | fromPosition: Int 182 | ): ParseResult> { 183 | val res = parsers.asSequence().map { it to it.tryParse(tokens, fromPosition) }.firstOrNull { it.second is Parsed<*> } 184 | ?: return object : ErrorResult() {} 185 | val (parser1, res1) = res 186 | res1 as Parsed> 187 | val res2 = parser1.tryParse(tokens, res1.toParsedOrThrow().nextPosition) 188 | return when (res2) { 189 | is ErrorResult -> res2 190 | is Parsed> -> ParsedValue( 191 | SyntaxTree( 192 | item = res1.toParsedOrThrow().value.item to res2.value.item, 193 | children = listOf(res1.value, res2.value), 194 | parser = parser, 195 | range = res1.value.range.first..res2.value.range.last 196 | ) as SyntaxTree, 197 | res2.nextPosition 198 | ) 199 | } 200 | } 201 | } 202 | else throw IllegalArgumentException("Unexpected parser type") 203 | } 204 | } 205 | 206 | val parser = ForcedDuplicate(listOf(booleanGrammar.and, booleanGrammar.or, booleanGrammar.impl)) 207 | 208 | val lifted = parser.liftToSyntaxTreeParser( 209 | structureParsers = booleanGrammar.declaredParsers, 210 | transformer = transformer 211 | ) 212 | 213 | val result = lifted.tryParse(booleanGrammar.tokenizer.tokenize("||"),0) 214 | val value = result.toParsedOrThrow().value 215 | 216 | @Suppress("USELESS_IS_CHECK") 217 | assertTrue(value is SyntaxTree<*>) 218 | 219 | assertTrue(value.children.size == 2 && value.children.all { it.parser === booleanGrammar.or }) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/TokenTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.lexer.* 3 | import com.github.h0tk3y.betterParse.parser.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class TokenTest { 8 | val a = regexToken("a", "a") 9 | val b = literalToken("b", "b") 10 | val ignoredX = RegexToken("ignoredX", "x", true) 11 | val num = regexToken("-?[0-9]*(?:\\.[0-9]*)?") 12 | 13 | @Test 14 | fun expectIgnoredToken() { 15 | val tokens = DefaultTokenizer(listOf(a, b, ignoredX)).tokenize("xxaba") 16 | val result = ignoredX.tryParse(tokens, 0).toParsedOrThrow() 17 | assertEquals("x", result.value.text) 18 | } 19 | 20 | @Test 21 | fun successfulParse() { 22 | val tokens = DefaultTokenizer(listOf(a)).tokenize("aaa") 23 | val result = a.tryParse(tokens, 0) as Parsed 24 | 25 | assertEquals(a, result.value.type) 26 | assertEquals(listOf(a, a), tokens.drop(result.nextPosition).toList().map { it.type }) 27 | } 28 | 29 | @Test 30 | fun unexpectedEof() { 31 | val tokens = TokenMatchesSequence(object : TokenProducer { 32 | override fun nextToken(): TokenMatch? = null 33 | }, arrayListOf()) 34 | 35 | val result = a.tryParseToEnd(tokens, 0) 36 | 37 | assertEquals(UnexpectedEof(a), result) 38 | } 39 | 40 | @Test 41 | fun noMatchingToken() { 42 | val input = "c" 43 | val tokens = DefaultTokenizer(listOf(a, b)).tokenize(input) 44 | val result = a.tryParse(tokens, 0) 45 | 46 | assertEquals(NoMatchingToken(TokenMatch(noneMatched, 0, input, 0, 1, 1, 1)), result) 47 | } 48 | 49 | @Test 50 | fun ignored() { 51 | val input = "xxxa" 52 | val tokens = DefaultTokenizer(listOf(ignoredX, a)).tokenize(input) 53 | val result = a.tryParse(tokens, 0) 54 | 55 | assertEquals(TokenMatch(a, 3, input, 3, 1, 1, 4), result.toParsedOrThrow().value) 56 | } 57 | 58 | @Test 59 | fun mismatched() { 60 | val input = "b" 61 | val tokens = DefaultTokenizer(listOf(a, b)).tokenize(input) 62 | val result = a.tryParse(tokens, 0) 63 | 64 | assertEquals(MismatchedToken(a, TokenMatch(b, 0, input, 0, 1, 1, 1)), result) 65 | } 66 | 67 | @Test 68 | fun mismatchedRegex() { 69 | val input = "b" 70 | val tokens = DefaultTokenizer(listOf(num)).tokenize(input) 71 | val result = num.tryParse(tokens, 0) 72 | 73 | assertEquals(NoMatchingToken(TokenMatch(noneMatched, 0, input, 0, 1, 1, 1)), result) 74 | } 75 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/TokenizerTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.grammar.Grammar 3 | import com.github.h0tk3y.betterParse.lexer.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class TokenizerTest { 8 | @Test fun simpleInput() { 9 | val aPlus = regexToken("aPlus", "a+", false) 10 | val bPlus = regexToken("bPlus", "b+", false) 11 | val justX = literalToken("justX", "x", false) 12 | val lexer = DefaultTokenizer(listOf(aPlus, bPlus, justX)) 13 | 14 | val input = "aaaxxxbbbaaa" 15 | val types = lexer.tokenize(input).toList().map { it.type } 16 | 17 | assertEquals(listOf(aPlus, justX, justX, justX, bPlus, aPlus), types) 18 | } 19 | 20 | @Test fun position() { 21 | val aPlus = RegexToken("aPlus", "a+") 22 | val bPlus = RegexToken("bPlus", "b+") 23 | val lexer = DefaultTokenizer(listOf(aPlus, bPlus)) 24 | 25 | val input = "abaaabbbaaaabbbb" 26 | val positions = lexer.tokenize(input).toList().map { it.offset } 27 | 28 | assertEquals(listOf(0, 1, 2, 5, 8, 12), positions) 29 | } 30 | 31 | @Test fun rowAndColumn() { 32 | val aPlus = regexToken("aPlus", "a+") 33 | val bPlus = regexToken("bPlus", "b+") 34 | val br = literalToken("break", "\n") 35 | val lexer = DefaultTokenizer(listOf(aPlus, bPlus, br)) 36 | 37 | val input = """ 38 | aaa 39 | bbb 40 | ab 41 | """.trimIndent() 42 | 43 | val rowCols = lexer.tokenize(input).toList().map { it.row to it.column } 44 | 45 | assertEquals(listOf(1 to 1, 1 to 4, 46 | 2 to 1, 2 to 4, 47 | 3 to 1, 3 to 2), rowCols) 48 | } 49 | 50 | @Test fun mismatchedToken() { 51 | val a = RegexToken("a", "a") 52 | val b = RegexToken("b", "b") 53 | val lexer = DefaultTokenizer(listOf(a, b)) 54 | 55 | val input = "aabbxxaa" 56 | 57 | val result = lexer.tokenize(input).toList() 58 | 59 | assertEquals(5, result.size) 60 | assertEquals(noneMatched, result.last().type) 61 | assertEquals("xxaa", result.last().text) 62 | assertEquals(4, result.last().offset) 63 | } 64 | 65 | @Test fun priority() { 66 | val a = regexToken("a", "a") 67 | val aa = literalToken("aa", "aa") 68 | 69 | val input = "aaaaaa" 70 | 71 | val lexAFirst = DefaultTokenizer(listOf(a, aa)) 72 | val resultAFirst = lexAFirst.tokenize(input).toList().map { it.type } 73 | assertEquals(6, resultAFirst.size) 74 | assertEquals(a, resultAFirst.distinct().single()) 75 | 76 | val lexAaFirst = DefaultTokenizer(listOf(aa, a)) 77 | val resultAaFirst = lexAaFirst.tokenize(input).toList().map { it.type } 78 | assertEquals(3, resultAaFirst.size) 79 | assertEquals(aa, resultAaFirst.distinct().single()) 80 | } 81 | 82 | @Test fun tokensWithGroups() { 83 | val a = regexToken("a(b)c") 84 | val b = regexToken("d(e)f") 85 | val c = regexToken("g(h)i") 86 | val input = "abcdefghi" 87 | val lex = DefaultTokenizer(listOf(a, b, c)) 88 | val result = lex.tokenize(input) 89 | 90 | assertEquals(listOf(a, b, c), result.toList().map { it.type }) 91 | } 92 | 93 | @Test fun issue28() { 94 | val a = regexToken("a+".toRegex()) 95 | val b = regexToken("b+".toRegex()) 96 | val lex = DefaultTokenizer(listOf(a, b)) 97 | val input = "abab" 98 | assertEquals(listOf(a, b, a, b), lex.tokenize(input).toList().map { it.type }) 99 | } 100 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/github/h0tk3y/betterParse/lexer/RegexToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | import kotlin.js.RegExp 4 | 5 | public actual class RegexToken : Token { 6 | private val pattern: String 7 | private val regex: Regex 8 | 9 | /** To ensure that the [regex] will only match its pattern from the index where it is called on with 10 | * Regex.find(input, startIndex), set the JS RegExp flag 'y', which makes the RegExp 'sticky'. 11 | * See: https://javascript.info/regexp-sticky */ 12 | private fun preprocessRegex(regex: Regex) { 13 | val possibleNames = listOf("nativePattern_1", "nativePattern_0", "_nativePattern") 14 | for(name in possibleNames) { 15 | val r = regex.asDynamic()[name] 16 | if(jsTypeOf(r) !== "undefined" && r !== null) { 17 | val src = r.source as String 18 | val flags = r.flags as String + if(r.sticky as Boolean) "" else "y" 19 | regex.asDynamic()[name] = RegExp(src, flags) 20 | break 21 | } 22 | } 23 | } 24 | 25 | public actual constructor(name: String?, patternString: String, ignored: Boolean) 26 | : super(name, ignored) { 27 | pattern = patternString 28 | regex = pattern.toRegex() 29 | preprocessRegex(regex) 30 | } 31 | 32 | public actual constructor(name: String?, regex: Regex, ignored: Boolean) 33 | : super(name, ignored) { 34 | pattern = regex.pattern 35 | this.regex = regex 36 | preprocessRegex(regex) 37 | } 38 | 39 | override fun match(input: CharSequence, fromIndex: Int): Int = 40 | regex.find(input, fromIndex)?.range?.let { 41 | val length = it.last - it.first + 1 42 | length 43 | } ?: 0 44 | 45 | override fun toString(): String = "${name ?: ""} [$pattern]" + if (ignored) " [ignorable]" else "" 46 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/h0tk3y/betterParse/lexer/Language.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | import org.intellij.lang.annotations.Language 4 | 5 | public actual typealias Language = Language -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/github/h0tk3y/betterParse/lexer/RegexToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | import java.util.* 4 | import java.util.regex.Matcher 5 | 6 | public actual class RegexToken private constructor( 7 | name: String?, 8 | ignored: Boolean, 9 | private val pattern: String, 10 | private val regex: Regex 11 | ) : Token(name, ignored) { 12 | 13 | private val threadLocalMatcher = object : ThreadLocal() { 14 | override fun initialValue() = regex.toPattern().matcher("") 15 | } 16 | 17 | private val matcher: Matcher get() = threadLocalMatcher.get() 18 | 19 | private companion object { 20 | private const val inputStartPrefix = "\\A" 21 | 22 | private fun prependPatternWithInputStart(patternString: String, options: Set) = 23 | if (patternString.startsWith(Companion.inputStartPrefix)) 24 | patternString.toRegex(options) 25 | else { 26 | val newlineAfterComments = if (RegexOption.COMMENTS in options) "\n" else "" 27 | val patternToEmbed = if (RegexOption.LITERAL in options) Regex.escape(patternString) else patternString 28 | ("${inputStartPrefix}(?:$patternToEmbed$newlineAfterComments)").toRegex(options - RegexOption.LITERAL) 29 | } 30 | 31 | } 32 | 33 | public actual constructor( 34 | name: String?, 35 | @Language("RegExp", "", "") patternString: String, 36 | ignored: Boolean 37 | ) : this( 38 | name, 39 | ignored, 40 | patternString, 41 | prependPatternWithInputStart(patternString, emptySet()) 42 | ) 43 | 44 | public actual constructor( 45 | name: String?, 46 | regex: Regex, 47 | ignored: Boolean 48 | ) : this( 49 | name, 50 | ignored, 51 | regex.pattern, 52 | prependPatternWithInputStart(regex.pattern, regex.options) 53 | ) 54 | 55 | override fun match(input: CharSequence, fromIndex: Int): Int { 56 | matcher.reset(input).region(fromIndex, input.length) 57 | 58 | if (!matcher.find()) { 59 | return 0 60 | } 61 | 62 | val end = matcher.end() 63 | return end - fromIndex 64 | } 65 | 66 | override fun toString(): String = "${name ?: ""} [$pattern]" + if (ignored) " [ignorable]" else "" 67 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/FlagsCompatibilityTest.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.github.h0tk3y.betterParse.combinators.times 3 | import com.github.h0tk3y.betterParse.combinators.use 4 | import com.github.h0tk3y.betterParse.grammar.Grammar 5 | import com.github.h0tk3y.betterParse.grammar.parseToEnd 6 | import com.github.h0tk3y.betterParse.lexer.DefaultTokenizer 7 | import com.github.h0tk3y.betterParse.lexer.regexToken 8 | import com.github.h0tk3y.betterParse.parser.Parser 9 | import com.github.h0tk3y.betterParse.utils.components 10 | import kotlin.test.Test 11 | import kotlin.test.assertEquals 12 | import kotlin.text.RegexOption.* 13 | 14 | class FlagsCompatibilityTest { 15 | @Test 16 | fun testJavaPatternFlags() { 17 | val patternsGrammar = object : Grammar() { 18 | val caseInsensitive by regexToken(Regex("abc", IGNORE_CASE)) 19 | 20 | val comments by regexToken( 21 | Regex( 22 | """ 23 | d # some comment 24 | e # some comment 25 | f # some comment 26 | """.trimIndent(), 27 | COMMENTS 28 | ) 29 | ) 30 | 31 | val dotall by regexToken(Regex("eol.*?dotall", DOT_MATCHES_ALL)) 32 | 33 | val literal by regexToken(Regex(".*.*.*", LITERAL)) 34 | 35 | val multiline by regexToken(Regex("eol$\n^multiline", MULTILINE)) 36 | 37 | val unixLines by regexToken(Regex(". x", UNIX_LINES)) 38 | 39 | override val rootParser: Parser 40 | get() = (caseInsensitive * comments * dotall * literal * multiline /** unicodeCase*/ * unixLines).use { 41 | components.joinToString("") { it.text } 42 | } 43 | } 44 | 45 | val input = """ 46 | AbCdefeol 47 | dotall.*.*.*eol 48 | multiline 49 | """.trimIndent() + "\r x" 50 | 51 | val result = patternsGrammar.parseToEnd(input) 52 | assertEquals(input, result) 53 | } 54 | 55 | @Test 56 | fun testKotlinRegexFlags() { 57 | val regexesGrammar = object : Grammar() { 58 | val caseInsensitive by regexToken("abc".toRegex(IGNORE_CASE)) 59 | 60 | val comments by regexToken( 61 | """ 62 | d # some comment 63 | e # some comment 64 | f # some comment 65 | """.trimIndent().toRegex(COMMENTS) 66 | ) 67 | 68 | val dotall by regexToken("eol.*?dotall".toRegex(DOT_MATCHES_ALL)) 69 | 70 | val literal by regexToken(".*.*.*".toRegex(LITERAL)) 71 | 72 | val multiline by regexToken("eol$\n^multiline".toRegex(MULTILINE)) 73 | 74 | val unixLines by regexToken(". x".toRegex(UNIX_LINES)) 75 | 76 | override val rootParser: Parser 77 | get() = (caseInsensitive * comments * dotall * literal * multiline * unixLines).use { 78 | components.joinToString("") { it.text } 79 | } 80 | } 81 | 82 | val input = """ 83 | AbCdefeol 84 | dotall.*.*.*eol 85 | multiline 86 | """.trimIndent() + "\r x" 87 | 88 | val result = regexesGrammar.parseToEnd(input) 89 | assertEquals(input, result) 90 | } 91 | 92 | @Test 93 | fun issue28WithRegexFlags() { 94 | val a = regexToken(Regex("\\\\", setOf(LITERAL))) 95 | val b = regexToken(Regex("b#comment", setOf(COMMENTS))) 96 | val input = "\\\\b\\\\b" 97 | val lex = DefaultTokenizer(listOf(a, b)) 98 | assertEquals(listOf(a, b, a, b), lex.tokenize(input).toList().map { it.type }) 99 | } 100 | } -------------------------------------------------------------------------------- /src/linuxX64Test/kotlin/ConcurrentExecution.kt: -------------------------------------------------------------------------------- 1 | import com.github.h0tk3y.betterParse.combinators.and 2 | import com.github.h0tk3y.betterParse.combinators.use 3 | import com.github.h0tk3y.betterParse.grammar.Grammar 4 | import com.github.h0tk3y.betterParse.lexer.regexToken 5 | import com.github.h0tk3y.betterParse.parser.Parser 6 | import com.github.h0tk3y.betterParse.parser.parseToEnd 7 | import com.github.h0tk3y.betterParse.utils.components 8 | import kotlin.native.concurrent.TransferMode 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.native.concurrent.Worker 12 | 13 | class ConcurrentExecution { 14 | 15 | object TestGrammar : Grammar() { 16 | override val rootParser: Parser get() = throw NoSuchElementException() 17 | 18 | val a by regexToken("a") 19 | val b by regexToken("b") 20 | 21 | val parser = a and b and a use { components.map { it.type } } 22 | } 23 | 24 | @Test 25 | fun foo() { 26 | val worker = Worker.start() 27 | worker.execute(TransferMode.UNSAFE, { "aba" }) { string -> 28 | val tokens = TestGrammar.tokenizer.tokenize(string) 29 | TestGrammar.parser.parseToEnd(tokens) 30 | }.consume { result -> 31 | assertEquals(listOf(TestGrammar.a, TestGrammar.b, TestGrammar.a), result) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/nativeMain/kotlin/com/github/h0tk3y/betterParse/lexer/com/github/h0tk3y/betterParse/lexer/RegexToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.lexer 2 | 3 | public actual class RegexToken : Token { 4 | private val pattern: String 5 | private val regex: Regex 6 | 7 | private companion object { 8 | const val inputStartPrefix = "\\A" 9 | } 10 | 11 | private fun prependPatternWithInputStart(patternString: String, options: Set) = 12 | if (patternString.startsWith(inputStartPrefix)) 13 | patternString.toRegex(options) 14 | else { 15 | ("$inputStartPrefix(?:$patternString)").toRegex(options) 16 | } 17 | 18 | public actual constructor(name: String?, @Language("RegExp", "", "") patternString: String, ignored: Boolean) 19 | : super(name, ignored) { 20 | pattern = patternString 21 | regex = prependPatternWithInputStart(patternString, emptySet()) 22 | } 23 | 24 | public actual constructor(name: String?, regex: Regex, ignored: Boolean) 25 | : super(name, ignored) { 26 | pattern = regex.pattern 27 | this.regex = prependPatternWithInputStart(pattern, emptySet()) 28 | } 29 | 30 | private class RelativeInput(val fromIndex: Int, val input: CharSequence) : CharSequence { 31 | override val length: Int get() = input.length - fromIndex 32 | override fun get(index: Int): Char = input[index + fromIndex] 33 | override fun subSequence(startIndex: Int, endIndex: Int) = 34 | input.subSequence(startIndex + fromIndex, endIndex + fromIndex) 35 | 36 | override fun toString(): String = error("unsupported operation") 37 | } 38 | 39 | override fun match(input: CharSequence, fromIndex: Int): Int { 40 | val relativeInput = RelativeInput(fromIndex, input) 41 | 42 | return regex.find(relativeInput)?.range?.let { 43 | val length = it.last - it.first + 1 44 | length 45 | } ?: 0 46 | } 47 | 48 | override fun toString(): String = "${name ?: ""} [$pattern]" + if (ignored) " [ignorable]" else "" 49 | } 50 | -------------------------------------------------------------------------------- /versions.settings.gradle.kts: -------------------------------------------------------------------------------- 1 | package com.github.h0tk3y.betterParse.build 2 | 3 | import org.gradle.api.plugins.ExtraPropertiesExtension 4 | import kotlin.reflect.full.memberProperties 5 | 6 | val kotlinVersion = KotlinPlugin.V1620 7 | 8 | enum class KotlinPlugin { 9 | V1620 10 | } 11 | 12 | val versions = when (kotlinVersion) { 13 | KotlinPlugin.V1620 -> Versions( 14 | version = "0.4.4", 15 | kotlinVersion = "1.6.20", 16 | serializationVersion = "1.3.2", 17 | benchmarkVersion = "0.4.2" 18 | ) 19 | } 20 | 21 | // Register all versions as system properties: 22 | 23 | versions.javaClass.kotlin.memberProperties.forEach { property -> 24 | val value = property.get(versions) 25 | addGlobalProperty(property.name, value.toString()) 26 | } 27 | 28 | gradle.allprojects { version = versions.version } 29 | 30 | data class Versions( 31 | val version: String, 32 | val kotlinVersion: String, 33 | val serializationVersion: String, 34 | val benchmarkVersion: String 35 | ) 36 | 37 | fun addGlobalProperty(key: String, value: String) { 38 | System.setProperty("build.$key", value) 39 | fun ExtraPropertiesExtension.addExt() { set(key, value) } 40 | settings.extensions.extraProperties.addExt() 41 | gradle.allprojects { extensions.extraProperties.addExt() } 42 | } 43 | --------------------------------------------------------------------------------