code: private Type parseRecordType(EnclosingScope scope) {
  		int start = index;
  		match(LeftCurly);
  		ArrayList<Type.Field> types = new ArrayList<>();
  		Pair<Type, Identifier> p = parseMixedType(scope);
  		types.add(new Type.Field(p.getSecond(), p.getFirst()));
  		HashSet<Identifier> names = new HashSet<>();
  		names.add(p.getSecond());
  		// Now, we continue to parse any remaining fields.
  		boolean isOpen = false;
  		while (eventuallyMatch(RightCurly) == null) {
  			match(Comma);
  			if (tryAndMatch(true, DotDotDot) != null) {
  				// this signals an "open" record type
  				match(RightCurly);
  				isOpen = true;
  				break;
  			} else {
  				p = parseMixedType(scope);
  				Identifier id = p.getSecond();
  				if (names.contains(id)) {
  					syntaxError("duplicate record key", id);
  				}
  				names.add(id);
  				types.add(new Type.Field(id, p.getFirst()));
  			}
  		}
  		// Done
  		Tuple<Type.Field> fields = new Tuple<>(types);
  		return annotateSourceLocation(new Type.Record(isOpen, fields), start);
  	}
docs: Parse a set, map or record type, which are of the form:
  
  <pre>
  SetType ::= '{' Type '}'
  MapType ::= '{' Type "=>" Type '}'
  RecordType ::= '{' Type Identifier (',' Type Identifier)* [ ',' "..." ] '}'
  </pre>
  
  Disambiguating these three forms is relatively straightforward as all
  three must be terminated by a right curly brace. Therefore, after parsing
  the first Type, we simply check what follows. One complication is the
  potential for "mixed types" where the field name and type and intertwined
  (e.g. function read()->[byte]).
  
  @return