Skip to content

Commit

Permalink
Initial variant support in PathCompiler
Browse files Browse the repository at this point in the history
  • Loading branch information
prdoyle committed Jul 20, 2024
1 parent 7b534cb commit bc45de7
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package works.bosk.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target({ FIELD }) // TODO: Also METHOD
public @interface VariantTypeMap {
/**
* The names of the fields for which we're supplying a variant type map.
*/
String[] value();
}
9 changes: 5 additions & 4 deletions bosk-core/src/main/java/works/bosk/ReferenceUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@

import static java.lang.String.format;
import static java.lang.reflect.Modifier.STATIC;
import static java.util.stream.Collectors.toList;

/**
* Collection of utilities for implementing {@link Reference}s.
Expand Down Expand Up @@ -277,9 +276,11 @@ public static Method getterMethod(Class<?> objectClass, String fieldName) throws
public static <T> Constructor<T> theOnlyConstructorFor(Class<T> nodeClass) {
List<Constructor<?>> constructors = Stream.of(nodeClass.getDeclaredConstructors())
.filter(ctor -> !ctor.isSynthetic())
.collect(toList());
if (constructors.size() != 1) {
throw new IllegalArgumentException("Ambiguous constructor list: " + constructors);
.toList();
if (constructors.isEmpty()) {
throw new IllegalArgumentException("No suitable constructor for " + nodeClass.getSimpleName());
} else if (constructors.size() > 1) {
throw new IllegalArgumentException("Ambiguous constructor list for " + nodeClass.getSimpleName() + ": " + constructors);
}
@SuppressWarnings("unchecked")
Constructor<T> theConstructor = (Constructor<T>) constructors.get(0);
Expand Down
51 changes: 41 additions & 10 deletions bosk-core/src/main/java/works/bosk/SerializationPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -22,6 +23,7 @@
import works.bosk.annotations.Enclosing;
import works.bosk.annotations.Polyfill;
import works.bosk.annotations.Self;
import works.bosk.annotations.VariantTypeMap;
import works.bosk.exceptions.DeserializationException;
import works.bosk.exceptions.InvalidTypeException;
import works.bosk.exceptions.MalformedPathException;
Expand Down Expand Up @@ -240,7 +242,11 @@ public static boolean hasDeserializationPath(Class<?> nodeClass, Parameter param
return infoFor(nodeClass).annotatedParameters_DeserializationPath().containsKey(parameter.getName());
}

public <R extends StateTreeNode> void initializeEnclosingPolyfills(Reference<?> target, BoskDriver<R> formatDriver) {
public static Map<String, Type> getVariantTypeMapIfAny(Class<?> nodeClass, String parameterName) {
return infoFor(nodeClass).variantMaps().get(parameterName);
}

public <R extends StateTreeNode> void initializeEnclosingPolyfills(Reference<?> target, BoskDriver<R> driver) {
if (!ANY_POLYFILLS.get()) {
return;
}
Expand All @@ -256,15 +262,15 @@ public <R extends StateTreeNode> void initializeEnclosingPolyfills(Reference<?>
*/
if (!target.path().isEmpty()) {
try {
initializePolyfills(target.enclosingReference(Object.class), formatDriver);
initializePolyfills(target.enclosingReference(Object.class), driver);
} catch (InvalidTypeException e) {
throw new AssertionError("Every non-root reference has an enclosing reference: " + target);
}
}
}

private <R extends StateTreeNode, T> void initializePolyfills(Reference<T> ref, BoskDriver<R> formatDriver) {
initializeEnclosingPolyfills(ref, formatDriver);
private <R extends StateTreeNode, T> void initializePolyfills(Reference<T> ref, BoskDriver<R> driver) {
initializeEnclosingPolyfills(ref, driver);
if (!ref.path().isEmpty()) {
Class<?> enclosing;
try {
Expand All @@ -275,7 +281,7 @@ private <R extends StateTreeNode, T> void initializePolyfills(Reference<T> ref,
if (StateTreeNode.class.isAssignableFrom(enclosing)) {
Object result = infoFor(enclosing).polyfills().get(ref.path().lastSegment());
if (result != null) {
formatDriver.submitInitialization(ref, ref.targetClass().cast(result));
driver.submitInitialization(ref, ref.targetClass().cast(result));
}
}
}
Expand Down Expand Up @@ -331,9 +337,10 @@ private static ParameterInfo computeInfoFor(Class<?> nodeClassArg) {
Set<String> enclosingParameters = new HashSet<>();
Map<String, DeserializationPath> deserializationPathParameters = new HashMap<>();
Map<String, Object> polyfills = new HashMap<>();
Map<String, Map<String, Type>> variantMaps = new HashMap<>();
for (Parameter parameter: ReferenceUtils.theOnlyConstructorFor(nodeClassArg).getParameters()) {
scanForInfo(parameter, parameter.getName(),
selfParameters, enclosingParameters, deserializationPathParameters, polyfills);
selfParameters, enclosingParameters, deserializationPathParameters, polyfills, variantMaps);
}

// Bosk generally ignores an object's fields, looking only at its
Expand All @@ -345,13 +352,14 @@ private static ParameterInfo computeInfoFor(Class<?> nodeClassArg) {
for (Class<?> c = nodeClassArg; c != Object.class; c = c.getSuperclass()) {
for (Field field: c.getDeclaredFields()) {
scanForInfo(field, field.getName(),
selfParameters, enclosingParameters, deserializationPathParameters, polyfills);
selfParameters, enclosingParameters, deserializationPathParameters, polyfills, variantMaps);
}
}
return new ParameterInfo(selfParameters, enclosingParameters, deserializationPathParameters, polyfills);
return new ParameterInfo(selfParameters, enclosingParameters, deserializationPathParameters, polyfills, variantMaps);
}

private static void scanForInfo(AnnotatedElement thing, String name, Set<String> selfParameters, Set<String> enclosingParameters, Map<String, DeserializationPath> deserializationPathParameters, Map<String, Object> polyfills) {
@SuppressWarnings({"rawtypes","unchecked"})
private static void scanForInfo(AnnotatedElement thing, String name, Set<String> selfParameters, Set<String> enclosingParameters, Map<String, DeserializationPath> deserializationPathParameters, Map<String, Object> polyfills, Map<String, Map<String, Type>> variantMaps) {
if (thing.isAnnotationPresent(Self.class)) {
selfParameters.add(name);
} else if (thing.isAnnotationPresent(Enclosing.class)) {
Expand Down Expand Up @@ -384,14 +392,37 @@ private static void scanForInfo(AnnotatedElement thing, String name, Set<String>
} else {
throw new IllegalStateException("@Polyfill annotation is only valid on non-private static fields; found on " + thing);
}
} else if (thing.isAnnotationPresent(VariantTypeMap.class)) {
// TODO: Lots of code duplication with Polyfill
if (thing instanceof Field f && isStatic(f.getModifiers()) && !isPrivate(f.getModifiers())) {
f.setAccessible(true);
for (VariantTypeMap variantTypeMap : thing.getAnnotationsByType(VariantTypeMap.class)) {
Map value;
try {
value = (Map) f.get(null);
} catch (IllegalAccessException e) {
throw new AssertionError("Field should not be inaccessible: " + f, e);
}
if (value == null) {
throw new NullPointerException("VariantTypeMap cannot be null: " + f);
}
for (String fieldName: variantTypeMap.value()) {
Map previous = variantMaps.put(fieldName, value);
if (previous != null) {
throw new IllegalStateException("Multiple polyfills for the same field \"" + fieldName + "\": " + f);
}
}
}
}
}
}

private record ParameterInfo(
Set<String> annotatedParameters_Self,
Set<String> annotatedParameters_Enclosing,
Map<String, DeserializationPath> annotatedParameters_DeserializationPath,
Map<String, Object> polyfills
Map<String, Object> polyfills,
Map<String, Map<String, Type>> variantMaps
) { }

private static final Map<Class<?>, ParameterInfo> PARAMETER_INFO_MAP = new ConcurrentHashMap<>();
Expand Down
49 changes: 35 additions & 14 deletions bosk-core/src/main/java/works/bosk/TypeValidation.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Optional;
Expand All @@ -25,6 +26,7 @@
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Arrays.asList;
import static java.util.Collections.newSetFromMap;
import static works.bosk.SerializationPlugin.getVariantTypeMapIfAny;
import static works.bosk.SerializationPlugin.hasDeserializationPath;
import static works.bosk.SerializationPlugin.isEnclosingReference;
import static works.bosk.SerializationPlugin.isSelfReference;
Expand Down Expand Up @@ -146,16 +148,17 @@ private static void validateStateTreeNodeClass(Class<?> nodeClass, Set<Type> alr

// Every constructor parameter must have an appropriate getter and wither
for (Parameter p: constructors[0].getParameters()) {
validateConstructorParameter(nodeClass, p);
var typesToValidate = validateConstructorParameter(nodeClass, p);
validateGetter(nodeClass, p);

// Recurse to check that the field type itself is valid.
// For troubleshooting reasons, wrap any thrown exception so the
// user is able to follow the reference chain.
try {
validateType(p.getParameterizedType(), alreadyValidated);
} catch (InvalidTypeException e) {
throw new InvalidFieldTypeException(nodeClass, p.getName(), e.getMessage(), e);
for (Type type : typesToValidate) {// Recurse to check that the field type itself is valid.
// For troubleshooting reasons, wrap any thrown exception so the
// user is able to follow the reference chain.
try {
validateType(type, alreadyValidated);
} catch (InvalidTypeException e) {
throw new InvalidFieldTypeException(nodeClass, p.getName(), e.getMessage(), e);
}
}
}

Expand Down Expand Up @@ -192,13 +195,13 @@ private static void validateFieldsAreFinal(Class<?> nodeClass) throws InvalidFie
}
}

private static void validateConstructorParameter(Class<?> containingClass, Parameter parameter) throws InvalidFieldTypeException {
/**
* @return the set of types this <code>parameter</code> might use;
* usually, that's just the declared parameterized type of the parameter.
*/
private static Collection<Type> validateConstructorParameter(Class<?> containingClass, Parameter parameter) throws InvalidFieldTypeException {
String fieldName = parameter.getName();
for (int i = 0; i < fieldName.length(); i++) {
if (!isValidFieldNameChar(fieldName.codePointAt(i))) {
throw new InvalidFieldTypeException(containingClass, fieldName, "Only ASCII letters, numbers, and underscores are allowed in field names; illegal character '" + fieldName.charAt(i) + "' at offset " + i);
}
}
validateFieldName(containingClass, fieldName);
if (hasDeserializationPath(containingClass, parameter)) {
throw new InvalidFieldTypeException(containingClass, fieldName, "@" + DeserializationPath.class.getSimpleName() + " not valid inside the bosk");
} else if (isEnclosingReference(containingClass, parameter)) {
Expand All @@ -220,6 +223,24 @@ private static void validateConstructorParameter(Class<?> containingClass, Param
if (!ReferenceUtils.rawClass(referencedType).isAssignableFrom(containingClass)) {
throw new InvalidFieldTypeException(containingClass, fieldName, "@" + Self.class.getSimpleName() + " reference to " + ReferenceUtils.rawClass(referencedType).getSimpleName() + " incompatible with containing class " + containingClass.getSimpleName());
}
} else {
var variantTypeMap = getVariantTypeMapIfAny(containingClass, parameter.getName());
if (variantTypeMap != null) {
for (String variantName : variantTypeMap.keySet()) {
// TODO: this gives a confusing error message
validateFieldName(containingClass, variantName);
}
return variantTypeMap.values();
}
}
return List.of(parameter.getParameterizedType());
}

private static void validateFieldName(Class<?> containingClass, String fieldName) throws InvalidFieldTypeException {
for (int i = 0; i < fieldName.length(); i++) {
if (!isValidFieldNameChar(fieldName.codePointAt(i))) {
throw new InvalidFieldTypeException(containingClass, fieldName, "Only ASCII letters, numbers, and underscores are allowed in field names; illegal character '" + fieldName.charAt(i) + "' at offset " + i);
}
}
}

Expand Down
71 changes: 68 additions & 3 deletions bosk-core/src/main/java/works/bosk/dereferencers/PathCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.experimental.Delegate;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -31,6 +32,7 @@
import works.bosk.Path;
import works.bosk.Phantom;
import works.bosk.Reference;
import works.bosk.SerializationPlugin;
import works.bosk.SideTable;
import works.bosk.StateTreeNode;
import works.bosk.bytecode.LocalVariable;
Expand Down Expand Up @@ -212,14 +214,16 @@ public StepwiseDereferencerBuilder(Path path, StackWalker.StackFrame sourceFileO
assert !path.isEmpty();
steps = new ArrayList<>();
Type currentType = sourceType;
Step priorStep = null;
for (int i = 0; i < path.length(); i++) {
Step step = newSegmentStep(currentType, path.segment(i), i);
Step step = newSegmentStep(currentType, path.segment(i), i, priorStep);
steps.add(step);
currentType = step.targetType();
priorStep = step;
}
}

private Step newSegmentStep(Type currentType, String segment, int segmentNum) throws InvalidTypeException {
private Step newSegmentStep(Type currentType, String segment, int segmentNum, Step priorStep) throws InvalidTypeException {
Class<?> currentClass = rawClass(currentType);
if (Catalog.class.isAssignableFrom(currentClass)) {
return new CatalogEntryStep(parameterType(currentType, Catalog.class, 0), segmentNum);
Expand All @@ -229,6 +233,8 @@ private Step newSegmentStep(Type currentType, String segment, int segmentNum) th
Type keyType = parameterType(currentType, SideTable.class, 0);
Type targetType = parameterType(currentType, SideTable.class, 1);
return new SideTableEntryStep(keyType, targetType, segmentNum);
} else if (priorStep instanceof VariantFieldStep v) {
return new VariantCaseStep(segment, v.caseType(segment));
} else if (StateTreeNode.class.isAssignableFrom(currentClass)) {
if (isParameterSegment(segment)) {
throw new InvalidTypeException("Invalid parameter location: expected a field of " + currentClass.getSimpleName());
Expand All @@ -243,7 +249,10 @@ private Step newSegmentStep(Type currentType, String segment, int segmentNum) th

Step fieldStep = newFieldStep(segment, getters, theOnlyConstructorFor(currentClass));
Class<?> fieldClass = rawClass(fieldStep.targetType());
if (Optional.class.isAssignableFrom(fieldClass)) {
Map<String, Type> typeMap = SerializationPlugin.getVariantTypeMapIfAny(currentClass, segment);
if (typeMap != null) {
return new VariantFieldStep(typeMap, fieldStep);
} else if (Optional.class.isAssignableFrom(fieldClass)) {
return new OptionalValueStep(parameterType(fieldStep.targetType(), Optional.class, 0), fieldStep);
} else if (Phantom.class.isAssignableFrom(fieldClass)) {
return new PhantomValueStep(parameterType(fieldStep.targetType(), Phantom.class, 0), segment);
Expand Down Expand Up @@ -544,6 +553,62 @@ public class PhantomValueStep implements DeletableStep {
@Override public void generate_without() { /* No effect */ }
}

/**
* Augments another step to capture info for fields using
* {@link works.bosk.annotations.VariantTypeMap}.
*/
public record VariantFieldStep(
Map<String, Type> variantMap,
@Delegate Step fieldStep
) implements Step {
public Type caseType(String caseName) throws InvalidTypeException {
Type result = variantMap.get(caseName);
if (result == null) {
throw new InvalidTypeException("Unknown variant case \"" + caseName + "\" of " + fieldStep.targetClass().getSimpleName() + "." + fieldStep.fullyParameterizedPathSegment());
} else {
return result;
}
}
}

@Value
public class VariantCaseStep implements Step {
String name;
Type targetType;

@Override
public Type targetType() {
return targetType;
}

@Override
public String fullyParameterizedPathSegment() {
return name;
}

/**
* The "containing object" is actually the object we want.
* If it's the right type,
* then picking one case of a variant is nothing but a downcast,
* which {@link StepwiseDereferencerBuilder#generate_get} already does.
* If it's the wrong type, then we want to treat that as nonexistent.
*/
@Override
public void generate_get() {
// TODO: nonexistent case
}

/**
* We actually replace the entire "containing object".
* Simply discard the old value and return the new one.
*/
@Override
public void generate_with() {
swap(); // Move old value to top of stack
pop(); // Discard old value
}
}

@Value
public class CustomStep implements Step {
Type targetType;
Expand Down
Loading

0 comments on commit bc45de7

Please sign in to comment.