mirror of
https://github.com/kanaka/mal.git
synced 2024-09-21 02:27:10 +03:00
4aa0ebdf47
Add a step1 test to make sure that implementations are properly throwing an error on unclosed strings. Fix 47 implementations and update the guide to note the correct behavior.
147 lines
3.3 KiB
Dart
147 lines
3.3 KiB
Dart
import 'types.dart';
|
|
|
|
final malRegExp = new RegExp(
|
|
r"""[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)""");
|
|
|
|
class Reader {
|
|
final List<String> tokens;
|
|
int _position = 0;
|
|
|
|
Reader(this.tokens);
|
|
|
|
String next() {
|
|
var token = peek();
|
|
_position++;
|
|
return token;
|
|
}
|
|
|
|
String peek() {
|
|
if (_position >= tokens.length) return null;
|
|
return tokens[_position];
|
|
}
|
|
}
|
|
|
|
class ParseException implements Exception {
|
|
final String message;
|
|
|
|
ParseException(this.message);
|
|
}
|
|
|
|
class NoInputException implements Exception {}
|
|
|
|
MalType read_str(String code) {
|
|
var tokens = tokenizer(code);
|
|
if (tokens.isEmpty) {
|
|
throw new NoInputException();
|
|
}
|
|
var reader = new Reader(tokens);
|
|
return read_form(reader);
|
|
}
|
|
|
|
List<String> tokenizer(String code) {
|
|
var matches = malRegExp.allMatches(code);
|
|
return matches
|
|
.map((m) => m.group(1))
|
|
.where((token) => token.isNotEmpty && !token.startsWith(';'))
|
|
.toList();
|
|
}
|
|
|
|
MalType read_form(Reader reader) {
|
|
const macros = const <String, String>{
|
|
"'": 'quote',
|
|
'`': 'quasiquote',
|
|
'~': 'unquote',
|
|
'~@': 'splice-unquote',
|
|
'@': 'deref',
|
|
'^': 'with-meta',
|
|
};
|
|
const sequenceStarters = const <String, String>{'(': ')', '[': ']', '{': '}'};
|
|
var token = reader.peek();
|
|
if (sequenceStarters.containsKey(token)) {
|
|
var elements = read_sequence(reader, token, sequenceStarters[token]);
|
|
if (token == '(') {
|
|
return new MalList(elements);
|
|
}
|
|
if (token == '[') {
|
|
return new MalVector(elements);
|
|
}
|
|
|
|
if (token == '{') {
|
|
return new MalHashMap.fromSequence(elements);
|
|
}
|
|
|
|
throw new StateError("Impossible!");
|
|
} else if (macros.containsKey(token)) {
|
|
var macro = new MalSymbol(macros[token]);
|
|
reader.next();
|
|
var form = read_form(reader);
|
|
if (token == '^') {
|
|
var meta = read_form(reader);
|
|
return new MalList([macro, meta, form]);
|
|
} else {
|
|
return new MalList([macro, form]);
|
|
}
|
|
} else {
|
|
return read_atom(reader);
|
|
}
|
|
}
|
|
|
|
List<MalType> read_sequence(Reader reader, String open, String close) {
|
|
// Consume opening token
|
|
var actualOpen = reader.next();
|
|
assert(actualOpen == open);
|
|
|
|
var elements = <MalType>[];
|
|
for (var token = reader.peek();; token = reader.peek()) {
|
|
if (token == null) {
|
|
throw new ParseException("expected '$close', got EOF");
|
|
}
|
|
if (token == close) break;
|
|
elements.add(read_form(reader));
|
|
}
|
|
|
|
var actualClose = reader.next();
|
|
assert(actualClose == close);
|
|
|
|
return elements;
|
|
}
|
|
|
|
MalType read_atom(Reader reader) {
|
|
var token = reader.next();
|
|
|
|
var intAtom = int.parse(token, onError: (_) => null);
|
|
if (intAtom != null) {
|
|
return new MalInt(intAtom);
|
|
}
|
|
|
|
if (token[0] == '"') {
|
|
if (token[token.length -1 ] != '"') {
|
|
throw new ParseException("expected '\"', got EOF");
|
|
}
|
|
var sanitizedToken = token
|
|
// remove surrounding quotes
|
|
.substring(1, token.length - 1)
|
|
.replaceAllMapped(new RegExp("\\\\(.)"),
|
|
(Match m) => m[1] == 'n' ? '\n' : m[1]);
|
|
return new MalString(sanitizedToken);
|
|
}
|
|
|
|
if (token[0] == ':') {
|
|
return new MalKeyword(token.substring(1));
|
|
}
|
|
|
|
if (token == 'nil') {
|
|
return new MalNil();
|
|
}
|
|
|
|
if (token == 'true') {
|
|
return new MalBool(true);
|
|
}
|
|
|
|
if (token == 'false') {
|
|
return new MalBool(false);
|
|
}
|
|
|
|
return new MalSymbol(token);
|
|
}
|