Skip to content

Instantly share code, notes, and snippets.

@thacuber2a03
Created June 2, 2025 04:55
Show Gist options
  • Select an option

  • Save thacuber2a03/449ddb0fb7556b5aeaa538cb5cf5d030 to your computer and use it in GitHub Desktop.

Select an option

Save thacuber2a03/449ddb0fb7556b5aeaa538cb5cf5d030 to your computer and use it in GitHub Desktop.
a parser combinator library written in Wren
class Error {
construct new(message) {
_message = message
}
toString { "[error] %(_message)" }
}
/* abstract */ class Input {
peek(n) { Fiber.abort("%(type) does not implement peek(_)") }
get(n) { Fiber.abort("%(type) does not implement get(_)") }
pos { _pos }
}
class StringInput is Input {
construct new(s) {
_s = s.codePoints.toList
_pos = 0
}
peek(n) {
var p = _pos + n
if (p > _s.count) return ""
return _s[_pos...p]
.map {|x| String.fromCodePoint(x) }
.toList
.join("")
}
get(n) {
var s = peek(n)
_pos = _pos + n
return s
}
default { "" }
}
class ListInput is Input {
construct new(l) {
_l = l
_pos = 0
}
peek(n) { _l[_pos...(_pos+n)] }
get(n) {
var l = peek(n)
_pos = _pos + n
return l
}
default { [] }
}
class Parser is Sequence {
construct new(f) { _f = f }
go(i) { _f.call(Parse.makeInput(i)) }
then(other) {Parser.new {|i|
var r1 = go(i)
if (r1 is Error) return r1
var r2 = other.go(i)
if (r2 is Error) return r2
return [r1, r2]
}}
thenIgnore(other) {Parser.new {|i| then(other).map {|x| x[0]} .go(i) }}
ignoreThen(other) {Parser.new {|i| then(other).map {|x| x[1]} .go(i) }}
andIgnore {Parser.new {|i| this.map {|x| i.default }.go(i) }}
or(other) {Parser.new {|i|
var r1 = go(i)
if (!(r1 is Error)) return r1
var r2 = other.go(i)
if (!(r2 is Error)) return r2
return Error.new("no parser matched")
}}
map(f) {Parser.new {|i|
var r = go(i)
if (r is Error) return r
return f.call(r)
}}
anyTimes {Parser.new {|i| this.atLeastOnce.or(Parse.none).go(i) }}
atLeastOnce {Parser.new {|i|
var strin = i is StringInput
var res = strin ? "" : []
while (true) {
var r = go(i)
if (r is Error) break
if (strin) res = res + r else res.add(r)
}
if (res.count == 0) return Error.new("expected at least one")
return res
}}
maybe {Parser.new {|i| this.or(Parse.none).go(i) }}
delimitedBy(start, end) {Parser.new {|i| start.ignoreThen(this).thenIgnore(end).go(i) }}
paddedBy(pad) {Parser.new {|i| delimitedBy(pad, pad).go(i) }}
}
class Parse {
static makeInput(i) {
if (i is Input) return i
if (i is String) return StringInput.new(i)
if (i is List) return ListInput.new(i)
Fiber.abort("invalid input %(i)")
}
static just(e) {Parser.new {|i|
var s = i.peek(e.count)
if (s is Error) return s
if (s == e) return i.get(e.count)
if (s.count == 0) s = "end of input"
return Error.new("expected %(e), got %(s)")
}}
static anyOf(cs) {Parser.new {|i|
var s = i.peek(1)
if (s is Error) return s
for (c in cs) if (s == c) return i.get(1)
return Error.new("couldn't match any of %(cs)")
}}
static digit {Parser.new {|i| anyOf("0123456789").go(i) }}
static alpha {Parser.new {|i| anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ").go(i) }}
static fail {Parser.new {|i| Error.new("fail") }}
static none {Parser.new {|i| i.default }}
static any {Parser.new {|i| i.get(1) }}
static eof {Parser.new {|i|
var e = i.peek(1)
if (e.count != 0) return Error.new("expected end of input, got %(e)")
}}
static ifMatches(f) {Parser.new {|i|
var r = i.peek(1)
if (r is Error) return r
if (!f.call(r)) return Error.new("%(r) does not match the predicate")
return i.get(1)
}}
static whitespace { Parse.ifMatches {|x| x.trim().isEmpty } }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment