├── .editorconfig ├── .gitignore ├── Makefile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── arrdem │ │ └── ox │ │ ├── Named.java │ │ ├── Namespaced.java │ │ ├── Nat.java │ │ └── Number.java └── kotlin │ └── me │ └── arrdem │ └── ox │ ├── compiler.kt │ ├── core.kt │ ├── iterators.kt │ ├── memfn.kt │ ├── printer.kt │ ├── reader.kt │ ├── repl.kt │ └── scanner.kt └── test └── kotlin └── me └── arrdem └── ox ├── iterators.kt ├── natural.kt ├── printer.kt ├── reader.kt └── scanner.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | ## tweete 13 | max_line_length = 100 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | .idea/* 4 | .attach_pid* 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: format 2 | format: 3 | astyle --recursive -i --mode=java -s2 src/java\* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ox - A tractable Lisp 2 | 3 | Ox is a small lisp for the JVM, intended to make it possible to write 4 | JVM programs without all the ceremony of Java. 5 | 6 | ## Usage 7 | 8 | Don't. Ox was built to make good on years of threats, and for fun. 9 | 10 | ## License 11 | 12 | Ox is copyright © Reid 'arrdem' McKenzie 2019. 13 | 14 | Source code is made available at my pleasure for the education of the 15 | reader. No license to package, use or distribute is granted, nor is 16 | any warranty of suitability for any purpose. 17 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | me.arrdem 5 | ox 6 | jar 7 | 1.0-SNAPSHOT 8 | ox 9 | http://ox.arrdem.com 10 | 11 | 12 | 1.3.31 13 | 4.12 14 | 1.10 15 | 1.10 16 | 17 | 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-antrun-plugin 23 | 1.8 24 | 25 | 26 | ktlint 27 | verify 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | run 37 | 38 | 39 | ktlint-format 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | run 50 | 51 | 52 | 53 | 54 | com.pinterest 55 | ktlint 56 | 0.32.0 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.jetbrains.kotlin 64 | kotlin-maven-plugin 65 | ${kotlin.version} 66 | 67 | 68 | compile 69 | compile 70 | 71 | 72 | ${project.basedir}/src/main/kotlin 73 | ${project.basedir}/src/main/java 74 | 75 | 76 | 77 | 78 | test-compile 79 | test-compile 80 | 81 | 82 | ${project.basedir}/src/test/kotlin 83 | ${project.basedir}/src/test/java 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-compiler-plugin 93 | 3.5.1 94 | 95 | 96 | 97 | default-compile 98 | none 99 | 100 | 101 | 102 | default-testCompile 103 | none 104 | 105 | 106 | java-compile 107 | compile 108 | compile 109 | 110 | 111 | java-test-compile 112 | test-compile 113 | testCompile 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-assembly-plugin 121 | 2.6 122 | 123 | 124 | make-assembly 125 | package 126 | 127 | single 128 | 129 | 130 | 131 | 132 | me.arrdem.ox.Scanners 133 | 134 | 135 | 136 | jar-with-dependencies 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | io.lacuna 148 | bifurcan 149 | 0.2.0-alpha1 150 | 151 | 152 | 153 | org.jetbrains.kotlin 154 | kotlin-stdlib 155 | ${kotlin.version} 156 | 157 | 158 | 159 | org.ow2.asm 160 | asm 161 | 7.1 162 | 163 | 164 | 165 | junit 166 | junit 167 | ${junit.version} 168 | test 169 | 170 | 171 | 172 | org.jetbrains.kotlin 173 | kotlin-test-junit 174 | ${kotlin.version} 175 | test 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/me/arrdem/ox/Named.java: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox; 2 | 3 | public interface Named { 4 | public String name(); 5 | 6 | static boolean isNamePiped(String name) { 7 | for (int chr: name.chars().toArray()) { 8 | if (Character.isWhitespace(chr)) { 9 | return true; 10 | } 11 | } 12 | return false; 13 | } 14 | 15 | default boolean isPiped() { 16 | return isNamePiped(this.name()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/arrdem/ox/Namespaced.java: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox; 2 | 3 | public interface Namespaced extends Named { 4 | public T namespace(); 5 | 6 | default boolean isPiped() { 7 | if (Named.isNamePiped(this.name())) { 8 | return Boolean.TRUE; 9 | } else if(this.namespace() != null) { 10 | return this.namespace().isPiped(); 11 | } else { 12 | return Boolean.FALSE; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/arrdem/ox/Nat.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-9-13 3 | * 4 | * The natural numbers. 5 | * 6 | * Inspired by 'What about the natural numbers' ~ Runciman 1989 7 | * https://dl.acm.org/citation.cfm?id=66989 8 | * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.56.3442&rep=rep1&type=pdf 9 | * 10 | * With thanks to José Manuel Calderón Trilla who presented this paper at PWLConf 2019. 11 | * https://pwlconf.org/2019/jose-trilla/ 12 | * 13 | * Provides an implementation of REALLY BIG NUMBERS modeled as a pair (BigInteger, [thunk]) where the 14 | * BigInt holds the realized portion of the number, and the thunks are continuations which 15 | * will produce addends when called. This allows the value of the Nat to be incrementally realized. 16 | * 17 | * Nat implements the usual operations - addition, subtraction (saturating at zero), multiplication 18 | * and division (with div/0 defined to be infinity per Runciman). 19 | */ 20 | 21 | package me.arrdem.ox; 22 | 23 | import io.lacuna.bifurcan.IList; 24 | import io.lacuna.bifurcan.Lists; 25 | import kotlin.jvm.functions.Function0; 26 | import kotlin.jvm.functions.Function2; 27 | import org.jetbrains.annotations.NotNull; 28 | 29 | import java.math.BigInteger; 30 | 31 | public final class Nat implements Number { 32 | private static IList> FEL = (IList>) Lists.EMPTY; 33 | 34 | private BigInteger value; 35 | private IList> thunks; 36 | 37 | public static Nat ZERO = Nat.of(BigInteger.ZERO); 38 | public static Nat ONE = Nat.of(BigInteger.ONE); 39 | public static Nat TWO = Nat.of(BigInteger.TWO); 40 | public static Nat TEN = Nat.of(BigInteger.TEN); 41 | public static Nat INFINITY = Nat.of(BigInteger.valueOf(Long.MAX_VALUE), () -> Nat.INFINITY); 42 | 43 | private Nat(BigInteger value, IList> thunks) { 44 | assert value.compareTo(BigInteger.ZERO) >= 0; 45 | this.value = value; 46 | this.thunks = thunks; 47 | } 48 | 49 | public static Nat of(long value) { 50 | return new Nat(BigInteger.valueOf(value), FEL); 51 | } 52 | 53 | public static Nat of(BigInteger value) { 54 | return new Nat(value, FEL); 55 | } 56 | 57 | public static Nat of(long value, Function0 thunk) { 58 | if (!(thunk instanceof MemFn0)) 59 | thunk = MemFns.of(thunk); 60 | 61 | Function0 finalThunk = thunk; 62 | return new Nat(BigInteger.valueOf(value), FEL.addLast(() -> { 63 | Object v = finalThunk.invoke(); 64 | assert v instanceof Nat : "Contract violation! Thunk did not return nat!"; 65 | return (Nat) v; 66 | })); 67 | } 68 | 69 | public static Nat of(BigInteger value, Function0 thunk) { 70 | if (!(thunk instanceof MemFn0)) 71 | thunk = MemFns.of(thunk); 72 | 73 | Function0 finalThunk = thunk; 74 | return new Nat(value, FEL.addLast(() -> { 75 | Object v = finalThunk.invoke(); 76 | assert v instanceof Nat : "Contract violation! Thunk did not return nat!"; 77 | return (Nat) v; 78 | })); 79 | } 80 | 81 | public Nat succ() { 82 | return new Nat(BigInteger.ONE, FEL.addLast(() -> this)); 83 | } 84 | 85 | public Nat add(Nat other) { 86 | return new Nat(this.value.add(other.value), Lists.concat(this.thunks, other.thunks)); 87 | } 88 | 89 | private boolean maybeStep() { 90 | if(!this.thunks.equals(FEL)) { 91 | Function0 thunk = this.thunks.first(); 92 | this.thunks = this.thunks.removeFirst(); 93 | 94 | /** 95 | * FIXME (arrdem 2019-09-16) 96 | * This could be optimized by skipping zero terms maybe? 97 | */ 98 | Nat next = thunk.invoke(); 99 | if (next != null) { 100 | this.value = this.value.add(next.value); 101 | if(!next.thunks.equals(FEL)) 102 | this.thunks = Lists.concat(this.thunks, next.thunks); 103 | 104 | return true; 105 | } 106 | } 107 | return false; 108 | } 109 | 110 | public Nat decs() { 111 | return this.subtract(ONE); 112 | } 113 | 114 | /** 115 | * Subtraction is implemented by LAZILY evaluating both ths and other until either other has been 116 | * fully evaluated, or the subtrand has been reduced to zero. 117 | * 118 | * Note that this is LAZY subtraction, so it WILL loop given infinite structures. 119 | */ 120 | public Nat subtract(Nat other) { 121 | while(true) { 122 | // If the other side is smaller and it can be stepped, step it. 123 | if(!other.thunks.equals(Lists.EMPTY) && other.value.compareTo(this.value) <= 0) 124 | other.maybeStep(); 125 | 126 | // If our side is smaller and it can be stepped, step it 127 | else if (!this.thunks.equals(Lists.EMPTY) && this.value.compareTo(other.value) <= 0) 128 | this.maybeStep(); 129 | 130 | // If we've grounded out and one side is fully evaluated, do the math. 131 | else if (this.thunks.equals(Lists.EMPTY) || other.thunks.equals(Lists.EMPTY)) { 132 | if (this.value.compareTo(other.value) > 0) 133 | return new Nat(this.value.subtract(other.value), this.thunks); 134 | else 135 | return ZERO; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Multiplication is tricky. Product of sums is sum of products, so one possible coding here is 142 | * to maintain a sequence of factors against which the Nat has been multiplied much like the 143 | * sequence of unrealized addends. Under this coding the tricky bit is as usual when does it 144 | * make sense to step what terms. Multiplying infinities for instance is a really essential case 145 | * which should be possible in a small amount of time - not a divergent computation. 146 | * 147 | * So the coding we use here is lazy iterated addition. We construct a new nat defined by 148 | * recursively (lazily!) adding this to itself other times. This strategy provides the incremental 149 | * realization behavior we want although perhaps not the performance one may wish. 150 | */ 151 | public Nat multiply(Nat other) { 152 | // Multiplying by zero is the base case and zero 153 | if(this.equals(ZERO) || (other.thunks.equals(FEL) && other.value.equals(BigInteger.ZERO))) 154 | return ZERO; 155 | 156 | else if(other.equals(ONE)) 157 | return this; 158 | 159 | // Multiplying realized numbers is done eagerly 160 | else if (other.thunks.equals(FEL) && this.thunks.equals(FEL)) 161 | return Nat.of(this.value.multiply(other.value)); 162 | 163 | // Otherwise we use the recursive lazy coding 164 | else 165 | return this.add(this).add(Nat.of(0, () -> this.multiply(other.subtract(TWO)))); 166 | } 167 | 168 | /** 169 | * Division is slightly less tricky - Division by zero is well defined (to be infinity). The 170 | * machine number case of division is just delegation, and the general case is iterated 171 | * subtraction. That is - since quotient * divisor <= dividend we can compute the dividend by 172 | * counting the number of times we can subtract the divisor from the dividend. 173 | * 174 | * Consequently this implementation uses a helper function which constructs a thunk which tracks 175 | * the (remaining) dividend (as current) and the divisor (as other). Each thunk call produces a 1 176 | * with a new thunk reduced by the divisor - so this way we're incrementally counting down. Not 177 | * ideal - but correct for deeply thunked structures eg the minimal infinity. 178 | */ 179 | private static final Function2> df = ((Nat current, Nat other) -> () -> { 180 | Nat difference = current.subtract(other); 181 | if (current.compareTo(other) >= 0 && difference.compareTo(ZERO) >= 0) { 182 | return Nat.of(1, () -> difference.divide(other)); 183 | } else { 184 | return ZERO; 185 | } 186 | }); 187 | 188 | public Nat divide(Nat other) { 189 | if(other.equals(ZERO)) 190 | return INFINITY; 191 | 192 | else if(this.equals(ZERO)) 193 | return ZERO; 194 | 195 | else if(this.thunks.equals(FEL) && other.thunks.equals(FEL)) 196 | return Nat.of(this.value.divide(other.value)); 197 | 198 | else 199 | return df.invoke(this, other).invoke(); 200 | } 201 | 202 | /** 203 | * There are two obvious strategies here. One is to compute the division, then as division rounds 204 | * down multiply and subtract to get the loss. This is HUGELY expensive and undefined when 205 | * computing modulus by zero, so instead we re-implement the division machinery as a lazy cons of 206 | * 0-valued cells which perform one subtraction until the remainder is computed. Relative to the 207 | * first strategy this saves us a "lazy" multiplication which must be forced completely for an 208 | * equally "lazy" subtraction so the lexical complexity of duplication division is worthwhile. 209 | */ 210 | private static final Function2> rf = ((Nat current, Nat other) -> () -> { 211 | Nat difference = current.subtract(other); 212 | System.out.println(String.format("current: %s, other: %s => %s", current, other, difference)); 213 | if (difference.compareTo(other) < 0) { 214 | System.out.println(String.format("Yielding %s", difference)); 215 | return difference; 216 | } else { 217 | System.out.println("Not done yet..."); 218 | return Nat.of(0, () -> difference.remainder(other)); 219 | } 220 | }); 221 | 222 | public Nat remainder(Nat other) { 223 | if(other.equals(ZERO)) 224 | return INFINITY; 225 | 226 | else if(this.equals(ZERO)) 227 | return ZERO; 228 | 229 | else if(other.equals(ONE)) 230 | return ZERO; 231 | 232 | else if(this.thunks.equals(FEL) && other.thunks.equals(FEL)) 233 | return Nat.of(this.value.remainder(other.value)); 234 | 235 | else 236 | return rf.invoke(this, other).invoke(); 237 | } 238 | 239 | /** 240 | * A way to force the entire (potentially infinite!) value of the current Nat. 241 | */ 242 | public BigInteger get() { 243 | while(this.maybeStep()) {} 244 | return this.value; 245 | } 246 | 247 | /** 248 | * Comparison. Which is hard because we want to realize as little of either side as we can 249 | * possibly get away with. 250 | */ 251 | @Override 252 | public int compareTo(@NotNull Nat other) { 253 | if(this == other) 254 | return 0; 255 | 256 | while(true) { 257 | // We've exhausted this value, other has more AND is gt -> lt 258 | if (this.thunks.equals(Lists.EMPTY) && 259 | this.value.compareTo(other.value) < 0) 260 | return -1; 261 | 262 | // We've exhausted the other, and other's lt -> we're gt 263 | else if (other.thunks.equals(Lists.EMPTY) && 264 | this.value.compareTo(other.value) > 0) 265 | return 1; 266 | 267 | // We've exhausted both 268 | else if (this.thunks.equals(Lists.EMPTY) && 269 | other.thunks.equals(Lists.EMPTY)) 270 | return this.value.compareTo(other.value); 271 | 272 | else { 273 | // If the other side is smaller and it can be stepped, step it. 274 | if (!other.thunks.equals(Lists.EMPTY) && 275 | other.value.compareTo(this.value) <= 0) 276 | other.maybeStep(); 277 | 278 | // If our side is smaller and it can be stepped, step it 279 | else if (!this.thunks.equals(Lists.EMPTY) && 280 | this.value.compareTo(other.value) <= 0) 281 | this.maybeStep(); 282 | } 283 | } 284 | } 285 | 286 | public boolean equals(Object other) { 287 | if (this == other) 288 | return true; 289 | 290 | if (!(other instanceof Nat)) 291 | return false; 292 | 293 | return this.compareTo((Nat) other) == 0; 294 | } 295 | 296 | public String toString() { 297 | return String.format("", 298 | this.value.toString(10), 299 | !this.thunks.equals(Lists.EMPTY)); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/me/arrdem/ox/Number.java: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox; 2 | 3 | public interface Number extends Comparable { 4 | T add(T other); 5 | T subtract(T other); 6 | T multiply(T other); 7 | T divide(T other); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/compiler.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-09-07 3 | * 4 | * The Ox compiler 5 | */ 6 | 7 | package me.arrdem.ox 8 | 9 | import io.lacuna.bifurcan.Maps 10 | import java.lang.ref.ReferenceQueue 11 | import java.lang.ref.SoftReference 12 | import java.time.Instant 13 | import java.util.* 14 | import javax.naming.Name 15 | 16 | class OxClassLoader : ClassLoader() { 17 | private var classCache = Maps.EMPTY as Map>> 18 | private val rq = ReferenceQueue>() 19 | 20 | @Synchronized 21 | private fun cleanCache() { 22 | while (rq.poll() != null) 23 | for (e in classCache) { 24 | val v = e.value() 25 | if (v.get() == null) 26 | classCache = classCache.remove(e.key()) 27 | } 28 | } 29 | 30 | @Synchronized 31 | fun defineClass(name: String, bytes: ByteArray): Class<*> { 32 | cleanCache() 33 | val c = defineClass(name, bytes, 0, bytes.size) 34 | classCache = classCache.put(name, SoftReference(c, rq)) 35 | return c 36 | } 37 | 38 | override fun findClass(name: String): Class<*> { 39 | val cr: SoftReference>? = classCache.get(name, null) 40 | if (cr != null) { 41 | val c = cr.get() 42 | if (c != null) return c 43 | else classCache.remove(name, cr) 44 | } 45 | cleanCache() 46 | return super.findClass(name) 47 | } 48 | } 49 | 50 | class Environment(val loader: OxClassLoader = OxClassLoader()) { 51 | 52 | } 53 | 54 | object Compiler { 55 | fun analyze(o: Any) { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/core.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-9-31 3 | * 4 | * The Ox 'core'. 5 | * Really just a pile of small bits and bats that didn't deserve their own files. 6 | */ 7 | 8 | package me.arrdem.ox 9 | 10 | /** 11 | * Type aliases that win by default so we just use Zach's fine datastructures everywhere. 12 | */ 13 | typealias Map = io.lacuna.bifurcan.Map 14 | typealias List = io.lacuna.bifurcan.List 15 | typealias Set = io.lacuna.bifurcan.Set 16 | 17 | /** 18 | * A less fine datastructure 19 | */ 20 | data class Cons(public val car: T, public val cdr: Cons? = null); 21 | 22 | /** 23 | * Symbol - and the Symbols helper class. 24 | */ 25 | data class Symbol(private val name: String, 26 | private val namespace: Symbol? = null): 27 | Namespaced 28 | { 29 | override fun name(): String { 30 | return this.name 31 | } 32 | 33 | override fun namespace(): Symbol? { 34 | return this.namespace 35 | } 36 | } 37 | 38 | object Symbols { 39 | @JvmStatic 40 | fun of(s: String): Symbol { 41 | var name: String? = null 42 | var segments = List() 43 | val chunks = s.split(Regex("/"), 1) 44 | if (chunks.count() == 2) { 45 | name = chunks[1] 46 | for (segment in chunks[0].split(".")) { 47 | segments.addLast(segment) 48 | } 49 | var root = Symbol(segments.first(), null) 50 | segments = segments.removeFirst() 51 | for (segment in segments) { 52 | root = Symbol(segment, root) 53 | } 54 | return Symbol(name, root) 55 | } else { 56 | return Symbol(s, null) 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Keyword - and the Keywords helper class 63 | * 64 | * Basically the same as symbols. Should really figure out how to factor this better. 65 | */ 66 | data class Keyword(private val name: String, 67 | private val namespace: Keyword? = null): 68 | Namespaced 69 | { 70 | override fun name(): String { 71 | return this.name 72 | } 73 | 74 | override fun namespace(): Keyword? { 75 | return this.namespace 76 | } 77 | } 78 | 79 | object Keywords { 80 | @JvmStatic 81 | fun of(s: String): Keyword { 82 | var name: String? = null 83 | var segments = List() 84 | val chunks = s.split(Regex("/"), 1) 85 | if (chunks.count() == 2) { 86 | name = chunks[1] 87 | for (segment in chunks[0].split(".")) { 88 | segments.addLast(segment) 89 | } 90 | var root = Keyword(segments.first(), null) 91 | segments = segments.removeFirst() 92 | for (segment in segments) { 93 | root = Keyword(segment, root) 94 | } 95 | return Keyword(name, root) 96 | } else { 97 | return Keyword(s, null) 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * A container for "meta" tag syntax objects. 104 | */ 105 | data class Meta(val meta: Any?, val expr: Any?); 106 | 107 | /** 108 | * A container for #tag <> objects. 109 | */ 110 | data class Tag(val tag: Any, val expr: Any); 111 | 112 | fun sneakyThrow(e: Throwable) { 113 | throw e as E 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/iterators.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import java.util.Iterator 4 | 5 | interface ICurrentIterator: Iterator { 6 | fun current(): T 7 | } 8 | 9 | class CurrentIterator(private val iter: Iterator, 10 | private var current: T? = if (iter.hasNext()) iter.next() else null): 11 | ICurrentIterator 12 | { 13 | override fun current(): T { 14 | return current as T 15 | } 16 | 17 | override fun hasNext(): Boolean { 18 | return iter.hasNext() 19 | } 20 | 21 | override fun next(): T { 22 | current = iter.next() 23 | return current as T 24 | } 25 | } 26 | 27 | interface IPeekIterator: ICurrentIterator { 28 | fun hasPeek(): Boolean 29 | fun peek(): T 30 | } 31 | 32 | class PeekIterator(private val iter: Iterator, 33 | private var current: T? = if (iter.hasNext()) iter.next() else null, 34 | private var next: T? = if (iter.hasNext()) iter.next() else null): 35 | IPeekIterator 36 | { 37 | override fun hasPeek(): Boolean { 38 | return next != null || iter.hasNext() 39 | } 40 | 41 | override fun peek(): T { 42 | return next as T 43 | } 44 | 45 | override fun current(): T { 46 | return current as T 47 | } 48 | 49 | override fun hasNext(): Boolean { 50 | return next != null 51 | } 52 | 53 | override fun next(): T { 54 | current = next 55 | if (iter.hasNext()) { 56 | next = iter.next() 57 | } else { 58 | next = null 59 | } 60 | return current as T 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/memfn.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-9-13 3 | * 4 | * A memoizing function. 5 | * Calls the parent thunk once, caches its value and always returns that value. 6 | * Also re-throws any exception raised on the first call every time. 7 | */ 8 | 9 | package me.arrdem.ox 10 | 11 | class MemFn0 internal constructor(private val fn: Function0) : Function0 { 12 | private var `val`: R? = null 13 | private var ex: Exception? = null 14 | private var valid = false 15 | 16 | override fun invoke(): R { 17 | if (!this.valid) { 18 | this.valid = true 19 | try { 20 | this.`val` = fn.invoke() 21 | } catch (e: Exception) { 22 | this.ex = e 23 | throw e 24 | } 25 | 26 | } 27 | if (this.ex != null) 28 | sneakyThrow(this.ex!!) 29 | return this.`val`!! 30 | } 31 | } 32 | 33 | class MemFn1 internal constructor(private val fn: Function1) : Function1 { 34 | private var `val`: R? = null 35 | private var ex: Exception? = null 36 | private var valid = false 37 | 38 | override fun invoke(o0: A): R { 39 | if (!this.valid) { 40 | this.valid = true 41 | try { 42 | this.`val` = fn.invoke(o0) 43 | } catch (e: Exception) { 44 | this.ex = e 45 | throw e 46 | } 47 | 48 | } 49 | if (this.ex != null) 50 | sneakyThrow(this.ex!!) 51 | return this.`val`!! 52 | } 53 | } 54 | 55 | class MemFn2 internal constructor(private val fn: Function2) : Function2 { 56 | private var `val`: R? = null 57 | private var ex: Exception? = null 58 | private var valid = false 59 | 60 | override fun invoke(o0: A, o1: B): R { 61 | if (!this.valid) { 62 | this.valid = true 63 | try { 64 | this.`val` = fn.invoke(o0, o1) 65 | } catch (e: Exception) { 66 | this.ex = e 67 | throw e 68 | } 69 | 70 | } 71 | if (this.ex != null) 72 | sneakyThrow(this.ex!!) 73 | return this.`val`!! 74 | } 75 | } 76 | 77 | object MemFns { 78 | @JvmStatic 79 | fun of(fn: Function0): MemFn0 { 80 | return MemFn0(fn) 81 | } 82 | 83 | @JvmStatic 84 | fun of(fn: Function1): MemFn1 { 85 | return MemFn1(fn) 86 | } 87 | 88 | @JvmStatic 89 | fun of(fn: Function2): MemFn2 { 90 | return MemFn2(fn) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/printer.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-9-31 3 | * 4 | * The Ox printer. 5 | * Dual to the scanner/reader in theory. 6 | */ 7 | 8 | package me.arrdem.ox 9 | 10 | import io.lacuna.bifurcan.Maps 11 | import java.io.ByteArrayOutputStream 12 | import java.io.OutputStreamWriter 13 | import io.lacuna.bifurcan.List as BList 14 | import io.lacuna.bifurcan.Map as BMap 15 | import io.lacuna.bifurcan.Set as BSet 16 | 17 | 18 | /** 19 | * Sadly in Kotlin, type aliases are not recursive. 20 | * They're just macros it seems. 21 | * 22 | * This means that I can't actually capture the recursive type of PrintFn in Kotlin. 23 | * So this whole package has to play some games. 24 | * Define EPM (Erased Print Map) - the ground type we'll have to upcast from 25 | * Define EFN (Erased Print FuNction) - the erased form of the individual print methods 26 | * Define the "Real"(est) PrintMap type 27 | * Define the "Real"(est) PrintFn type 28 | * 29 | * When we call PrintFns, we'll have ALMOST the fully recursive type signature to hand. 30 | * But when we go through the dispatch table, we'll have to "promote" the erased functions we get 31 | * out of the dispatch table to "real" functions which have a near-full signature. 32 | */ 33 | typealias EPM = BMap> 34 | typealias EFN = ((Printer, EPM, OutputStreamWriter, Any) -> Unit) 35 | typealias PrintMap = BMap 36 | typealias PrintFn = ((Printer, PrintMap, OutputStreamWriter, Any?) -> Unit) 37 | 38 | interface Printable { 39 | fun print(p: Printer, pm: PrintMap, o: OutputStreamWriter): Unit 40 | } 41 | 42 | interface Tagged: Printable { 43 | fun tag(): Symbol 44 | fun value(): Any 45 | 46 | override fun print(p: Printer, pm: PrintMap, o: OutputStreamWriter) { 47 | o.write("#") 48 | p.print(pm, o, this.tag()) 49 | p.print(pm, o, this.value()) 50 | } 51 | } 52 | 53 | /** 54 | * Helper for printing sequential types, the grammar for which is "$start(<>($delim<>)+)$end" 55 | * where <> is taken to mean recursive printing. 56 | */ 57 | fun printListy(p: Printer, 58 | pm: PrintMap, 59 | w: OutputStreamWriter, 60 | coll: Iterable, 61 | start: String, 62 | f: PrintFn, 63 | delim: String?, 64 | end: String) { 65 | var isFirst = true 66 | w.write(start) 67 | for (e in coll) { 68 | if (!isFirst && delim != null) { 69 | w.write(delim) 70 | } else { 71 | isFirst = false 72 | } 73 | f.invoke(p, pm, w, e) 74 | } 75 | w.write(end) 76 | } 77 | 78 | /** 79 | * Helper for printing named types, the grammar for which is 80 | * "$prefix(<>(.<>)+)/$name)$suffix" 81 | * where <> is taken to be a name segment pulled from a parent namespace. 82 | */ 83 | fun >?> 84 | printNamespacy(p: Printer, 85 | pm: PrintMap, 86 | w: OutputStreamWriter, 87 | prefix: String, 88 | o: Namespaced, 89 | suffix: String) { 90 | var segments = BList() 91 | var parent = o.namespace() 92 | while (parent != null) { 93 | segments = segments.addFirst(parent.name()) 94 | parent = parent.namespace() as T 95 | } 96 | 97 | w.write(prefix) 98 | if (segments.size() != 0L) { 99 | printListy(p, pm, w, segments, "", Printer::print, ".", "") 100 | w.write("/") 101 | } 102 | w.write(o.name()) 103 | w.write(suffix) 104 | } 105 | 106 | /** 107 | * The Printer interface. 108 | * 109 | * The Printer is implemented as a trivial directly recursive call through a dispatch-by-type table 110 | * (this may later be refactored to extensible protocol / predicative dispatch) where table entries 111 | * are handlers which produce formatting for a given type and recursively call the printer with a 112 | * dispatching table. 113 | * 114 | * Callees may manipulate the dispatch table, but as it is immutable changes will not propagate 115 | * beyond the scope of a single print handler. 116 | * 117 | * The PrintMap is regrettably erased, but must be a map from Class instances to KFunction<> 118 | * instances. 119 | * 120 | * Each entry handles a concrete class (unlike a protocol which handles interfaces). 121 | * The handler convention is that the printer and the print map are arguments, and calless 122 | * are expected to recurse through the printer and print map rather than hijacking control. 123 | * 124 | * For instance, we know when printing a BMap that the elements will be Map.Entry and 125 | * so one could be tempted to directly call the appropriate function, but this leads to a loss of 126 | * extension capability as a user who isn't aware of this direct linking must discover it the hard 127 | * way and provide two overloads, not one. 128 | */ 129 | class Printer(val defaultPrinter: PrintFn = Printer::printDefault as PrintFn) { 130 | fun printMapEntry(pm: PrintMap, w: OutputStreamWriter, o: Maps.Entry) { 131 | this.print(pm, w, o.key()) 132 | w.write(" ") 133 | this.print(pm, w, o.value()) 134 | } 135 | 136 | fun printMap(pm: PrintMap, w: OutputStreamWriter, o: BMap) { 137 | printListy(this, pm, w, o, "{", Printer::print, ", ", "}") 138 | } 139 | 140 | fun printList(pm: PrintMap, w: OutputStreamWriter, o: BList) { 141 | printListy(this, pm, w, o, "(", Printer::print, " ", ")") 142 | } 143 | 144 | fun printSet(pm: PrintMap, w: OutputStreamWriter, o: BSet) { 145 | printListy(this, pm, w, o, "#{", Printer::print, " ", "}") 146 | } 147 | 148 | fun printSymbol(pm: PrintMap, w: OutputStreamWriter, o: Symbol) { 149 | val prefix = if (o.isPiped) "|" else "" 150 | printNamespacy(this, pm, w, prefix, o, prefix) 151 | } 152 | 153 | fun printKeyword(pm: PrintMap, w: OutputStreamWriter, o: Keyword) { 154 | val prefix = if (o.isPiped) "|" else "" 155 | printNamespacy(this, pm, w, ":$prefix", o, prefix) 156 | } 157 | 158 | fun printString(pm: PrintMap, w: OutputStreamWriter, s: String) { 159 | // As a nice trick re-use the (reversed!) character escape map. 160 | val rem = Map.from(ESCAPE_MAP.map { i -> Maps.Entry(i.value(), i.key()) }) 161 | assert(rem.size() == ESCAPE_MAP.size()) 162 | w.write("\"") 163 | for (c in s) { 164 | when (val ech = rem.get(c, null)) { 165 | null -> w.write(c.toInt()) 166 | else -> {w.write("\\"); w.write(ech.toInt())} 167 | } 168 | } 169 | w.write("\"") 170 | } 171 | 172 | fun printDefault(pm: PrintMap, s: OutputStreamWriter, o: Any?) { 173 | if (o == null) 174 | s.write("null") 175 | else if (o is Printable) { 176 | o.print(this, pm, s) 177 | } else { 178 | s.write(o.toString()) 179 | } 180 | } 181 | 182 | fun print(pm: PrintMap, w: OutputStreamWriter, o: Any?): Unit { 183 | val fn = pm.get(o?.javaClass ?: null as Any?, this.defaultPrinter as EFN) as PrintFn 184 | fn(this, pm, w, o) 185 | } 186 | } 187 | 188 | object Printers { 189 | /** 190 | * Note that upcasts from EPM to PrintMap are presumed to be safe, as this is hwo we're breaking 191 | * type-level recursion which Kotlin doesn't support see the comment at the top of the file 192 | * which explains the type aliases. 193 | * 194 | * Note that upcasts FROM OBJECT TO THE KEY TYPE must be safe by definition of print(). 195 | * 196 | * So we suppress all unchecked (generic to generic) casts. 197 | */ 198 | @JvmStatic 199 | @Suppress("UNCHECKED_CAST") 200 | val BASE_PRINT_MAP: PrintMap = ( 201 | PrintMap() 202 | // Print string 203 | .put(String::class.java as Any 204 | ) { p, pm, w, o -> p.printString(pm as PrintMap, w, o as String) } 205 | // Print Symbol 206 | .put(Symbol::class.java as Any 207 | ) { p, pm, w, o -> p.printSymbol(pm as PrintMap, w, o as Symbol) } 208 | 209 | // Print keyword 210 | .put(Keyword::class.java as Any 211 | ) { p, pm, w, o -> p.printKeyword(pm as PrintMap, w, o as Keyword) } 212 | 213 | // Print List 214 | .put(BList::class.java as Any 215 | ) { p, pm, w, o -> p.printList(pm as PrintMap, w, o as BList) } 216 | 217 | // Print Map.Entry 218 | .put(Maps.Entry::class.java as Any 219 | ) { p, pm, w, o -> p.printMapEntry(pm as PrintMap, w, o as Maps.Entry) } 220 | 221 | // Print Map 222 | .put(BMap::class.java as Any 223 | ) { p, pm , w, o -> p.printMap(pm as PrintMap, w, o as BMap) } 224 | 225 | // Print Set 226 | .put(BSet::class.java as Any 227 | ) { p, pm, w, o -> p.printSet(pm as PrintMap, w, o as BSet) } 228 | 229 | // Print TokenType, which is an enum and a witch to implement manually 230 | .put(TokenType::class.java as Any 231 | ) { printer, pm, w, v -> w.write(":ox.parser/${v.toString().toLowerCase()}") } 232 | ) 233 | 234 | @JvmStatic 235 | fun pr(o: Any?): String { 236 | val w = ByteArrayOutputStream() 237 | val wtr = w.writer() 238 | Printer().print(BASE_PRINT_MAP, wtr, o) 239 | wtr.close() 240 | return String(w.toByteArray()) 241 | } 242 | 243 | @JvmStatic 244 | fun println(o: Any?) { 245 | val w = System.out.writer() 246 | Printer().print(BASE_PRINT_MAP, w, o) 247 | w.write("\n") 248 | w.flush() 249 | } 250 | } 251 | 252 | object PrinterTest { 253 | @JvmStatic 254 | fun main(args: Array) { 255 | val p = Printer() 256 | val pm = Printers.BASE_PRINT_MAP 257 | val ow = System.out.writer() 258 | 259 | println("The print map ") 260 | for (kv in pm) { 261 | val key = kv.key() 262 | val value = kv.value() 263 | println(" $key => $value") 264 | } 265 | 266 | // symbols 267 | p.print(pm, ow, Symbols.of("test")) 268 | ow.write("\n") 269 | ow.flush() 270 | 271 | p.print(pm, ow, Symbols.of("me.arrdem/test")) 272 | ow.write("\n") 273 | ow.flush() 274 | 275 | p.print(pm, ow, Symbols.of("piped test")) 276 | ow.write("\n") 277 | ow.flush() 278 | 279 | // keywords 280 | p.print(pm, ow, Keywords.of("test2")) 281 | ow.write("\n") 282 | ow.flush() 283 | 284 | p.print(pm, ow, Keywords.of("io.oxlang/test2")) 285 | ow.write("\n") 286 | ow.flush() 287 | 288 | p.print(pm, ow, Keywords.of("piped test")) 289 | ow.write("\n") 290 | ow.flush() 291 | // maps 292 | p.print(pm, ow, BMap().put("a", 1).put("b", 2).put("c", 3)) 293 | ow.write("\n") 294 | ow.flush() 295 | 296 | // lists 297 | p.print(pm, ow, BList.of(1L, 2L, 3L, 4L)) 298 | ow.write("\n") 299 | ow.flush() 300 | 301 | // sets 302 | p.print(pm, ow, BSet.of(1L, 2L, 3L, 4L)) 303 | ow.write("\n") 304 | ow.flush() 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/reader.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-8-31 3 | * 4 | * The Ox reader. 5 | * Consumes the Scanners to implement reading syntax or data (they aren't entirely the same). 6 | * 7 | * The data reader - DataReader - produces a tree of straight values which makes no attempt to 8 | * retain syntax information for later analysis. 9 | * 10 | * The syntax reader - SyntaxReader - produces a tree of datums wrapped in SyntaxObjects which 11 | * record start/end token positions. This provides some tools for the compiler to provide errors 12 | * and warnings without having to be tightly coupled to the precise machinery of the reader or 13 | * sharing any state between the two. 14 | */ 15 | 16 | package me.arrdem.ox 17 | 18 | import io.lacuna.bifurcan.Maps 19 | import java.io.ByteArrayInputStream 20 | import java.util.Iterator 21 | 22 | typealias ERM = Map> 23 | typealias TokenIter = ICurrentIterator> 24 | typealias ReadFn = (Reader, ERM, Any, TokenIter) -> Any? 25 | typealias ReadMap = Map 26 | 27 | /** 28 | * The Reader. 29 | * 30 | * Contains shared machinery between the data and syntax readers. In fact the data reader is 31 | * expected to be pretty much "just" a shim to this reader. 32 | * 33 | * As a nod to the printer, which supports 34 | */ 35 | 36 | data class SyntaxObject( 37 | val obj: Any?, 38 | val meta: Any? = null, 39 | val start: Token<*>? = null): 40 | Tagged 41 | { 42 | override fun tag(): Symbol { 43 | return Symbols.of("ox.reader/syntax") 44 | } 45 | 46 | override fun value(): Any { 47 | return List.of(this.obj, this.start) 48 | } 49 | } 50 | 51 | class ParseException(val location: StreamLocation<*>?, 52 | override val message: String?, 53 | override val cause: Throwable? = null) : 54 | Exception(message, cause) { 55 | override fun toString(): String { 56 | return "Parsing at $location: $message" 57 | } 58 | } 59 | 60 | open class Reader(private val notFound: ReadFn = Reader::readError as ReadFn) { 61 | open fun readListy(startType: TokenType, 62 | endType: TokenType, 63 | msg: String, 64 | ctor: ((Iterable) -> Any), 65 | rm: ReadMap, 66 | i: TokenIter): Any { 67 | val start = i.current().location 68 | val actualStartType = i.current().tokenType 69 | if (actualStartType != startType) { 70 | throw ParseException(start, "$msg: Expected open token $startType, got $actualStartType instead!") 71 | } 72 | try { 73 | i.next() // discard the start token 74 | var v = List() 75 | val sentinel = Object() 76 | val srm = rm.put(endType) { _: Any, _: Any, _: Any, _: Any -> sentinel } as ReadMap 77 | while (i.current().tokenType != endType) { 78 | val el = this.read(srm, i) 79 | if (el != sentinel) 80 | v = v.addLast(el) 81 | } 82 | i.next() // discard the end token 83 | return ctor(v) 84 | } catch (e: Exception) { 85 | throw ParseException(start, msg, e) 86 | } 87 | } 88 | 89 | fun readError(_rm: ERM, discard: Any, i: TokenIter): Any? { 90 | val t = i.current() 91 | val ttype = t.tokenType 92 | throw ParseException(t.location, "Unmapped token type: $ttype") 93 | } 94 | 95 | fun readNothing(_rm: ERM, discard: Any, i: TokenIter): Any? { 96 | i.next(); 97 | return discard 98 | } 99 | 100 | fun readValue(_rm: ERM, discard: Any, i: TokenIter): Any? { 101 | val token = i.current() 102 | i.next() 103 | return token.value 104 | } 105 | 106 | fun readList(rm: ERM, discard: Any, i: TokenIter): Any? { 107 | return readListy( 108 | TokenType.LPAREN, 109 | TokenType.RPAREN, 110 | "While parsing () list, an exception occurred", 111 | { i: Iterable -> List.from(i) }, 112 | rm as ReadMap, 113 | i 114 | ) 115 | } 116 | 117 | fun readSqList(rm: ERM, discard: Any, i: TokenIter): Any? { 118 | return readListy( 119 | TokenType.LBRACKET, 120 | TokenType.RBRACKET, 121 | "While parsing [] list, an exception occurred", 122 | { i: Iterable -> List.from(i) }, 123 | rm as ReadMap, 124 | i 125 | ) 126 | } 127 | 128 | fun readSet(rm: ERM, discard: Any, i: TokenIter): Any { 129 | return readListy( 130 | TokenType.HASH_LBRACE, 131 | TokenType.RBRACE, 132 | "While parsing #{} set, an exception occurred", 133 | { i: Iterable -> Set.from(i) }, 134 | rm as ReadMap, 135 | i 136 | ) 137 | } 138 | 139 | fun readMap(rm: ERM, discard: Any, i: TokenIter): Any { 140 | val startLoc = i.current().location 141 | val kvs: Iterator = readListy( 142 | TokenType.LBRACE, 143 | TokenType.RBRACE, 144 | "While parsing {} map, an exception occurred", 145 | { i: Iterable -> i.iterator() }, 146 | rm as ReadMap, 147 | i) as Iterator 148 | 149 | val m = Maps.EMPTY.linear() 150 | while (kvs.hasNext()) { 151 | val k = kvs.next() 152 | if (!kvs.hasNext()) 153 | throw ParseException(startLoc, "Unmatched key $k") 154 | val v = kvs.next() 155 | m.put(k, v) 156 | } 157 | return m.forked() 158 | } 159 | 160 | /** 161 | * FIXME (arrdem 9/7/2019) 162 | * How to wire user constructors in here? 163 | */ 164 | fun readTag(rm: ERM, discard: Any, i: TokenIter): Any? { 165 | i.next() 166 | val tag = this.read(rm as ReadMap, i) 167 | val obj = this.read(rm as ReadMap, i) 168 | return Tag(tag!!, obj!!) 169 | } 170 | 171 | fun readQuote(rm: ERM, discard: Any, i: TokenIter): Any? { 172 | i.next() 173 | val obj = this.read(rm as ReadMap, i) 174 | return List.of(Symbols.of("quote"), obj!!) 175 | } 176 | 177 | fun readMeta(rm: ERM, discard: Any, i: TokenIter): Any? { 178 | val startToc = i.current() 179 | i.next() 180 | val meta = this.read(rm as ReadMap, i) 181 | val obj = this.read(rm as ReadMap, i) 182 | return Meta(meta, obj) 183 | } 184 | 185 | /** 186 | * FIXME (arrdem 9/7/2019) 187 | * Can this be factored out as a reader parameter? 188 | */ 189 | fun readSymbol(_rm: ERM, discard: Any, i: TokenIter): Any? { 190 | val token = i.current() 191 | i.next() 192 | return when (token.value as Symbol) { 193 | Symbols.of("null") -> null 194 | /** 195 | * FIXME (arrdem 9/7/2019) 196 | * Given a warning(s) / messages framework, warn on these or make 'em pluggable. 197 | */ 198 | Symbols.of("nil") -> null 199 | Symbols.of("none") -> null 200 | 201 | Symbols.of("+nan"), 202 | Symbols.of("+NaN"), 203 | Symbols.of("NaN"), 204 | Symbols.of("nan") -> Double.NaN 205 | 206 | Symbols.of("-NaN"), 207 | Symbols.of("-nan") -> -1 * Double.NaN 208 | 209 | else -> token.value 210 | } 211 | } 212 | 213 | open fun read(rm: ReadMap, i: TokenIter): Any? { 214 | // yo dawg I heard u leik streams 215 | val startPos = i.current().location 216 | val discard: Any = Object() 217 | var read: Any? = discard 218 | 219 | while (i.current() != null && read == discard) { 220 | val handler = rm.get(i.current().tokenType, notFound) as ReadFn 221 | read = handler(this, rm as ERM, discard, i) 222 | } 223 | 224 | if (read != discard) { 225 | return read 226 | } else { 227 | throw ParseException(startPos, "Got end of file while reading!") 228 | } 229 | } 230 | } 231 | 232 | class SyntaxReader(private val notFound: ReadFn = Reader::readError as ReadFn): 233 | Reader(notFound = notFound) { 234 | override fun readListy(startType: TokenType, 235 | endType: TokenType, 236 | msg: String, 237 | ctor: ((Iterable) -> Any), 238 | rm: ReadMap, 239 | i: TokenIter): Any { 240 | val start = i.current().location 241 | val actualStartType = i.current().tokenType 242 | if (actualStartType != startType) { 243 | throw ParseException(start, "$msg: Expected open token $startType, got $actualStartType instead!") 244 | } 245 | try { 246 | i.next() // discard the start token 247 | var v = List() 248 | val sentinel = Object() 249 | val srm = rm.put(endType) { _: Any, _: Any, _: Any, _: Any -> sentinel } as ReadMap 250 | while (i.current().tokenType != endType) { 251 | val el: SyntaxObject? = this.read(srm, i) 252 | if (el!!.obj != sentinel) 253 | v = v.addLast(el) 254 | } 255 | i.next() // discard the end token 256 | return ctor(v) 257 | } catch (e: Exception) { 258 | throw ParseException(start, msg, e) 259 | } 260 | } 261 | 262 | override fun read(rm: ReadMap, i: TokenIter): SyntaxObject? { 263 | // yo dawg I heard u leik streams 264 | val current: Token<*> = i.current() 265 | return SyntaxObject(super.read(rm, i), current) 266 | } 267 | } 268 | 269 | object Readers { 270 | @JvmStatic 271 | val BASE_READ_MAP: ReadMap = ( 272 | ReadMap() 273 | // Things we discard by default 274 | .put(TokenType.COMMENT, Reader::readNothing) 275 | .put(TokenType.WHITESPACE, Reader::readNothing) 276 | .put(TokenType.NEWLINE, Reader::readNothing) 277 | 278 | // Atoms 279 | .put(TokenType.NUMBER, Reader::readValue) 280 | .put(TokenType.KEYWORD, Reader::readValue) 281 | .put(TokenType.SYMBOL, Reader::readSymbol) 282 | .put(TokenType.STRING, Reader::readValue) 283 | 284 | // Lists 285 | .put(TokenType.LPAREN, Reader::readList) 286 | .put(TokenType.LBRACKET, Reader::readSqList) 287 | .put(TokenType.LBRACE, Reader::readMap) 288 | .put(TokenType.HASH_LBRACE, Reader::readSet) 289 | 290 | .put(TokenType.META, Reader::readMeta) 291 | .put(TokenType.HASH, Reader::readTag) 292 | .put(TokenType.QUOTE, Reader::readQuote) 293 | 294 | // RHS stuff that is an error by default 295 | .put(TokenType.RBRACE, Reader::readError) 296 | .put(TokenType.RBRACKET, Reader::readError) 297 | .put(TokenType.RPAREN, Reader::readError) 298 | ) 299 | 300 | @JvmStatic 301 | val SYNTAX_READ_MAP: ReadMap = ( 302 | BASE_READ_MAP 303 | .put(TokenType.COMMENT, Reader::readValue) 304 | .put(TokenType.WHITESPACE, Reader::readValue) 305 | .put(TokenType.NEWLINE, Reader::readValue) 306 | ) 307 | 308 | fun read(rm: ReadMap, reader: java.io.Reader, streamIdentifier: Any): Any? { 309 | return Reader().read(rm, CurrentIterator(Scanners.scan(reader, streamIdentifier) as Iterator>)) 310 | } 311 | 312 | fun read(rm: ReadMap, buff: String, streamIdentifier: Any): Any? { 313 | return read(rm, ByteArrayInputStream(buff.toByteArray()).reader(), streamIdentifier) 314 | } 315 | 316 | fun read(buff: String, streamIdentifier: Any): Any? { 317 | return read(BASE_READ_MAP, buff, streamIdentifier) 318 | } 319 | 320 | fun read(rdr: java.io.Reader, streamIdentifier: Any): Any? { 321 | return read(BASE_READ_MAP, rdr, streamIdentifier) 322 | } 323 | } 324 | 325 | object ReaderTest { 326 | @JvmStatic 327 | fun main(args: Array) { 328 | val iter = CurrentIterator(Scanners.scan(System.`in`.reader(), "STDIN") as Iterator>) 329 | val rdr = Reader() 330 | 331 | while (iter.hasNext()) { 332 | val obj = rdr.read(Readers.BASE_READ_MAP, iter) 333 | if (obj != null) { 334 | Printers.println(obj) 335 | } 336 | } 337 | } 338 | } 339 | 340 | object SyntaxReaderTest { 341 | @JvmStatic 342 | fun main(args: Array) { 343 | val iter = CurrentIterator(Scanners.scan(System.`in`.reader(), "STDIN") as Iterator>) 344 | val rdr = SyntaxReader() 345 | 346 | while (iter.hasNext()) { 347 | val obj = rdr.read(Readers.BASE_READ_MAP, iter) 348 | if (obj != null) { 349 | Printers.println(obj) 350 | } 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/repl.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-09-07 3 | * 4 | * A sketch driver for a command line REPL. 5 | * 6 | * When complete, this'll be the "bare" REPL entry point. 7 | */ 8 | 9 | package me.arrdem.ox 10 | 11 | import java.io.IOError 12 | import java.io.Reader 13 | import java.lang.Exception 14 | 15 | object ConsoleRepl { 16 | @JvmStatic 17 | fun main(args: Array) { 18 | val rdr = System.`in`.reader() 19 | var environment = null 20 | var input = 0 21 | 22 | while(true) { 23 | try { 24 | print("=> ") 25 | val expr = Readers.read(rdr as Reader, "STDIN: $input") 26 | val res = expr // FIXME: evaluate 27 | Printers.println(res) 28 | } catch (e: IOError) { 29 | break 30 | } catch (e: Exception) { 31 | 32 | } finally { 33 | input += 1 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/me/arrdem/ox/scanner.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Reid 'arrdem' McKenzie 2019-5-20 3 | * 4 | * The Ox scanner. 5 | * Used to implement the reader. 6 | */ 7 | 8 | package me.arrdem.ox 9 | 10 | import java.io.PushbackReader 11 | import java.io.Reader 12 | import java.io.StringReader 13 | import java.lang.IllegalStateException 14 | import java.nio.charset.Charset 15 | import java.util.Iterator 16 | 17 | /** 18 | * Tokens are read from streams, which are identified by a 19 | * StreamIdentifer of type T, defined by the user. 20 | */ 21 | data class StreamLocation( 22 | val streamIdentifer: T, 23 | val offset: Long, 24 | val lineNumber: Long, 25 | val columnNumber: Long 26 | ): Tagged { 27 | override fun tag(): Symbol { 28 | return Symbols.of("ox.scanner/streamloc") 29 | } 30 | 31 | override fun value(): Any { 32 | return List.of(streamIdentifer, offset, lineNumber, columnNumber) 33 | } 34 | } 35 | 36 | // Tokens are of a type 37 | enum class TokenType { 38 | // lists are () 39 | LPAREN, 40 | RPAREN, 41 | 42 | // lists are also [] 43 | LBRACKET, 44 | RBRACKET, 45 | 46 | // mappings and sets use {} 47 | LBRACE, 48 | RBRACE, 49 | 50 | // Whitespace 51 | NEWLINE, 52 | WHITESPACE, 53 | 54 | // reader macros 55 | HASH, // Used for #foo reader macros 56 | HASH_LPAREN, // Used for #() 57 | HASH_LBRACE, // Used for #{} 58 | HASH_LBRACKET, // Used for #[] 59 | 60 | // More weirdness 61 | QUOTE, 62 | META, 63 | COMMENT, 64 | 65 | // atoms 66 | STRING, 67 | NUMBER, SYMBOL, KEYWORD 68 | 69 | // FIXME: 70 | // - NaN 71 | // - Inf 72 | // - Nil / Null 73 | // True, False 74 | } 75 | 76 | // Tokens themselves 77 | 78 | data class Token( 79 | val tokenType: TokenType, 80 | val location: StreamLocation, 81 | val value: Any 82 | ): Tagged { 83 | override fun tag(): Symbol { 84 | return Symbols.of("ox/token") 85 | } 86 | 87 | override fun value(): Any { 88 | return List.of(tokenType, location, value) 89 | } 90 | } 91 | 92 | // Before we can define the scanner, we need the exception it throws 93 | 94 | class ScannerException( 95 | val location: StreamLocation, 96 | message: String, 97 | cause: Throwable? = null 98 | ) : Exception(message, cause) 99 | 100 | val ESCAPE_MAP = Map() 101 | .put('n', '\n') 102 | .put('t', '\t') 103 | .put('r', '\r') 104 | .put('"', '"') 105 | .put('\\', '\\') 106 | .put('f', 12.toChar()) 107 | .put('b', '\b') 108 | 109 | // And now for the scanner 110 | private class TokenScanner( 111 | val stream: PushbackReader, 112 | val streamIdentifer: T, 113 | val offset: Long = 0, 114 | val lineNumber: Long = 0, 115 | val columnNumber: Long = 0, 116 | var firstColumnIndex: Long = 0 117 | ) : Iterator> { 118 | // HACK (arrdem 2019-05-20): 119 | // Newlines and every other character is a part of its own line. 120 | // If we were to compute the "next" position each time we read(), we'd be off by one. 121 | // The computed position is after all ONE AHEAD of the current position, which is 0-indexed. 122 | // So we do this game where we keep two locations, the "current" and "next" locations. 123 | private var curLoc: StreamLocation = 124 | StreamLocation(streamIdentifer, 0, 0, 0) 125 | 126 | private var nextLoc: StreamLocation = 127 | StreamLocation(streamIdentifer, offset, lineNumber, columnNumber) 128 | 129 | private fun read(): Int { 130 | val c: Int = this.stream.read() 131 | 132 | if (c == -1) { 133 | return c 134 | } else { 135 | // Maintain location bookkeeping 136 | 137 | // FIXME (arrdem 2019-05-20): 138 | // This isn't strictly correct - since we're using a UTF-8 backed 139 | // reader, each char we read is quite possibly multibyte. Oh well. 140 | this.curLoc = this.nextLoc 141 | 142 | if (c == 10) { 143 | this.nextLoc = StreamLocation( 144 | streamIdentifer, 145 | this.curLoc.offset + 1, 146 | this.curLoc.lineNumber + 1, 147 | firstColumnIndex 148 | ) 149 | } else { 150 | // Yes this is broken for tabs, no I don't care, tabs are 1spc 151 | // Variable width characters too >.> 152 | this.nextLoc = StreamLocation( 153 | streamIdentifer, 154 | this.curLoc.offset + 1, 155 | this.curLoc.lineNumber, 156 | this.curLoc.columnNumber + 1 157 | ) 158 | } 159 | 160 | return c 161 | } 162 | } 163 | 164 | private fun unread(c: Int) { 165 | this.stream.unread(c) 166 | this.nextLoc = this.curLoc 167 | } 168 | 169 | private fun scanString(tt: TokenType, _c: Char): Token { 170 | val start = curLoc 171 | val buff = StringBuilder() 172 | 173 | var escaped = false 174 | while (true) { 175 | val i = this.read() 176 | if (i == -1) { 177 | throw ScannerException( 178 | start as StreamLocation, 179 | "Reached end of stream while scanning a string!") 180 | } else { 181 | val c = i.toChar() 182 | if (escaped) { 183 | when (val escapedChar: Char? = ESCAPE_MAP.get(c, null)) { 184 | null -> throw ScannerException( 185 | start as StreamLocation, 186 | "Encountered illegal escaped character $c (ord $i) while scanning a string!") 187 | else -> { 188 | escaped = false; buff.append(escapedChar) 189 | } 190 | } 191 | } else if (c == '\\') { 192 | escaped = true 193 | } else if (c == '\"') { 194 | break 195 | } else { 196 | buff.append(c) 197 | } 198 | } 199 | } 200 | return Token(tt, start, buff.toString()) 201 | } 202 | 203 | private fun scanSimpleSymbol(buff: StringBuilder) { 204 | read@ while (true) { 205 | val i = this.read() 206 | if (i == -1) { 207 | return 208 | } else { 209 | val c = i.toChar() 210 | when (c) { 211 | '(', ')', '[', ']', '{', '}', ';', '#', '\'', ',', '^' -> { 212 | this.stream.unread(i) 213 | break@read 214 | } 215 | else -> when { 216 | Character.isWhitespace(i) -> { 217 | this.stream.unread(i); break@read 218 | } 219 | else -> buff.append(c) 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | private fun scanPipedSymbol(start: StreamLocation, buff: StringBuilder) { 227 | while (true) { 228 | val i = this.read() 229 | if (i == -1) { 230 | throw ScannerException( 231 | start as StreamLocation, 232 | String.format("Encountered end of stream while scanning a piped symbol!") 233 | ) 234 | } else { 235 | val c = i.toChar() 236 | if (c == '|') { 237 | break 238 | } else { 239 | buff.append(c) 240 | } 241 | } 242 | } 243 | } 244 | 245 | private fun scanSymbolKw( 246 | tt: TokenType, 247 | startChar: Char, 248 | start: StreamLocation = curLoc 249 | ): Token { 250 | val buff = StringBuilder() 251 | when (startChar) { 252 | '|' -> scanPipedSymbol(start, buff) 253 | ':' -> return scanSymbolKw(TokenType.KEYWORD, this.read().toChar(), curLoc) 254 | else -> { 255 | buff.append(startChar); scanSimpleSymbol(buff) 256 | } 257 | } 258 | 259 | val res = when (tt) { 260 | TokenType.SYMBOL -> Symbols.of(buff.toString()) 261 | TokenType.KEYWORD -> Keywords.of(buff.toString()) 262 | else -> throw IllegalStateException("Got tokentype $tt while processing symbol/kw!") 263 | } 264 | return Token(tt, start, res) 265 | } 266 | 267 | private fun scanComment( 268 | tt: TokenType, 269 | startChar: Char, 270 | start: StreamLocation = curLoc 271 | ): Token { 272 | val buff = StringBuilder() 273 | buff.append(startChar) 274 | 275 | while (true) { 276 | val i = this.read() 277 | if (i == -1) { 278 | break 279 | } else { 280 | val c = i.toChar() 281 | if (c == '\n') { 282 | this.unread(i) 283 | break 284 | } else { 285 | buff.append(c) 286 | } 287 | } 288 | } 289 | 290 | return Token(tt, start, buff.toString()) 291 | } 292 | 293 | private fun scanNumber( 294 | tt: TokenType, 295 | startChar: Char, 296 | start: StreamLocation = curLoc 297 | ): Token { 298 | /* Problems here: 299 | * - Doesn't do bases other than 10 300 | * - Doesn't do decimal 301 | * - Doesn't do exponents 302 | * - Doesn't do fractions/rationals 303 | * 304 | * It does however do signs which is at least something 305 | */ 306 | var value = when (startChar) { 307 | '-', '+' -> 0 308 | else -> startChar.toInt() - 48 309 | } 310 | val negated: Boolean = when (startChar) { 311 | '-' -> true 312 | else -> false 313 | } 314 | 315 | while (true) { 316 | val i = this.read() 317 | if (i == -1) { 318 | break 319 | } else if (Character.isDigit(i)) { 320 | value = value * 10 + (i - 48) // 48 is ord('0') 321 | } else { 322 | // We're done. For now. This will have to be more involved later. 323 | this.unread(i) 324 | break 325 | } 326 | } 327 | 328 | return Token(tt, start, if (negated) -1 * value else value) 329 | } 330 | 331 | override fun hasNext(): Boolean { 332 | // I think this is correct - the iterator has 333 | // SOMETHING as long as the underlying PBR has 334 | // SOMETHING. 335 | val i = this.stream.read() 336 | try { 337 | return i != -1 338 | } finally { 339 | this.stream.unread(i) 340 | } 341 | } 342 | 343 | override fun next(): Token? { 344 | val c: Int = this.read() 345 | 346 | if (c == -1) { 347 | return null 348 | } 349 | 350 | var ch = Character.valueOf(c.toChar()) 351 | 352 | val tt = when (c.toChar()) { 353 | '(' -> TokenType.LPAREN 354 | ')' -> TokenType.RPAREN 355 | '[' -> TokenType.LBRACKET 356 | ']' -> TokenType.RBRACKET 357 | '{' -> TokenType.LBRACE 358 | '}' -> TokenType.RBRACE 359 | ';' -> TokenType.COMMENT 360 | '\'' -> TokenType.QUOTE 361 | '\"' -> TokenType.STRING 362 | ',' -> TokenType.WHITESPACE 363 | '^' -> TokenType.META 364 | '\n' -> TokenType.NEWLINE 365 | 366 | // Note that - is ambiguous without lookahead - so look ahead and cheat 367 | '-', '+' -> { 368 | val next: Int = this.stream.read() 369 | try { 370 | when { 371 | next == -1 -> TokenType.SYMBOL 372 | Character.isDigit(next) -> TokenType.NUMBER 373 | else -> TokenType.SYMBOL 374 | } 375 | } finally { 376 | this.stream.unread(next) 377 | } 378 | } 379 | 380 | '#' -> { 381 | val next: Int = this.stream.read() 382 | ch = Character.valueOf(next.toChar()) 383 | val tt = when (ch.toChar()) { 384 | '{' -> TokenType.HASH_LBRACE 385 | '[' -> TokenType.HASH_LBRACKET 386 | '(' -> TokenType.HASH_LPAREN 387 | else -> { 388 | this.stream.unread(next) 389 | TokenType.HASH 390 | } 391 | } 392 | return Token(tt, curLoc, "#$ch") 393 | } 394 | 395 | else -> when { 396 | // really getting fancy here, gonna need some more logic 397 | Character.isWhitespace(c) -> TokenType.WHITESPACE 398 | Character.isDigit(c) -> TokenType.NUMBER 399 | else -> TokenType.SYMBOL 400 | } 401 | } 402 | 403 | // String scanning 404 | return when (tt) { 405 | TokenType.STRING -> scanString(tt, ch) 406 | TokenType.SYMBOL -> scanSymbolKw(tt, ch) 407 | TokenType.COMMENT -> scanComment(tt, ch) 408 | TokenType.NUMBER -> scanNumber(tt, ch) 409 | else -> Token(tt, curLoc, "$ch") 410 | } 411 | } 412 | 413 | override fun remove() { 414 | throw UnsupportedOperationException() 415 | } 416 | } 417 | 418 | // Forcing the generated class name 419 | object Scanners { 420 | @JvmStatic 421 | fun scan(stream: Reader, streamIdentifier: T): Iterator> { 422 | // yo dawg I heard u leik streams 423 | return TokenScanner(PushbackReader(stream), streamIdentifier) 424 | } 425 | 426 | @JvmStatic 427 | fun scanStr(buff: String, streamIdentifier: T): Iterator> { 428 | return scan(StringReader(buff), streamIdentifier) 429 | } 430 | 431 | @JvmStatic 432 | fun scanStrEager(buff: String, streamIdentifier: T): Iterable> { 433 | var l = List>() 434 | val iter = scanStr(buff, streamIdentifier) 435 | while (iter.hasNext()) { 436 | l = l.addLast(iter.next()) 437 | } 438 | return l 439 | } 440 | } 441 | 442 | object ScannerTest { 443 | @JvmStatic 444 | fun main(args: Array) { 445 | val scanner = Scanners.scan(System.`in`.reader(), String.format("stdin")) 446 | while (scanner.hasNext()) { 447 | val obj = scanner.next() 448 | if (obj != null) { 449 | Printers.println(obj) 450 | } 451 | } 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/test/kotlin/me/arrdem/ox/iterators.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import org.junit.Test 4 | import java.util.Iterator; 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class CurrentIteratorTest { 9 | @Test 10 | fun easyTest() { 11 | var iter = CurrentIterator(List.of(1, 2, 3, 4, 5).iterator() as Iterator) 12 | assertTrue(iter.hasNext()) 13 | assertEquals(iter.current(), 1) 14 | assertEquals(iter.next(), 2) 15 | assertEquals(iter.current(), 2) 16 | assertEquals(iter.current(), 2) 17 | 18 | while(iter.hasNext()) { 19 | assertEquals(iter.next(), iter.current()) 20 | } 21 | } 22 | } 23 | 24 | class PeekIteratorTest { 25 | @Test 26 | fun easyTest() { 27 | var iter = PeekIterator(List.of(1, 2, 3, 4, 5).iterator() as Iterator) 28 | assertTrue(iter.hasNext()) 29 | while(iter.hasNext()) { 30 | val pv = iter.peek() 31 | val nv = iter.next() 32 | val cv = iter.current() 33 | assertEquals(pv, nv) 34 | assertEquals(nv, cv) 35 | assertEquals(pv, cv) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/kotlin/me/arrdem/ox/natural.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import org.junit.Test 4 | import java.math.BigInteger 5 | import kotlin.random.Random 6 | import kotlin.test.assertEquals 7 | 8 | class NaturalTest { 9 | val workFactor: Int = 1000 10 | 11 | private fun rint(r: Random): BigInteger { 12 | val l = r.nextLong(0, 10000) 13 | if(l < 0) 14 | return BigInteger.valueOf(l * -1); 15 | else 16 | return BigInteger.valueOf(l); 17 | } 18 | 19 | private fun rint(r: Random, max: Long): BigInteger { 20 | val l = r.nextLong(0, max) 21 | if(l < 0) 22 | return BigInteger.valueOf(l * -1); 23 | else 24 | return BigInteger.valueOf(l); 25 | } 26 | 27 | fun degenerateOf(value: Long, step: Long = 1): Nat { 28 | return if (value <= 0L) 29 | Nat.ZERO 30 | else 31 | Nat.of(1) { degenerateOf(value - step) } 32 | } 33 | 34 | fun degenerateOf(value: BigInteger): Nat { 35 | return if (value == BigInteger.ZERO) 36 | Nat.ZERO 37 | else 38 | Nat.of(1) { degenerateOf(value.subtract(BigInteger.ONE)) } 39 | } 40 | 41 | 42 | @Test 43 | fun testIdentity() { 44 | for (i in 0..workFactor) 45 | assertEquals(Nat.of(i.toLong()), degenerateOf(i.toLong())) 46 | 47 | val random = Random(System.currentTimeMillis()) 48 | for (i in 1..workFactor) { 49 | val v = rint(random) 50 | assertEquals(Nat.of(v), degenerateOf(v)) 51 | assertEquals(degenerateOf(v), Nat.of(v)) 52 | } 53 | } 54 | 55 | @Test 56 | fun testDegenerateAddition() { 57 | val random = Random(System.currentTimeMillis()) 58 | for (i in 1..workFactor) { 59 | val a = rint(random) 60 | val b = rint(random) 61 | val sum = a + b 62 | 63 | println("$a + $b = $sum") 64 | 65 | assertEquals(Nat.of(a).add(Nat.of(b)), Nat.of(sum)); 66 | assertEquals(degenerateOf(a).add(Nat.of(b)), Nat.of(sum)); 67 | assertEquals(Nat.of(a).add(degenerateOf(b)), Nat.of(sum)); 68 | assertEquals(degenerateOf(a).add(degenerateOf(b)), Nat.of(sum)); 69 | } 70 | } 71 | 72 | @Test 73 | fun testDegenerateSubtraction() { 74 | val random = Random(System.currentTimeMillis()) 75 | for (i in 1..workFactor) { 76 | var a = rint(random) 77 | var b = rint(random) 78 | 79 | // Ensure that A is greater 80 | if(a < b) { 81 | val t = a 82 | a = b 83 | b = t 84 | } 85 | val difference = a.subtract(b) 86 | 87 | println("$a - $b = $difference") 88 | 89 | assertEquals(Nat.of(a).subtract(Nat.of(b)), Nat.of(difference)); 90 | assertEquals(Nat.of(a).subtract(degenerateOf(b)), Nat.of(difference)); 91 | assertEquals(degenerateOf(a).subtract(Nat.of(b)), Nat.of(difference)); 92 | assertEquals(degenerateOf(a).subtract(degenerateOf(b)), Nat.of(difference)); 93 | } 94 | } 95 | 96 | @Test 97 | fun testDegenerateMultiplication() { 98 | val random = Random(System.currentTimeMillis()) 99 | for (i in 1..workFactor) { 100 | var a = rint(random, 100) 101 | var b = rint(random, 100) 102 | val product = a.multiply(b) 103 | 104 | println("$a * $b = $product") 105 | 106 | assertEquals(Nat.of(a).multiply(Nat.of(b)), Nat.of(product)) 107 | assertEquals(degenerateOf(a).multiply(Nat.of(b)).get(), product) 108 | assertEquals(Nat.of(a).multiply(degenerateOf(b)), Nat.of(product)) 109 | assertEquals(degenerateOf(a).multiply(degenerateOf(b)), Nat.of(product)) 110 | } 111 | } 112 | 113 | @Test 114 | fun testDegenerateDivision() { 115 | val random = Random(System.currentTimeMillis()) 116 | for (i in 1..workFactor) { 117 | var a = rint(random).inc() 118 | var b = rint(random).inc() 119 | 120 | // Ensure that A is greater 121 | if(a < b) { 122 | val t = a 123 | a = b 124 | b = t 125 | } 126 | val dividend = a.divide(b) 127 | 128 | println("$a / $b = $dividend") 129 | 130 | assertEquals(Nat.of(a).divide(Nat.of(b)), Nat.of(dividend)); 131 | assertEquals(Nat.of(a).divide(degenerateOf(b)), Nat.of(dividend)); 132 | assertEquals(degenerateOf(a).divide(Nat.of(b)), Nat.of(dividend)); 133 | assertEquals(degenerateOf(a).divide(degenerateOf(b)), Nat.of(dividend)); 134 | } 135 | } 136 | 137 | @Test 138 | fun testDegenerateRemainder() { 139 | val random = Random(System.currentTimeMillis()) 140 | for (i in 1..workFactor) { 141 | var a = rint(random).inc() 142 | var b = rint(random).inc() 143 | 144 | // Ensure that A is greater 145 | if(a < b) { 146 | val t = a 147 | a = b 148 | b = t 149 | } 150 | val rem = a.remainder(b) 151 | 152 | println("$a % $b = $rem") 153 | 154 | assertEquals(Nat.of(a).remainder(Nat.of(b)), Nat.of(rem)); 155 | assertEquals(Nat.of(a).remainder(degenerateOf(b)), Nat.of(rem)); 156 | assertEquals(degenerateOf(a).remainder(Nat.of(b)), Nat.of(rem)); 157 | assertEquals(degenerateOf(a).remainder(degenerateOf(b)), Nat.of(rem)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/kotlin/me/arrdem/ox/printer.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import io.lacuna.bifurcan.Lists 4 | import io.lacuna.bifurcan.Maps 5 | import io.lacuna.bifurcan.Sets 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotEquals 9 | import kotlin.test.assertNotNull 10 | 11 | class PrinterTest { 12 | fun readPrintReadTest(text: String) { 13 | val v = Readers.read(text, "in") 14 | assertEquals(Readers.read(Printers.pr(v), "in2"), v) 15 | } 16 | 17 | @Test fun testRprNum() { 18 | readPrintReadTest(Long.MAX_VALUE.toString()) 19 | readPrintReadTest(Long.MIN_VALUE.toString()) 20 | } 21 | 22 | @Test fun testRprSymbol() { 23 | readPrintReadTest("foo.bar/baz") 24 | readPrintReadTest("|foo bar/baz|") 25 | } 26 | 27 | @Test fun testRprKw() { 28 | readPrintReadTest(":foo.bar/baz") 29 | readPrintReadTest(":|foo bar/baz|") 30 | } 31 | 32 | @Test fun testRprList() { 33 | readPrintReadTest("(() (() ()))") 34 | } 35 | 36 | @Test fun testRprMap() { 37 | readPrintReadTest("{:foo 1, :bar 2}") 38 | } 39 | 40 | @Test fun testRprSet() { 41 | readPrintReadTest("#{1, 2, 3, 4, 5}") 42 | } 43 | 44 | @Test fun testRprString() { 45 | readPrintReadTest("\"foo bar \n \r \t \\f \\b \\\\ \\\" \"") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/kotlin/me/arrdem/ox/reader.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import io.lacuna.bifurcan.Lists 4 | import io.lacuna.bifurcan.Maps 5 | import io.lacuna.bifurcan.Sets 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotEquals 9 | import kotlin.test.assertNotNull 10 | 11 | class ReaderTest { 12 | fun read(input: String, exId: String): Any? { 13 | return Readers.read(input, exId) 14 | } 15 | 16 | @Test fun testReadList() { 17 | assertEquals(Lists.EMPTY, read("()", "test-0")) 18 | assertEquals(List.of(null), read("(null)", "test-0")) 19 | assertEquals(List.of(Keywords.of("foo")), read("(:foo\n; inline comment\n)", "test-0")) 20 | assertEquals(List.of(Lists.EMPTY, Lists.EMPTY, Lists.EMPTY), read("(()()())", "test-0")) 21 | } 22 | 23 | @Test fun testReadSet() { 24 | assertEquals(Sets.EMPTY, read("#{}", "test-1")) 25 | assertEquals(Set.of(1, 2, 3), read("#{1, 2, 3}", "test-1")) 26 | } 27 | 28 | @Test fun testReadMap() { 29 | assertEquals(Maps.EMPTY, read("{}", "test-2")) 30 | assertEquals(Maps.EMPTY 31 | .put(Keywords.of("foo"), 1) 32 | .put(Keywords.of("bar"), 2), 33 | read("{:foo 1, :bar 2\n; inline comment\n}", "test-2")) 34 | } 35 | 36 | @Test fun testReadSymbol() { 37 | assertEquals(Symbols.of("foo"), read("foo", "test-3")) 38 | assertEquals(Symbols.of("foo.bar/baz"), read("foo.bar/baz", "test-3")) 39 | assertEquals(Symbols.of("foo bar/baz"), read("|foo bar/baz|", "test-3")) 40 | assertEquals(Symbols.of("+foo-bar-baz+"), read("+foo-bar-baz+", "test-3")) 41 | } 42 | 43 | @Test fun testReadKeyword() { 44 | assertEquals(Keywords.of("foo"), read("::foo", "test-4")) 45 | assertEquals(Keywords.of("foo.bar/baz"), read("::foo.bar/baz", "test-4")) 46 | assertEquals(Keywords.of("foo bar/baz"), read("::|foo bar/baz|", "test-4")) 47 | assertEquals(Keywords.of("+foo-bar-baz+"), read("::+foo-bar-baz+", "test-4")) 48 | } 49 | 50 | @Test fun testReadNumber() { 51 | assertEquals(1, read("1", "test-5")) 52 | assertEquals(-1, read("-1", "test-5")) 53 | } 54 | 55 | @Test fun testReadString() { 56 | assertEquals("foo bar baz", read("\"foo bar baz\"", "test-6")) 57 | assertEquals("\n\r\t\b\\", read("\"\n\r\t\b\\\\\"", "test-6")) 58 | assertEquals("\n\r\t\b\\", read("\"\\n\\r\\t\\b\\\\\"", "test-6")) 59 | } 60 | 61 | @Test fun testReadTag() { 62 | assertNotNull(read("#foo [1, 2, 3]", "test-7")) 63 | } 64 | 65 | @Test fun testReadQuote() { 66 | assertEquals(List.of(Symbols.of("quote"), Symbols.of("foo")), read("'foo", "test-8")) 67 | assertEquals(List.of(Symbols.of("quote"), 1), read("'1", "test-8")) 68 | assertEquals(List.of(Symbols.of("quote"), Maps.EMPTY), read("'{}", "test-8")) 69 | assertEquals(List.of(Symbols.of("quote"), Sets.EMPTY), read("'#{}", "test-8")) 70 | } 71 | 72 | @Test fun testReadMeta() { 73 | assertNotNull(read("^:foo ^:bar baz", "test-9")) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/kotlin/me/arrdem/ox/scanner.kt: -------------------------------------------------------------------------------- 1 | package me.arrdem.ox 2 | 3 | import kotlin.test.assertEquals 4 | import org.junit.Test 5 | 6 | class ScannerTest { 7 | fun scanTypes(input: String, exId: String): kotlin.collections.List { 8 | return Scanners.scanStrEager(input, exId).map { t: Token -> t.tokenType} 9 | } 10 | 11 | fun scanOne(input: String, exId: String): Token { 12 | return Scanners.scanStrEager(input, exId).first() 13 | } 14 | 15 | @Test fun testScanParens(): Unit { 16 | assertEquals( 17 | listOf( 18 | TokenType.LPAREN, 19 | TokenType.RPAREN 20 | ), 21 | scanTypes("()", "test-1") 22 | ) 23 | } 24 | 25 | @Test fun testScanBrackets(): Unit { 26 | assertEquals( 27 | listOf( 28 | TokenType.LBRACKET, 29 | TokenType.RBRACKET 30 | ), 31 | scanTypes("[]", "test-2") 32 | ) 33 | } 34 | 35 | @Test fun testScanBraces(): Unit { 36 | assertEquals( 37 | listOf( 38 | TokenType.LBRACE, 39 | TokenType.RBRACE 40 | ), 41 | scanTypes("{}", "test-3") 42 | ) 43 | } 44 | 45 | @Test fun testScanSymbols(): Unit { 46 | assertEquals( 47 | listOf( 48 | TokenType.SYMBOL, 49 | TokenType.WHITESPACE, 50 | TokenType.SYMBOL, 51 | TokenType.WHITESPACE, 52 | TokenType.SYMBOL 53 | ), 54 | // Simple symbols 55 | scanTypes("foo-bar +foo-bar+ |baz qux|", "test-4") 56 | ) 57 | 58 | assertEquals( 59 | Token(TokenType.SYMBOL, 60 | StreamLocation("test-4", 0, 0, 0), Symbols.of("foo")), 61 | scanOne("foo", "test-4") 62 | ) 63 | 64 | assertEquals( 65 | Token(TokenType.SYMBOL, 66 | StreamLocation("test-4", 0, 0, 0), Symbols.of("foo bar baz")), 67 | scanOne("|foo bar baz|", "test-4") 68 | ) 69 | 70 | assertEquals( 71 | Token(TokenType.SYMBOL, 72 | StreamLocation("test-4", 0, 0, 0), Symbols.of("foo.bar/baz")), 73 | scanOne("foo.bar/baz", "test-4") 74 | ) 75 | } 76 | 77 | @Test fun testPuncBreaksSymbols(): Unit { 78 | assertEquals( 79 | listOf( 80 | TokenType.SYMBOL, 81 | 82 | TokenType.LPAREN, 83 | TokenType.SYMBOL, 84 | TokenType.RPAREN, 85 | 86 | TokenType.SYMBOL, 87 | 88 | TokenType.LBRACKET, 89 | TokenType.SYMBOL, 90 | TokenType.RBRACKET, 91 | 92 | TokenType.SYMBOL, 93 | 94 | TokenType.LBRACE, 95 | TokenType.SYMBOL, 96 | TokenType.RBRACE, 97 | 98 | TokenType.SYMBOL, 99 | TokenType.HASH, 100 | TokenType.SYMBOL, 101 | TokenType.QUOTE, 102 | TokenType.SYMBOL, 103 | TokenType.META, 104 | TokenType.SYMBOL 105 | ), 106 | // This isn't one long symbol, it's a whole pile of them 107 | scanTypes("f(o)o[b]a{r}b#a'z^q", "test-5") 108 | ) 109 | } 110 | 111 | @Test fun testScanKeyword() { 112 | assertEquals( 113 | listOf( 114 | TokenType.KEYWORD, 115 | TokenType.WHITESPACE, 116 | TokenType.KEYWORD, 117 | TokenType.WHITESPACE, 118 | TokenType.KEYWORD, 119 | TokenType.WHITESPACE, 120 | TokenType.KEYWORD 121 | ), 122 | scanTypes(":foo-bar :|foo bar| ::foo-bar ::|foo bar|", "test-6") 123 | ) 124 | } 125 | 126 | @Test fun testScanNumber() { 127 | assertEquals( 128 | Token(TokenType.NUMBER, StreamLocation("test-7", 0, 0, 0), -13), 129 | scanOne("-13", "test-7") 130 | ) 131 | 132 | assertEquals( 133 | Token(TokenType.NUMBER, StreamLocation("test-7", 0, 0, 0), 123), 134 | scanOne("+123", "test-7") 135 | ) 136 | 137 | assertEquals( 138 | Token(TokenType.NUMBER, StreamLocation("test-7", 0, 0, 0), 6), 139 | scanOne("6", "test-7") 140 | ) 141 | } 142 | 143 | @Test fun testScanComment() { 144 | assertEquals( 145 | Token(TokenType.COMMENT, StreamLocation("test-8", 0, 0, 0), "; foo"), 146 | scanOne("; foo", "test-8") 147 | ) 148 | 149 | assertEquals( 150 | listOf( 151 | TokenType.SYMBOL, 152 | TokenType.WHITESPACE, 153 | TokenType.COMMENT, 154 | TokenType.NEWLINE 155 | ), 156 | scanTypes("foo ;bar\n", "test-9") 157 | ) 158 | } 159 | 160 | @Test fun testScanStr() { 161 | assertEquals( 162 | Token(TokenType.STRING, 163 | StreamLocation("test-10", 0, 0, 0), "foo\tbar baz\nqux\r\b\"'{}()#{}[]"), 164 | scanOne("\"foo\\tbar baz\nqux\r\b\\\"'{}()#{}[]\"", "test-10") 165 | ) 166 | } 167 | 168 | @Test fun testScanSet() { 169 | assertEquals( 170 | listOf( 171 | TokenType.HASH_LBRACE, 172 | TokenType.KEYWORD, 173 | TokenType.RBRACE 174 | ), 175 | scanTypes("#{:foo}", "test-11") 176 | ) 177 | } 178 | } 179 | --------------------------------------------------------------------------------