diff --git a/src/main/kotlin/blitz/Either.kt b/src/main/kotlin/blitz/Either.kt
index dbe9b8c..42be82f 100644
--- a/src/main/kotlin/blitz/Either.kt
+++ b/src/main/kotlin/blitz/Either.kt
@@ -62,6 +62,12 @@ class Either private constructor(
fun Either.flatten(): R where A: R, B: R =
getAOrNull() ?: getB()
+fun Either>.partiallyFlatten(): Either =
+ mapA> { Either.ofA(it) }.flatten()
+
+fun Either, B>.partiallyFlatten(): Either =
+ mapB> { Either.ofB(it) }.flatten()
+
fun Either>.mapBA(fn: (BA) -> BAN): Either> =
mapB { it.mapA(fn) }
diff --git a/src/main/kotlin/blitz/Obj.kt b/src/main/kotlin/blitz/Obj.kt
index 425e742..5321fb5 100644
--- a/src/main/kotlin/blitz/Obj.kt
+++ b/src/main/kotlin/blitz/Obj.kt
@@ -30,6 +30,10 @@ fun Obj.map(transform: (I) -> O): Obj =
interface MutObj {
var v: T
+ inline fun modify(fn: (T) -> T) {
+ v = fn(v)
+ }
+
companion object {
fun of(v: T): MutObj =
object : MutObj {
diff --git a/src/main/kotlin/blitz/parse/comb2/Parser.kt b/src/main/kotlin/blitz/parse/comb2/Parser.kt
new file mode 100644
index 0000000..0a2178d
--- /dev/null
+++ b/src/main/kotlin/blitz/parse/comb2/Parser.kt
@@ -0,0 +1,146 @@
+package blitz.parse.comb2
+
+import blitz.Either
+import blitz.partiallyFlatten
+
+data class ParseCtx(
+ val input: List,
+ var idx: Int
+) {
+ fun loadFrom(old: ParseCtx) {
+ idx = old.idx
+ }
+}
+
+data class ParseError(
+ val loc: Int,
+ val message: String?,
+)
+
+typealias ParseResult = Either>
+typealias Parser = (ParseCtx) -> ParseResult
+
+inline fun Parser.mapValue(crossinline fn: (M) -> O): Parser =
+ { invoke(it).mapA { fn(it) } }
+
+inline fun Parser.mapErrors(crossinline fn: (List) -> List): Parser =
+ { invoke(it).mapB { fn(it) } }
+
+fun Parser.then(other: Parser): Parser> =
+ { ctx ->
+ invoke(ctx).mapA { first ->
+ other.invoke(ctx)
+ .mapA { first to it }
+ }.partiallyFlatten()
+ }
+
+fun Parser.thenIgnore(other: Parser): Parser =
+ { ctx ->
+ invoke(ctx).mapA { first ->
+ other.invoke(ctx)
+ .mapA { first }
+ }.partiallyFlatten()
+ }
+
+fun Parser.orElse(other: Parser): Parser =
+ {
+ val old = it.copy()
+ this(it).mapB { err ->
+ it.loadFrom(old)
+ other.invoke(it)
+ .mapB { err + it }
+ }.partiallyFlatten()
+ }
+
+fun Parser.repeated(): Parser> =
+ { ctx ->
+ val out = mutableListOf()
+ var ret: List? = null
+ while (true) {
+ val old = ctx.copy()
+ val t = invoke(ctx)
+ if (t.isA) {
+ out += t.getA()
+ } else {
+ ctx.loadFrom(old)
+ ret = t.getB()
+ break
+ }
+ }
+ if (ret == null) {
+ Either.ofA(out)
+ } else Either.ofB(ret)
+ }
+
+fun Parser.delimitedBy(delim: Parser): Parser> =
+ thenIgnore(delim)
+ .repeated()
+ .then(this)
+ .mapValue { (a, b) -> a + b }
+ .orElse(value(listOf()))
+
+inline fun Parser.verifyValue(crossinline verif: (O) -> String?): Parser =
+ { ctx ->
+ invoke(ctx).mapA> {
+ verif(it)?.let { Either.ofB(listOf(ParseError(ctx.idx, it))) }
+ ?: Either.ofA(it)
+ }.partiallyFlatten()
+ }
+
+inline fun Parser>.verifyValueWithSpan(crossinline fn: (O) -> String?): Parser =
+ { ctx ->
+ invoke(ctx).mapA> { (span, v) ->
+ fn(v)?.let { Either.ofB(listOf(ParseError(span.first, it))) }
+ ?: Either.ofA(v)
+ }.partiallyFlatten()
+ }
+
+fun Parser.errIfNull(msg: String = "parser value was null internally"): Parser =
+ verifyValue { if (it == null) msg else null }
+ .mapValue { it!! }
+
+inline fun location(crossinline fn: (Int) -> O): Parser =
+ { Either.ofA(fn(it.idx)) }
+
+fun location(): Parser =
+ location { it }
+
+fun withSpan(p: Parser): Parser> =
+ location()
+ .then(p)
+ .then(location())
+ .mapValue { (beginAndV, end) ->
+ (beginAndV.first..end) to beginAndV.second
+ }
+
+fun value(value: O): Parser =
+ { Either.ofA(value) }
+
+fun whitespaces(): Parser =
+ regex("\\s+")
+
+fun just(want: I): Parser =
+ { ctx ->
+ val i = ctx.input[ctx.idx ++]
+ if (i == want) Either.ofA(i)
+ else Either.ofB(listOf(ParseError(ctx.idx - 1, "expected $want")))
+ }
+
+/** group values 0 is the entire match */
+fun regex(pattern: Regex, fn: (groups: MatchGroupCollection) -> O): Parser =
+ { ctx ->
+ pattern.matchAt(ctx.input.toString(), ctx.idx)?.let {
+ ctx.idx = it.range.last + 1
+ Either.ofA(fn(it.groups))
+ } ?: Either.ofB(listOf(
+ ParseError(ctx.idx, "regular expression \"$pattern\" does not apply")
+ ))
+ }
+
+fun regex(pattern: Regex) = regex(pattern) { it[0]!!.value }
+
+/** group values 0 is the entire match */
+fun regex(pattern: String, fn: (groups: MatchGroupCollection) -> O): Parser =
+ regex(Regex(pattern), fn)
+
+fun regex(pattern: String) = regex(pattern) { it[0]!!.value }
\ No newline at end of file