From 4a378c6fa7b16c2d2940c969333276b7dfbf0e95 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 20:00:00 +0200 Subject: [PATCH] feat(reader): add s-expression reader and datum parser --- Sources/KiteCore/Reader.swift | 268 ++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 Sources/KiteCore/Reader.swift diff --git a/Sources/KiteCore/Reader.swift b/Sources/KiteCore/Reader.swift new file mode 100644 index 0000000..9ea9829 --- /dev/null +++ b/Sources/KiteCore/Reader.swift @@ -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[.. 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) +}