Files

269 lines
7.8 KiB
Swift

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)
}