package moe.nea.lisp import java.io.File class LispParser private constructor(filename: String, string: String) { val racer = StringRacer(filename, string) val program = parseProgram() companion object { fun parse(filename: String, string: String): LispAst.Program { return LispParser(filename, string).program } fun parse(file: File): LispAst.Program { return parse(file.absolutePath, file.readText()) } fun isValidIdentifier(name: String): Boolean { return name.isNotEmpty() && name.first() in validStartingIdentifiers && name.all { it in validIdentifiers } } val digits = "1234567890" val hexDigits = digits + "abcdefABCDEF" val alphabet = "abcdefghijklmnopqrstuvwxyz" val validStartingIdentifiers = "-.#+*'!$%&/=?_~|^" + alphabet + alphabet.uppercase() val validIdentifiers = validStartingIdentifiers + digits val parenthesisMatches = mapOf( "(" to ")", "[" to "]", "{" to "}", "<" to ">", ) } fun parseProgram(): LispAst.Program { val start = racer.idx val nodes = mutableListOf() while (true) { racer.skipWhitespace() if (racer.finished()) break nodes.add(parseNode()) } return LispAst.Program(racer.span(start), nodes) } private fun parseNode(): LispAst.LispNode { val start = racer.idx val paren = racer.peekReq(1) ?: racer.error("Expected start of expression") val matchingParen = parenthesisMatches[paren] if (matchingParen != null) { val paren = parseParenthesis(paren, matchingParen) return LispAst.Parenthesis(racer.span(start), paren) } if (paren == "\"") { return parseString() } if (paren in digits) { return parseNumber() } if (paren == ":") { return parseAtom() } val ident = parseIdentifier() return LispAst.Reference(racer.span(start), ident) } fun parseNumber(): LispAst.NumberLiteral { val start = racer.idx racer.pushState() val number = racer.consumeWhile { it.last().let { it in hexDigits || it == '.' || it == 'x' } } var double = number.toDoubleOrNull() if (number.startsWith("0x")) { double = number.substring(2).toLong(16).toDouble() } if (double == null) { racer.popState() racer.error("Could not parse number $number") } racer.discardState() return LispAst.NumberLiteral(racer.span(start), double) } fun parseAtom(): LispAst.Atom { val start = racer.idx racer.expect(":", "Expected : at start of atom") val ident = parseIdentifier() return LispAst.Atom(racer.span(start), ident) } fun parseIdentifier(): String { return racer.consumeWhile { it.first() in validStartingIdentifiers && it.last() in validIdentifiers }.also { if (it.isEmpty()) racer.error("Expected identifier") } } fun parseString(): LispAst.StringLiteral { val start = racer.idx val quoted = parseQuotedString() return LispAst.StringLiteral(racer.span(start), quoted) } fun parseQuotedString(): String { racer.expect("\"", "Expected '\"' at string start") val sb = StringBuilder() while (true) { when (val peek = racer.consumeCountReq(1)) { "\"" -> break "\\" -> { val escaped = racer.consumeCountReq(1) ?: racer.error("Unfinished backslash escape") if (escaped != "\"" && escaped != "\\") { // Surprisingly i couldn't find unicode escapes to be generated by the original minecraft 1.8.9 implementation racer.idx-- racer.error("Invalid backslash escape '$escaped'") } sb.append(escaped) } null -> racer.error("Unfinished string") else -> { sb.append(peek) } } } return sb.toString() } private fun parseParenthesis(opening: String, closing: String): List { val l = mutableListOf() racer.expect(opening, "Expected $opening") while (true) { racer.skipWhitespace() if (racer.tryConsume(closing)) { return l } l.add(parseNode()) } } }