First pieces of Kernel text serialization

Combinator-based serialization of Kernel basic literals.

Change-Id: I6d08c80f47f233b178aa57ee6fc887ac91ed9e9c
Reviewed-on: https://dart-review.googlesource.com/c/87320
Commit-Queue: Kevin Millikin <kmillikin@google.com>
Reviewed-by: Daniel Hillerström <hillerstrom@google.com>
Reviewed-by: Dmitry Stefantsov <dmitryas@google.com>
diff --git a/pkg/kernel/lib/text/text_serializer.dart b/pkg/kernel/lib/text/text_serializer.dart
new file mode 100644
index 0000000..6b0d487
--- /dev/null
+++ b/pkg/kernel/lib/text/text_serializer.dart
@@ -0,0 +1,280 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library kernel.text_serializer;
+
+import '../ast.dart';
+import '../visitor.dart' show ExpressionVisitor;
+
+// ==== Serialize/deserialize combinators ====
+abstract class TextSerializer<T> {
+  const TextSerializer();
+
+  T readFrom(Iterator<Object> stream);
+  void writeTo(StringBuffer buffer, T object);
+
+  /// True if this serializer/deserializer writes/reads nothing.  This is true
+  /// for the serializer [Nothing] and also some serializers derived from it.
+  bool get isEmpty => false;
+}
+
+class Nothing extends TextSerializer<void> {
+  const Nothing();
+
+  void readFrom(Iterator<Object> stream) {}
+
+  void writeTo(StringBuffer buffer, void ignored) {}
+
+  bool get isEmpty => true;
+}
+
+// == Serializer/deserializers for basic Dart types
+class DartInt extends TextSerializer<int> {
+  const DartInt();
+
+  int readFrom(Iterator<Object> stream) {
+    if (stream.current is! String) {
+      throw StateError("expected an atom, found a list");
+    }
+    int result = int.parse(stream.current);
+    stream.moveNext();
+    return result;
+  }
+
+  void writeTo(StringBuffer buffer, int object) {
+    buffer.write(object);
+  }
+}
+
+class DartDouble extends TextSerializer<double> {
+  const DartDouble();
+
+  double readFrom(Iterator<Object> stream) {
+    if (stream.current is! String) {
+      throw StateError("expected an atom, found a list");
+    }
+    double result = double.parse(stream.current);
+    stream.moveNext();
+    return result;
+  }
+
+  void writeTo(StringBuffer buffer, double object) {
+    buffer.write(object);
+  }
+}
+
+class DartBool extends TextSerializer<bool> {
+  const DartBool();
+
+  bool readFrom(Iterator<Object> stream) {
+    if (stream.current is! String) {
+      throw StateError("expected an atom, found a list");
+    }
+    bool result;
+    if (stream.current == "true") {
+      result = true;
+    } else if (stream.current == "false") {
+      result = false;
+    } else {
+      throw StateError("expected 'true' or 'false', found '${stream.current}'");
+    }
+    stream.moveNext();
+    return result;
+  }
+
+  void writeTo(StringBuffer buffer, bool object) {
+    buffer.write(object ? 'true' : 'false');
+  }
+}
+
+// == Serializers for tagged (disjoint) unions.
+//
+// They require a function mapping serializables to a tag string.  This is
+// implemented by Tagger visitors.
+class ExpressionTagger extends ExpressionVisitor<String> {
+  const ExpressionTagger();
+
+  String visitIntLiteral(IntLiteral _) => "int";
+  String visitDoubleLiteral(DoubleLiteral _) => "double";
+  String visitBoolLiteral(BoolLiteral _) => "bool";
+  String visitNullLiteral(NullLiteral _) => "null";
+}
+
+// A tagged union of serializer/deserializers.
+class Case<T extends Expression> extends TextSerializer<T> {
+  final List<String> tags;
+  final List<TextSerializer<T>> serializers;
+
+  const Case(this.tags, this.serializers);
+
+  T readFrom(Iterator<Object> stream) {
+    if (stream.current is! Iterator) {
+      throw StateError("expected list, found atom");
+    }
+    Iterator nested = stream.current;
+    nested.moveNext();
+    if (nested.current is! String) {
+      throw StateError("expected atom, found list");
+    }
+    String tag = nested.current;
+    for (int i = 0; i < tags.length; ++i) {
+      if (tags[i] == tag) {
+        nested.moveNext();
+        T result = serializers[i].readFrom(nested);
+        if (stream.moveNext()) {
+          throw StateError("extra cruft in tagged '${tag}'");
+        }
+        return result;
+      }
+    }
+    throw StateError("unrecognized tag '${tag}'");
+  }
+
+  void writeTo(StringBuffer buffer, T object) {
+    String tag = object.accept(const ExpressionTagger());
+    for (int i = 0; i < tags.length; ++i) {
+      if (tags[i] == tag) {
+        buffer.write("(${tag}");
+        if (!serializers[i].isEmpty) {
+          buffer.write(" ");
+        }
+        serializers[i].writeTo(buffer, object);
+        buffer.write(")");
+        return;
+      }
+    }
+    throw StateError("unrecognized tag '${tag}");
+  }
+}
+
+// A serializer/deserializer that unwraps/wraps nodes before serialization and
+// after deserialization.
+class Wrapped<S, K> extends TextSerializer<K> {
+  final S Function(K) unwrap;
+  final K Function(S) wrap;
+  final TextSerializer<S> contents;
+
+  const Wrapped(this.unwrap, this.wrap, this.contents);
+
+  K readFrom(Iterator<Object> stream) {
+    return wrap(contents.readFrom(stream));
+  }
+
+  void writeTo(StringBuffer buffer, K object) {
+    contents.writeTo(buffer, unwrap(object));
+  }
+
+  bool get isEmpty => contents.isEmpty;
+}
+
+// S-expressions
+//
+// An S-expression is an atom or an S-list, an atom is a string that does not
+// contain the delimiters '(', ')', or ' ', and an S-list is a space delimited
+// sequence of S-expressions enclosed in parentheses:
+//
+// <S-expression> ::= <Atom>
+//                  | <S-list>
+// <S-list>       ::= '(' ')'
+//                  | '(' <S-expression> {' ' <S-expression>}* ')'
+//
+// We use an iterator to read S-expressions.  The iterator produces a stream
+// of atoms (strings) and nested iterators (S-lists).
+class TextIterator implements Iterator<Object /* String | TextIterator */ > {
+  static int space = ' '.codeUnitAt(0);
+  static int lparen = '('.codeUnitAt(0);
+  static int rparen = ')'.codeUnitAt(0);
+
+  final String input;
+  int index;
+
+  TextIterator(this.input, this.index);
+
+  // Consume spaces.
+  void skipWhitespace() {
+    while (index < input.length && input.codeUnitAt(index) == space) {
+      ++index;
+    }
+  }
+
+  // Consume the rest of a nested S-expression and the closing delimiter.
+  void skipToEndOfNested() {
+    if (current is TextIterator) {
+      TextIterator it = current;
+      while (it.moveNext());
+      index = it.index + 1;
+    }
+  }
+
+  void skipToEndOfAtom() {
+    do {
+      if (index >= input.length) return;
+      int codeUnit = input.codeUnitAt(index);
+      if (codeUnit == space || codeUnit == rparen) return;
+      ++index;
+    } while (true);
+  }
+
+  @override
+  Object current = null;
+
+  @override
+  bool moveNext() {
+    skipToEndOfNested();
+    skipWhitespace();
+    if (index >= input.length || input.codeUnitAt(index) == rparen) {
+      current = null;
+      return false;
+    }
+    if (input.codeUnitAt(index) == lparen) {
+      current = new TextIterator(input, index + 1);
+      return true;
+    }
+    int start = index;
+    skipToEndOfAtom();
+    current = input.substring(start, index);
+    return true;
+  }
+}
+
+// ==== Serializers for BasicLiterals
+const TextSerializer<BasicLiteral> basicLiteralSerializer = Case([
+  "int",
+  "double",
+  "bool",
+  "null"
+], [
+  intLiteralSerializer,
+  doubleLiteralSerializer,
+  boolLiteralSerializer,
+  nullLiteralSerializer
+]);
+
+const TextSerializer<IntLiteral> intLiteralSerializer =
+    Wrapped(unwrapIntLiteral, wrapIntLiteral, DartInt());
+
+int unwrapIntLiteral(IntLiteral literal) => literal.value;
+
+IntLiteral wrapIntLiteral(int value) => new IntLiteral(value);
+
+const TextSerializer<DoubleLiteral> doubleLiteralSerializer =
+    Wrapped(unwrapDoubleLiteral, wrapDoubleLiteral, DartDouble());
+
+double unwrapDoubleLiteral(DoubleLiteral literal) => literal.value;
+
+DoubleLiteral wrapDoubleLiteral(double value) => new DoubleLiteral(value);
+
+const TextSerializer<BoolLiteral> boolLiteralSerializer =
+    Wrapped(unwrapBoolLiteral, wrapBoolLiteral, DartBool());
+
+bool unwrapBoolLiteral(BoolLiteral literal) => literal.value;
+
+BoolLiteral wrapBoolLiteral(bool value) => new BoolLiteral(value);
+
+const TextSerializer<NullLiteral> nullLiteralSerializer =
+    Wrapped(unwrapNullLiteral, wrapNullLiteral, Nothing());
+
+void unwrapNullLiteral(NullLiteral literal) {}
+
+NullLiteral wrapNullLiteral(void ignored) => new NullLiteral();
diff --git a/pkg/kernel/test/text_serializer_test.dart b/pkg/kernel/test/text_serializer_test.dart
new file mode 100644
index 0000000..dbb5ece
--- /dev/null
+++ b/pkg/kernel/test/text_serializer_test.dart
@@ -0,0 +1,53 @@
+// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library kernel.text_serializer_test;
+
+import 'package:kernel/ast.dart';
+import 'package:kernel/text/text_serializer.dart';
+
+void main() {
+  test();
+}
+
+// Wrappers for testing.
+BasicLiteral readBasicLiteral(String input) {
+  TextIterator stream = new TextIterator(input, 0);
+  stream.moveNext();
+  BasicLiteral result = basicLiteralSerializer.readFrom(stream);
+  if (stream.moveNext()) {
+    throw StateError("extra cruft in basic literal");
+  }
+  return result;
+}
+
+String writeBasicLiteral(BasicLiteral literal) {
+  StringBuffer buffer = new StringBuffer();
+  basicLiteralSerializer.writeTo(buffer, literal);
+  return buffer.toString();
+}
+
+void test() {
+  List<String> failures = [];
+  List<String> tests = [
+    "(int 42)",
+    "(int 0)",
+    "(int -1001)",
+    "(double 3.14159)",
+    "(bool true)",
+    "(bool false)",
+    "(null)"
+  ];
+  for (var test in tests) {
+    var literal = readBasicLiteral(test);
+    var output = writeBasicLiteral(literal);
+    if (output != test) {
+      failures.add('* input "${test}" gave output "${output}"');
+    }
+  }
+  if (failures.isNotEmpty) {
+    print('Round trip failures:');
+    failures.forEach(print);
+    throw StateError('Round trip failures');
+  }
+}