summaryrefslogtreecommitdiff
path: root/src/LispParser.kt
blob: 111739727223cf449ac68d8241d640259eb8f127 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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())
        }

        val digits = "1234567890"
        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<LispAst.LispNode>()
        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 == ":") {
            return parseAtom()
        }
        val ident = parseIdentifier()
        return LispAst.Reference(racer.span(start), ident)
    }


    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<LispAst.LispNode> {
        val l = mutableListOf<LispAst.LispNode>()
        racer.expect(opening, "Expected $opening")
        while (true) {
            racer.skipWhitespace()
            if (racer.tryConsume(closing)) {
                return l
            }
            l.add(parseNode())
        }
    }
}