package scotch.symbol; import static java.lang.Character.isUpperCase; import static java.util.Arrays.asList; import static java.util.regex.Pattern.compile; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.apache.commons.lang.StringEscapeUtils.escapeJava; import static scotch.util.Pair.pair; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import scotch.util.Pair; public abstract class Symbol implements Comparable<Symbol> { private static final Pattern tuplePattern = compile("\\(,*\\)"); private static final String moduleSubPattern = "[A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*"; private static final String memberSubPattern = "(\\[\\]|\\((?:[^\\)]+)\\)|(?:[^\\.]+)|\\(,*\\))"; private static final Pattern qualifiedPattern = compile("^(\\$?" + moduleSubPattern + ")\\." + memberSubPattern + "$"); private static final Pattern containsSymbolsPattern = compile("\\W"); private static final Set<String> javaWords = ImmutableSet.of( "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "void", "volatile", "while" ); private static final Map<Symbol, String> javaTypeMap = ImmutableMap.<Symbol, String>builder() .put(qualified("scotch.data.int", "Int"), "java/lang/Integer") .put(qualified("scotch.data.string", "String"), "java/lang/String") .put(qualified("scotch.data.char.Char", "Char"), "java/lang/Character") .put(qualified("scotch.data.bool", "Bool"), "java/lang/Boolean") .put(qualified("scotch.data.double", "Double"), "java/lang/Double") .build(); private static final Map<Character, String> javaSymbolMap = ImmutableMap.<Character, String>builder() .put('~', "$twiddle") .put('|', "$or") .put('!', "$bang") .put('$', "$bux") .put('%', "$chunk") .put('^', "$point") .put('&', "$ditto") .put('*', "$splat") .put('-', "$down") .put('=', "$same") .put('+', "$up") .put('/', "$split") .put('?', "$wat") .put('<', "$left") .put('>', "$right") .put('.', "$dot") .put(':', "$doot") .put('#', "$") .build(); public static String getPackageName(String moduleName) { return getPackageFor(moduleName, "."); } public static String getPackagePath(String moduleName) { return getPackageFor(moduleName, "/"); } public static String moduleClass(String symbol) { return getPackagePath(symbol) + "/$$Module"; } public static String normalizeQualified(String moduleName, String memberName) { if (memberName.matches("^\\d$")) { return moduleName + ".(#" + memberName + ")"; } else if ("[]".equals(memberName) || tuplePattern.matcher(memberName).find() || !containsSymbolsPattern.matcher(memberName).find()) { return moduleName + '.' + memberName; } else { return moduleName + ".(" + memberName + ")"; } } public static String normalizeQualified(String moduleName, List<String> memberNames) { if (memberNames.size() == 1) { return normalizeQualified(moduleName, memberNames.get(0)); } else { return moduleName + ".(" + join(memberNames) + ")"; } } public static Symbol qualified(String moduleName, String memberName) { return qualified(moduleName, asList(memberName.split("#"))); } public static Symbol qualified(String moduleName, List<String> memberNames) { return new QualifiedSymbol(moduleName, memberNames); } public static Pair<Optional<String>, String> splitQualified(String name) { Matcher matcher = qualifiedPattern.matcher(name); if (matcher.matches()) { if (tuplePattern.matcher(matcher.group(2)).matches()) { return pair(Optional.of(matcher.group(1)), matcher.group(2)); } else { return pair(Optional.of(matcher.group(1)), matcher.group(2).replaceAll("[\\(\\)]", "")); } } else { return pair(Optional.empty(), name); } } public static Symbol symbol(String name) { return splitQualified(name).into( (optionalModuleName, memberName) -> optionalModuleName .map(moduleName -> qualified(moduleName, memberName)) .orElseGet(() -> unqualified(memberName)) ); } public static String toJavaName(String memberName) { return memberName.chars() .mapToObj(i -> javaSymbolMap.getOrDefault((char) i, String.valueOf(Character.toChars(i)))) .collect(joining()); } public static Symbol unqualified(String memberName) { return unqualified(asList(memberName.split("#"))); } public static Symbol unqualified(List<String> memberNames) { return new UnqualifiedSymbol(memberNames); } private static Integer compareMemberNames(Symbol left, Symbol right) { int result = 0; Iterator<String> thisIterator = left.getMemberNames().iterator(); Iterator<String> thatIterator = right.getMemberNames().iterator(); while (thisIterator.hasNext()) { if (!thatIterator.hasNext()) { return 1; } result = thisIterator.next().compareTo(thatIterator.next()); if (result != 0) { return result; } } return result; } private static String getPackageFor(String moduleName, String delimiter) { return Arrays.stream(moduleName.split("\\.")) .map(section -> javaWords.contains(section) ? section + "_" : section) .collect(joining(delimiter)); } private static String join(List<String> memberNames) { if (memberNames.get(0).matches("^\\d.*$")) { return "#" + join_(memberNames); } else { return join_(memberNames); } } private static String join_(List<String> memberNames) { return memberNames.stream().collect(joining("#")); } private static List<String> normalize(List<String> memberNames) { return ImmutableList.copyOf(memberNames.stream() .filter(name -> !name.isEmpty()) .map(name -> name.startsWith("#") ? name.substring(1) : name) .collect(toList())); } private Symbol() { // intentionally empty } public abstract <T> T accept(SymbolVisitor<T> visitor); @Override public abstract int compareTo(Symbol other); @Override public abstract boolean equals(Object o); public abstract String getCanonicalName(); public abstract String getClassName(); public String getClassNameAsChildOf(Symbol dataTypeDescriptor) { return dataTypeDescriptor.getClassName() + "$" + getSimpleName(); } public String getMemberName() { String memberName = getMemberNames().stream().collect(joining("#")); if (memberName.matches("^\\d+.*")) { return "#" + memberName; } else { return memberName; } } public abstract List<String> getMemberNames(); public String getMethodName() { return toJavaName(getSimpleName()); } public abstract String getModuleClass(); public String getSimpleName() { String simpleName = getMemberNames().get(getMemberNames().size() - 1); if (simpleName.matches("^\\d+.*")) { return "#" + simpleName; } else { return simpleName; } } @Override public abstract int hashCode(); public boolean isConstructorName() { return isSumName() || ":".equals(getSimpleName()) || "[]".equals(getSimpleName()); } public boolean isList() { return "[]".equals(getMemberName()); } public boolean isSumName() { return isUpperCase(getMemberName().charAt(0)) || isList() || isTuple(); } public boolean isTuple() { return tuplePattern.matcher(getMemberName()).find(); } public abstract Symbol map(Function<QualifiedSymbol, Symbol> function); public Symbol nest(List<String> parentNames) { List<String> memberNames = new ArrayList<>(); memberNames.addAll(parentNames); memberNames.add(getSimpleName()); return withMemberNames(memberNames); } public abstract Symbol qualifyWith(String moduleName); public String quote() { return "'" + escapeJava(getCanonicalName()) + "'"; } @Override public String toString() { return getCanonicalName(); } public abstract Symbol unqualify(); protected abstract Symbol withMemberNames(List<String> memberNames); public interface SymbolVisitor<T> { T visit(QualifiedSymbol symbol); T visit(UnqualifiedSymbol symbol); } public static class QualifiedSymbol extends Symbol { private final String moduleName; private final List<String> memberNames; private QualifiedSymbol(String moduleName, List<String> memberNames) { this.moduleName = moduleName; this.memberNames = normalize(memberNames); } @Override public <T> T accept(SymbolVisitor<T> visitor) { return visitor.visit(this); } @Override public int compareTo(Symbol other) { return other.accept(new SymbolVisitor<Integer>() { @Override public Integer visit(QualifiedSymbol symbol) { int result = moduleName.compareTo(symbol.moduleName); if (result != 0) { return result; } else { return compareMemberNames(QualifiedSymbol.this, other); } } @Override public Integer visit(UnqualifiedSymbol symbol) { return 1; } }); } @Override public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof QualifiedSymbol) { QualifiedSymbol other = (QualifiedSymbol) o; return Objects.equals(moduleName, other.moduleName) && Objects.equals(memberNames, other.memberNames); } else { return false; } } @Override public String getCanonicalName() { return normalizeQualified(moduleName, memberNames); } @Override public String getClassName() { return Optional.ofNullable(javaTypeMap.get(this)) .orElseGet(() -> getPackagePath() + "/" + toJavaName(getMemberName())); } @Override public List<String> getMemberNames() { return memberNames; } @Override public String getModuleClass() { return Optional.ofNullable(javaTypeMap.get(this)) .orElseGet(() -> getPackagePath() + "/$$Module"); } public String getModuleName() { return moduleName; } @Override public int hashCode() { return Objects.hash(moduleName, memberNames); } @Override public Symbol map(Function<QualifiedSymbol, Symbol> function) { return function.apply(this); } @Override public Symbol qualifyWith(String moduleName) { return qualified(moduleName, memberNames); } @Override public Symbol unqualify() { return unqualified(memberNames); } private String getPackagePath() { return getPackagePath(moduleName); } @Override protected Symbol withMemberNames(List<String> memberNames) { return new QualifiedSymbol(moduleName, memberNames); } } public static class UnqualifiedSymbol extends Symbol { private final List<String> memberNames; public UnqualifiedSymbol(List<String> memberNames) { this.memberNames = normalize(memberNames); } @Override public <T> T accept(SymbolVisitor<T> visitor) { return visitor.visit(this); } @Override public int compareTo(Symbol other) { return other.accept(new SymbolVisitor<Integer>() { @Override public Integer visit(QualifiedSymbol symbol) { return -1; } @Override public Integer visit(UnqualifiedSymbol symbol) { return compareMemberNames(UnqualifiedSymbol.this, symbol); } }); } @Override public boolean equals(Object o) { return o == this || o instanceof UnqualifiedSymbol && Objects.equals(memberNames, ((UnqualifiedSymbol) o).memberNames); } @Override public String getCanonicalName() { return join(memberNames); } @Override public String getClassName() { throw new IllegalStateException("Can't get unqualified class name from symbol " + quote()); } @Override public List<String> getMemberNames() { return memberNames; } @Override public String getModuleClass() { throw new IllegalStateException(); } @Override public int hashCode() { return Objects.hash(memberNames); } @Override public Symbol map(Function<QualifiedSymbol, Symbol> function) { return this; } @Override public Symbol qualifyWith(String moduleName) { return qualified(moduleName, memberNames); } @Override public Symbol unqualify() { return this; } @Override protected Symbol withMemberNames(List<String> memberNames) { return new UnqualifiedSymbol(memberNames); } } }