package scotch.compiler;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static me.qmx.jitescript.util.CodegenUtils.p;
import static scotch.compiler.text.TextUtil.quote;
import static scotch.symbol.Operator.operator;
import static scotch.symbol.Symbol.qualified;
import static scotch.symbol.Symbol.symbol;
import static scotch.symbol.Value.Fixity.NONE;
import static scotch.symbol.descriptor.DataFieldDescriptor.field;
import static scotch.symbol.descriptor.TypeClassDescriptor.typeClass;
import static scotch.symbol.descriptor.TypeInstanceDescriptor.typeInstance;
import static scotch.util.Pair.pair;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
import lombok.Getter;
import scotch.symbol.DataConstructor;
import scotch.symbol.DataField;
import scotch.symbol.DataFieldType;
import scotch.symbol.DataType;
import scotch.symbol.InstanceGetter;
import scotch.symbol.Member;
import scotch.symbol.MethodSignature;
import scotch.symbol.Module;
import scotch.symbol.ReExportModule;
import scotch.symbol.Symbol;
import scotch.symbol.SymbolEntry;
import scotch.symbol.SymbolEntry.ImmutableEntryBuilder;
import scotch.symbol.TypeClass;
import scotch.symbol.TypeInstance;
import scotch.symbol.TypeParameters;
import scotch.symbol.Value;
import scotch.symbol.ValueType;
import scotch.symbol.descriptor.DataConstructorDescriptor;
import scotch.symbol.descriptor.DataTypeDescriptor;
import scotch.symbol.descriptor.TypeInstanceDescriptor;
import scotch.symbol.exception.IncompleteDataTypeError;
import scotch.symbol.exception.IncompleteTypeInstanceError;
import scotch.symbol.exception.InvalidMethodSignatureError;
import scotch.symbol.exception.SymbolResolutionError;
import scotch.symbol.type.TypeDescriptor;
import scotch.symbol.type.TypeDescriptors;
public class ModuleScanner {
private final String moduleName;
private final List<Class<?>> classes;
private final Map<Symbol, ImmutableEntryBuilder> builders;
private final Set<TypeInstanceDescriptor> typeInstances;
private final Map<String, String> reExports;
public ModuleScanner(String moduleName, List<Class<?>> classes) {
this.moduleName = moduleName;
this.classes = classes;
this.builders = new HashMap<>();
this.typeInstances = new HashSet<>();
this.reExports = new LinkedHashMap<>();
}
public ScanResult scan() {
classes.forEach(this::processModules);
classes.forEach(this::processDataTypes);
classes.forEach(this::processDataConstructors);
classes.forEach(clazz -> {
processTypeClasses(clazz);
processTypeInstances(clazz);
processValues(clazz);
});
return new ScanResult(
builders.values().stream().map(ImmutableEntryBuilder::build).collect(toSet()),
typeInstances,
reExports
);
}
private List<Symbol> computeMembers(Class<?> clazz) {
return stream(clazz.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(Member.class))
.map(method -> method.getAnnotation(Member.class))
.map(Member::value)
.map(member -> qualified(moduleName, member))
.collect(toList());
}
private List<TypeDescriptor> computeParameters(TypeClass typeClass) {
return stream(typeClass.parameters())
.map(parameter -> TypeDescriptors.var(parameter.name(), asList(parameter.constraints())))
.collect(toList());
}
private Optional<Method> findMethod(Class<?> clazz, Class<? extends Annotation> annotation) {
return stream(clazz.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(annotation))
.findFirst();
}
private ImmutableEntryBuilder getBuilder(String memberName) {
return builders.computeIfAbsent(qualify(memberName), SymbolEntry::immutableEntry);
}
private ImmutableEntryBuilder getBuilder(Symbol memberSymbol) {
return builders.computeIfAbsent(memberSymbol, SymbolEntry::immutableEntry);
}
private IncompleteDataTypeError incompleteDataType(Class<?> clazz, Class<? extends Annotation> missingAnnotation) {
return new IncompleteDataTypeError("Data type definition for class " + quote(clazz) + " in module "
+ quote(moduleName) + " is incomplete: missing method annotated with " + pp(missingAnnotation));
}
private IncompleteTypeInstanceError incompleteTypeInstance(TypeInstance typeInstance, Class<? extends Annotation> missingAnnotation) {
return new IncompleteTypeInstanceError("Type instance definition for class " + quote(typeInstance.typeClass())
+ " in module " + quote(moduleName) + " is incomplete"
+ ": missing method annotated with " + pp(missingAnnotation));
}
@SuppressWarnings("unchecked")
private <T> T invoke(Method method) {
try {
method.setAccessible(true);
return (T) method.invoke(null);
} catch (ReflectiveOperationException exception) {
throw new SymbolResolutionError(exception);
}
}
private String pp(Class<?> clazz) {
return clazz.getCanonicalName();
}
private String pp(Method method) {
return pp(method.getDeclaringClass()) + "#" + method.getName();
}
private void processDataConstructors(Class<?> clazz) {
Optional.ofNullable(clazz.getAnnotation(DataConstructor.class)).ifPresent(annotation -> {
Symbol constructor = qualify(annotation.memberName());
Symbol dataType = qualify(annotation.dataType());
DataConstructorDescriptor.Builder builder = getBuilder(constructor).dataConstructor(annotation.ordinal(), dataType, clazz.getName().replace('.', '/'));
Map<String, TypeDescriptor> fieldTypes = stream(clazz.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(DataFieldType.class))
.map(method -> pair(method.getAnnotation(DataFieldType.class).forMember(), method))
.collect(
HashMap::new,
(map, pair) -> pair.into((name, method) -> {
map.put(name, invoke(method));
return map;
}),
HashMap::putAll
);
stream(clazz.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(DataField.class))
.map(method -> pair(method, method.getAnnotation(DataField.class)))
.forEach(pair -> pair.into((method, field) -> builder.addField(field(
field.ordinal(),
field.memberName(),
method.getName(),
Optional.ofNullable(fieldTypes.get(field.memberName()))
.orElseThrow(() -> incompleteDataType(clazz, DataFieldType.class))
))));
getBuilder(dataType).dataType().addConstructor(builder.build());
});
}
private void processDataTypes(Class<?> clazz) {
Optional.ofNullable(clazz.getAnnotation(DataType.class)).ifPresent(annotation -> {
Symbol symbol = qualify(annotation.memberName());
DataTypeDescriptor.Builder builder = getBuilder(symbol).dataType();
Method parametersGetter = findMethod(clazz, TypeParameters.class).orElseThrow(() -> incompleteDataType(clazz, TypeParameters.class));
validateParametersGetter(parametersGetter);
List<TypeDescriptor> parametersList = invoke(parametersGetter);
for (int i = 0; i < annotation.parameters().length; i++) {
builder.addParameter(parametersList.get(i));
}
builder.withClassName(p(clazz));
});
}
private void processModules(Class<?> clazz) {
if (!reExports.isEmpty()) {
throw new SymbolResolutionError(
"Multiple classes in module " + quote(moduleName) + " found with annotation @Module:"
+ " duplicate class is " + quote(clazz.getName())
);
} else if (clazz.isAnnotationPresent(Module.class)) {
for (ReExportModule module : clazz.getAnnotation(Module.class).reExports()) {
stream(module.members()).forEach(member -> reExports.put(member.memberName(), module.moduleName()));
}
}
}
private void processTypeClasses(Class<?> clazz) {
Optional.ofNullable(clazz.getAnnotation(TypeClass.class)).ifPresent(typeClass -> {
ImmutableEntryBuilder builder = getBuilder(typeClass.memberName());
Symbol symbol = qualify(typeClass.memberName());
List<Symbol> members = computeMembers(clazz);
builder.withTypeClass(typeClass(symbol, computeParameters(typeClass), members));
members.forEach(member -> getBuilder(member).withMemberOf(symbol));
});
}
private void processTypeInstances(Class<?> clazz) {
Optional.ofNullable(clazz.getAnnotation(TypeInstance.class)).ifPresent(typeInstance -> {
Method parametersGetter = findMethod(clazz, TypeParameters.class).orElseThrow(() -> incompleteTypeInstance(typeInstance, TypeParameters.class));
Method instanceGetter = findMethod(clazz, InstanceGetter.class).orElseThrow(() -> incompleteTypeInstance(typeInstance, InstanceGetter.class));
validateParametersGetter(parametersGetter);
typeInstances.add(typeInstance(
moduleName,
symbol(typeInstance.typeClass()),
invoke(parametersGetter),
MethodSignature.fromMethod(instanceGetter)
));
});
}
private void processValues(Class<?> clazz) {
stream(clazz.getDeclaredMethods()).forEach(method -> {
Optional.ofNullable(method.getAnnotation(Value.class)).ifPresent(value -> {
ImmutableEntryBuilder builder = getBuilder(value.memberName());
builder.withValueMethod(MethodSignature.fromMethod(method));
if (value.fixity() != NONE && value.precedence() != -1) {
builder.withOperator(operator(value.fixity(), value.precedence()));
}
});
Optional.ofNullable(method.getAnnotation(ValueType.class)).ifPresent(valueType -> {
ImmutableEntryBuilder builder = getBuilder(valueType.forMember());
if (TypeDescriptor.class.isAssignableFrom(method.getReturnType())) {
builder.withValueType(invoke(method));
} else {
throw new InvalidMethodSignatureError("Method " + pp(method)
+ " annotated by " + pp(ValueType.class)
+ " does not return " + pp(TypeDescriptor.class));
}
});
});
}
private Symbol qualify(String memberName) {
return qualified(moduleName, memberName);
}
private void validateParametersGetter(Method parametersGetter) {
ParameterizedType returnType = (ParameterizedType) parametersGetter.getGenericReturnType();
if (!List.class.isAssignableFrom((Class<?>) returnType.getRawType()) || !TypeDescriptor.class.isAssignableFrom((Class<?>) returnType.getActualTypeArguments()[0])) {
throw new InvalidMethodSignatureError("Method " + pp(parametersGetter)
+ " annotated by " + pp(TypeParameters.class)
+ " does not return " + pp(List.class) + "<" + pp(TypeDescriptor.class) + ">");
} else if (parametersGetter.getParameterCount() != 0) {
throw new InvalidMethodSignatureError("Method " + pp(parametersGetter)
+ " annotated by " + pp(TypeParameters.class)
+ " should not accept arguments");
}
}
public static final class ScanResult {
@Getter
private final Set<SymbolEntry> entries;
@Getter
private final Set<TypeInstanceDescriptor> instances;
private final Map<String, String> reExports;
public ScanResult(Set<SymbolEntry> entries, Set<TypeInstanceDescriptor> instances, Map<String, String> reExports) {
this.entries = ImmutableSet.copyOf(entries);
this.instances = ImmutableSet.copyOf(instances);
this.reExports = new LinkedHashMap<>(reExports);
}
public Map<String, String> getReExports() {
return new LinkedHashMap<>(reExports);
}
}
}