Adding the beginning components of a parser and some tests.

This commit is contained in:
Pat Garrity 2024-02-10 22:29:21 -06:00
parent c3d84bc4a6
commit a10f5fa1b1
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
42 changed files with 633 additions and 5 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target/
project/target/
project/project/
modules/core/target/

View file

@ -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
View 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
View 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.

View file

@ -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
View 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"
)
)

View 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

View 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

View 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
View file

@ -0,0 +1 @@
sbt.version=1.9.8

10
project/plugins.sbt Normal file
View 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")