Adding the beginning components of a parser and some tests.
This commit is contained in:
		
							parent
							
								
									c3d84bc4a6
								
							
						
					
					
						commit
						a10f5fa1b1
					
				
					 42 changed files with 633 additions and 5 deletions
				
			
		
							
								
								
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | target/ | ||||||
|  | project/target/ | ||||||
|  | project/project/ | ||||||
|  | modules/core/target/ | ||||||
|  | @ -5,8 +5,11 @@ repos: | ||||||
|     hooks: |     hooks: | ||||||
|       - id: end-of-file-fixer |       - id: end-of-file-fixer | ||||||
|       - id: trailing-whitespace |       - id: trailing-whitespace | ||||||
|  |       - id: check-yaml | ||||||
|       - id: fix-byte-order-marker |       - id: fix-byte-order-marker | ||||||
|       - id: mixed-line-ending |       - id: mixed-line-ending | ||||||
|         args: ['--fix=lf'] |         args: [--fix=lf] | ||||||
|         description: Enforces using only 'LF' line endings. |   - repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala | ||||||
|       - id: trailing-whitespace |     rev: v0.1.3 | ||||||
|  |     hooks: | ||||||
|  |       - id: scalafmt | ||||||
|  |  | ||||||
							
								
								
									
										71
									
								
								.scalafmt.conf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								.scalafmt.conf
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | // See: https://github.com/scalameta/scalafmt/tags for the latest tags. | ||||||
|  | version = 3.7.11 | ||||||
|  | runner.dialect = scala3 | ||||||
|  | maxColumn = 80 | ||||||
|  | 
 | ||||||
|  | rewrite { | ||||||
|  |     rules = [RedundantBraces, RedundantParens, Imports, SortModifiers] | ||||||
|  |     imports.expand = true | ||||||
|  |     imports.sort = scalastyle | ||||||
|  |     redundantBraces.ifElseExpressions = true | ||||||
|  |     redundantBraces.stringInterpolation = true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | indent { | ||||||
|  |     main = 2 | ||||||
|  |     callSite = 2 | ||||||
|  |     defnSite = 2 | ||||||
|  |     extendSite = 4 | ||||||
|  |     withSiteRelativeToExtends = 2 | ||||||
|  |     commaSiteRelativeToExtends = 2 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | align { | ||||||
|  |     preset = more | ||||||
|  |     openParenCallSite = false | ||||||
|  |     openParenDefnSite = false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | newlines { | ||||||
|  |     implicitParamListModifierForce = [before,after] | ||||||
|  |     topLevelStatementBlankLines = [ | ||||||
|  |         { | ||||||
|  |             blanks = 1 | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  |     afterCurlyLambdaParams = squash | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | danglingParentheses { | ||||||
|  |     defnSite = true | ||||||
|  |     callSite = true | ||||||
|  |     ctrlSite = true | ||||||
|  |     exclude = [] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | verticalMultiline { | ||||||
|  |     atDefnSite = true | ||||||
|  |     arityThreshold = 2 | ||||||
|  |     newlineAfterOpenParen = true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | comments { | ||||||
|  |     wrap = standalone | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | docstrings { | ||||||
|  |     style = "SpaceAsterisk" | ||||||
|  |     oneline = unfold | ||||||
|  |     wrap = yes | ||||||
|  |     forceBlankLineBefore = true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | project { | ||||||
|  |   excludePaths = [ | ||||||
|  |     "glob:**target/**", | ||||||
|  |     "glob:**.metals/**", | ||||||
|  |     "glob:**.bloop/**", | ||||||
|  |     "glob:**.bsp/**", | ||||||
|  |     "glob:**metals.sbt" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										202
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,202 @@ | ||||||
|  | 
 | ||||||
|  |                                  Apache License | ||||||
|  |                            Version 2.0, January 2004 | ||||||
|  |                         http://www.apache.org/licenses/ | ||||||
|  | 
 | ||||||
|  |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||||
|  | 
 | ||||||
|  |    1. Definitions. | ||||||
|  | 
 | ||||||
|  |       "License" shall mean the terms and conditions for use, reproduction, | ||||||
|  |       and distribution as defined by Sections 1 through 9 of this document. | ||||||
|  | 
 | ||||||
|  |       "Licensor" shall mean the copyright owner or entity authorized by | ||||||
|  |       the copyright owner that is granting the License. | ||||||
|  | 
 | ||||||
|  |       "Legal Entity" shall mean the union of the acting entity and all | ||||||
|  |       other entities that control, are controlled by, or are under common | ||||||
|  |       control with that entity. For the purposes of this definition, | ||||||
|  |       "control" means (i) the power, direct or indirect, to cause the | ||||||
|  |       direction or management of such entity, whether by contract or | ||||||
|  |       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||||
|  |       outstanding shares, or (iii) beneficial ownership of such entity. | ||||||
|  | 
 | ||||||
|  |       "You" (or "Your") shall mean an individual or Legal Entity | ||||||
|  |       exercising permissions granted by this License. | ||||||
|  | 
 | ||||||
|  |       "Source" form shall mean the preferred form for making modifications, | ||||||
|  |       including but not limited to software source code, documentation | ||||||
|  |       source, and configuration files. | ||||||
|  | 
 | ||||||
|  |       "Object" form shall mean any form resulting from mechanical | ||||||
|  |       transformation or translation of a Source form, including but | ||||||
|  |       not limited to compiled object code, generated documentation, | ||||||
|  |       and conversions to other media types. | ||||||
|  | 
 | ||||||
|  |       "Work" shall mean the work of authorship, whether in Source or | ||||||
|  |       Object form, made available under the License, as indicated by a | ||||||
|  |       copyright notice that is included in or attached to the work | ||||||
|  |       (an example is provided in the Appendix below). | ||||||
|  | 
 | ||||||
|  |       "Derivative Works" shall mean any work, whether in Source or Object | ||||||
|  |       form, that is based on (or derived from) the Work and for which the | ||||||
|  |       editorial revisions, annotations, elaborations, or other modifications | ||||||
|  |       represent, as a whole, an original work of authorship. For the purposes | ||||||
|  |       of this License, Derivative Works shall not include works that remain | ||||||
|  |       separable from, or merely link (or bind by name) to the interfaces of, | ||||||
|  |       the Work and Derivative Works thereof. | ||||||
|  | 
 | ||||||
|  |       "Contribution" shall mean any work of authorship, including | ||||||
|  |       the original version of the Work and any modifications or additions | ||||||
|  |       to that Work or Derivative Works thereof, that is intentionally | ||||||
|  |       submitted to Licensor for inclusion in the Work by the copyright owner | ||||||
|  |       or by an individual or Legal Entity authorized to submit on behalf of | ||||||
|  |       the copyright owner. For the purposes of this definition, "submitted" | ||||||
|  |       means any form of electronic, verbal, or written communication sent | ||||||
|  |       to the Licensor or its representatives, including but not limited to | ||||||
|  |       communication on electronic mailing lists, source code control systems, | ||||||
|  |       and issue tracking systems that are managed by, or on behalf of, the | ||||||
|  |       Licensor for the purpose of discussing and improving the Work, but | ||||||
|  |       excluding communication that is conspicuously marked or otherwise | ||||||
|  |       designated in writing by the copyright owner as "Not a Contribution." | ||||||
|  | 
 | ||||||
|  |       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||||
|  |       on behalf of whom a Contribution has been received by Licensor and | ||||||
|  |       subsequently incorporated within the Work. | ||||||
|  | 
 | ||||||
|  |    2. Grant of Copyright License. Subject to the terms and conditions of | ||||||
|  |       this License, each Contributor hereby grants to You a perpetual, | ||||||
|  |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||||
|  |       copyright license to reproduce, prepare Derivative Works of, | ||||||
|  |       publicly display, publicly perform, sublicense, and distribute the | ||||||
|  |       Work and such Derivative Works in Source or Object form. | ||||||
|  | 
 | ||||||
|  |    3. Grant of Patent License. Subject to the terms and conditions of | ||||||
|  |       this License, each Contributor hereby grants to You a perpetual, | ||||||
|  |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||||
|  |       (except as stated in this section) patent license to make, have made, | ||||||
|  |       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||||
|  |       where such license applies only to those patent claims licensable | ||||||
|  |       by such Contributor that are necessarily infringed by their | ||||||
|  |       Contribution(s) alone or by combination of their Contribution(s) | ||||||
|  |       with the Work to which such Contribution(s) was submitted. If You | ||||||
|  |       institute patent litigation against any entity (including a | ||||||
|  |       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||||
|  |       or a Contribution incorporated within the Work constitutes direct | ||||||
|  |       or contributory patent infringement, then any patent licenses | ||||||
|  |       granted to You under this License for that Work shall terminate | ||||||
|  |       as of the date such litigation is filed. | ||||||
|  | 
 | ||||||
|  |    4. Redistribution. You may reproduce and distribute copies of the | ||||||
|  |       Work or Derivative Works thereof in any medium, with or without | ||||||
|  |       modifications, and in Source or Object form, provided that You | ||||||
|  |       meet the following conditions: | ||||||
|  | 
 | ||||||
|  |       (a) You must give any other recipients of the Work or | ||||||
|  |           Derivative Works a copy of this License; and | ||||||
|  | 
 | ||||||
|  |       (b) You must cause any modified files to carry prominent notices | ||||||
|  |           stating that You changed the files; and | ||||||
|  | 
 | ||||||
|  |       (c) You must retain, in the Source form of any Derivative Works | ||||||
|  |           that You distribute, all copyright, patent, trademark, and | ||||||
|  |           attribution notices from the Source form of the Work, | ||||||
|  |           excluding those notices that do not pertain to any part of | ||||||
|  |           the Derivative Works; and | ||||||
|  | 
 | ||||||
|  |       (d) If the Work includes a "NOTICE" text file as part of its | ||||||
|  |           distribution, then any Derivative Works that You distribute must | ||||||
|  |           include a readable copy of the attribution notices contained | ||||||
|  |           within such NOTICE file, excluding those notices that do not | ||||||
|  |           pertain to any part of the Derivative Works, in at least one | ||||||
|  |           of the following places: within a NOTICE text file distributed | ||||||
|  |           as part of the Derivative Works; within the Source form or | ||||||
|  |           documentation, if provided along with the Derivative Works; or, | ||||||
|  |           within a display generated by the Derivative Works, if and | ||||||
|  |           wherever such third-party notices normally appear. The contents | ||||||
|  |           of the NOTICE file are for informational purposes only and | ||||||
|  |           do not modify the License. You may add Your own attribution | ||||||
|  |           notices within Derivative Works that You distribute, alongside | ||||||
|  |           or as an addendum to the NOTICE text from the Work, provided | ||||||
|  |           that such additional attribution notices cannot be construed | ||||||
|  |           as modifying the License. | ||||||
|  | 
 | ||||||
|  |       You may add Your own copyright statement to Your modifications and | ||||||
|  |       may provide additional or different license terms and conditions | ||||||
|  |       for use, reproduction, or distribution of Your modifications, or | ||||||
|  |       for any such Derivative Works as a whole, provided Your use, | ||||||
|  |       reproduction, and distribution of the Work otherwise complies with | ||||||
|  |       the conditions stated in this License. | ||||||
|  | 
 | ||||||
|  |    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||||
|  |       any Contribution intentionally submitted for inclusion in the Work | ||||||
|  |       by You to the Licensor shall be under the terms and conditions of | ||||||
|  |       this License, without any additional terms or conditions. | ||||||
|  |       Notwithstanding the above, nothing herein shall supersede or modify | ||||||
|  |       the terms of any separate license agreement you may have executed | ||||||
|  |       with Licensor regarding such Contributions. | ||||||
|  | 
 | ||||||
|  |    6. Trademarks. This License does not grant permission to use the trade | ||||||
|  |       names, trademarks, service marks, or product names of the Licensor, | ||||||
|  |       except as required for reasonable and customary use in describing the | ||||||
|  |       origin of the Work and reproducing the content of the NOTICE file. | ||||||
|  | 
 | ||||||
|  |    7. Disclaimer of Warranty. Unless required by applicable law or | ||||||
|  |       agreed to in writing, Licensor provides the Work (and each | ||||||
|  |       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||||
|  |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||||
|  |       implied, including, without limitation, any warranties or conditions | ||||||
|  |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||||
|  |       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||||
|  |       appropriateness of using or redistributing the Work and assume any | ||||||
|  |       risks associated with Your exercise of permissions under this License. | ||||||
|  | 
 | ||||||
|  |    8. Limitation of Liability. In no event and under no legal theory, | ||||||
|  |       whether in tort (including negligence), contract, or otherwise, | ||||||
|  |       unless required by applicable law (such as deliberate and grossly | ||||||
|  |       negligent acts) or agreed to in writing, shall any Contributor be | ||||||
|  |       liable to You for damages, including any direct, indirect, special, | ||||||
|  |       incidental, or consequential damages of any character arising as a | ||||||
|  |       result of this License or out of the use or inability to use the | ||||||
|  |       Work (including but not limited to damages for loss of goodwill, | ||||||
|  |       work stoppage, computer failure or malfunction, or any and all | ||||||
|  |       other commercial damages or losses), even if such Contributor | ||||||
|  |       has been advised of the possibility of such damages. | ||||||
|  | 
 | ||||||
|  |    9. Accepting Warranty or Additional Liability. While redistributing | ||||||
|  |       the Work or Derivative Works thereof, You may choose to offer, | ||||||
|  |       and charge a fee for, acceptance of support, warranty, indemnity, | ||||||
|  |       or other liability obligations and/or rights consistent with this | ||||||
|  |       License. However, in accepting such obligations, You may act only | ||||||
|  |       on Your own behalf and on Your sole responsibility, not on behalf | ||||||
|  |       of any other Contributor, and only if You agree to indemnify, | ||||||
|  |       defend, and hold each Contributor harmless for any liability | ||||||
|  |       incurred by, or claims asserted against, such Contributor by reason | ||||||
|  |       of your accepting any such warranty or additional liability. | ||||||
|  | 
 | ||||||
|  |    END OF TERMS AND CONDITIONS | ||||||
|  | 
 | ||||||
|  |    APPENDIX: How to apply the Apache License to your work. | ||||||
|  | 
 | ||||||
|  |       To apply the Apache License to your work, attach the following | ||||||
|  |       boilerplate notice, with the fields enclosed by brackets "[]" | ||||||
|  |       replaced with your own identifying information. (Don't include | ||||||
|  |       the brackets!)  The text should be enclosed in the appropriate | ||||||
|  |       comment syntax for the file format. We also recommend that a | ||||||
|  |       file or class name and description of purpose be included on the | ||||||
|  |       same "printed page" as the copyright notice for easier | ||||||
|  |       identification within third-party archives. | ||||||
|  | 
 | ||||||
|  |    Copyright [yyyy] [name of copyright owner] | ||||||
|  | 
 | ||||||
|  |    Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |    you may not use this file except in compliance with the License. | ||||||
|  |    You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |        http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | 
 | ||||||
|  |    Unless required by applicable law or agreed to in writing, software | ||||||
|  |    distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |    See the License for the specific language governing permissions and | ||||||
|  |    limitations under the License. | ||||||
|  | @ -3,11 +3,16 @@ | ||||||
| This repository is currently a research project. The project is to explore ideas | This repository is currently a research project. The project is to explore ideas | ||||||
| in programming language design by specifying _Ava_. | in programming language design by specifying _Ava_. | ||||||
| 
 | 
 | ||||||
| To start reading, please use the [Table of Contents](./table-of-contents.md) |  | ||||||
| 
 |  | ||||||
| ## What is Ava? | ## What is Ava? | ||||||
| 
 | 
 | ||||||
| Ava is a programming language research project that is in the design and | Ava is a programming language research project that is in the design and | ||||||
| specification phase. There is no grammar, parser, compiler, or way to use any | specification phase. There is no grammar, parser, compiler, or way to use any | ||||||
| Ava code. Ava is a way to get ideas out of my head, challenged, and further | Ava code. Ava is a way to get ideas out of my head, challenged, and further | ||||||
| explored. | explored. | ||||||
|  | 
 | ||||||
|  | ## Notes | ||||||
|  | 
 | ||||||
|  | These notes are not guaranteed to be up to date and represent a large amount of | ||||||
|  | brainstorming and trying/discarding of ideas. | ||||||
|  | 
 | ||||||
|  | Please start with the [Table of Contents](./notes/table-of-contents.md) | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								build.sbt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								build.sbt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | ||||||
|  | val scala3: String = "3.3.1" | ||||||
|  | 
 | ||||||
|  | ThisBuild / scalaVersion  := scala3 | ||||||
|  | ThisBuild / versionScheme := Some("semver-spec") | ||||||
|  | ThisBuild / gsProjectName := "ava" | ||||||
|  | 
 | ||||||
|  | val sharedSettings = Seq( | ||||||
|  |   scalaVersion := scala3, | ||||||
|  |   version      := semVerSelected.value | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | lazy val testSettings = Seq( | ||||||
|  |   libraryDependencies ++= Seq( | ||||||
|  |     "org.scalameta" %% "munit" % "1.0.0-M11" % Test | ||||||
|  |   ) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | lazy val ava = project | ||||||
|  |   .in(file(".")) | ||||||
|  |   .aggregate(parser) | ||||||
|  |   .settings(sharedSettings) | ||||||
|  |   .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") | ||||||
|  | 
 | ||||||
|  | lazy val parser = project | ||||||
|  |   .in(file("modules/parser")) | ||||||
|  |   .settings(sharedSettings) | ||||||
|  |   .settings(testSettings) | ||||||
|  |   .settings(name := s"${gsProjectName.value}-parser-v${semVerMajor.value}") | ||||||
|  |   .settings( | ||||||
|  |     libraryDependencies ++= Seq( | ||||||
|  |       "co.fs2" %% "fs2-core" % "3.9.4", | ||||||
|  |       "co.fs2" %% "fs2-io"   % "3.9.4" | ||||||
|  |     ) | ||||||
|  |   ) | ||||||
							
								
								
									
										128
									
								
								modules/parser/src/main/scala/ava/parser/CharacterReader.scala
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								modules/parser/src/main/scala/ava/parser/CharacterReader.scala
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | ||||||
|  | package ava.parser | ||||||
|  | 
 | ||||||
|  | import java.io.BufferedReader | ||||||
|  | 
 | ||||||
|  | /** Used to consume characters from the input stream. | ||||||
|  |   * | ||||||
|  |   * @param input | ||||||
|  |   *   Input stream capable of reading _characters_ rather than bytes. This | ||||||
|  |   *   stream is owned by the character reader. | ||||||
|  |   */ | ||||||
|  | class CharacterReader( | ||||||
|  |   private val input: BufferedReader | ||||||
|  | ): | ||||||
|  |   // Internal constants. | ||||||
|  |   private val Capacity: Int         = 4096 | ||||||
|  |   private val LookBackCapacity: Int = 16 | ||||||
|  | 
 | ||||||
|  |   // Internal buffers. | ||||||
|  |   private val lastChars: Ring[Char] = Ring[Char](LookBackCapacity, 0) | ||||||
|  |   private val buffer: Array[Char]   = Array.fill(Capacity)(0) | ||||||
|  | 
 | ||||||
|  |   // Internal mutable state. | ||||||
|  |   private var length: Int          = 0 | ||||||
|  |   private var index: Int           = 0 | ||||||
|  |   private var eof: Boolean         = false | ||||||
|  |   private var peekedAhead: Boolean = false | ||||||
|  |   private var lookAhead: Char      = 0 | ||||||
|  | 
 | ||||||
|  |   /** Close the underlying stream. | ||||||
|  |     */ | ||||||
|  |   def close(): Unit = input.close() | ||||||
|  | 
 | ||||||
|  |   /** Set EOF and close the underlying stream. | ||||||
|  |     */ | ||||||
|  |   private def setEof(): Unit = | ||||||
|  |     eof = true | ||||||
|  |     close() | ||||||
|  | 
 | ||||||
|  |   /** Used when the buffer has been fully consumed. Reads as many characters as | ||||||
|  |     * possible and resets internal state. Detects EOF and sets state | ||||||
|  |     * accordingly. | ||||||
|  |     */ | ||||||
|  |   private def fillBuffer(): Unit = | ||||||
|  |     // TODO: Short circuit on EOF! | ||||||
|  |     var numberOfCharacters = 0 | ||||||
|  |     if peekedAhead then | ||||||
|  |       buffer(0) = lookAhead | ||||||
|  |       numberOfCharacters = input.read(buffer, 1, Capacity - 1) | ||||||
|  |     else numberOfCharacters = input.read(buffer, 0, Capacity) | ||||||
|  | 
 | ||||||
|  |     // Record the number of characters ACTUALLY consumed by the stream. | ||||||
|  |     // This is our working buffer size. | ||||||
|  |     length = numberOfCharacters | ||||||
|  | 
 | ||||||
|  |     // If no characters could be read, we're done. | ||||||
|  |     // EDGE CASE: If we peeked ahead to the last character, we still technically | ||||||
|  |     // have a valid buffer of length 1. | ||||||
|  |     val _ = | ||||||
|  |       if numberOfCharacters <= 0 && peekedAhead then | ||||||
|  |         // Edge case: buffer of size 1. | ||||||
|  |         length = 1 | ||||||
|  |       else if length <= 0 then | ||||||
|  |         // EOF case: no characters remain. | ||||||
|  |         length = 0 | ||||||
|  |         setEof() | ||||||
|  |         input.close() | ||||||
|  |       else | ||||||
|  |         // Normal case: we read some number of characters. | ||||||
|  |         length = numberOfCharacters + (if peekedAhead then 1 else 0) | ||||||
|  | 
 | ||||||
|  |     // Reset the peeked state -- we already consumed it. | ||||||
|  |     peekedAhead = false | ||||||
|  | 
 | ||||||
|  |     // Reset the index position to start iterating through the buffer again. | ||||||
|  |     index = 0 | ||||||
|  | 
 | ||||||
|  |   /** Observe, but do not consume, the next character in the stream. Note that | ||||||
|  |     * if the buffer is fully consumed, this function must read the underlying | ||||||
|  |     * stream but will _not_ refresh the buffer until the next character is | ||||||
|  |     * consumed. | ||||||
|  |     * | ||||||
|  |     * @return | ||||||
|  |     *   The next character value, or None if the stream is EOF. | ||||||
|  |     */ | ||||||
|  |   def peek(): Option[Char] = | ||||||
|  |     if eof then None | ||||||
|  |     else if index == length then | ||||||
|  |       // Special case -- try to read one. | ||||||
|  |       peekedAhead = true | ||||||
|  |       lookAhead = input.read().toChar | ||||||
|  |       if lookAhead < 0 then | ||||||
|  |         eof = true | ||||||
|  |         None | ||||||
|  |       else Some(lookAhead) | ||||||
|  |     else Some(buffer(index)) | ||||||
|  | 
 | ||||||
|  |   def consume(): Option[Char] = | ||||||
|  |     if eof then None | ||||||
|  |     else if peekedAhead then | ||||||
|  |       fillBuffer() | ||||||
|  |       lastChars.push(lookAhead) | ||||||
|  |       Some(lookAhead) | ||||||
|  |     else if index == length then | ||||||
|  |       fillBuffer() | ||||||
|  |       if eof then None | ||||||
|  |       else | ||||||
|  |         val pos = index | ||||||
|  |         index = index + 1 | ||||||
|  |         lastChars.push(buffer(pos)) | ||||||
|  |         Some(buffer(pos)) | ||||||
|  |     else | ||||||
|  |       val pos = index | ||||||
|  |       index = index + 1 | ||||||
|  |       lastChars.push(buffer(pos)) | ||||||
|  |       Some(buffer(pos)) | ||||||
|  | 
 | ||||||
|  |   /** @return | ||||||
|  |     *   True if all data in the _buffer_ has been consumed, false otherwise. | ||||||
|  |     */ | ||||||
|  |   def isBufferExhausted(): Boolean = | ||||||
|  |     index >= length | ||||||
|  | 
 | ||||||
|  |   /** @return | ||||||
|  |     *   True if the stream is EOF, false otherwise. An EOF stream does NOT imply | ||||||
|  |     *   that the buffer is exhausted. | ||||||
|  |     */ | ||||||
|  |   def isEof(): Boolean = | ||||||
|  |     eof | ||||||
							
								
								
									
										101
									
								
								modules/parser/src/main/scala/ava/parser/Ring.scala
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								modules/parser/src/main/scala/ava/parser/Ring.scala
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | ||||||
|  | package ava.parser | ||||||
|  | 
 | ||||||
|  | import scala.reflect.ClassTag | ||||||
|  | 
 | ||||||
|  | /** Basic ring buffer implementation. | ||||||
|  |   * | ||||||
|  |   * @param capacity | ||||||
|  |   *   Buffer capacity. | ||||||
|  |   * @param unit | ||||||
|  |   *   The unit value -- used to populate the initial buffer. | ||||||
|  |   */ | ||||||
|  | final class Ring[A: ClassTag] private ( | ||||||
|  |   val capacity: Int, | ||||||
|  |   val unit: A | ||||||
|  | ): | ||||||
|  |   private var currentSize: Int = 0 | ||||||
|  |   private var oldestIndex: Int = -1 | ||||||
|  |   private var newestIndex: Int = -1 | ||||||
|  |   private val data: Array[A]   = Array.fill(capacity)(unit) | ||||||
|  | 
 | ||||||
|  |   /** Push new data into this buffer. If the buffer is at capacity, replace the | ||||||
|  |     * oldest item in the buffer. | ||||||
|  |     * | ||||||
|  |     * @param value | ||||||
|  |     *   The value to push into the buffer. | ||||||
|  |     */ | ||||||
|  |   def push(value: A): Unit = | ||||||
|  |     if currentSize <= 0 then | ||||||
|  |       // Only used the first time data is pushed to the ring. | ||||||
|  |       data(0) = value | ||||||
|  |       oldestIndex = 0 | ||||||
|  |       newestIndex = 0 | ||||||
|  |       currentSize = 1 | ||||||
|  |     else if currentSize < capacity then | ||||||
|  |       // Used when filling up the ring. | ||||||
|  |       data(currentSize) = value | ||||||
|  |       newestIndex = currentSize | ||||||
|  |       currentSize = currentSize + 1 | ||||||
|  |     else | ||||||
|  |       // We have met the capacity and need to start rolling through the oldest | ||||||
|  |       // entries, one at a time | ||||||
|  |       data(oldestIndex) = value | ||||||
|  |       newestIndex = oldestIndex | ||||||
|  |       oldestIndex = calculateNextOldest(oldestIndex) | ||||||
|  | 
 | ||||||
|  |   /** Get the value of the newest item in the buffer. Does not care whether the | ||||||
|  |     * buffer has been populated. | ||||||
|  |     * | ||||||
|  |     * @return | ||||||
|  |     *   The newest value in the buffer. | ||||||
|  |     */ | ||||||
|  |   def newest(): A = | ||||||
|  |     if newestIndex < 0 then | ||||||
|  |       throw new IllegalStateException( | ||||||
|  |         "This ring has not been initialized with any data." | ||||||
|  |       ) | ||||||
|  |     else data(newestIndex) | ||||||
|  | 
 | ||||||
|  |   /** Extract a copy of this ring, ordered from newest to oldest. | ||||||
|  |     * | ||||||
|  |     * @return | ||||||
|  |     *   Ordered copy of this ring from newest to oldest. | ||||||
|  |     */ | ||||||
|  |   def newestToOldest(): Array[A] = | ||||||
|  |     val output      = new Array[A](currentSize) | ||||||
|  |     var outputIndex = 0 | ||||||
|  |     var ringPointer = newestIndex | ||||||
|  |     while outputIndex < currentSize do | ||||||
|  |       output(outputIndex) = data(ringPointer) | ||||||
|  |       outputIndex = outputIndex + 1 | ||||||
|  |       ringPointer = calculateNextNewest(ringPointer) | ||||||
|  |     output | ||||||
|  | 
 | ||||||
|  |   /** Get the current size of the buffer. | ||||||
|  |     * | ||||||
|  |     * @return | ||||||
|  |     *   The current size of the buffer. | ||||||
|  |     */ | ||||||
|  |   def size(): Int = currentSize | ||||||
|  | 
 | ||||||
|  |   private def calculateNextOldest(from: Int): Int = | ||||||
|  |     if from < (currentSize - 1) then from + 1 | ||||||
|  |     else 0 | ||||||
|  | 
 | ||||||
|  |   private def calculateNextNewest(from: Int): Int = | ||||||
|  |     if from > 0 then from - 1 | ||||||
|  |     else currentSize - 1 | ||||||
|  | 
 | ||||||
|  | object Ring: | ||||||
|  | 
 | ||||||
|  |   def apply[A: ClassTag]( | ||||||
|  |     capacity: Int, | ||||||
|  |     unit: A | ||||||
|  |   ): Ring[A] = | ||||||
|  |     if capacity > 0 then new Ring[A](capacity, unit) | ||||||
|  |     else | ||||||
|  |       throw new IllegalArgumentException( | ||||||
|  |         "Rings may only be constructed with capacity > 0" | ||||||
|  |       ) | ||||||
|  | 
 | ||||||
|  | end Ring | ||||||
							
								
								
									
										69
									
								
								modules/parser/src/test/scala/ava/parser/RingTests.scala
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								modules/parser/src/test/scala/ava/parser/RingTests.scala
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | ||||||
|  | package ava.parser | ||||||
|  | 
 | ||||||
|  | class RingTests extends munit.FunSuite: | ||||||
|  | 
 | ||||||
|  |   test("should fail to instantiate with <= 0 capacity") { | ||||||
|  |     interceptMessage[IllegalArgumentException]( | ||||||
|  |       "Rings may only be constructed with capacity > 0" | ||||||
|  |     ) { | ||||||
|  |       val _ = Ring[Int](-1, 0) | ||||||
|  |     } | ||||||
|  |     interceptMessage[IllegalArgumentException]( | ||||||
|  |       "Rings may only be constructed with capacity > 0" | ||||||
|  |     ) { | ||||||
|  |       val _ = Ring[Int](0, 0) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   test("should return the buffer size independent of capacity") { | ||||||
|  |     val capacity: Int = 4 | ||||||
|  |     val ring          = Ring[Int](capacity, 0) | ||||||
|  |     assertEquals(ring.size(), 0) | ||||||
|  |     assertEquals(ring.capacity, capacity) | ||||||
|  |     assertEquals(ring.unit, 0) | ||||||
|  |     var item = 1 | ||||||
|  |     while item <= capacity do | ||||||
|  |       ring.push(item) | ||||||
|  |       assertEquals(ring.size(), item) | ||||||
|  |       item = item + 1 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   test("should fail to retrieve the newest element if not initialized") { | ||||||
|  |     interceptMessage[IllegalStateException]( | ||||||
|  |       "This ring has not been initialized with any data." | ||||||
|  |     ) { | ||||||
|  |       val ring = Ring[Int](1, 0) | ||||||
|  |       val _    = ring.newest() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   test("should return the newest element of the ring") { | ||||||
|  |     val capacity: Int = 4 | ||||||
|  |     val unit: Int     = 0 | ||||||
|  |     val ring          = Ring[Int](capacity, unit) | ||||||
|  |     var item          = 1 | ||||||
|  |     while item <= capacity do | ||||||
|  |       ring.push(item) | ||||||
|  |       assertEquals(ring.newest(), item) | ||||||
|  |       item = item + 1 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   test("should respect ordering across cycles") { | ||||||
|  |     val capacity: Int = 4 | ||||||
|  |     val unit: Int     = 0 | ||||||
|  |     val ring          = Ring[Int](capacity, unit) | ||||||
|  |     var item          = 1 | ||||||
|  |     while item <= capacity do | ||||||
|  |       ring.push(item) | ||||||
|  |       item = item + 1 | ||||||
|  | 
 | ||||||
|  |     assertEquals(ring.size(), capacity) | ||||||
|  |     val expected = Array[Int](5, 4, 3, 2) | ||||||
|  |     item = 1 | ||||||
|  |     while item <= capacity + 1 do | ||||||
|  |       ring.push(item) | ||||||
|  |       item = item + 1 | ||||||
|  |       assertEquals(ring.size(), capacity) | ||||||
|  |     val extract = ring.newestToOldest() | ||||||
|  |     assert(extract.sameElements(expected)) | ||||||
|  |   } | ||||||
							
								
								
									
										1
									
								
								project/build.properties
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								project/build.properties
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | sbt.version=1.9.8 | ||||||
							
								
								
									
										10
									
								
								project/plugins.sbt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								project/plugins.sbt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | credentials += Credentials(Path.userHome / ".sbt" / ".credentials") | ||||||
|  | 
 | ||||||
|  | externalResolvers := Seq( | ||||||
|  |   "Garrity Software Mirror" at "https://maven.garrity.co/releases", | ||||||
|  |   "Garrity Software Releases" at "https://maven.garrity.co/gs" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | addSbtPlugin("org.scoverage" % "sbt-scoverage"        % "2.0.8") | ||||||
|  | addSbtPlugin("gs"            % "sbt-garrity-software" % "0.2.0") | ||||||
|  | addSbtPlugin("gs"            % "sbt-gs-semver"        % "0.2.0") | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue