feat(reader): add s-expression reader and datum parser
This commit is contained in:
1 parent
549631fe99
commit
4a378c6fa7
1 file changed
+268
@@ -0,0 +1,268 @@
|
||||
public final class Reader {
|
||||
private let chars: [Character]
|
||||
private var pos: Int
|
||||
|
||||
public init(_ source: String) {
|
||||
self.chars = Array(source)
|
||||
self.pos = 0
|
||||
}
|
||||
|
||||
public func readProgram() throws -> [Value] {
|
||||
var forms: [Value] = []
|
||||
while true {
|
||||
skipAtmosphere()
|
||||
if pos >= chars.count { break }
|
||||
forms.append(try readDatum())
|
||||
}
|
||||
return forms
|
||||
}
|
||||
|
||||
public func readOne() throws -> Value? {
|
||||
skipAtmosphere()
|
||||
if pos >= chars.count { return nil }
|
||||
return try readDatum()
|
||||
}
|
||||
|
||||
private func peek() -> Character? {
|
||||
pos < chars.count ? chars[pos] : nil
|
||||
}
|
||||
|
||||
private func advance() -> Character {
|
||||
let c = chars[pos]
|
||||
pos += 1
|
||||
return c
|
||||
}
|
||||
|
||||
private func isDelimiter(_ c: Character) -> Bool {
|
||||
c == "(" || c == ")" || c == "\"" || c == ";" || c == "'"
|
||||
|| c == "`" || c == "," || c.isWhitespace
|
||||
}
|
||||
|
||||
private func skipAtmosphere() {
|
||||
while pos < chars.count {
|
||||
let c = chars[pos]
|
||||
if c.isWhitespace {
|
||||
pos += 1
|
||||
} else if c == ";" {
|
||||
while pos < chars.count && chars[pos] != "\n" { pos += 1 }
|
||||
} else if c == "#" && pos + 1 < chars.count && chars[pos + 1] == "|" {
|
||||
skipBlockComment()
|
||||
} else if c == "#" && pos + 1 < chars.count && chars[pos + 1] == ";" {
|
||||
pos += 2
|
||||
skipAtmosphere()
|
||||
_ = try? readDatum()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func skipBlockComment() {
|
||||
pos += 2
|
||||
var depth = 1
|
||||
while pos < chars.count && depth > 0 {
|
||||
if chars[pos] == "#" && pos + 1 < chars.count && chars[pos + 1] == "|" {
|
||||
depth += 1
|
||||
pos += 2
|
||||
} else if chars[pos] == "|" && pos + 1 < chars.count && chars[pos + 1] == "#" {
|
||||
depth -= 1
|
||||
pos += 2
|
||||
} else {
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readDatum() throws -> Value {
|
||||
skipAtmosphere()
|
||||
guard let c = peek() else {
|
||||
throw schemeError("unexpected end of input")
|
||||
}
|
||||
switch c {
|
||||
case "(", "[":
|
||||
return try readList(close: c == "(" ? ")" : "]")
|
||||
case ")", "]":
|
||||
throw schemeError("unexpected )")
|
||||
case "\"":
|
||||
return try readString()
|
||||
case "'":
|
||||
pos += 1
|
||||
return listOf("quote", try readDatum())
|
||||
case "`":
|
||||
pos += 1
|
||||
return listOf("quasiquote", try readDatum())
|
||||
case ",":
|
||||
pos += 1
|
||||
if peek() == "@" {
|
||||
pos += 1
|
||||
return listOf("unquote-splicing", try readDatum())
|
||||
}
|
||||
return listOf("unquote", try readDatum())
|
||||
case "#":
|
||||
return try readHash()
|
||||
default:
|
||||
return try readAtom()
|
||||
}
|
||||
}
|
||||
|
||||
private func listOf(_ head: String, _ rest: Value) -> Value {
|
||||
cons(.symbol(head), cons(rest, .null))
|
||||
}
|
||||
|
||||
private func readList(close: Character) throws -> Value {
|
||||
pos += 1
|
||||
var elements: [Value] = []
|
||||
var tail: Value = .null
|
||||
while true {
|
||||
skipAtmosphere()
|
||||
guard let c = peek() else {
|
||||
throw schemeError("unexpected end of input in list")
|
||||
}
|
||||
if c == close || c == ")" || c == "]" {
|
||||
pos += 1
|
||||
break
|
||||
}
|
||||
if c == "." && isDotSeparator() {
|
||||
pos += 1
|
||||
tail = try readDatum()
|
||||
skipAtmosphere()
|
||||
guard let cc = peek(), cc == close || cc == ")" || cc == "]" else {
|
||||
throw schemeError("malformed dotted list")
|
||||
}
|
||||
pos += 1
|
||||
break
|
||||
}
|
||||
elements.append(try readDatum())
|
||||
}
|
||||
return arrayToList(elements, tail: tail)
|
||||
}
|
||||
|
||||
private func isDotSeparator() -> Bool {
|
||||
guard chars[pos] == "." else { return false }
|
||||
let next = pos + 1
|
||||
if next >= chars.count { return true }
|
||||
return isDelimiter(chars[next])
|
||||
}
|
||||
|
||||
private func readString() throws -> Value {
|
||||
pos += 1
|
||||
var out = ""
|
||||
while pos < chars.count {
|
||||
let c = advance()
|
||||
if c == "\"" {
|
||||
return .string(out)
|
||||
}
|
||||
if c == "\\" {
|
||||
guard pos < chars.count else { break }
|
||||
let e = advance()
|
||||
switch e {
|
||||
case "n": out.append("\n")
|
||||
case "t": out.append("\t")
|
||||
case "r": out.append("\r")
|
||||
case "\\": out.append("\\")
|
||||
case "\"": out.append("\"")
|
||||
case "0": out.append("\u{0}")
|
||||
default: out.append(e)
|
||||
}
|
||||
} else {
|
||||
out.append(c)
|
||||
}
|
||||
}
|
||||
throw schemeError("unterminated string")
|
||||
}
|
||||
|
||||
private func readHash() throws -> Value {
|
||||
pos += 1
|
||||
guard let c = peek() else { throw schemeError("bad # token") }
|
||||
if c == "t" {
|
||||
consumeWord()
|
||||
return .bool(true)
|
||||
}
|
||||
if c == "f" {
|
||||
consumeWord()
|
||||
return .bool(false)
|
||||
}
|
||||
if c == "\\" {
|
||||
pos += 1
|
||||
return try readChar()
|
||||
}
|
||||
if c == "(" {
|
||||
let list = try readList(close: ")")
|
||||
return list
|
||||
}
|
||||
throw schemeError("unsupported # syntax: #\(c)")
|
||||
}
|
||||
|
||||
private func consumeWord() {
|
||||
while pos < chars.count && !isDelimiter(chars[pos]) {
|
||||
pos += 1
|
||||
}
|
||||
}
|
||||
|
||||
private func readChar() throws -> Value {
|
||||
guard pos < chars.count else { throw schemeError("bad char literal") }
|
||||
var name = ""
|
||||
name.append(advance())
|
||||
while pos < chars.count && !isDelimiter(chars[pos]) {
|
||||
name.append(advance())
|
||||
}
|
||||
if name.count == 1 {
|
||||
return .char(name.first!)
|
||||
}
|
||||
switch name.lowercased() {
|
||||
case "space": return .char(" ")
|
||||
case "newline": return .char("\n")
|
||||
case "tab": return .char("\t")
|
||||
case "return": return .char("\r")
|
||||
case "null", "nul": return .char("\u{0}")
|
||||
case "delete", "rubout": return .char("\u{7f}")
|
||||
default:
|
||||
return .char(name.first!)
|
||||
}
|
||||
}
|
||||
|
||||
private func readAtom() throws -> Value {
|
||||
var token = ""
|
||||
while pos < chars.count && !isDelimiter(chars[pos]) {
|
||||
token.append(advance())
|
||||
}
|
||||
return classifyAtom(token)
|
||||
}
|
||||
|
||||
private func classifyAtom(_ token: String) -> Value {
|
||||
if let n = parseInteger(token) {
|
||||
return .integer(n)
|
||||
}
|
||||
if let rat = parseRational(token) {
|
||||
return rat
|
||||
}
|
||||
if let d = parseReal(token) {
|
||||
return .real(d)
|
||||
}
|
||||
return .symbol(token)
|
||||
}
|
||||
}
|
||||
|
||||
func parseInteger(_ token: String) -> Int? {
|
||||
if token.isEmpty { return nil }
|
||||
if token == "+" || token == "-" || token == "..." || token == "." { return nil }
|
||||
return Int(token)
|
||||
}
|
||||
|
||||
func parseRational(_ token: String) -> Value? {
|
||||
guard let slash = token.firstIndex(of: "/") else { return nil }
|
||||
let numPart = String(token[..<slash])
|
||||
let denPart = String(token[token.index(after: slash)...])
|
||||
guard let n = Int(numPart), let d = Int(denPart), d != 0 else { return nil }
|
||||
return makeRational(n, d)
|
||||
}
|
||||
|
||||
func parseReal(_ token: String) -> Double? {
|
||||
if token.isEmpty { return nil }
|
||||
let first = token.first!
|
||||
let looksNumeric = first.isNumber || first == "+" || first == "-" || first == "."
|
||||
guard looksNumeric else { return nil }
|
||||
let hasDigit = token.contains(where: { $0.isNumber })
|
||||
guard hasDigit else { return nil }
|
||||
return Double(token)
|
||||
}
|
||||
Reference in new issue
Block a user