Merge pull request #387 from asarhaddon/test-macroexpand-no-quasiquote
[jackhill/mal.git] / skew / reader.sk
1 class Reader {
2 const tokens List<string>
3 var position = 0
4
5 def peek string {
6 if position >= tokens.count {
7 return null
8 }
9 return tokens[position]
10 }
11
12 def next string {
13 const token = peek
14 position++
15 return token
16 }
17 }
18
19 def tokenize(str string) List<string> {
20 var re = RegExp.new("[\\s,]*(~@|[\\[\\]{}()'`~^@]|\"(?:\\\\.|[^\\\\\"])*\"?|;.*|[^\\s\\[\\]{}('\"`,;)]*)", "g")
21 var tokens List<string> = []
22 var match string
23 while (match = re.exec(str)[1]) != "" {
24 if match[0] == ';' {
25 continue
26 }
27 tokens.append(match)
28 }
29 return tokens
30 }
31
32 def unescape(s string) string {
33 return s.replaceAll("\\\\", "\x01").replaceAll("\\\"", "\"").replaceAll("\\n", "\n").replaceAll("\x01", "\\")
34 }
35
36 def read_atom(rdr Reader) MalVal {
37 const token = rdr.peek
38 if token == "nil" {
39 rdr.next
40 return gNil
41 }
42 if token == "true" {
43 rdr.next
44 return gTrue
45 }
46 if token == "false" {
47 rdr.next
48 return gFalse
49 }
50 switch token[0] {
51 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
52 case '-' {
53 if token.count <= 1 { return MalSymbol.new(rdr.next) }
54 switch token[1] {
55 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
56 default { return MalSymbol.new(rdr.next) }
57 }
58 }
59 case '"' {
60 const s = rdr.next
61 if s[s.count - 1] == '"' {
62 return MalString.new(unescape(s.slice(1, s.count - 1)))
63 } else {
64 throw MalError.new("expected '\"', got EOF")
65 }
66 }
67 case ':' { return MalKeyword.new(rdr.next.slice(1)) }
68 default { return MalSymbol.new(rdr.next) }
69 }
70 }
71
72 def read_sequence(rdr Reader, open string, close string) List<MalVal> {
73 if rdr.next != open {
74 throw MalError.new("expected '" + open + "'")
75 }
76 var token string
77 var items List<MalVal> = []
78 while (token = rdr.peek) != close {
79 if token == null {
80 throw MalError.new("expected '" + close + "', got EOF")
81 }
82 items.append(read_form(rdr))
83 }
84 rdr.next # consume the close paren/bracket/brace
85 return items
86 }
87
88 def read_list(rdr Reader) MalList {
89 return MalList.new(read_sequence(rdr, "(", ")"))
90 }
91
92 def read_vector(rdr Reader) MalVector {
93 return MalVector.new(read_sequence(rdr, "[", "]"))
94 }
95
96 def read_hash_map(rdr Reader) MalHashMap {
97 return MalHashMap.fromList(read_sequence(rdr, "{", "}"))
98 }
99
100 def reader_macro(rdr Reader, symbol_name string) MalVal {
101 rdr.next
102 return MalList.new([MalSymbol.new(symbol_name), read_form(rdr)])
103 }
104
105 def read_form(rdr Reader) MalVal {
106 switch rdr.peek[0] {
107 case '\'' { return reader_macro(rdr, "quote") }
108 case '`' { return reader_macro(rdr, "quasiquote") }
109 case '~' {
110 if rdr.peek == "~" { return reader_macro(rdr, "unquote") }
111 else if rdr.peek == "~@" { return reader_macro(rdr, "splice-unquote") }
112 else { return read_atom(rdr) }
113 }
114 case '^' {
115 rdr.next
116 const meta = read_form(rdr)
117 return MalList.new([MalSymbol.new("with-meta"), read_form(rdr), meta])
118 }
119 case '@' { return reader_macro(rdr, "deref") }
120 case ')' { throw MalError.new("unexpected ')'") }
121 case '(' { return read_list(rdr) }
122 case ']' { throw MalError.new("unexpected ']'") }
123 case '[' { return read_vector(rdr) }
124 case '}' { throw MalError.new("unexpected '}'") }
125 case '{' { return read_hash_map(rdr) }
126 default { return read_atom(rdr) }
127 }
128 }
129
130 def read_str(str string) MalVal {
131 const tokens = tokenize(str)
132 if tokens.isEmpty { return null }
133 var rdr = Reader.new(tokens)
134 return read_form(rdr)
135 }
136
137 @import {
138 const RegExp dynamic
139 }