gensym: hide the counter in an environment, define inc in stepA.
[jackhill/mal.git] / dart / reader.dart
CommitLineData
3934e3f8
HT
1import 'types.dart';
2
3final malRegExp = new RegExp(
4aa0ebdf 4 r"""[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)""");
3934e3f8
HT
5
6class Reader {
7 final List<String> tokens;
8 int _position = 0;
9
10 Reader(this.tokens);
11
12 String next() {
13 var token = peek();
14 _position++;
15 return token;
16 }
17
18 String peek() {
19 if (_position >= tokens.length) return null;
20 return tokens[_position];
21 }
22}
23
24class ParseException implements Exception {
25 final String message;
26
27 ParseException(this.message);
28}
29
30class NoInputException implements Exception {}
31
32MalType read_str(String code) {
33 var tokens = tokenizer(code);
34 if (tokens.isEmpty) {
35 throw new NoInputException();
36 }
37 var reader = new Reader(tokens);
38 return read_form(reader);
39}
40
41List<String> tokenizer(String code) {
42 var matches = malRegExp.allMatches(code);
43 return matches
44 .map((m) => m.group(1))
45 .where((token) => token.isNotEmpty && !token.startsWith(';'))
46 .toList();
47}
48
49MalType read_form(Reader reader) {
50 const macros = const <String, String>{
51 "'": 'quote',
52 '`': 'quasiquote',
53 '~': 'unquote',
54 '~@': 'splice-unquote',
55 '@': 'deref',
56 '^': 'with-meta',
57 };
58 const sequenceStarters = const <String, String>{'(': ')', '[': ']', '{': '}'};
59 var token = reader.peek();
60 if (sequenceStarters.containsKey(token)) {
61 var elements = read_sequence(reader, token, sequenceStarters[token]);
62 if (token == '(') {
63 return new MalList(elements);
64 }
65 if (token == '[') {
66 return new MalVector(elements);
67 }
68
69 if (token == '{') {
70 return new MalHashMap.fromSequence(elements);
71 }
72
73 throw new StateError("Impossible!");
74 } else if (macros.containsKey(token)) {
75 var macro = new MalSymbol(macros[token]);
76 reader.next();
77 var form = read_form(reader);
78 if (token == '^') {
79 var meta = read_form(reader);
80 return new MalList([macro, meta, form]);
81 } else {
82 return new MalList([macro, form]);
83 }
84 } else {
85 return read_atom(reader);
86 }
87}
88
89List<MalType> read_sequence(Reader reader, String open, String close) {
90 // Consume opening token
91 var actualOpen = reader.next();
92 assert(actualOpen == open);
93
94 var elements = <MalType>[];
95 for (var token = reader.peek();; token = reader.peek()) {
96 if (token == null) {
97 throw new ParseException("expected '$close', got EOF");
98 }
99 if (token == close) break;
100 elements.add(read_form(reader));
101 }
102
103 var actualClose = reader.next();
104 assert(actualClose == close);
105
106 return elements;
107}
108
109MalType read_atom(Reader reader) {
110 var token = reader.next();
111
112 var intAtom = int.parse(token, onError: (_) => null);
113 if (intAtom != null) {
114 return new MalInt(intAtom);
115 }
116
117 if (token[0] == '"') {
4aa0ebdf
JM
118 if (token[token.length -1 ] != '"') {
119 throw new ParseException("expected '\"', got EOF");
120 }
3934e3f8
HT
121 var sanitizedToken = token
122 // remove surrounding quotes
123 .substring(1, token.length - 1)
a7d9ace3
JM
124 .replaceAllMapped(new RegExp("\\\\(.)"),
125 (Match m) => m[1] == 'n' ? '\n' : m[1]);
3934e3f8
HT
126 return new MalString(sanitizedToken);
127 }
128
129 if (token[0] == ':') {
130 return new MalKeyword(token.substring(1));
131 }
132
133 if (token == 'nil') {
134 return new MalNil();
135 }
136
137 if (token == 'true') {
138 return new MalBool(true);
139 }
140
141 if (token == 'false') {
142 return new MalBool(false);
143 }
144
145 return new MalSymbol(token);
146}