From f4b8d1b83d9f975f10114027c2e1fa0845d3392f Mon Sep 17 00:00:00 2001 From: LadyCailin Date: Tue, 17 Feb 2026 19:35:17 +0100 Subject: [PATCH] Merge in select generics classes from genericsTake2. This cherry-picked merge is phase 1 of merging in support for generics. By itself, this code does nothing, but some changes to CClassType are necessarily not dark. Most methods stubbed, but some actually call in to the dark code, but should not be used yet. --- .../PureUtilities/Common/StringUtils.java | 24 + .../com/laytonsmith/PureUtilities/Either.java | 116 +++ .../core/MethodScriptCompiler.java | 8 +- .../java/com/laytonsmith/core/Procedure.java | 10 +- .../core/compiler/FileOptions.java | 4 + .../compiler/signature/FunctionSignature.java | 8 + .../com/laytonsmith/core/constructs/Auto.java | 1 + .../laytonsmith/core/constructs/CArray.java | 23 + .../core/constructs/CClassType.java | 121 ++- .../laytonsmith/core/constructs/CClosure.java | 6 +- .../core/constructs/InstanceofUtil.java | 13 + .../core/constructs/LeftHandSideType.java | 768 ++++++++++++++++++ .../core/constructs/SourceType.java | 26 + .../generics/ConcreteGenericParameter.java | 146 ++++ .../generics/ConstraintLocation.java | 37 + .../ConstraintToConstraintValidator.java | 47 ++ .../generics/ConstraintValidator.java | 283 +++++++ .../core/constructs/generics/Constraints.java | 553 +++++++++++++ .../generics/GenericDeclaration.java | 80 ++ .../generics/GenericParameters.java | 315 +++++++ .../generics/GenericTypeParameters.java | 421 ++++++++++ .../generics/LeftHandGenericUse.java | 270 ++++++ .../generics/LeftHandGenericUseParameter.java | 85 ++ .../generics/UnqualifiedConstraint.java | 45 + .../generics/UnqualifiedConstraints.java | 48 ++ .../UnqualifiedGenericDeclaration.java | 36 + .../UnqualifiedGenericTypeParameters.java | 108 +++ .../UnqualifiedLeftHandGenericUse.java | 38 + .../constraints/BoundaryConstraint.java | 46 ++ .../generics/constraints/Constraint.java | 191 +++++ .../constraints/ConstructorConstraint.java | 119 +++ .../constraints/ExactTypeConstraint.java | 195 +++++ .../constraints/LowerBoundConstraint.java | 106 +++ .../constraints/UnboundedConstraint.java | 101 +++ .../constraints/UpperBoundConstraint.java | 115 +++ .../constraints/VariadicTypeConstraint.java | 94 +++ .../CRE/CREGenericConstraintException.java | 49 ++ .../core/functions/DataHandling.java | 2 +- .../core/constructs/LeftHandSideTypeTest.java | 48 ++ 39 files changed, 4678 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/laytonsmith/PureUtilities/Either.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/SourceType.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/ConcreteGenericParameter.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/ConstraintLocation.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/ConstraintToConstraintValidator.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/ConstraintValidator.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/Constraints.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/GenericDeclaration.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/GenericParameters.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/GenericTypeParameters.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUse.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUseParameter.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraints.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericDeclaration.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericTypeParameters.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedLeftHandGenericUse.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/BoundaryConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/Constraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/ConstructorConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/ExactTypeConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/LowerBoundConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/UnboundedConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/UpperBoundConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/constructs/generics/constraints/VariadicTypeConstraint.java create mode 100644 src/main/java/com/laytonsmith/core/exceptions/CRE/CREGenericConstraintException.java create mode 100644 src/test/java/com/laytonsmith/core/constructs/LeftHandSideTypeTest.java diff --git a/src/main/java/com/laytonsmith/PureUtilities/Common/StringUtils.java b/src/main/java/com/laytonsmith/PureUtilities/Common/StringUtils.java index 1f17720ce8..15789b18c7 100644 --- a/src/main/java/com/laytonsmith/PureUtilities/Common/StringUtils.java +++ b/src/main/java/com/laytonsmith/PureUtilities/Common/StringUtils.java @@ -303,6 +303,30 @@ public static String Join(List list, String glue) { return Join(list, glue, null, null, null); } + /** + * Joins a list together, rendering each item with the custom renderer. + * @param The list type + * @param list The list to concatenate + * @param glue The glue to use + * @param renderer The item renderer. If null, toString will be used by default on each item. + * @return The concatenated string + */ + public static String Join(List list, String glue, Renderer renderer) { + return Join(list, glue, null, null, null, renderer); + } + + /** + * Joins an array together, rendering each item with the custom renderer. + * @param The array type + * @param list The array to concatenate + * @param glue The glue to use + * @param renderer The item renderer. If null, toString will be used by default on each item. + * @return The concatenated string + */ + public static String Join(T[] list, String glue, Renderer renderer) { + return Join(list, glue, null, null, null, renderer); + } + /** * Joins a list together (using StringBuilder's {@link StringBuilder#append(Object)} method * to "toString" the Object) using the specified string for glue. diff --git a/src/main/java/com/laytonsmith/PureUtilities/Either.java b/src/main/java/com/laytonsmith/PureUtilities/Either.java new file mode 100644 index 0000000000..64021d6193 --- /dev/null +++ b/src/main/java/com/laytonsmith/PureUtilities/Either.java @@ -0,0 +1,116 @@ +package com.laytonsmith.PureUtilities; + +import java.util.Objects; +import java.util.Optional; + +/** + * The {@code Either} class can contain neither of, or one of either the specified types of values. + * @param The first possible type. + * @param The second possible type. + */ +public final class Either { + + /** + * Creates a new Either object with the first possible type. + * @param The first possible type, which is the type of the input parameter. + * @param The other possible type, which this value will not contain. + * @param value The value to store. + * @return A new Either object. + */ + public static Either left(L value) { + return new Either<>(Optional.of(value), Optional.empty()); + } + + /** + * Creates a new Either object with the second possible type. + * @param The other possible type, which this value will not contain. + * @param The second possible type, which is the type of the input parameter. + * @param value The value to store. + * @return A new Either object. + */ + public static Either right(R value) { + return new Either<>(Optional.empty(), Optional.of(value)); + } + + /** + * Creates a new Either object, which does not contain any value. + * @param The first possible type. + * @param The second possible type. + * @return A new, empty, Either object. + */ + public static Either neither() { + return new Either<>(Optional.empty(), Optional.empty()); + } + + private final Optional left; + private final Optional right; + + private Either(Optional left, Optional right) { + this.left = left; + this.right = right; + } + + /** + * Returns the left value. May be empty, even if the other value is also empty. + * @return + */ + public Optional getLeft() { + return this.left; + } + + /** + * Returns the left value. May be empty, even if the other value is also empty. + * @return + */ + public Optional getRight() { + return this.right; + } + + /** + * Returns if there was a left side. + * @return + */ + public boolean hasLeft() { + return this.left.isPresent(); + } + + /** + * Returns if there was a right side. + * @return + */ + public boolean hasRight() { + return this.right.isPresent(); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 19 * hash + Objects.hashCode(this.left); + hash = 19 * hash + Objects.hashCode(this.right); + return hash; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final Either other = (Either) obj; + if(!Objects.equals(this.left, other.left)) { + return false; + } + return Objects.equals(this.right, other.right); + } + + @Override + public String toString() { + return "Either{" + "left=" + left + ", right=" + right + '}'; + } + +} diff --git a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java index bf1655b232..8858208ec7 100644 --- a/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java +++ b/src/main/java/com/laytonsmith/core/MethodScriptCompiler.java @@ -21,7 +21,6 @@ import com.laytonsmith.core.compiler.TokenStream; import com.laytonsmith.core.compiler.analysis.StaticAnalysis; import com.laytonsmith.core.constructs.CBareString; -import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.CDecimal; import com.laytonsmith.core.constructs.CDouble; import com.laytonsmith.core.constructs.CFunction; @@ -37,6 +36,7 @@ import com.laytonsmith.core.constructs.CVoid; import com.laytonsmith.core.constructs.Construct; import com.laytonsmith.core.constructs.IVariable; +import com.laytonsmith.core.constructs.SourceType; import com.laytonsmith.core.constructs.Target; import com.laytonsmith.core.constructs.Token; import com.laytonsmith.core.constructs.Token.TType; @@ -1753,11 +1753,11 @@ public static ParseTree compile(TokenStream stream, Environment environment, ParseTree previous = tree.getChildAt(tree.getChildren().size() - 1); // TODO: Add LHSType as well, though this will not work as is with user objects. It may need // to be moved into a node modifier or something. - if(!(previous.getData() instanceof CClassType)) { + if(!(previous.getData() instanceof SourceType)) { throw new ConfigCompileException("Unexpected varargs token (\"...\"). This can only be used with types.", t.target); } - if(previous.getData() instanceof CClassType c) { - previous.setData(c.asVarargs()); + if(previous.getData() instanceof SourceType st) { + previous.setData(st.asVariadicType(null)); } continue; } else if(t.type == TType.LIT) { diff --git a/src/main/java/com/laytonsmith/core/Procedure.java b/src/main/java/com/laytonsmith/core/Procedure.java index e65a71c301..8e62f139c3 100644 --- a/src/main/java/com/laytonsmith/core/Procedure.java +++ b/src/main/java/com/laytonsmith/core/Procedure.java @@ -68,7 +68,7 @@ public Procedure(String name, CClassType returnType, List varList, Sm this.procComment = procComment; for(int i = 0; i < varList.size(); i++) { IVariable var = varList.get(i); - if(var.getDefinedType().isVarargs() && i != varList.size() - 1) { + if(var.getDefinedType().isVariadicType() && i != varList.size() - 1) { throw new CREFormatException("Varargs can only be added to the last argument.", t); } try { @@ -77,7 +77,7 @@ public Procedure(String name, CClassType returnType, List varList, Sm this.varList.put(var.getVariableName(), var); } this.varIndex.add(var); - if(var.getDefinedType().isVarargs() && var.ival() != CNull.UNDEFINED) { + if(var.getDefinedType().isVariadicType() && var.ival() != CNull.UNDEFINED) { throw new CREFormatException("Varargs may not have default values", t); } this.originals.put(var.getVariableName(), var.ival()); @@ -218,10 +218,10 @@ public Mixed execute(List args, Environment oldEnv, Target t) { arguments.push(c, t); if(this.varIndex.size() > varInd || (!this.varIndex.isEmpty() - && this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVarargs())) { + && this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType())) { IVariable var; if(varInd < this.varIndex.size() - 1 - || !this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVarargs()) { + || !this.varIndex.get(this.varIndex.size() - 1).getDefinedType().isVariadicType()) { var = this.varIndex.get(varInd); } else { var = this.varIndex.get(this.varIndex.size() - 1); @@ -242,7 +242,7 @@ public Mixed execute(List args, Environment oldEnv, Target t) { } // Type check vararg parameter. - if(var.getDefinedType().isVarargs()) { + if(var.getDefinedType().isVariadicType()) { if(InstanceofUtil.isInstanceof(c.typeof(), var.getDefinedType().getVarargsBaseType(), env)) { vararg.push(c, t); continue; diff --git a/src/main/java/com/laytonsmith/core/compiler/FileOptions.java b/src/main/java/com/laytonsmith/core/compiler/FileOptions.java index 5ed2c7cc36..b938915151 100644 --- a/src/main/java/com/laytonsmith/core/compiler/FileOptions.java +++ b/src/main/java/com/laytonsmith/core/compiler/FileOptions.java @@ -368,6 +368,10 @@ public static enum SuppressWarning implements Documentation { MSVersion.V3_3_5, SeverityLevel.MEDIUM), UnexpectedStatement("In strict mode, unexpected statements are an error, but in non-strict mode, they are" + " a warning.", MSVersion.V3_3_5, SeverityLevel.HIGH), + GenericTypeOverrides("When using generics, defining a typename which matches an in-scope actual object type" + + " can lead to confusion, since references to the original type will no longer reference the" + + " actual type, but rather the typename. Thus, this should be avoided.", MSVersion.V3_3_5, + SeverityLevel.HIGH), PossibleUnexpectedExecution("If the parenthesis following a token is on a different line as the previous" + " token, and it in general looks like a value that might be executable, this warning is" + " issued, as it will cause an attempt at executing the previous statement. This" diff --git a/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java b/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java index 8f21c33771..39e628719d 100644 --- a/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java +++ b/src/main/java/com/laytonsmith/core/compiler/signature/FunctionSignature.java @@ -8,6 +8,7 @@ import com.laytonsmith.PureUtilities.Common.StringUtils; import com.laytonsmith.core.constructs.CClassType; import com.laytonsmith.core.constructs.InstanceofUtil; +import com.laytonsmith.core.constructs.generics.GenericDeclaration; import com.laytonsmith.core.environments.Environment; /** @@ -44,6 +45,13 @@ public FunctionSignature(ReturnType returnType) { this(returnType, new ArrayList<>(), new ArrayList<>(), false); } + /** + * Stub for generics support — not yet implemented. + */ + public GenericDeclaration getGenericDeclaration() { + throw new UnsupportedOperationException("Not yet implemented"); + } + protected void addParam(Param param) { this.params.add(param); } diff --git a/src/main/java/com/laytonsmith/core/constructs/Auto.java b/src/main/java/com/laytonsmith/core/constructs/Auto.java index e69221f97b..cf3a3b582d 100644 --- a/src/main/java/com/laytonsmith/core/constructs/Auto.java +++ b/src/main/java/com/laytonsmith/core/constructs/Auto.java @@ -8,4 +8,5 @@ public class Auto { public static final CClassType TYPE = CClassType.AUTO; + public static final LeftHandSideType LHSTYPE = TYPE.asLeftHandSideType(); } diff --git a/src/main/java/com/laytonsmith/core/constructs/CArray.java b/src/main/java/com/laytonsmith/core/constructs/CArray.java index 66919232e9..07a05ad31b 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CArray.java +++ b/src/main/java/com/laytonsmith/core/constructs/CArray.java @@ -7,6 +7,8 @@ import com.laytonsmith.core.MSLog; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.LogLevel; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREFormatException; import com.laytonsmith.core.exceptions.CRE.CREIndexOverflowException; @@ -58,6 +60,27 @@ public CArray(Target t) { this(t, 0, (Mixed[]) null); } + /** + * Constructor with generic parameters and environment. GenericParameters are ignored for now. + */ + public CArray(Target t, GenericParameters genericParameters, Environment env) { + this(t); + } + + /** + * Constructor with initial capacity, generic parameters and environment. GenericParameters are ignored for now. + */ + public CArray(Target t, int initialCapacity, GenericParameters genericParameters, Environment env) { + this(t, initialCapacity); + } + + /** + * Constructor with items, generic parameters and environment. GenericParameters are ignored for now. + */ + public CArray(Target t, GenericParameters genericParameters, Environment env, Mixed... items) { + this(t, items); + } + public CArray(Target t, Mixed... items) { this(t, 0, items); } diff --git a/src/main/java/com/laytonsmith/core/constructs/CClassType.java b/src/main/java/com/laytonsmith/core/constructs/CClassType.java index 0a0594ae53..554e3d1996 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClassType.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClassType.java @@ -7,6 +7,9 @@ import com.laytonsmith.core.FullyQualifiedClassName; import com.laytonsmith.core.MSVersion; import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.constructs.generics.GenericDeclaration; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.constructs.generics.GenericTypeParameters; import com.laytonsmith.core.environments.Environment; import com.laytonsmith.core.exceptions.CRE.CRECastException; import com.laytonsmith.core.exceptions.CRE.CREUnsupportedOperationException; @@ -34,7 +37,7 @@ */ @typeof("ms.lang.ClassType") @SuppressWarnings("checkstyle:overloadmethodsdeclarationorder") -public final class CClassType extends Construct implements com.laytonsmith.core.natives.interfaces.Iterable { +public final class CClassType extends Construct implements com.laytonsmith.core.natives.interfaces.Iterable, SourceType { public static final String PATH_SEPARATOR = FullyQualifiedClassName.PATH_SEPARATOR; @@ -46,6 +49,10 @@ public final class CClassType extends Construct implements com.laytonsmith.core. @SuppressWarnings("FieldNameHidesFieldInSuperclass") public static final CClassType TYPE; public static final CClassType AUTO; + /** + * Sentinel value for recursive type definitions in generics. + */ + public static final CClassType RECURSIVE_DEFINITION; /** * Used to differentiate between null and uninitialized. * @@ -60,6 +67,7 @@ public final class CClassType extends Construct implements com.laytonsmith.core. } catch (ClassNotFoundException e) { throw new Error(e); } + RECURSIVE_DEFINITION = defineClass(FullyQualifiedClassName.forFullyQualifiedClass("ms.lang.RecursiveDefinition")); } /** @@ -96,9 +104,9 @@ public final class CClassType extends Construct implements com.laytonsmith.core. private final SortedSet types = new TreeSet<>(); /** - * If this represents a vararg type, which is actually an array type. + * If this represents a variadic type, which is actually an array type. */ - private boolean isVararg = false; + private boolean isVariadicType = false; /** * Returns the singular instance of CClassType that represents this type. @@ -151,6 +159,24 @@ public static CClassType get(FullyQualifiedClassName type) throws ClassNotFoundE return ctype; } + public static CClassType get(FullyQualifiedClassName type, Target t) throws ClassNotFoundException { + return get(type); + } + + /** + * Stub for generics support — returns the naked type, ignoring generic parameters for now. + */ + public static CClassType get(FullyQualifiedClassName type, Target t, GenericTypeParameters generics, Environment env) throws ClassNotFoundException { + return get(type); + } + + /** + * Stub for generics support — returns the naked type, ignoring generic parameters for now. + */ + public static CClassType get(Class type, Target t, GenericTypeParameters generics, Environment env) { + return get(type); + } + /** * Returns the singular instance of CClassType that represents this type union. string|int and int|string are both * considered the same type union, as they are first normalized into a canonical form. @@ -657,42 +683,107 @@ public boolean getBooleanValue(Target t) { } /** - * Returns a new type based on the parent type, which is a vararg variant. - * @return A new vararg type. + * Returns a new type based on the parent type, which is a variadic variant. + * @return A new variadic type. */ - public CClassType asVarargs() { + @Override + public SourceType asVariadicType(Environment env) { CClassType newType; try { newType = new CClassType(fqcn, Target.UNKNOWN, isTypeUnion); } catch(ClassNotFoundException ex) { throw new Error(ex); } - newType.isVararg = true; + newType.isVariadicType = true; return newType; } /** - * Returns true if this is a var arg type, for instance `string...`. This is actually an array type in most - * cases. + * Returns true if this is a variadic (varargs) type, for instance `string...`. * @return */ - public boolean isVarargs() { - return this.isVararg; + @Override + public boolean isVariadicType() { + return this.isVariadicType; } /** - * Returns the base type of this varargs type. + * Returns the base type of this variadic type. Note that if the type has generics, those are included + * in the returned result. * @return The base {@link CClassType} of this type (e.g. `string` for `string...`). - * @throws IllegalStateException - If this is not a vararg type. + * @throws IllegalStateException - If this is not a variadic type. */ public CClassType getVarargsBaseType() throws IllegalStateException { - if(!this.isVararg) { + if(!this.isVariadicType) { throw new IllegalStateException("CClassType is not a vararg type."); } try { return CClassType.get(this.fqcn); } catch (ClassNotFoundException e) { - throw new Error(e); // Existence of this vararg type implies that its base type exists. + throw new Error(e); } } + + /** + * Stub for generics support — returns null until generics are fully implemented. + */ + public GenericDeclaration getGenericDeclaration() { + return null; + } + + /** + * Stub for generics support — returns null until generics are fully implemented. + */ + public GenericTypeParameters getTypeGenericParameters() { + return null; + } + + /** + * Stub for generics support — not yet implemented. + */ + public GenericParameters getGenericParameters() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Returns a LeftHandSideType equivalent of this CClassType. + */ + public LeftHandSideType asLeftHandSideType() { + return LeftHandSideType.fromHardCodedType(this); + } + + /** + * Returns the naked (non-generic) version of this type. Stub — not yet implemented. + */ + public CClassType getNakedType(Environment env) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Gets the naked CClassType for the given FQCN. Stub — not yet implemented. + */ + public static CClassType getNakedClassType(FullyQualifiedClassName type, Environment env) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Checks whether this type extends the given superClass, with environment context. Stub — not yet implemented. + */ + public static boolean doesExtend(Environment env, CClassType checkClass, CClassType superClass) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Checks whether this type extends the given superClass, with environment context. Stub — not yet implemented. + */ + public boolean doesExtend(Environment env, CClassType superClass) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Stub for generics support — not yet implemented. + */ + public static CClassType getFromGenericTypeName(String genericTypeName, Target t) { + throw new UnsupportedOperationException("Not yet implemented"); + } } diff --git a/src/main/java/com/laytonsmith/core/constructs/CClosure.java b/src/main/java/com/laytonsmith/core/constructs/CClosure.java index 7ada714479..ae8d0131c4 100644 --- a/src/main/java/com/laytonsmith/core/constructs/CClosure.java +++ b/src/main/java/com/laytonsmith/core/constructs/CClosure.java @@ -58,7 +58,7 @@ public CClosure(ParseTree node, Environment env, CClassType returnType, String[] this.types = types; if(types.length > 0) { for(int i = 0; i < types.length - 1; i++) { - if(types[i].isVarargs()) { + if(types[i].isVariadicType()) { throw new CREFormatException("Varargs can only be added to the last argument.", t); } } @@ -234,10 +234,10 @@ protected void execute(Mixed... values) throws ConfigRuntimeException, ProgramFl boolean isVarArg = false; if(this.names.length > i || (this.names.length != 0 - && this.types[this.names.length - 1].isVarargs())) { + && this.types[this.names.length - 1].isVariadicType())) { String name; if(i < this.names.length - 1 - || !this.types[this.types.length - 1].isVarargs()) { + || !this.types[this.types.length - 1].isVariadicType()) { name = names[i]; } else { name = this.names[this.names.length - 1]; diff --git a/src/main/java/com/laytonsmith/core/constructs/InstanceofUtil.java b/src/main/java/com/laytonsmith/core/constructs/InstanceofUtil.java index d598ba3894..fd1a37c764 100644 --- a/src/main/java/com/laytonsmith/core/constructs/InstanceofUtil.java +++ b/src/main/java/com/laytonsmith/core/constructs/InstanceofUtil.java @@ -168,6 +168,19 @@ public static boolean isInstanceof(CClassType type, CClassType instanceofThis, E return castableClasses.contains(instanceofThis); } + public static boolean isInstanceof(CClassType type, LeftHandSideType instanceofThis, Environment env) { + // TODO: Stopgap — asConcreteType won't error in practice now, but needs proper LeftHandSideType support later + CClassType target = instanceofThis.asConcreteType(Target.UNKNOWN); + return isInstanceof(type, target, env); + } + + public static boolean isInstanceof(LeftHandSideType type, LeftHandSideType instanceofThis, Environment env) { + // TODO: Stopgap — asConcreteType won't error in practice now, but needs proper LeftHandSideType support later + CClassType concreteType = type.asConcreteType(Target.UNKNOWN); + CClassType concreteInstanceof = instanceofThis.asConcreteType(Target.UNKNOWN); + return isInstanceof(concreteType, concreteInstanceof, env); + } + private static FullyQualifiedClassName typeof(Class c) { typeof type = ClassDiscovery.GetClassAnnotation(c, typeof.class); if(type == null) { diff --git a/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java b/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java new file mode 100644 index 0000000000..ae7ffc0ec8 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/LeftHandSideType.java @@ -0,0 +1,768 @@ +package com.laytonsmith.core.constructs; + +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.PureUtilities.Either; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.Pair; +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.core.MSLog; +import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.constructs.generics.ConcreteGenericParameter; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.constructs.generics.Constraints; +import com.laytonsmith.core.constructs.generics.GenericDeclaration; +import com.laytonsmith.core.constructs.generics.GenericParameters; +import com.laytonsmith.core.constructs.generics.LeftHandGenericUse; +import com.laytonsmith.core.constructs.generics.LeftHandGenericUseParameter; +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; +import com.laytonsmith.core.natives.interfaces.Mixed; +import com.laytonsmith.core.objects.ObjectModifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +/** + * A LeftHandSideType is a type reference that belongs on the left hand side (LHS) of a value definition. This is + * opposed to a right hand side (RHS) type, which is generally represented with CClassType. A LHS type is different in + * the sense that, while it may contain a concrete, instantiatable type, it does not necessarily have to. For instance, + * in the definition {@code string | int @x = 42;}, the LHS type is "string | int", meaning that the variable @x may + * validly contain an instance of either a string or an int, but 42 is ONLY an int (and the specific value and type may + * only be known at runtime anyways.) Therefore, in places where the type being represented is a LHS value, a different + * container must be used rather than the standard CClassType. While in many cases, this may look exactly like a simple + * RHS value, the semantics are different, and in less common cases, the rules are substantially different. For + * instance, the generics on the LHS can contain a declaration such as {@code array}, but having such + * a value on the RHS is not legal. + *

+ * A LHS value is used in 3 different locations: the left hand of a variable declaration, parameter definitions in + * callables such as procs and closures, and the return type of a callable. RHS values are used in for instance a + * {@code new} instantiation, and are represented with CClassType. + *

+ * While less common use cases exist such that a CClassType can't represent the LHS, the common use case can, for + * instance {@code int @i = 1;}. Therefore, there are convenience methods for easily converting a CClassType into a + * LeftHandSideType. Additionally, all the actual types represented are naked CClassType values. + */ +public final class LeftHandSideType extends Construct implements SourceType { + + /** + * Merges the inputs to create a single type union class. For instance, if {@code int | string} and + * {@code array | string} are passed in, the resulting type would be {@code int | string | array}. Note that for + * subtypes with generic parameters, these are not merged unless they are completely equal. + * + * @param t + * @param env + * @param types + * @return + */ + public static LeftHandSideType fromTypeUnion(Target t, Environment env, LeftHandSideType... types) { + Set set = new HashSet<>(); + List isTypenameList = new ArrayList<>(); + for(LeftHandSideType union : types) { + set.addAll(union.getTypes()); + isTypenameList.add(union.isTypeName); + } + return createCClassTypeUnion(t, env, new ArrayList<>(set), isTypenameList); + } + + /** + * Merges the inputs to create a single type union class.For instance, if {@code int | string} and + * {@code array | string} are passed in, the resulting type would be {@code int | string | array}. Note that for + * subtypes with generic parameters, these are not merged unless they are completely equal. This can only be + * used for types that exclusively represent native types. + * @param types + * @return + */ + public static LeftHandSideType fromNativeTypeUnion(LeftHandSideType... types) { + return fromTypeUnion(Target.UNKNOWN, null, types); + } + + /** + * Creates a LeftHandSideType based on the specified CClassType. This is only suitable for hardcoded types, such as + * {@code CString.TYPE}, and never for user input! With user input, it's important to specify the code target. Only + * use this version if you would have otherwise used {@code Target.UNKNOWN}. + * + * @param type The simple CClassType that this LHS represents + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromHardCodedType(CClassType type) { + return fromCClassType(type, Target.UNKNOWN, null); + } + + /** + * When creating a type in a GenericDeclaration object, the type reference doesn't actually exist, but it still + * needs to be represented as a left hand type.For instance, if we have {@code class C}, then within the class, + * {@code T} can be used as method return types, parameter types, field types, etc.This is not a "real" type in the + * sense that it can be used at runtime, but within the compilation system, this needs to be representable somehow, + * without actually using CClassType.Note that the relevant GenericDeclaration needs to be passed in as well. It is + * validated that such a type parameter exists, and it also is used to pull out the appropriate constraints so that + * additional validation can be done elsewhere. + * + * @param declaration The declaration which contains this type name. + * @param genericTypeName The generic type name, for instance {@code T} + * @param genericLHGU The generic parameters for this type, for instance {@code ? extends int} in + * {@code T} + * @param t The code target + * @param env The environment. + * @return The LeftHandSideType wrapping this generic typename. + * @throws IllegalArgumentException If the GenericDeclaration does not contain a Constraints value with the + * specified typename. + */ + public static LeftHandSideType fromGenericDefinitionType(GenericDeclaration declaration, String genericTypeName, + LeftHandGenericUse genericLHGU, Target t, Environment env) throws IllegalArgumentException { + Constraints constraints = null; + for(Constraints c : declaration.getConstraints()) { + if(c.getTypeName().equals(genericTypeName)) { + constraints = c; + break; + } + } + if(constraints == null) { + throw new IllegalArgumentException("Provided GenericDeclaration does not contain the specified type name."); + } + return new LeftHandSideType(genericTypeName, t, env, null, Arrays.asList(true), genericTypeName, genericLHGU); + } + + /** + * When creating a type in a GenericDeclaration object, the type reference doesn't actually exist, but it still + * needs to be represented as a left hand type.For instance, if we have {@code class C}, then within the class, + * {@code T} can be used as method return types, parameter types, field types, etc.This is not a "real" type in the + * sense that it can be used at runtime, but within the compilation system, this needs to be representable somehow, + * without actually using CClassType.Note that the relevant GenericDeclaration needs to be passed in as well. It is + * validated that such a type parameter exists, and it also is used to pull out the appropriate constraints so that + * additional validation can be done elsewhere. + * + * @param declaration The declaration which contains this type name. + * @param genericTypeName The generic type name, for instance {@code T} + * @param genericLHGU The generic parameters for this type, for instance {@code ? extends int} in + * {@code T} + * @param t The code target + * @param env The environment. + * @return The LeftHandSideType wrapping this generic typename. + * @throws IllegalArgumentException If the GenericDeclaration does not contain a Constraints value with the + * specified typename. + */ + public static LeftHandSideType fromNativeGenericDefinitionType(GenericDeclaration declaration, String genericTypeName, + LeftHandGenericUse genericLHGU) throws IllegalArgumentException { + return fromGenericDefinitionType(declaration, genericTypeName, genericLHGU, Target.UNKNOWN, null); + } + + /** + * Creates a LeftHandSideType which contains no generics. + * + * @param classType The simple CClassType that this LHS represents. + * @param t The code target. + * @param env The environment. + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromCClassType(CClassType classType, Target t, Environment env) { + List isTypenameList = new ArrayList<>(); + isTypenameList.add(false); + LeftHandSideType lhst = createCClassTypeUnion(t, env, Arrays.asList( + new ConcreteGenericParameter(classType, null, Target.UNKNOWN, null)), isTypenameList); + if(classType != null) { + lhst.isVariadicType = classType.isVariadicType(); + } + return lhst; + } + + /** + * Creates a new LeftHandSideType from the given ConcreteGenericParameter. + * + * @param type The class type. + * @param t The code target. + * @param env + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromCClassType(ConcreteGenericParameter type, Target t, Environment env) { + List isTypenameList = new ArrayList<>(); + isTypenameList.add(false); + return createCClassTypeUnion(t, env, Arrays.asList(type), isTypenameList); + } + + /** + * Creates a new LeftHandSideType from the given native class. + * + * @param nativeClass The class type. + * @param generics The value returned from {@link #toNativeLeftHandGenericUse()}. The native class and + * position will be passed in for you. + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromNativeCClassType(CClassType nativeClass, Renderer... generics) { + LeftHandGenericUseParameter[] rendered = new LeftHandGenericUseParameter[generics.length]; + for(int i = 0; i < rendered.length; i++) { + rendered[i] = generics[i].render(nativeClass, i); + } + return fromCClassType(new ConcreteGenericParameter(nativeClass, + LeftHandGenericUse.forNativeParameters(nativeClass, rendered), Target.UNKNOWN, null), Target.UNKNOWN, null); + } + + /** + * Creates a new LeftHandSideType from the given native types. + * + * @param type The native class type. + * @param lhgu The LeftHandGenericUse for this type, which contains only native classes. + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromNativeCClassType(CClassType type, LeftHandGenericUse lhgu) { + return fromCClassType(new ConcreteGenericParameter(type, lhgu, Target.UNKNOWN, null), Target.UNKNOWN, null); + } + + /** + * Creates a new LeftHandSideType from the given union of CClassTypes with no generics. + * + * @param t The code target. + * @param env The environment. + * @param classTypes The class types. + * @return A new LeftHandSideType + */ + public static LeftHandSideType fromCClassTypeUnion(Target t, Environment env, CClassType... classTypes) { + List pairs = new ArrayList<>(); + List isTypenameList = new ArrayList<>(); + List tempTypes = new ArrayList<>(Arrays.asList(classTypes)); + // `int | primitive` is just `primitive`, so walk through the list, and for each type that extends + // another type, remove it. + Iterator it = tempTypes.iterator(); + while(it.hasNext()) { + boolean remove = false; + CClassType subType = it.next(); + for(CClassType superType : tempTypes) { + if(subType == superType) { + continue; + } + if(InstanceofUtil.isInstanceof(subType.asLeftHandSideType(), superType.asLeftHandSideType(), env)) { + remove = true; + break; + } + } + if(remove) { + it.remove(); + } + } + for(CClassType type : tempTypes) { + pairs.add(new ConcreteGenericParameter(type, null, t, env)); + isTypenameList.add(false); + } + return createCClassTypeUnion(t, env, pairs, isTypenameList); + } + + /** + * Creates a new LeftHandSideType from the given list of CClassTypes and LeftHandGenericUse pairs. Each pair + * represents a single type in the type union. The LeftHangGenericUse in each pair may be null, but the CClassTypes + * may not, except when representing the none type. + *

+ * If any of the types in the union are {@code auto}, then simply {@code auto} is returned. + * + * @param t The code target. + * @param classTypes The type union types. + * @param isTypenameList A List of booleans representing if each value is a typename or not. + * @return A new LeftHandSideType + * @throws IllegalArgumentException If the classTypes list is empty. + */ + private static LeftHandSideType createCClassTypeUnion(Target t, Environment env, + List classTypes, + List isTypenameList) { + Objects.requireNonNull(classTypes); + if(classTypes.isEmpty()) { + throw new IllegalArgumentException("A LeftHandSideType object must contain at least one type"); + } + + String value = StringUtils.Join(classTypes, " | ", (pair) -> { + if(pair.getType() == null) { + return "none"; + } + return pair.toString(); + }); + for(ConcreteGenericParameter classType : classTypes) { + if(Auto.TYPE.equals(classType.getType())) { + if(Auto.LHSTYPE == null) { + // Bootstrapping problem, Auto.LHSTYPE calls us, and so is null at this point. So is ConcreteGenericParameter.AUTO. + List types = Arrays.asList(new ConcreteGenericParameter(Auto.TYPE, null, Target.UNKNOWN, null)); + List itl = Arrays.asList(false); + return new LeftHandSideType("auto", Target.UNKNOWN, env, types, itl, null, null); + } else { + return Auto.LHSTYPE; + } + } + } + return new LeftHandSideType(value, t, env, classTypes, isTypenameList, null, null); + } + + /** + * Given a LeftHandSideType object {@code type} that might be a type union containing type names, resolves each + * component of the type union into concrete types. Typenames can only be used in their defined context, and if they + * need to leak beyond that, must be resolved. This usually entails taking the generic parameters for the given call + * site, but might also involve using the inferredType or perhaps simply returning auto. If the value passed in is + * not a typename, it is simply returned, so this can be used in general, without first checking if it would need to + * be called. + * + * @param t The code target. + * @param env The environment. + * @param types The type to convert, which might be a type union (or not, if it isn't a typename) + * @param parameters The type parameters passed to the function. + * @param declaration The generic declaration for the function. + * @param inferredTypes The inferredTypes to use, in case the type parameter is not explicitly provided. The map + * maps from typename to inferredType. + * @return + */ + public static LeftHandSideType resolveTypeFromGenerics(Target t, Environment env, LeftHandSideType types, + GenericParameters parameters, GenericDeclaration declaration, Map inferredTypes) { + if(types == null) { + // Type is none, cannot have generics + return types; + } + LeftHandSideType[] lhst = new LeftHandSideType[types.getTypes().size()]; + for(int i = 0; i < lhst.length; i++) { + ConcreteGenericParameter type = types.getTypes().get(i); + LeftHandSideType inferredType = inferredTypes == null + ? Auto.LHSTYPE : inferredTypes.get(type.getType().getFQCN().getFQCN()); + LeftHandGenericUse lhgu = type.getLeftHandGenericUse(); + if(lhgu != null && lhgu.hasTypename()) { + // We need to create a new LHGU object with the typenames replaced. + List newParameters = new ArrayList<>(); + int k = 0; + for(LeftHandGenericUseParameter parameter : type.getLeftHandGenericUse().getParameters()) { + if(parameter.getValue().hasLeft()) { + // Just add it on + newParameters.add(parameter); + } else { + // Do the typename replacement + if(inferredType == null) { + newParameters.add(Auto.LHSTYPE.toNativeLeftHandGenericUse(type.getType(), k)); + } else { + newParameters.add(inferredType.toLeftHandGenericUse(type.getType(), t, env, + ConstraintLocation.LHS, k)); + } + } + k++; + } + lhgu = new LeftHandGenericUse(type.getType(), t, env, newParameters); + } + LeftHandSideType newType = LeftHandSideType.fromCClassType(new ConcreteGenericParameter( + type.getType(), lhgu, t, env), t, env); + newType.isTypeName = types.isTypenameList.get(i); + if(newType.isTypeName) { + newType.genericTypeName = type.getType().getFQCN().getFQCN(); + } + lhst[i] = resolveTypeFromGenerics(t, env, newType, parameters, declaration, inferredType); + } + return LeftHandSideType.fromTypeUnion(t, env, lhst); + } + + /** + * Given a LeftHandSideType object {@code type}, resolves this into a non-typename if it is a typename.Typenames can + * only be used in their defined context, and if they need to leak beyond that, must be resolved. This usually + * entails taking the generic parameters for the given call site, but might also involve using the inferredType or + * perhaps simply returning auto. If the value passed in is not a typename, it is simply returned, so this can be + * used in general, without first checking if it would need to be called. + * + * @param t The code target. + * @param env The environment. + * @param type The type to convert (or not, if it isn't a typename) + * @param parameters The type parameters passed to the function. + * @param declaration The generic declaration for the function. + * @param inferredType The inferredType to use, in case the type parameter is not explicitly provided. + * @return + */ + @SuppressWarnings("checkstyle:localvariablename") + public static LeftHandSideType resolveTypeFromGenerics(Target t, Environment env, LeftHandSideType type, + GenericParameters parameters, GenericDeclaration declaration, LeftHandSideType inferredType) { + if(type == null) { + // Type is none, cannot have generics + return type; + } + if(type.isTypeUnion()) { + LeftHandSideType[] lhst = new LeftHandSideType[type.getTypes().size()]; + for(int i = 0; i < lhst.length; i++) { + ConcreteGenericParameter _type = type.getTypes().get(i); + LeftHandSideType newType = LeftHandSideType.fromCClassType(_type, t, env); + newType.isTypeName = type.isTypenameList.get(i); + if(newType.isTypeName) { + newType.genericTypeName = _type.getType().getFQCN().getFQCN(); + } + lhst[i] = resolveTypeFromGenerics(t, env, newType, parameters, declaration, inferredType); + } + return LeftHandSideType.fromTypeUnion(t, env, lhst); + } + if(!type.isTypeName()) { + return type; + } + // Validate the parameters against the declaration, and then return the type of the correct parameter + ConstraintValidator.ValidateParametersToDeclaration(t, env, parameters, declaration, inferredType); + + if(parameters == null && inferredType == null) { + // Return auto for no type. This already passed, since ValidateParametersToDeclaration would have + // failed if this were null and either auto or the inferred type wasn't sufficient due to the constraints. + return Auto.LHSTYPE; + } + // It passes. Lookup the correct parameter based on the typename. + String typename = type.getTypename(); + for(int i = 0; i < declaration.getParameterCount(); i++) { + if(declaration.getConstraints().get(i).getTypeName().equals(typename)) { + // Found it + if(parameters != null) { + LeftHandSideType p = parameters.getParameters().get(i); + return p; + } else if(inferredType != null) { + return inferredType; + } + } + } + // Would be good to unit test for this, but this won't be able to happen generally in user + // classes. + throw new Error("Typename returned by native function is not in the GenericDeclaration!"); + } + + private boolean isTypeName; + + @ObjectHelpers.StandardField + private final List types; + private final List isTypenameList; + + @ObjectHelpers.StandardField + private String genericTypeName; + + @ObjectHelpers.StandardField + private boolean isVariadicType = false; + + private LeftHandSideType(String value, Target t, Environment env, List types, + List isTypenameList, String genericTypeName, LeftHandGenericUse genericTypeLHGU) { + super(value, ConstructType.CLASS_TYPE, t); + this.isTypenameList = isTypenameList; + if(types != null) { + isTypeName = false; + + // Sort the list with TreeSet first + Set tempSet = new TreeSet<>((o1, o2) -> { + String o1Index = o1 == null ? "none" : o1.toString(); + String o2Index = o2 == null ? "none" : o2.toString(); + return o1Index.compareTo(o2Index); + }); + tempSet.addAll(types); + this.types = new ArrayList<>(tempSet); + if(isTypeUnion()) { + for(ConcreteGenericParameter type : types) { + if(Auto.TYPE.equals(type.getType())) { + throw new CREIllegalArgumentException("auto type cannot be used in a type union", t); + } + } + } + this.genericTypeName = null; + } else { + this.types = new ArrayList<>(); + this.types.add(new ConcreteGenericParameter(CClassType.getFromGenericTypeName(genericTypeName, t), genericTypeLHGU, t, env)); + isTypeName = true; + this.genericTypeName = genericTypeName; + } + } + + @Override + public boolean isDynamic() { + return false; + } + + /** + * + * @return + */ + public List getTypes() { + return new ArrayList<>(types); + } + + public boolean isExtendedBy(CClassType type, Environment env) { + // TODO: Stopgap — asConcreteType won't error in practice now, but needs proper LeftHandSideType support later + return CClassType.doesExtend(env, type, this.asConcreteType(Target.UNKNOWN)); + } + + public boolean doesExtend(CClassType type, Environment env) { + return this.doesExtend(type.asLeftHandSideType(), env); + } + + public boolean doesExtend(LeftHandSideType type, Environment env) { + // TODO: Stopgap — asConcreteType won't error in practice now, but needs proper LeftHandSideType support later + return CClassType.doesExtend(env, this.asConcreteType(Target.UNKNOWN), type.asConcreteType(Target.UNKNOWN)); + } + + /** + * Returns true if this type represents a type union, that is, there is more than one class returned in the types. + * + * @return + */ + public boolean isTypeUnion() { + return types.size() > 1; + } + + /** + * Returns true if this represents a typename. A typename is a "fake" type that only exists in the scope of the + * given class or method, and cannot be resolved into a real type except by converting it based on the + * GenericDeclaration associated with it. Note that in general, you need to know if this is a read only or write + * only context, as the type returned will be different. + *

+ * Type unions are never typenames, though they may consist only of, or partially of other type unions. + * + * @return + */ + public boolean isTypeName() { + return isTypeName; + } + + /** + * If this was defined as a typename, returns the typename, null otherwise. + * + * @return + */ + public String getTypename() { + return this.genericTypeName; + } + + public String getSimpleName() { + return StringUtils.Join(types, " | ", pair -> pair.toSimpleString()); + } + + /** + * Returns an array of the set of interfaces that all of the underlying types implements.Note that if this is a type + * union, and not all types in the underlying types implement an interface, it is not included in this list. + * + * @param env + * @return + */ + public CClassType[] getTypeInterfaces(Environment env) { + if(!isTypeUnion()) { + return types.get(0).getType().getTypeInterfaces(env); + } + Set interfaces = new HashSet<>(Arrays.asList(types.get(0).getType().getTypeInterfaces(env))); + for(ConcreteGenericParameter subTypes : getTypes()) { + CClassType type = subTypes.getType(); + Iterator it = interfaces.iterator(); + while(it.hasNext()) { + CClassType iface = it.next(); + if(!Arrays.asList(type.getTypeInterfaces(env)).contains(iface)) { + it.remove(); + } + } + } + return interfaces.toArray(CClassType[]::new); + } + + /** + * Returns an array of the set of superclasses that all of the underlying types implements.Note that if this is a + * type union, and not all types in the underlying types implement a superclass, it is not included in this list. + * + * @param env + * @return + */ + public CClassType[] getTypeSuperclasses(Environment env) { + if(!isTypeUnion()) { + return types.get(0).getType().getTypeInterfaces(env); + } + Set superclasses = new HashSet<>(Arrays.asList(types.get(0).getType().getTypeSuperclasses(env))); + for(ConcreteGenericParameter subTypes : getTypes()) { + CClassType type = subTypes.getType(); + Iterator it = superclasses.iterator(); + while(it.hasNext()) { + CClassType iface = it.next(); + if(!Arrays.asList(type.getTypeSuperclasses(env)).contains(iface)) { + it.remove(); + } + } + } + return superclasses.toArray(CClassType[]::new); + } + + /** + * If and only if this was constructed in such a way that it could have been a CClassType to begin with, this + * function will return the CClassType. This is generally useful when converting initially from a CClassType, and + * then getting that value back, however, it can be used anyways if the parameters are such that it's allowed. In + * particular, this cannot be a type union, and the LeftHandGenericUse statement must be null. (There may be + * concrete generic parameters attached to the underlying CClassType though.) If these requirements are not met, a + * CREIllegalArgumentException is thrown. + * + * @param t + * @return + */ + public CClassType asConcreteType(Target t) throws CREIllegalArgumentException { + MSLog.StringProvider exMsg = () -> "Cannot use the type \"" + getSimpleName() + "\" in this context."; + ConcreteGenericParameter type = types.get(0); + if(type.getLeftHandGenericUse() != null && !type.getLeftHandGenericUse().getConstraints().isEmpty()) { + throw new CREIllegalArgumentException(exMsg.getString(), t); + } + return type.getType(); + } + + /** + * Returns true if the underlying type is a single type, and that type is void. + * + * @return + */ + public boolean isVoid() { + if(isTypeUnion()) { + return false; + } + return CVoid.TYPE.equals(types.get(0).getType()); + } + + /** + * Returns true if the underlying type is a single type, and that type is auto. + * + * @return + */ + public boolean isAuto() { + if(isTypeUnion()) { + return false; + } + return CClassType.AUTO.equals(types.get(0).getType()); + } + + public boolean isNull() { + if(isTypeUnion()) { + return false; + } + return CNull.TYPE.equals(types.get(0).getType()); + } + + /** + * This should ONLY be used when building native signatures, but otherwise behaves the same as + * {@link #toLeftHandGenericUse()} + * + * @return An equivalent LeftHandGenericUse statement. + */ + public LeftHandGenericUseParameter toNativeLeftHandGenericUse(CClassType forType, int parameterPosition) { + return toLeftHandGenericUse(forType, Target.UNKNOWN, null, ConstraintLocation.LHS, parameterPosition); + } + + public static interface Renderer { + + LeftHandGenericUseParameter render(CClassType forType, int parameterPosition); + } + + public Renderer toNativeLeftHandGenericUse() { + return new Renderer() { + @Override + public LeftHandGenericUseParameter render(CClassType forType, int parameterPosition) { + return toNativeLeftHandGenericUse(forType, parameterPosition); + } + }; + } + + /** + * Returns a LeftHandGenericUse statement.This works with both typename, and concrete types, not including type + * unions.The underlying constraints is an ExactType constraint. This will not work if the underlying type cannot be + * converted into a concrete type. + * + * @param forType The type that will contain this LHGU. + * @param t The code target, for exceptions + * @param env The environment. + * @param location The location this will be used at. + * @return An equivalent LeftHandGenericUse statement. + */ + public LeftHandGenericUseParameter toLeftHandGenericUse(CClassType forType, Target t, Environment env, + ConstraintLocation location, int parameterPosition) { + if(isTypeName) { + return new LeftHandGenericUseParameter(Either.right(new Pair<>(getTypename(), + forType.getGenericDeclaration().getConstraints().get(parameterPosition)))); + } else { + return new LeftHandGenericUseParameter(Either.left(new Constraints(t, location, new ExactTypeConstraint(t, this)))); + } + } + + /** + * Returns the naked type for each type in the type union. + * @param t + * @param env + * @return + */ + public LeftHandSideType getNakedType(Target t, Environment env) { + List newTypes = new ArrayList<>(); + for(ConcreteGenericParameter m : types) { + if(m.getType() == null) { + return null; + } else if(m.getType().equals(Auto.TYPE)) { + newTypes.add(Auto.LHSTYPE); + } else { + newTypes.add(m.getType().getNakedType(env).asLeftHandSideType()); + } + } + return LeftHandSideType.fromTypeUnion(t, env, newTypes.toArray(LeftHandSideType[]::new)); + } + + @Override + public Set getObjectModifiers() { + return EnumSet.of(ObjectModifier.FINAL); + } + + /** + * Returns a List of Sets of ObjectModifers for each underlying type in the union. + * + * @return + */ + public List> getTypeObjectModifiers() { + List> ret = new ArrayList<>(); + for(ConcreteGenericParameter type : types) { + ret.add(type.getType().getObjectModifiers()); + } + return ret; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + public CClassType[] getInterfaces() { + return CClassType.EMPTY_CLASS_ARRAY; + } + + @Override + public CClassType[] getSuperclasses() { + return new CClassType[]{Mixed.TYPE}; + } + + @Override + public String docs() { + return "Represents a left hand side type expression. This has LHS semantics, including supporting bounded" + + " generics and type unions."; + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object obj) { + return ObjectHelpers.DoEquals(this, obj); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + @Override + public String toString() { + return val(); + } + + public GenericParameters getGenericParameters() { + return null; + } + + @Override + public SourceType asVariadicType(Environment env) { + LeftHandSideType newType = new LeftHandSideType(val() + "...", getTarget(), env, types, isTypenameList, + genericTypeName, null); + newType.isVariadicType = true; + return newType; + } + + @Override + public boolean isVariadicType() { + return isVariadicType; + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/SourceType.java b/src/main/java/com/laytonsmith/core/constructs/SourceType.java new file mode 100644 index 0000000000..b1fa101f50 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/SourceType.java @@ -0,0 +1,26 @@ +package com.laytonsmith.core.constructs; + +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.natives.interfaces.Mixed; + +/** + * A SourceType is a type that can be represented in source code. + */ +public interface SourceType extends Mixed { + + /** + * Returns true if this type was defined as a variadic type (i.e. `string ...`). + * + * @return + */ + boolean isVariadicType(); + + /** + * For a non-variadic type, this returns a new instance as a variadic type (i.e. if this represents `string` then + * `string ...` is returned). + * + * @param env + * @return + */ + SourceType asVariadicType(Environment env); +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/ConcreteGenericParameter.java b/src/main/java/com/laytonsmith/core/constructs/generics/ConcreteGenericParameter.java new file mode 100644 index 0000000000..571baf9baf --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/ConcreteGenericParameter.java @@ -0,0 +1,146 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.PureUtilities.Either; +import com.laytonsmith.PureUtilities.Pair; +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import java.util.Objects; + +/** + * A ConcreteGenericParameter is a single, actual CClassType container, which optionally may have a LeftHandGenericUse + * attached with it. + */ +public class ConcreteGenericParameter { + + private final CClassType type; + private final LeftHandGenericUse lhgu; + private final Environment env; + + /** + * Creates a new ConcreteGenericParameter. The type may be null, representing the "none" type. If the type cannot + * contain parameters, but some where provided, or it can contain parameters, but not enough/too many were provided, + * an exception is thrown. + * + * @param type + * @param lhgu + * @param t + * @param env The environment. + * @throws CREGenericConstraintException If generic parameters were provided, but were erroneous according to the + * type. + */ + public ConcreteGenericParameter(CClassType type, LeftHandGenericUse lhgu, Target t, Environment env) { + if(type == null && lhgu != null) { + throw new CREGenericConstraintException("\"none\" type cannot have generic parameters.", t); + } + if(type != null && lhgu != null + && (type.getGenericDeclaration() == null || type.getGenericDeclaration().getParameterCount() < 1)) { + throw new CREGenericConstraintException(type.getSimpleName() + " cannot contain generic parameters.", t); + } + if(type != null && lhgu != null) { + ConstraintValidator.ValidateLHS(t, type, lhgu, env); + } + this.type = type; + if(type != null && type.getGenericDeclaration() != null && type.getTypeGenericParameters() == null && lhgu == null) { + // Go ahead and prefill the types with auto + GenericParameters.GenericParametersBuilder builder = GenericParameters.emptyBuilder(type); + for(Constraints c : type.getGenericDeclaration().getConstraints()) { + builder.addParameter(c.convertFromNull(t).getType()); + } + lhgu = builder.build(t, env).toLeftHandEquivalent(type, env); + } + this.lhgu = lhgu; + this.env = env; + } + + /** + * Constructs a new ConcreteGenericParameter from a native type. This doesn't require a target or environment, + * but only works with definitely native types. + * @param nativeType The type. + * @param nativeLHGU The generic parameters for the native type. + * @return + */ + public static ConcreteGenericParameter fromNativeType(CClassType nativeType, LeftHandGenericUse nativeLHGU) { + return new ConcreteGenericParameter(nativeType, nativeLHGU, Target.UNKNOWN, null); + } + + /** + * The ConcreteGenericParameter for the {@code auto} type. + */ + public static final ConcreteGenericParameter AUTO = fromNativeType(Auto.TYPE, null); + + public CClassType getType() { + return this.type; + } + + public LeftHandGenericUse getLeftHandGenericUse() { + if(this.type.getTypeGenericParameters() != null) { + return this.type.getTypeGenericParameters().toLeftHandGenericUse(); + } + return this.lhgu; + } + + public Pair getAsPair() { + return new Pair<>(this.type, this.lhgu); + } + + public LeftHandSideType asLeftHandSideType() { + return LeftHandSideType.fromCClassType(this, Target.UNKNOWN, env); + } + + /** + * Returns the individual parameter as an ExactTypeConstraint Constraints object. + * @return + */ + public LeftHandGenericUseParameter asLeftHandGenericUseParameter() { + Constraints constraints = new Constraints(Target.UNKNOWN, ConstraintLocation.RHS, + new ExactTypeConstraint(Target.UNKNOWN, asLeftHandSideType())); + return new LeftHandGenericUseParameter(Either.left(constraints)); + } + + @Override + public String toString() { + String typeString = "none"; + if(this.type != null) { + typeString = this.type.toString(); + } + return typeString + (this.lhgu == null ? "" : "<" + this.lhgu.toString() + ">"); + } + + public String toSimpleString() { + String typeString = "none"; + if(this.type != null) { + typeString = this.type.getSimpleName(); + } + return typeString + (this.lhgu == null ? "" : "<" + this.lhgu.toSimpleString() + ">"); + } + + @Override + public int hashCode() { + int hash = 7; + return hash; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final ConcreteGenericParameter other = (ConcreteGenericParameter) obj; + if(!Objects.equals(this.type, other.type)) { + return false; + } + return Objects.equals(this.lhgu, other.lhgu); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintLocation.java b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintLocation.java new file mode 100644 index 0000000000..257841744c --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintLocation.java @@ -0,0 +1,37 @@ +package com.laytonsmith.core.constructs.generics; + +/** + * This enum represents where in the code the Constraint type may be placed. + */ +public enum ConstraintLocation { + /** + * The definition point, that is, the <T> in class A<T> or + * >T< T method(){...} + */ + DEFINITION("generic type definition"), + /** + * The left hand side of a statement, either the left hand of an assignment, or the parameter definition + * of a function call. This can be a bit confusing sometimes, because you can have a left hand side value + * on the right hand side, particularly during typechecking, where the concrete runtime values may not be + * known. + */ + LHS("left hand type definition"), + /** + * The right hand side of a statement, either the right hand of an assignment, or the parameter sent to + * a function. + */ + RHS("right hand type definition"); + + /** + * The value here should flow with the sentence "at the location of the ..." + */ + private final String locationName; + + private ConstraintLocation(String locationName) { + this.locationName = locationName; + } + + public String getLocationName() { + return this.locationName; + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintToConstraintValidator.java b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintToConstraintValidator.java new file mode 100644 index 0000000000..c7a8893def --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintToConstraintValidator.java @@ -0,0 +1,47 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.constructs.generics.constraints.UnboundedConstraint; +import com.laytonsmith.core.constructs.generics.constraints.UpperBoundConstraint; +import com.laytonsmith.core.constructs.generics.constraints.LowerBoundConstraint; +import com.laytonsmith.core.constructs.generics.constraints.ConstructorConstraint; +import com.laytonsmith.core.constructs.generics.constraints.VariadicTypeConstraint; + +public interface ConstraintToConstraintValidator { + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(ConstructorConstraint lhs); + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(ExactTypeConstraint lhs); + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(LowerBoundConstraint lhs); + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(UpperBoundConstraint lhs); + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(UnboundedConstraint lhs); + /** + * If "this" is the class definition, then lhs is the LHS of the statement. + * @param lhs + * @return + */ + Boolean isWithinBounds(VariadicTypeConstraint lhs); +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintValidator.java b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintValidator.java new file mode 100644 index 0000000000..f507ae86ee --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/ConstraintValidator.java @@ -0,0 +1,283 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import com.laytonsmith.core.exceptions.CRE.CREIllegalArgumentException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +public final class ConstraintValidator { + + private ConstraintValidator() { + } + + /** + * Validates and returns a list of the typenames for a list of Constraints objects. + * + * @param parameters The Constraints objects to validate. + * @param t The code target, for exceptions + * @return The typenames for each parameters, for instance T, U in T extends number, U. + * @throws CREGenericConstraintException If validation fails. + */ + public static List ValidateDefinition(List parameters, Target t) + throws CREGenericConstraintException { + List typenames = new ArrayList<>(); + for(int i = 0; i < parameters.size(); i++) { + Constraints c = parameters.get(i); + if(c.isVariadic() && i != parameters.size() - 1) { + throw new CREGenericConstraintException("Variadic generic types can only be the last parameter.", t); + } + + typenames.add(ValidateDefinition(new TreeSet<>(c.getInDefinitionOrder()), t)); + } + return typenames; + } + + /** + * Validates and returns the typename for a set of Constraint objects. + * + * @param constraints The constraint object(s) to validate + * @param t The code target, for exceptions + * @return The typename, for instance T in T extends number. + * @throws CREGenericConstraintException If validation fails. + */ + public static String ValidateDefinition(SortedSet constraints, Target t) + throws CREGenericConstraintException { + String typename = null; + for(Constraint c : constraints) { + if(typename == null) { + typename = c.getTypeName(); + } else if(!typename.equals(c.getTypeName())) { + throw new CREGenericConstraintException("Multiple constraints in the same parameter must be named" + + " with the same type name.", t); + } + if(!c.validLocations().contains(ConstraintLocation.DEFINITION)) { + throw new CREGenericConstraintException("The " + c.getConstraintName() + " constraint type cannot be" + + " used at the location of the " + ConstraintLocation.DEFINITION.getLocationName(), + c.getTarget()); + } + if(c.isWildcard()) { + throw new CREGenericConstraintException("Constraints cannot use wildcards at the definition site.", + c.getTarget()); + } + for(Constraint cc : constraints) { + // Check for duplicate constraints + if(c == cc) { + continue; + } + if(c.equals(cc)) { + throw new CREGenericConstraintException("Duplicate constraint found. One constraint" + + " defined at " + c.getTarget() + ", the other constraint at " + cc.getTarget(), t); + } + } + } + if(constraints.size() == 1) { + // Only 1 constraint is always valid + return typename; + } + // TODO: Need to write the constraint error solver. + // This will require additional work to ensure that for instance, a type does not have an impossible + // upper and lower bound, among others. + throw new CREGenericConstraintException("Multiple constraints are not yet supported.", t); + } + + /** + * Validates the RHS against the LHS of a definition. This should be called with null if no generic parameters were + * defined, as that is not always allowed, depending on the ClassType, and this case is accounted for. It is assumed + * that the LHS fits the constraints defined in the constraint definition. + * + * @param t The code target, for exceptions. + * @param type The LHS type. + * @param genericParameters The generic parameters. + * @param env The environment. + */ + public static void ValidateLHS(Target t, CClassType type, LeftHandGenericUse genericParameters, Environment env) { + ValidateLHS(t, type, genericParameters == null ? null : genericParameters.getConstraints(), env); + } + + /** + * Validates the RHS against the LHS of a definition. This should be called with null if no generic parameters were + * defined, as that is not always allowed, depending on the ClassType, and this case is accounted for. It is assumed + * that the LHS fits the constraints defined in the constraint definition. + * + * @param t + * @param type + * @param c + * @param env + * @throws CREGenericConstraintException + */ + public static void ValidateLHS(Target t, CClassType type, List c, Environment env) + throws CREGenericConstraintException { + GenericDeclaration dec = type.getGenericDeclaration(); + if(dec == null) { + // Nothing to validate here + if(c != null && !c.isEmpty()) { + // However, they provided something anyways... + throw new CREGenericConstraintException(type.getFQCN().getFQCN() + " does not define generic parameters," + + " but they were provided anyways", t); + } + return; + } + List declarationConstraints = dec.getConstraints(); + if(c == null) { + // If nothing was passed in, then this was declared without parameters, and they would be inferred ones. + // This is generally fine, except when they're specifically required due to the class definition requiring + // them, such as with the ConstructorConstraint. Therefore, we simply loop through the parameters, and try to + // infer them, and if they all pass, we're good. + for(Constraints cc : declarationConstraints) { + cc.convertFromDiamond(t); + } + } else { + ValidateLHStoLHS(t, c, declarationConstraints, env); + } + } + + public static void ValidateRHStoLHS(Constraints declarationConstraints, Target t, LeftHandSideType type, Environment env) { + List exactType = new ArrayList<>(); + exactType.add(new Constraints(t, ConstraintLocation.RHS, new ExactTypeConstraint(t, type))); + ValidateLHStoLHS(t, exactType, Arrays.asList(declarationConstraints), env); + } + + /** + * Checks that the given constraints are within the bounds of the other constraints. + * + * @param t + * @param checkIfTheseConstraints These are the constraints to check to see if they are within the bounds of the + * other constraints. + * @param areWithinBoundsOfThese These are the constraints to check against. These can be thought of as the + * "definition" even though that's not the case using previously defined terminology. + * @param env + * @throws CREGenericConstraintException + */ + @SuppressWarnings("null") + public static void ValidateLHStoLHS(Target t, List checkIfTheseConstraints, List areWithinBoundsOfThese, Environment env) + throws CREGenericConstraintException { + if((checkIfTheseConstraints == null || checkIfTheseConstraints.isEmpty()) + && (areWithinBoundsOfThese == null || areWithinBoundsOfThese.isEmpty())) { + // This is ok, nothing to validate on either side + return; + } + if(areWithinBoundsOfThese != null && checkIfTheseConstraints == null) { + throw new RuntimeException("Missing constraints."); + } + + boolean isVariadic; + if(areWithinBoundsOfThese.get(areWithinBoundsOfThese.size() - 1).isVariadic()) { + if(checkIfTheseConstraints.size() < areWithinBoundsOfThese.size() - 1) { + throw new CREGenericConstraintException("Expected at least " + (areWithinBoundsOfThese.size() - 1) + " parameter(s), but found" + + " only " + checkIfTheseConstraints.size(), t); + } + isVariadic = true; + } else { + if(checkIfTheseConstraints.size() != areWithinBoundsOfThese.size()) { + throw new CREGenericConstraintException("Expected " + areWithinBoundsOfThese.size() + " parameter(s), but found" + + " " + checkIfTheseConstraints.size(), t); + } + isVariadic = false; + } + for(int i = 0; i < checkIfTheseConstraints.size(); i++) { + Constraints definition; + if(isVariadic) { + if(i >= areWithinBoundsOfThese.size()) { + definition = areWithinBoundsOfThese.get(areWithinBoundsOfThese.size() - 1); + } else { + definition = areWithinBoundsOfThese.get(i); + } + } else { + definition = areWithinBoundsOfThese.get(i); + } + + Constraints lhs = checkIfTheseConstraints.get(i); + // Check that the LHS fits the bounds of the definition + List errors = new ArrayList<>(); + if(!definition.withinBounds(lhs, errors, env)) { + throw new CREGenericConstraintException("The constraint " + lhs.toString() + " does not fit within the" + + " bounds " + definition.toString() + ": " + + StringUtils.Join(errors, "\n"), t); + } + } + } + + /** + * Validates a parameter set against a given parameter declaration.This ensures that the parameters passed in + * conform in size and type to the given constraints of the definition. + * + * @param t The code target, used in case of failure to throw a CREGenericConstraintException. + * @param env The environment. + * @param parameters The parameters. + * @param declaration The declaration. + * @param inferredType If the parameters are not provided, the inferred type provides outside context to decide what + * the specified type should be. This is used in place of auto when parameters are not provided. Note that + * parameters always take precedence over this type. This should map to the values of the GenericDeclaration, that + * is, if the signature is {@code T function(U @value)}, and the GenericDeclaration defines {@code T, U}, and the + * call site looks like {@code string @s = function(1);}, then the inferredTypes would be + * {@code string, int}. (While the GenericParameters would still be {@code mixed, number}, and in this case, the + * inferredTypes would be unused.) This parameter may be null, in which case auto will be inferred, but this should + * in general mean that the return value is not used, though the type parameter may be used internally by the + * function, which may cause an error if it is auto. + * @throws CREIllegalArgumentException In the case that the inferred type cannot be converted into a concrete + * CClassType, this is thrown. This can only happen in the case that the parameters are null AND the inferred LHS + * types are incapable of being converted to a concrete type. For instance, if the inferred type were {@code int}, + * this would never happen, but if it were {@code int | string}, and the generics parameters are not specified, then + * it would be thrown, because type unions cannot be converted into a CClassType (and concrete types are required + * for generic parameters, no matter how they get passed in.) + */ + public static void ValidateParametersToDeclaration(Target t, Environment env, + GenericParameters parameters, GenericDeclaration declaration, LeftHandSideType inferredType) { + if(declaration == null) { + if(parameters != null) { + throw new CREGenericConstraintException("No generics are defined here, unexpected generic parameters" + + " provided.", t); + } else { + // No parameters, no declaration, nothing to validate. + return; + } + } + if(parameters == null) { + // Everything is auto, though this doesn't inherently work everywhere, so just fill it in with auto + // based on the parameter count, then do the validation normally. + GenericParameters.GenericParametersBuilder builder = GenericParameters.emptyBuilder((CClassType) null); + for(int i = 0; i < declaration.getParameterCount(); i++) { + LeftHandSideType type = inferredType == null ? Auto.LHSTYPE : inferredType; + builder.addParameter(type); + } + parameters = builder.buildWithoutValidation(); + } + + if(parameters.getParameters().size() != declaration.getParameterCount()) { + throw new CREGenericConstraintException(StringUtils.PluralTemplateHelper(declaration.getParameterCount(), + "Expected %d generic parameter", "Expected %d generic parameters") + " to be provided, but" + + " instead " + + StringUtils.PluralTemplateHelper(parameters.getParameters().size(), + "%d was found.", "%d were found."), t); + } + + for(int i = 0; i < declaration.getParameterCount(); i++) { + Constraints c = declaration.getConstraints().get(i); + LeftHandSideType param = parameters.getParameters().get(i); + if(!c.withinBounds(param, env)) { + throw new CREGenericConstraintException("Generic parameter at location " + i + + " does not satisfy the constraints: " + c.toString(), t); + } + } + } + + public static void ValidateTypename(String typename, Target t) throws CREGenericConstraintException { + String regex = "[a-zA-Z][a-zA-Z0-9_]*|\\?"; + if(!typename.matches(regex)) { + throw new CREGenericConstraintException("Typenames must match the regex " + regex + + " but found \"" + typename + "\"", t); + } + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/Constraints.java b/src/main/java/com/laytonsmith/core/constructs/generics/Constraints.java new file mode 100644 index 0000000000..79e3c47d17 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/Constraints.java @@ -0,0 +1,553 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.constructs.generics.constraints.UnboundedConstraint; +import com.laytonsmith.core.constructs.generics.constraints.UpperBoundConstraint; +import com.laytonsmith.core.constructs.generics.constraints.LowerBoundConstraint; +import com.laytonsmith.core.constructs.generics.constraints.ConstructorConstraint; +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.ObjectHelpers.StandardField; +import com.laytonsmith.core.FullyQualifiedClassName; +import com.laytonsmith.core.compiler.CompilerEnvironment; +import com.laytonsmith.core.compiler.CompilerWarning; +import com.laytonsmith.core.compiler.FileOptions; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.constraints.VariadicTypeConstraint; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CRECastException; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * A Constraints object contains the full list of Constraints for a given type parameter. + */ +@SuppressWarnings({"RedundantIfStatement", "ConstantConditions"}) +public class Constraints implements Iterable { + + @StandardField + private final SortedSet constraints; + @StandardField + private final String typename; + + private final boolean isVariadic; + + private final List unorderedConstraints; + + /** + * Constructs a new constraint object.Note that if this is being used on the LHS, no validation is done + * @param t The code target + * @param location The location of the Constraints + * @param constraints The constraints. This is an unordered list, but they will be normalized into their + * natural order. + */ + public Constraints(Target t, ConstraintLocation location, Constraint... constraints) { + this.unorderedConstraints = Arrays.asList(constraints); + this.constraints = new TreeSet<>(unorderedConstraints); + if(location == ConstraintLocation.RHS) { + if(constraints.length != 1 || !(constraints[0] instanceof ExactTypeConstraint)) { + throw new CREGenericConstraintException("Constraints (other than a single ExactType constraint)" + + " cannot be used on the RHS. This definition contains " + constraints.length + " " + + " constraint(s), of type(s): " + StringUtils.Join(constraints, ", ", + (item) -> item.getClass().toString()), t); + } + } + if(location == ConstraintLocation.DEFINITION) { + typename = ConstraintValidator.ValidateDefinition(this.constraints, t); + } else { + typename = "?"; + } + boolean isVariadic = false; + for(Constraint c : constraints) { + if(c instanceof VariadicTypeConstraint + || (c instanceof ExactTypeConstraint etc + && etc.getType() != null + && etc.getType().isVariadicType())) { + if(isVariadic) { + throw new CREGenericConstraintException("Only one variadic type definition may be in the parameter", t); + } + isVariadic = true; + } + } + this.isVariadic = isVariadic; + } + + public int size() { + return constraints.size(); + } + + /** + * Returns the name of the type. T for instance, or ? if this is a wildcard (defined on LHS). + * @return The typename + */ + public String getTypeName() { + return typename; + } + + /** + * Works like toString, but uses the class's simple name. + * @return + */ + public String toSimpleString() { + StringBuilder b = new StringBuilder(); + boolean doComma = false; + for(Constraint c : constraints) { + if(doComma) { + b.append(" & "); + } + doComma = true; + b.append(c.toSimpleString()); + } + return b.toString(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + boolean doComma = false; + for(Constraint c : constraints) { + if(doComma) { + b.append(" & "); + } + doComma = true; + b.append(c.toString()); + } + return b.toString(); + } + + /** + * Validates that the given set of Constraints is within the bounds of these contraints. This can be used to validate + * LHS against the class definition.Use {@link #withinBounds(CClassType, LeftHandGenericUse, Environment)} to validate RHS + * against LHS. + * @param lhs The other, presumably subtype to compare against. + * @param errors The ongoing list of errors with this statement. + * @param env The environment. + * @return If the provided constraints are within the bounds of these constraints. + */ + public boolean withinBounds(Constraints lhs, List errors, Environment env) { + for(Constraint t : constraints) { + boolean oneIsTrue = false; + for(Constraint c : lhs) { + Boolean res = t.isWithinConstraint(c, env); + if(res != null) { + if(res) { + oneIsTrue = true; + } else { + errors.add("The LHS constraint " + c + " does not" + + " suit the constraint defined on the class " + t); + } + } + } + if(!oneIsTrue) { + errors.add("The class defines the constraint " + t + ", but no constraints defined on the LHS suit" + + " this constraint."); + } + } + return errors.isEmpty(); + } + + /** + * Validates that this concrete type (and perhaps the concrete type's generics) fit within the boundary + * specified in the LHS constraint.This is used to validate the RHS against the LHS. Use + * {@link #withinBounds(Constraints, List, Environment)} to validate the LHS against the definition. + * @param type The type to check. + * @param rhsGenerics The LHS generics that are associated with the RHS. + * @param env + * @return If the specified RHS types passed in fit within the bounds of these Constraints + */ + public boolean withinBounds(LeftHandSideType type, Environment env) { + for(Constraint c : constraints) { + if(!c.isWithinConstraint(type, env)) { + return false; + } + } + return true; + } + + /** + * Returns whether ALL of this set of constraints can support type unions on the LHS. Note that type unions + * can never be used on the RHS. This is generally only used when validating LHS against LHS. + * @return + */ + public boolean supportsTypeUnions() { + for(Constraint c : constraints) { + if(!c.supportsTypeUnions()) { + return false; + } + } + return true; + } + + /** + * Given that this is the constraints on the LHS, returns the ExactType value that should be used on the RHS if + * the diamond operator was used. Not all Constraints support this, so this might throw an exception. + * @return The most narrow ExactTypeConstraint that suits these constraints, if it's possible to do so. + */ + public ExactTypeConstraint convertFromDiamond(Target t) throws CREGenericConstraintException { + // Diamond operator can currently only be used in simple cases, though we anyways check for definitely + // wrong cases. + ExactTypeConstraint type = null; + for(Constraint c : constraints) { + ExactTypeConstraint newType = c.convertFromDiamond(t); + if(type == null) { + type = newType; + } else { + throw new CREGenericConstraintException("Cannot infer generic type from LHS, please explicitely define" + + " the RHS generic parameters.", t); + } + } + if(type == null) { + throw new CREGenericConstraintException("Cannot infer generic type from LHS, please explicitely define the" + + " RHS generic parameters.", t); + } + return type; + } + + /** + * If the constraint is null, this returns the appropriate auto type. This may not pass, if auto is not valid + * for the given constraints. + * @return + */ + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + ExactTypeConstraint type = null; + for(Constraint c : constraints) { + ExactTypeConstraint newType = c.convertFromNull(t); + if(type == null) { + type = newType; + } else { + throw new CREGenericConstraintException("Cannot infer generic type from LHS, please explicitely define" + + " the RHS generic parameters.", t); + } + } + if(type == null) { + throw new CREGenericConstraintException("Cannot infer generic type from LHS, please explicitely define the" + + " RHS generic parameters.", t); + } + return type; + } + + /** + * Given a string such as ? extends array<number> & new ?(), parses it into a Constraints[] + * object. Note that the outer angle brackets should not be provided. + * + * @param constraintDefinition The constraint definition, without the angle brackets + * @param location The location the constraints are being defined in. This varies behavior slightly. + * @param declarationConstraints The type of the declaration, if location isn't DEFINITION. + * @param t The code target, for exceptions + * @param env The environment + * @return An array of Constraints, each one representing a single parameter. + */ + public static Constraints[] BuildFromString(FileOptions fileOptions, + String constraintDefinition, ConstraintLocation location, + List declarationConstraints, Target t, Environment env) { + int bracketStack = 0; + int parenthesisStack = 0; + constraintDefinition = constraintDefinition.replaceAll("\n", " "); + constraintDefinition = constraintDefinition.replaceAll("\r", " "); + constraintDefinition = constraintDefinition.replaceAll(" +", " "); + List constraintsS = new ArrayList<>(); + + StringBuilder buf = new StringBuilder(); + + int declarationCount = 0; + + for(Character c : constraintDefinition.toCharArray()) { + if(c == '<') { + bracketStack++; + } else if(c == '>') { + bracketStack--; + } else if(c == '(') { + parenthesisStack++; + } else if(c == ')') { + parenthesisStack--; + } else if(c == ',' && bracketStack == 0 && parenthesisStack == 0) { + constraintsS.add(GetConstraints(fileOptions, buf.toString(), t, location, declarationConstraints == null + ? null : declarationConstraints.get(declarationCount++), env)); + buf = new StringBuilder(); + continue; + } + buf.append(c); + } + + constraintsS.add(GetConstraints(fileOptions, buf.toString(), t, location, declarationConstraints == null + ? null : declarationConstraints.get(declarationCount++), env)); + + return constraintsS.toArray(Constraints[]::new); + } + + private static Constraints GetConstraints(FileOptions fileOptions, String s, Target t, ConstraintLocation location, + Constraints declarationConstraints, + Environment env) { + int bracketStack = 0; + int parenthesisStack = 0; + List constraints = new ArrayList<>(); + StringBuilder buf = new StringBuilder(); + boolean endOfConstraint = false; + for(Character c : s.toCharArray()) { + if(c == '(') { + parenthesisStack++; + } else if(c == ')') { + parenthesisStack--; + } else if(c == '<') { + bracketStack++; + } else if(c == '>') { + bracketStack--; + if(bracketStack == 0 && parenthesisStack == 0) { + // We've rebalanced, so this should be the end of the constraint + endOfConstraint = true; + buf.append(c); + continue; + } + } else if(bracketStack == 0 && c == '&') { + // end of constraint, process it + constraints.add(GetConstraint(fileOptions, buf.toString(), t, location, declarationConstraints, env)); + endOfConstraint = false; + buf = new StringBuilder(); + continue; + } + if(endOfConstraint && !Character.isWhitespace(c)) { + throw new CREGenericConstraintException("Improperly formatted generic statement", t); + } + buf.append(c); + } + + constraints.add(GetConstraint(fileOptions, buf.toString(), t, location, declarationConstraints, env)); + + return new Constraints(t, location, constraints.toArray(Constraint[]::new)); + } + + /** + * Parses a constraint from a string. This is only meant as a stopgap measure until the + * compiler is updated. + * @param fileOptions The file options. + * @param s The string to parse. + * @param t The code target. + * @param location The location of these constraints. + * @param declarationConstraints The Constraints for the relevant definition. + * Must be provided if location is not definition. + * @param env The environment. + * @return + * @throws ClassNotFoundException + */ + /*package*/ static Constraint GetConstraint(FileOptions fileOptions, String s, Target t, + ConstraintLocation location, Constraints declarationConstraints, Environment env) { + // Now we know we only have one constraint to process + s = s.trim(); + String name = ""; + String keyword = null; + LeftHandSideType clazz; + boolean inName = true; + boolean inKeyword = false; + boolean isNewConstraint = false; + int subGenericStack = 0; + int newParenthesisStack = 0; + StringBuilder buf = new StringBuilder(); + for(Character c : s.trim().toCharArray()) { + if(c == '<') { + subGenericStack++; + } + if(c == '>') { + subGenericStack--; + buf.append(c); + continue; + } + if(subGenericStack > 0) { + buf.append(c); + continue; + } + if(!isNewConstraint && inName && Character.isWhitespace(c)) { + name = buf.toString(); + buf = new StringBuilder(); + if("new".equals(name)) { + isNewConstraint = true; + name = ""; + continue; + } + inKeyword = true; + inName = false; + continue; + } + + if(inKeyword && Character.isWhitespace(c)) { + keyword = buf.toString(); + buf = new StringBuilder(); + inKeyword = false; + continue; + } + if(isNewConstraint && c == '(') { + name = buf.toString().trim(); + newParenthesisStack++; + buf = new StringBuilder(); + continue; + } + if(isNewConstraint && c == ')') { + newParenthesisStack--; + if(newParenthesisStack == 0) { + List types = GetNewTypes(fileOptions, buf.toString(), t, env); + return new ConstructorConstraint(t, name, types); + } + } + buf.append(c); + } + if(!inName && !inKeyword) { + // Now buf contains the class, which may need additional parsing + clazz = ParseClassType(fileOptions, buf.toString(), t, env); + if("extends".equals(keyword)) { + return new UpperBoundConstraint(t, name, clazz); + } else if("super".equals(keyword)) { + return new LowerBoundConstraint(t, name, clazz); + } + } + if(inName && keyword == null) { + // Unbounded or Type + if(location == ConstraintLocation.DEFINITION) { + if(fileOptions != null) { + try { + CClassType.get(FullyQualifiedClassName.forName(buf.toString(), t, env), t); + // This passed. It will still work just fine, this is an UnboundedConstraint + // though, not an ExactTypeConstraint, which cannot be used in the Definition site. However, + // this will hide the name of the real type, which should be warned against + CompilerWarning warning + = new CompilerWarning("Typename overrides a real type, which may be confusing.", + t, FileOptions.SuppressWarning.GenericTypeOverrides); + env.getEnv(CompilerEnvironment.class).addCompilerWarning(fileOptions, warning); + } catch(CRECastException ex) { + // Ok + } catch(ClassNotFoundException ex) { + // Ok — type doesn't exist, not a real type override + } + } + if(buf.toString().endsWith("...")) { + return new VariadicTypeConstraint(t, buf.toString().substring(0, buf.toString().length() - 3)); + } else { + return new UnboundedConstraint(t, buf.toString()); + } + } else { + String typename = buf.toString(); + if("?".equals(typename)) { + return ExactTypeConstraint.AsUnboundedWildcard(t, declarationConstraints); + } else { + clazz = ParseClassType(fileOptions, typename, t, env); + return new ExactTypeConstraint(t, clazz); + } + } + } + throw new CREGenericConstraintException("Malformed generic parameters", t); + } + + /** + * This will contain the types as comma separated fields. (Not including the outer parenthesis.) + */ + private static List GetNewTypes(FileOptions fileOptions, String s, Target t, Environment env) { + List list = new ArrayList<>(); + int bracketStack = 0; + int parenthesisStack = 0; + StringBuilder buf = new StringBuilder(); + for(Character c : s.toCharArray()) { + if(bracketStack == 0 && parenthesisStack == 0 && c == ',') { + list.add(ParseClassType(fileOptions, buf.toString(), t, env)); + buf = new StringBuilder(); + continue; + } + if(c == '<') { + bracketStack++; + } else if(c == '>') { + bracketStack--; + } else if(c == '(') { + parenthesisStack++; + } else if(c == ')') { + parenthesisStack--; + } + buf.append(c); + } + + if(!buf.isEmpty()) { + list.add(ParseClassType(fileOptions, buf.toString(), t, env)); + } + return list; + } + + @SuppressWarnings("null") + private static LeftHandSideType ParseClassType(FileOptions fileOptions, String s, Target t, Environment env) { + s = s.trim(); + CClassType nakedType = null; + LeftHandGenericUse lhgu = null; + boolean inLHS = false; + int bracketStack = 0; + StringBuilder buf = new StringBuilder(); + for(Character c : s.toCharArray()) { + if(c == '<') { + if(!inLHS) { + nakedType = CClassType.getNakedClassType(FullyQualifiedClassName.forName(buf.toString(), t, env), env); + buf = new StringBuilder(); + } + inLHS = true; + bracketStack++; + } + if(c == '>') { + bracketStack--; + if(bracketStack == 0) { + buf.append(c); + lhgu = new LeftHandGenericUse(nakedType, t, env, BuildFromString(fileOptions, buf.toString().trim() + .replaceAll("<(.*)>", "$1"), ConstraintLocation.LHS, + nakedType.getGenericDeclaration().getConstraints(), t, env)); + buf = new StringBuilder(); + continue; + } + } + buf.append(c); + } + if(!inLHS) { + lhgu = null; + } + + if(nakedType == null) { + nakedType = CClassType.getNakedClassType(FullyQualifiedClassName.forName(buf.toString(), t, env), env); + } + + return LeftHandSideType.fromCClassType(new ConcreteGenericParameter(nakedType, lhgu, t, env), t, env); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object o) { + return ObjectHelpers.DoEquals(this, o); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + @Override + public Iterator iterator() { + return constraints.iterator(); + } + + /** + * For testing and other meta purposes, it may be useful to get these as an unordered list in their original + * declaration order. Note that this list is not used in the equals comparison, but contains the same entries. + * @return The Constraints, in a List in the order they were defined. + */ + public List getInDefinitionOrder() { + return new ArrayList<>(this.unorderedConstraints); + } + + /** + * Returns true if this Constraints parameter represents a variadic type. (That is, does it contain a + * {@link VariadicTypeConstraint} Constraint in it.) + * @return + */ + public boolean isVariadic() { + return this.isVariadic; + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/GenericDeclaration.java b/src/main/java/com/laytonsmith/core/constructs/generics/GenericDeclaration.java new file mode 100644 index 0000000000..9eeeb3972e --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/GenericDeclaration.java @@ -0,0 +1,80 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.ObjectHelpers.StandardField; +import com.laytonsmith.core.constructs.Target; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@StandardField +public class GenericDeclaration { + + private final List constraints; + + public GenericDeclaration(Target t, Constraints... constraints) { + this.constraints = Arrays.asList(constraints); + } + + /** + * Returns a list of the Constraints objects. Each Constraints object represents a single type parameter, though + * itself can contain multiple individual Constraint objects. + * @return + */ + public List getConstraints() { + return new ArrayList<>(constraints); + } + + /** + * Returns the Constraints object at the specified location. Equivalent to {@code getConstraints().get(location)}. + * + * @param location The parameter location, 0 indexed. + * @return The Constraints object governing the given parameter. + */ + public Constraints getParameter(int location) { + return constraints.get(location); + } + + /** + * Returns the number of parameters in this declaration. + * @return + */ + public int getParameterCount() { + return constraints.size(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + boolean joinComma = false; + for(Constraints c : constraints) { + if(joinComma) { + b.append(", "); + } + joinComma = true; + boolean join = false; + for(Constraint cc : c) { + if(join) { + b.append(" & "); + } + join = true; + b.append(cc.toString()); + } + } + return b.toString(); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object that) { + return ObjectHelpers.DoEquals(this, that); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/GenericParameters.java b/src/main/java/com/laytonsmith/core/constructs/generics/GenericParameters.java new file mode 100644 index 0000000000..d3f5830ad0 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/GenericParameters.java @@ -0,0 +1,315 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.ObjectHelpers.StandardField; +import com.laytonsmith.core.compiler.signature.FunctionSignature; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents the RHS of a class type definition with generics. For instance, in the statement + * new A<B>, this represents B. In general, this contains only concrete classes, however these + * classes themselves may have generic parameters, in which case they will contain the LHS information for those + * parameters. However, at a top line level, everything maps to concrete class types. + *

+ * Note that in general, it's impossible to construct an instance of this with no parameters, and this is intentional. + * For instances of classes without generics, or those with generics, but whose instance used diamond inheritance, these + * are handled by representing the GenericParameters object with null, and requires special handling in general anyways. + */ +@StandardField +public final class GenericParameters { + + private GenericDeclaration genericDeclaration; + private final List parameters; + + /** + * When representing MethodScript generics in Java, and there is an inheritance chain with generics at each step, + * they have to be passed up the chain appropriately.For instance, given {@code class A} and + * {@code class B extends A}, then construction of the instance B will be passed the parameters for both U + * and T. However, the superclass, class A, also needs to know about parameter T. This method can help select the + * subset of parameters which needs to be passed to the superclass, by providing the selected parameter names, which + * will be pulled from this collection of parameters in order, and then returned as a new GenericParameters object, + * which can then be passed to the superclass (which may do further processing of the parameters to pass to any + * further direct super classes). + * + * @param superDeclaration The declaration object of the super class. + * @param parameters The names of the parameters that you wish to pull from the passed in parameters. They will be + * returned in the order selected. + * @return A subset of the parameters. + */ + public GenericParameters subset(GenericDeclaration superDeclaration, String... parameters) { + GenericParameters newParams = new GenericParameters(); + // Convert the parameter names to places + for(int i = 0; i < parameters.length; i++) { + String p = parameters[i]; + for(int j = 0; j < superDeclaration.getParameterCount(); j++) { + if(p.equals(superDeclaration.getConstraints().get(j).getTypeName())) { + newParams.parameters.add(this.parameters.get(j)); + } + } + } + newParams.genericDeclaration = superDeclaration; + return newParams; + } + + public static final class GenericParametersBuilder { + + private final GenericParameters p; + + private GenericParametersBuilder(GenericParameters p, GenericDeclaration forType) { + this.p = p; + p.genericDeclaration = forType; + } + + /** + * Adds a new parameter.Each parameter consists of a CClassType, and optionally a LeftHandGenericUse.For + * instance, in the statement new A<B<? extends C>> where A is the class being + * constructed, with signature class A<T> and B is a concrete class itself with a single + * template parameter, and C being another class, then this method would be called with the parameters + * B and a new instance of the LeftHandGenericUse class representing the constraint + * ? extends C. + * + * @param type The concrete class type + * @param genericStatement The LHS generic statement for this parameter. This may be null if the type did not + * include a generic statement. + * @param env The environment + * @param t The code target + * @return this, for easy chaining. Use build() to construct the final object. + * @throws CREGenericConstraintException If the generic statement does not validate against the type. + */ + public GenericParametersBuilder addParameter(CClassType type, LeftHandGenericUse genericStatement, Environment env, Target t) { + return addParameter(new ConcreteGenericParameter(type, genericStatement, t, env)); + } + + /** + * Adds a new ConcreteGenericParameter. + * + * @param type + * @return + */ + public GenericParametersBuilder addParameter(ConcreteGenericParameter type) { + return addParameter(type.asLeftHandSideType()); + } + + /** + * Add a LeftHandSideType parameter. + * + * @param type + * @return + */ + public GenericParametersBuilder addParameter(LeftHandSideType type) { + p.parameters.add(type); + return this; + } + + /** + * Adds a new native parameter. Unlike the normal method, this will cause an error if the parameters are + * incorrect. + * + * @param nativeType + * @param nativeGenericStatement + * @return + */ + public GenericParametersBuilder addNativeParameter(CClassType nativeType, LeftHandGenericUse nativeGenericStatement) { + return addParameter(nativeType, nativeGenericStatement, null, Target.UNKNOWN); + } + + /** + * Returns if this builder object is empty. If it is, calling build causes an error, so it's important to check + * this first if you are using this generically. + * + * @return + */ + public boolean isEmpty() { + return p.parameters.isEmpty(); + } + + /** + * If the parameter set only contains native classes, this method can be used instead, which will cause Errors + * instead of user compiler errors. + * + * @return + */ + public GenericParameters buildNative() { + return build(Target.UNKNOWN, null); + } + + /** + * Returns the fully constructed object. + * + * @param t + * @param env + * @return + */ + public GenericParameters build(Target t, Environment env) { + if(p.parameters.isEmpty()) { + throw new Error("Empty parameter builders cannot be used. Check for this condition with isEmpty()"); + } + ConstraintValidator.ValidateParametersToDeclaration(t, env, p, p.genericDeclaration, null); + return p; + } + + /** + * If these parameters are being built before the type is known (during compilation, mainly) then forType may be + * set to null, and then this method called. Calling the normal build methods if forType is null will cause an + * error. + *

+ * These parameters must be validated later independently. + * + * @return + */ + public GenericParameters buildWithoutValidation() { + if(p.parameters.isEmpty()) { + throw new Error("Empty parameter builders cannot be used. Check for this condition with isEmpty()"); + } + return p; + } + } + + /** + * Begins construction of a new GenericParameters object, which represents the RHS of the generic declaration.The + * actual GenericDeclaration object is passed in in order to validate the types against the constraints.Each + * instance of a class which has a GenericDeclaration will have one of these objects in it, associated with that + * particular instance. This data is not lost after compilation, and types are reified for runtime use. + *

+ * Each parameter consists of a CClassType, and optionally a LeftHandGenericUse. For instance, in the statement + * new A<B<? extends C>> where A is the class being constructed, with signature + * class A<T> and B is a concrete class itself with a single template parameter, and C being + * another class, then this method would be called with the parameters B and a new instance of the + * LeftHandGenericUse class representing the constraint ? extends C. + * + * @param forType The type that these parameters are being added to. + * @param type The concrete class type + * @param genericStatement The LHS generic statement for this parameter. This may be null if the type did not + * include a generic statement. + * @param env The environment + * @param t The code target + * @return this, for easy chaining. Use build() to construct the final object. + * @throws CREGenericConstraintException If the generic statement does not validate against the type. + */ + public static GenericParametersBuilder addParameter(CClassType forType, CClassType type, LeftHandGenericUse genericStatement, Environment env, Target t) { + return emptyBuilder(forType).addParameter(type, genericStatement, env, t); + } + + /** + * Begins construction of a new GenericParameters object.This should only be used with native, hardcoded classes, as + * incorrect usage will cause an Error. + * + * @param forType The type that these parameters are being added to. + * @param type + * @param nativeGenericStatement + * @return + */ + public static GenericParametersBuilder addNativeParameter(CClassType forType, CClassType type, + LeftHandGenericUse nativeGenericStatement) { + return emptyBuilder(forType).addParameter(type, nativeGenericStatement, null, Target.UNKNOWN); + } + + /** + * Returns an empty builder. Note that calling build on an empty builder is an error. + * + * @param forType The type that these parameters are being added to. + * @return + */ + public static GenericParametersBuilder emptyBuilder(CClassType forType) { + GenericParameters gp = new GenericParameters(); + return new GenericParametersBuilder(gp, forType == null ? null : forType.getGenericDeclaration()); + } + + /** + * Returns an empty builder. Note that calling build on an empty builder is an error. + * + * @param forSignature The function signature that these are targetting. + * @return + */ + public static GenericParametersBuilder emptyBuilder(FunctionSignature forSignature) { + GenericParameters gp = new GenericParameters(); + return new GenericParametersBuilder(gp, forSignature.getGenericDeclaration()); + } + + private GenericParameters() { + parameters = new ArrayList<>(); + } + + /** + * Returns a list of the parameters that were defined on this class type. + * + * @return + */ + public List getParameters() { + return new ArrayList<>(parameters); + } + + /** + * Converts this concrete GenericParameter object into a LeftHandGenericUse equivalent.It uses the + * ExactTypeConstraint constraint for all parameters, but makes for easier comparisons. + * + * @param forType The type of the containing object. In general, this is not tracked by the GenericParameters class, + * because it can be used more generically, though in practice, this will certainly belong to some instance of a + * class, and it is this class type that should be passed in. + * @param env The environment. + * @return An equivalent LeftHandGenericUse type. The ConstraintLocation of the underlying object is set to LHS. + */ + public LeftHandGenericUse toLeftHandEquivalent(CClassType forType, Environment env) { + Constraints[] constraints = new Constraints[parameters.size()]; + for(int i = 0; i < parameters.size(); i++) { + LeftHandSideType parameter = parameters.get(i); + Constraint c = new ExactTypeConstraint(Target.UNKNOWN, parameter); + constraints[i] = new Constraints(Target.UNKNOWN, ConstraintLocation.LHS, c); + } + LeftHandGenericUse lhgu = new LeftHandGenericUse(forType, Target.UNKNOWN, env, constraints); + return lhgu; + } + + /** + * Returns a new GenericTypeParameters equivalent object. Note that all the types will be single ExactType + * constraints, rather than typenames. + * + * @param forType The type that these generics will be associated with. + * @param t The code target where these were defined, for validation errors. + * @param env The environment. + * @return + */ + public GenericTypeParameters toGenericTypeParameters(CClassType forType, Target t, Environment env) { + GenericTypeParameters.GenericTypeParametersBuilder builder = GenericTypeParameters.emptyBuilder(forType, t, env); + for(LeftHandSideType lhst : parameters) { + builder.addParameter(lhst); + } + return builder.build(); + } + + @Override + public boolean equals(Object that) { + return ObjectHelpers.DoEquals(this, that); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("<"); + boolean doComma = false; + for(LeftHandSideType p : parameters) { + if(doComma) { + b.append(", "); + } + doComma = true; + b.append(p.toString()); + } + b.append(">"); + return b.toString(); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/GenericTypeParameters.java b/src/main/java/com/laytonsmith/core/constructs/generics/GenericTypeParameters.java new file mode 100644 index 0000000000..8dd0f42de9 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/GenericTypeParameters.java @@ -0,0 +1,421 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.PureUtilities.Either; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.Pair; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.constraints.ExactTypeConstraint; +import com.laytonsmith.core.environments.Environment; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A GenericTypeParameters object holds the either typename or concrete type parameters within a class or method + * definition. + */ +public class GenericTypeParameters { + + @ObjectHelpers.StandardField + private final List>> parameters; + @ObjectHelpers.StandardField + private final CClassType forType; + private final Target target; + private final Environment env; + + public static GenericTypeParameters fromTypes(CClassType forType, Target t, Environment env, ConcreteGenericParameter... constraints) { + return new GenericTypeParameters(forType, t, env, Arrays.asList(constraints).stream() + .map(item -> Either.left(item)).toList().toArray(Either[]::new)); + } + + public static GenericTypeParameters fromTypenames(CClassType forType, Target t, Environment env, Pair... typenames) { + return new GenericTypeParameters(forType, t, env, Arrays.asList(typenames).stream() + .map(item -> Either.right(item)).toList().toArray(Either[]::new)); + } + + public GenericTypeParameters(CClassType forType, Target t, Environment env, Either>... parameters) { + List validation = new ArrayList<>(); + for(Either> param : parameters) { + if(param.hasLeft()) { + LeftHandSideType type = param.getLeft().get(); + Constraints c = new Constraints(t, ConstraintLocation.LHS, + new ExactTypeConstraint(t, type)); + validation.add(c); + } else { + validation.add(param.getRight().get().getValue()); + } + } + ConstraintValidator.ValidateLHS(t, forType, validation, env); + this.parameters = Arrays.asList(parameters); + this.forType = forType; + this.target = t; + this.env = env; + } + + /** + * Returns a list of the Constraints objects. Each Constraints object represents a single type parameter, though + * itself can contain multiple individual Constraint objects. + * + * @return + */ + public List>> getParameters() { + return new ArrayList<>(parameters); + } + + /** + * Returns the Constraints object at the specified location. Equivalent to {@code getParameters().get(location)}. + * + * @param location The parameter location, 0 indexed. + * @return The Constraints object governing the given parameter. + */ + public Constraints getParameter(int location) { + Either> param = parameters.get(location); + Constraints c; + if(param.hasLeft()) { + LeftHandSideType type = param.getLeft().get(); + c = new Constraints(Target.UNKNOWN, ConstraintLocation.LHS, + new ExactTypeConstraint(Target.UNKNOWN, type)); + } else { + c = param.getRight().get().getValue(); + } + return c; + } + + /** + * Returns a List of LeftHandSideTypes. This will only work if all parameters are ConcreteGenericParameters, + * otherwise a RuntimeException is thrown. + * + * @return + */ + public List toLeftHandSideTypes() { + List ret = new ArrayList<>(); + for(Either> param : parameters) { + if(param.hasRight()) { + throw new RuntimeException("Cannot create LeftHandSideType from type parameters, contains typenames."); + } + ret.add(param.getLeft().get()); + } + return ret; + } + + /** + * @return Returns a LeftHandGenericUse equivalent, with each parameter converted into a single ExactTypeConstraint + * Constraints object. This only works if all parameters are ConcretGenericParameters. + */ + public LeftHandGenericUse toLeftHandGenericUse() { + List params = new ArrayList<>(); + int count = 0; + for(Either> param : parameters) { + if(param.hasRight()) { + throw new RuntimeException("Cannot create LeftHandSideType from type parameters, contains typenames."); + } + params.add(param.getLeft().get().toLeftHandGenericUse(forType, target, env, ConstraintLocation.RHS, count++)); + } + return new LeftHandGenericUse(forType, target, env, params); + } + + /** + * @return Returns true if any of the parameters are typenames, that is, they need to be resolved. + */ + public boolean hasTypenames() { + for(Either> param : parameters) { + if(param.hasRight()) { + return true; + } + } + return false; + } + + public boolean isInstanceof(LeftHandGenericUse lhgu, Environment env) { + if(lhgu.getConstraints().size() != parameters.size()) { + return false; + } + for(int i = 0; i < parameters.size(); i++) { + Constraints superClass = lhgu.getConstraints().get(i); + Either> sub = parameters.get(i); + if(sub.hasLeft()) { + if(!superClass.withinBounds(sub.getLeft().get(), env)) { + return false; + } + } else { + List errors = new ArrayList<>(); + if(!superClass.withinBounds(sub.getRight().get().getValue(), errors, env)) { + return false; + } + } + } + return true; + } + + /** + * Returns the number of parameters in this declaration. + * + * @return + */ + public int getParameterCount() { + return parameters.size(); + } + + /** + * The type these parameters are for. + * + * @return + */ + public CClassType getForType() { + return this.forType; + } + + public String toSimpleString() { + StringBuilder b = new StringBuilder(); + boolean joinComma = false; + for(Either> c : parameters) { + if(joinComma) { + b.append(", "); + } + joinComma = true; + if(c.hasLeft()) { + LeftHandSideType type = c.getLeft().get(); + b.append(type.getSimpleName()); + } else { + b.append(c.getRight().get().getKey()); + } + } + return b.toString(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + boolean joinComma = false; + for(Either> c : parameters) { + if(joinComma) { + b.append(", "); + } + joinComma = true; + if(c.hasLeft()) { + LeftHandSideType type = c.getLeft().get(); + b.append(type.toString()); + } else { + b.append(c.getRight().get().getKey()); + } + } + return b.toString(); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object that) { + return ObjectHelpers.DoEquals(this, that); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + /** + * Returns a new builder and adds a parameter to it. + * + * @param forType + * @param t + * @param env + * @param type + * @return + */ + public static GenericTypeParametersBuilder addParameter(CClassType forType, Target t, Environment env, Either> type) { + return new GenericTypeParametersBuilder(forType, t, env).addParameter(type); + } + + /** + * Returns a new builder and adds a parameter to it. + * + * @param forType + * @param t + * @param env + * @param typename + * @param constraints + * @return + */ + public static GenericTypeParametersBuilder addParameter(CClassType forType, Target t, Environment env, String typename, Constraints constraints) { + return new GenericTypeParametersBuilder(forType, t, env).addParameter(typename, constraints); + } + + /** + * Returns a new builder and adds a parameter to it. + * + * @param forType + * @param t + * @param env + * @param parameter + * @return + */ + public static GenericTypeParametersBuilder addParameter(CClassType forType, Target t, Environment env, ConcreteGenericParameter parameter) { + return new GenericTypeParametersBuilder(forType, t, env).addParameter(parameter); + } + + /** + * Returns a new builder and adds a parameter to it. + * + * @param forType + * @param t + * @param env + * @param type + * @param lhgu + * @return + */ + public static GenericTypeParametersBuilder addParameter(CClassType forType, Target t, Environment env, CClassType type, LeftHandGenericUse lhgu) { + return new GenericTypeParametersBuilder(forType, t, env).addParameter(type, lhgu); + } + + /** + * Returns a new, empty builder. + * + * @param forType + * @param t + * @param env + * @return + */ + public static GenericTypeParametersBuilder emptyBuilder(CClassType forType, Target t, Environment env) { + return new GenericTypeParametersBuilder(forType, t, env); + } + + /** + * Returns a new, empty builder. Note that this builder can only be used to add native types to. + * + * @param forType + * @return + */ + public static GenericTypeParametersBuilder nativeBuilder(CClassType forType) { + return new GenericTypeParametersBuilder(forType, Target.UNKNOWN, null); + } + + public static final class GenericTypeParametersBuilder { + + List>> p = new ArrayList<>(); + CClassType forType; + Target target; + Environment env; + + public GenericTypeParametersBuilder(CClassType forType, Target t, Environment env) { + this.forType = forType; + this.target = t; + this.env = env; + } + + /** + * Adds a new parameter. + * + * @param type The Either type. + * @return this, for easy chaining. Use build() to construct the final object. + */ + public GenericTypeParametersBuilder addParameter(Either> type) { + p.add(type); + return this; + } + + /** + * Adds a new parameter. + * + * @param typename The typename. + * @param constraints The constraints governing this typename. May be null if this should be inherited, in which + * case the typename is looked up in the forType, and the associated Constraints object is used. + * @return this, for easy chaining. Use build() to construct the final object. + */ + public GenericTypeParametersBuilder addParameter(String typename, Constraints constraints) { +// if(constraints == null) { +// for(Constraints c : forType.getGenericDeclaration().getConstraints()) { +// if(typename.equals(c.getTypeName())) { +// constraints = c; +// break; +// } +// } +// } + return addParameter(Either.right(new Pair<>(typename, constraints))); + } + + /** + * Adds a new parameter. + * + * @param type The type to add. + * @return this, for easy chaining. Use build() to construct the final object. + */ + public GenericTypeParametersBuilder addParameter(ConcreteGenericParameter type) { + return addParameter(type.asLeftHandSideType()); + } + + /** + * Adds a new parameter. + * + * @param type The type to add. + * @return this, for easy chaining. Use build() to construct the final object. + */ + public GenericTypeParametersBuilder addParameter(LeftHandSideType type) { + return addParameter(Either.left(type)); + } + + /** + * Shorthand for adding a ConcreteGenericParameter. Note that for generic declarations where the class is used + * as a generic parameter, you must instead use {@link CClassType#RECURSIVE_DEFINITION}, which is then replaced + * by the actual class type during building. Otherwise, using the type directly with Class.TYPE will fail, since + * by definition during instantiation, it is still null. + * + * @param type + * @param lhgu + * @return + */ + public GenericTypeParametersBuilder addParameter(CClassType type, LeftHandGenericUse lhgu) { + return addParameter(new ConcreteGenericParameter(type, lhgu, target, env)); + } + + /** + * Returns if this builder object is empty. If it is, calling build causes an error, so it's important to check + * this first if you are using this generically. + * + * @return + */ + public boolean isEmpty() { + return p.isEmpty(); + } + + /** + * Returns the fully constructed object. + * + * @return + */ + public GenericTypeParameters build() { + if(p.isEmpty()) { + throw new Error("Empty parameter builders cannot be used. Check for this condition with isEmpty()"); + } + return new GenericTypeParameters(forType, target, env, p.toArray(Either[]::new)); + } + + public GenericTypeParameters buildWithSubclassDefinition(CClassType t) { + List>> parameters = new ArrayList<>(); + for(Either> val : p) { + if(val.hasLeft()) { + if(CClassType.RECURSIVE_DEFINITION.getFQCN().getFQCN().equals(val.getLeft().get().val())) { + parameters.add(Either.left(t.asLeftHandSideType())); + } else { + parameters.add(val); + } + } else { + Pair pair = val.getRight().get(); + if(pair.getValue() == null) { + Constraints c = null; + for(Constraints r : t.getGenericDeclaration().getConstraints()) { + if(pair.getKey().equals(r.getTypeName())) { + c = r; + break; + } + } + pair = new Pair<>(pair.getKey(), c); + } + parameters.add(Either.right(pair)); + } + } + return new GenericTypeParameters(forType, target, env, parameters.toArray(Either[]::new)); + } + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUse.java b/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUse.java new file mode 100644 index 0000000000..9fd63cda05 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUse.java @@ -0,0 +1,270 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.PureUtilities.Either; +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.ObjectHelpers.StandardField; +import com.laytonsmith.PureUtilities.Pair; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This class represents the generic parameters on the LHS. In general, this can contain several types of constraints, + * including the ExactType constraint. Unlike the RHS, this information is not required to be kept around post + * compilation, since the RHS is typechecked against this information, and then once confirmed to be correct, is no + * longer needed for dynamic use of the reified type. The RHS can in general contain LHS information though, + * particularly when the class specified on the RHS itself contains generics, in which case, the information within + * those parameters will be LHS information. + */ +public class LeftHandGenericUse { + + @StandardField + private final List parameters; + + private static void ValidateCount(int inputParameters, CClassType forType, Target t) { + GenericDeclaration declaration = forType.getGenericDeclaration(); + List constraints = declaration.getConstraints(); + if(declaration.getParameterCount() != inputParameters) { + if(!constraints.isEmpty() + && constraints.get(constraints.size() - 1).isVariadic() + && inputParameters >= constraints.size() - 1) { + return; + } + throw new CREGenericConstraintException( + forType.getSimpleName() + " expects " + + StringUtils.PluralTemplateHelper(forType.getGenericDeclaration().getParameterCount(), + "%d parameter", "%d parameters") + + " but " + + StringUtils.PluralTemplateHelper(inputParameters, "only %1 was found.", "%d were found."), t); + } + } + + /** + * Constructs a new LeftHandGenericUse object when all parameters are regular Constraints. + * + * @param forType The type that is attached to this LHGU, for validation. + * @param t The code target. + * @param env The environment. + * @param constraints The list of Constraints. Each one represents one parameter. + */ + public LeftHandGenericUse(CClassType forType, Target t, Environment env, Constraints... constraints) { + ValidateCount(constraints.length, forType, t); + ConstraintValidator.ValidateLHS(t, forType, Arrays.asList(constraints), env); + parameters = new ArrayList<>(); + for(Constraints c : constraints) { + parameters.add(new LeftHandGenericUseParameter(Either.left(c))); + } + } + + /** + * Constructs a new LeftHandGenericUse object when all parameters are typenames. + * + * @param forType The type that is attached to this LHGU, for validation. + * @param t The code target. + * @param env The environment. + * @param typenames The list of typenames. + */ + public LeftHandGenericUse(CClassType forType, Target t, Environment env, Pair... typenames) { + ValidateCount(typenames.length, forType, t); + ConstraintValidator.ValidateLHS(t, forType, Arrays.asList(typenames).stream() + .map(item -> item.getValue()).collect(Collectors.toList()), env); + parameters = new ArrayList<>(); + for(Pair typename : typenames) { + parameters.add(new LeftHandGenericUseParameter(Either.right(typename))); + } + } + + /** + * Constructs a new LeftHandGenericUse object for the given mix of Constraints objects and String typenames. If all + * values are the same type, use of the other constructors is cleaner and preferred. + * + * @param forType The type that is attached to this LHGU, for validation. + * @param t The code target. + * @param env The environment. + * @param parameters The list of parameters. + */ + public LeftHandGenericUse(CClassType forType, Target t, Environment env, + Either>... parameters) { + ValidateCount(parameters.length, forType, t); + List constraints = new ArrayList<>(); + for(Either> param : parameters) { + Constraints c; + if(param.hasLeft()) { + c = param.getLeft().get(); + } else { + c = param.getRight().get().getValue(); + } + constraints.add(c); + } + ConstraintValidator.ValidateLHS(t, forType, constraints, env); + this.parameters = new ArrayList<>(); + for(Either> param : parameters) { + this.parameters.add(new LeftHandGenericUseParameter(param)); + } + } + + /** + * Constructs a new LeftHandGenericUse object with the premade parameter objects. + * + * @param forType The type that is attached to this LHGU, for validation. + * @param t The code target. + * @param env The environment. + * @param parameters The list of parameters. + */ + public LeftHandGenericUse(CClassType forType, Target t, Environment env, LeftHandGenericUseParameter... parameters) { + this(forType, t, env, Arrays.asList(parameters).stream().map(item -> item.getValue()) + .collect(Collectors.toList()).toArray(Either[]::new)); + } + + public LeftHandGenericUse(CClassType forType, Target t, Environment env, List parameters) { + this(forType, t, env, parameters.toArray(LeftHandGenericUseParameter[]::new)); + } + + /** + * Constructs a new LeftHandGenericUse object. This should only be used for native object construction. + * @param forType The type that is attached to this LHGU, for validation. + * @param parameters The parameters. + * @return A new LeftHandGenericUse object. + */ + public static LeftHandGenericUse forNativeParameters(CClassType forType, LeftHandGenericUseParameter... parameters) { + return new LeftHandGenericUse(forType, Target.UNKNOWN, null, parameters); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object that) { + return ObjectHelpers.DoEquals(this, that); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + public List getParameters() { + return new ArrayList<>(parameters); + } + + public List getConstraints() { + List constraints = new ArrayList<>(); + for(LeftHandGenericUseParameter param : parameters) { + constraints.add(param.getConstraints()); + } + return constraints; + } + + /** + * Returns whether or not the parameter at the specified location is a typename. + * @param parameterPlace The location of the parameter, 0 indexed. + * @return True if the parameter at the location is a typename. + */ + public boolean isTypename(int parameterPlace) { + return parameters.get(parameterPlace).getValue().hasRight(); + } + + /** + * Returns true if the LHGU has a typename parameter in any position. + * @return + */ + public boolean hasTypename() { + for(LeftHandGenericUseParameter param : parameters) { + if(param.getValue().hasRight()) { + return true; + } + } + return false; + } + + /** + * Works like toString, but uses the class's simple name. + * + * @return + */ + public String toSimpleString() { + StringBuilder b = new StringBuilder(); + boolean doComma = false; + for(LeftHandGenericUseParameter cc : parameters) { + if(doComma) { + b.append(", "); + } + doComma = true; + b.append(cc.toSimpleString()); + } + return b.toString(); + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + boolean doComma = false; + for(LeftHandGenericUseParameter cc : parameters) { + if(doComma) { + b.append(", "); + } + doComma = true; + b.append(cc.toString()); + } + return b.toString(); + } + + /** + * Checks if the generics on the RHS are within bounds of this LHS generic definition. + * + * @param env The environment + * @param types The types, each argument corresponding to the Constraints objects, that is, each argument to the + * generic parameter set. + * @return True if all types are within bounds. + */ + public boolean isWithinBounds(Environment env, LeftHandSideType... types) { + if(this.getConstraints().size() != types.length) { + return false; + } + for(int i = 0; i < this.getConstraints().size(); i++) { + Constraints lhs = this.parameters.get(i).getConstraints(); + LeftHandSideType type = types[i]; + if(!lhs.withinBounds(type, env)) { + return false; + } + } + return true; + } + + /** + * @param env The environment + * @param presumedSubtype The value to check if it is within the bounds represented by this object + * @return True if the input LHS type is a subtype of this LHS. + */ + public boolean isWithinBounds(Environment env, LeftHandGenericUse presumedSubtype) { + if(presumedSubtype == null) { + // We have generics, but they didn't provide any, so it doesn't match. + return false; + } + List checkIfTheseConstraints = presumedSubtype.getConstraints(); + List areWithinBoundsOfThese = this.getConstraints(); + boolean isVariadic = !areWithinBoundsOfThese.isEmpty() + && areWithinBoundsOfThese.get(areWithinBoundsOfThese.size() - 1).isVariadic(); + if((isVariadic && checkIfTheseConstraints.size() < areWithinBoundsOfThese.size() - 1) + || (!isVariadic && checkIfTheseConstraints.size() != areWithinBoundsOfThese.size())) { + return false; + } + for(int i = 0; i < Math.max(checkIfTheseConstraints.size(), areWithinBoundsOfThese.size()); i++) { + Constraints definition = areWithinBoundsOfThese.get(Math.min(i, areWithinBoundsOfThese.size() - 1)); + Constraints lhs = checkIfTheseConstraints.get(Math.min(i, checkIfTheseConstraints.size() - 1)); + // Check that the LHS fits the bounds of the definition + List errors = new ArrayList<>(); + if(!definition.withinBounds(lhs, errors, env)) { + return false; + } + } + return true; + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUseParameter.java b/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUseParameter.java new file mode 100644 index 0000000000..481e3aab55 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/LeftHandGenericUseParameter.java @@ -0,0 +1,85 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.PureUtilities.Either; +import com.laytonsmith.PureUtilities.Pair; +import java.util.Objects; + +/** + * A LeftHandGenericUseParameter is a container that holds a single parameter in a LeftHandGenericUse object. + * In general, these can either be an actual value (a Constraints object) or simply a typename which must + * be fully resolved before passing out of the defined scope. + *

+ * For typenames, they contain two separate pieces of information. Their actual name (a string) and their Constraints + * object. For validation purposes, the Constraints must fit within the definition constraints, and so those are + * required to do validation regardless. But the typename is used for correct value alignment later. Thus, both + * pieces of information are required for typenames. + */ +public class LeftHandGenericUseParameter { + private final Either> value; + + public LeftHandGenericUseParameter(Either> value) { + if(!value.hasLeft() && !value.hasRight()) { + throw new Error("LeftHandGenericUseParameter must contain one or the other type, and cannot be empty"); + } + this.value = value; + } + + public Either> getValue() { + return this.value; + } + + /** + * Both types of values contain constraints, and this method simplifies getting it, no matter if it's a typename + * or a full Constraints object. + * @return + */ + public Constraints getConstraints() { + if(this.value.hasLeft()) { + return this.value.getLeft().get(); + } else { + return this.value.getRight().get().getValue(); + } + } + + @Override + public String toString() { + if(value.hasLeft()) { + return value.getLeft().get().toString(); + } else { + return value.getRight().get().getKey(); + } + } + + public String toSimpleString() { + if(value.hasLeft()) { + return value.getLeft().get().toSimpleString(); + } else { + return value.getRight().get().getKey(); + } + } + + @Override + public int hashCode() { + int hash = 3; + hash = 97 * hash + Objects.hashCode(this.value); + return hash; + } + + @Override + public boolean equals(Object obj) { + if(this == obj) { + return true; + } + if(obj == null) { + return false; + } + if(getClass() != obj.getClass()) { + return false; + } + final LeftHandGenericUseParameter other = (LeftHandGenericUseParameter) obj; + return Objects.equals(this.value, other.value); + } + + + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraint.java new file mode 100644 index 0000000000..7f1032d053 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraint.java @@ -0,0 +1,45 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.compiler.FileOptions; +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +/** + * The UnqualifiedConstraint class, like the other Unqualified classes, is a simple wrapper around the unqualified class + * names and other relevant information which is eventually qualified in a second pass, once the relevant types have all + * had a chance to be defined. + */ +public class UnqualifiedConstraint { + + private final String constraint; + private final ConstraintLocation location; + private final Target constraintTarget; + private final FileOptions fileOptions; + + /** + * Constructs a new UnqualifiedConstraint. + * @param fileOptions Used to determine the suppression instructions for warnings. + * @param constraint The constraint itself. + * @param location The location of the definition. + * @param constraintTarget The code target where the constraint is being defined. + */ + public UnqualifiedConstraint(FileOptions fileOptions, String constraint, ConstraintLocation location, + Target constraintTarget) { + this.constraint = constraint; + this.location = location; + this.constraintTarget = constraintTarget; + this.fileOptions = fileOptions; + } + + /** + * Qualifies the value and returns a Constraint object. This has the potential to throw Exceptions. + * @param env The environment. + * @param declarationConstraints The Constraints object which governs this Constraint. This should be + * null if location is {@link ConstraintLocation#DEFINITION}. + * @return A new Constraint object, which works with full types. + */ + public Constraint qualify(Environment env, Constraints declarationConstraints) { + return Constraints.GetConstraint(fileOptions, constraint, constraintTarget, location, declarationConstraints, env); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraints.java b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraints.java new file mode 100644 index 0000000000..1f6839ea73 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedConstraints.java @@ -0,0 +1,48 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.generics.constraints.Constraint; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +import java.util.Arrays; +import java.util.List; + +/** + * The UnqualifiedConstraints class, like the other Unqualified classes, is a simple wrapper around the unqualified + * class names and other relevant information which is eventually qualified in a second pass, once the relevant types + * have all had a chance to be defined. + */ +public class UnqualifiedConstraints { + + private final List unorderedConstraints; + private final Target definitionTarget; + /** + * Constructs a new constraint object.Note that if this is being used on the LHS, no validation is done + * + * @param t The code target. + * @param constraints The constraints. This is an unordered list, but they will be normalized into their natural + * order. + */ + public UnqualifiedConstraints(Target t, UnqualifiedConstraint... constraints) { + this.unorderedConstraints = Arrays.asList(constraints); + this.definitionTarget = t; + } + + /** + * Qualifies the value and returns a Constraints object. This has the potential to throw Exceptions. + * @param location The location that this Constraints object is being used. + * @param declarationConstraints The Constraints object which governs this Constraint. This should be + * null if location is {@link ConstraintLocation#DEFINITION}. + * @param env The environment. + * @return A new Constraints object, which works with full types. + */ + public Constraints qualify(ConstraintLocation location, Constraints declarationConstraints, + Environment env) { + Constraint[] constraints = new Constraint[unorderedConstraints.size()]; + for(int i = 0; i < constraints.length; i++) { + constraints[i] = unorderedConstraints.get(i).qualify(env, declarationConstraints); + } + return new Constraints(definitionTarget, location, constraints); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericDeclaration.java b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericDeclaration.java new file mode 100644 index 0000000000..1cb39b5e68 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericDeclaration.java @@ -0,0 +1,36 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +import java.util.Arrays; +import java.util.List; + +/** + * The UnqualifiedGenericDeclaration class, like the other Unqualified classes, is a simple wrapper around the + * unqualified class names and other relevant information which is eventually qualified in a second pass, once the + * relevant types have all had a chance to be defined. + */ +public class UnqualifiedGenericDeclaration { + + private final List constraints; + private final Target target; + + /** + * Constructs a new UnqualifiedGenericDeclaration. + * @param t The code target where this is being defined. + * @param constraints + */ + public UnqualifiedGenericDeclaration(Target t, UnqualifiedConstraints... constraints) { + this.constraints = Arrays.asList(constraints); + this.target = t; + } + + public GenericDeclaration qualify(Environment env) throws ClassNotFoundException { + Constraints[] c = new Constraints[constraints.size()]; + for(int i = 0; i < c.length; i++) { + c[i] = constraints.get(i).qualify(ConstraintLocation.DEFINITION, null, env); + } + return new GenericDeclaration(target, c); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericTypeParameters.java b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericTypeParameters.java new file mode 100644 index 0000000000..1f5bd4c526 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedGenericTypeParameters.java @@ -0,0 +1,108 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.PureUtilities.Pair; +import com.laytonsmith.core.UnqualifiedClassName; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +import java.util.ArrayList; +import java.util.List; + +/** + * An UnqualifiedGenericParameters class represents the generic parameters at the intermediate stage of compilation, + * before all classes are fully defined. Once all classes are defined, then the generics can be qualified. + * + * Currently, the parameters are stored internally as a mass string, and the compiler doesn't provide different code + * targets for each individual component, so compilation errors are less specific than they could be. In the future, + * Unqualified versions of each component can be created and stored with their own code targets, so that compiler errors + * can be more specific, instead of highlighting the entire parameter set. + */ +public class UnqualifiedGenericTypeParameters { + + private final List> parameters = new ArrayList<>(); + + /** + * Qualifies the UnqualifiedGenericParamaters. This may return null if the type does not have them, or if this was + * defined with a null parameter set. + * + * @param forType + * @param env + * @param t + * @return + * @throws ClassNotFoundException + */ + public GenericTypeParameters qualify(CClassType forType, Environment env, Target t) throws ClassNotFoundException { + GenericTypeParameters.GenericTypeParametersBuilder p = GenericTypeParameters.emptyBuilder(forType, t, env); + for(int i = 0; i < parameters.size(); i++) { + Pair pair = parameters.get(i); + UnqualifiedClassName ucn = pair.getKey(); + UnqualifiedLeftHandGenericUse ulhgu = pair.getValue(); + CClassType type = CClassType.getNakedClassType(ucn.getFQCN(env), env); + LeftHandGenericUse lhgu = ulhgu.qualify(forType, env); + if(p == null) { + p = GenericTypeParameters.addParameter(forType, t, env, type, lhgu); + } else { + p.addParameter(type, lhgu); + } + } + return p == null ? null : p.build(); + } + + public static final class UnqualifiedGenericParametersBuilder { + + UnqualifiedGenericTypeParameters p; + + private UnqualifiedGenericParametersBuilder(UnqualifiedGenericTypeParameters p) { + this.p = p; + } + + /** + * Adds a new parameter. Each parameter consists of a CClassType, and optionally a LeftHandGenericUse. For + * instance, in the statement new A<B<? extends C>> where A is the class being + * constructed, with signature class A<T> and B is a concrete class itself with a single + * template parameter, and C being another class, then this method would be called with the parameters + * B and a new instance of the LeftHandGenericUse class representing the constraint + * ? extends C. + * + * @param type The concrete class type + * @param genericStatement The LHS generic statement for this parameter. This may be null if the type did not + * include a generic statement. + * @return this, for easy chaining. Use build() to construct the final object. + */ + public UnqualifiedGenericParametersBuilder addParameter(UnqualifiedClassName type, + UnqualifiedLeftHandGenericUse genericStatement) { + p.parameters.add(new Pair<>(type, genericStatement)); + return this; + } + + /** + * Returns the fully constructed object. + * + * @return + */ + public UnqualifiedGenericTypeParameters build() { + return p; + } + } + + /** + * Starts building a new UnqualifiedGenericParameterBuilder, and adds the first parameter. Each parameter consists + * of an UnqualifiedClassName, and optionally an UnqualifiedLeftHandGenericUse. For instance, in the statement + * new A<B<? extends C>> where A is the class being constructed, with signature + * class A<T> and B is a concrete class itself with a single template parameter, and C being + * another class, then this method would be called with the parameters B and a new instance of the + * LeftHandGenericUse class representing the constraint ? extends C. + * + * @param type The concrete class type + * @param genericStatement The LHS generic statement for this parameter. This may be null if the type did not + * include a generic statement. + * @return A new UnqualifiedGenericParametersBuilder. Use build() to construct the final object. + */ + public static UnqualifiedGenericParametersBuilder addParameter(UnqualifiedClassName type, + UnqualifiedLeftHandGenericUse genericStatement) { + return new UnqualifiedGenericParametersBuilder(new UnqualifiedGenericTypeParameters()) + .addParameter(type, genericStatement); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedLeftHandGenericUse.java b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedLeftHandGenericUse.java new file mode 100644 index 0000000000..d3f45baff8 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/UnqualifiedLeftHandGenericUse.java @@ -0,0 +1,38 @@ +package com.laytonsmith.core.constructs.generics; + +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.environments.Environment; + +import java.util.Arrays; +import java.util.List; + +/** + * The UnqualifiedLeftHandGenericUse class, like the other Unqualified classes, is a simple wrapper around the + * unqualified class names and other relevant information which is eventually qualified in a second pass, once the + * relevant types have all had a chance to be defined. + */ +public class UnqualifiedLeftHandGenericUse { + + private final List constraints; + private final Target target; + + /** + * Constructs a new UnqualifiedLeftHandGenericUse object. + * @param t The code target where the usage is declared in code. + * @param constraints The UnqualifiedConstraints for this generic use. + */ + public UnqualifiedLeftHandGenericUse(Target t, UnqualifiedConstraints... constraints) { + this.target = t; + this.constraints = Arrays.asList(constraints); + } + + public LeftHandGenericUse qualify(CClassType forType, Environment env) { + Constraints[] c = new Constraints[constraints.size()]; + List declarationConstraints = forType.getGenericDeclaration().getConstraints(); + for(int i = 0; i < c.length; i++) { + c[i] = constraints.get(i).qualify(ConstraintLocation.LHS, declarationConstraints.get(i), env); + } + return new LeftHandGenericUse(forType, target, env, c); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/BoundaryConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/BoundaryConstraint.java new file mode 100644 index 0000000000..c7a2e3dcd8 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/BoundaryConstraint.java @@ -0,0 +1,46 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +/** + * A TypeConstraint is a value that has a ClassType boundary. + */ +public abstract class BoundaryConstraint extends Constraint { + /** + * The bound (either upper or lower). Will never be auto. + */ + protected LeftHandSideType bound; + + protected BoundaryConstraint(Target t, String typename, LeftHandSideType bound) { + super(t, typename); + ConstraintValidator.ValidateTypename(typename, t); + if(Auto.LHSTYPE.equals(bound)) { + throw new CREGenericConstraintException("Cannot use auto types on " + getConstraintName() + + " constraints", t); + } + this.bound = bound; + } + + /** + * {@inheritDoc} + * @param type The concrete type to check + * @return + */ + @Override + public boolean isWithinConstraint(LeftHandSideType type, Environment env) { + return isConcreteClassWithinConstraint(type, env); + } + + protected abstract boolean isConcreteClassWithinConstraint(LeftHandSideType type, Environment env); + + @Override + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, Auto.LHSTYPE); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/Constraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/Constraint.java new file mode 100644 index 0000000000..a85998542b --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/Constraint.java @@ -0,0 +1,191 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.PureUtilities.Common.Annotations.ForceImplementation; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +import java.util.Set; + +/** + * A Constraint is a single part of the general declaration. For instance, in the declaration + * T extends number & new T(A, B, C), this has two constraints, the first being + * T extends number and the second being new T(A, B, C). The first is an UpperBoundConstraint, + * and the second is a ConstructorConstraint. A third example is a LowerBoundConstraint, which is for constraints + * defined with T super number, and finally, an exact constraint, which is simply a ClassType. + *

+ * In general, two different Constraints may be contradictory, and when used in combination, erroneous. The solver for + * this isn't built in to the Constraint class however, since you need to know all constraints in the generic to + * determine if the combination is erroneous. Individually, a constraint cannot itself be erroneous. + *

+ * Constraints can be placed in 3 different locations. + *

    + *
  • Definitions - That is, a class or method definition
  • + *
  • LHS - Function parameter definitions or LHS of an assignment
  • + *
  • RHS - Function parameters, RHS of an assignment
  • + *
+ * + * Different constraints are valid in different locations, though the RHS can only contain an ExactTypeConstraint + * constraint, and so isn't grouped as part of the Constraint heirarchy itself. + *

+ * For the Definition site and LHS sites though, some constraints are simply not valid at all, and others must be used + * as a wildcard. + *

+ * Note that while the class implements Comparable, the order is not well defined, and is subject to change. This is an + * implementation detail to allow lists of Contraint objects to be ordered in a normal, deterministic way, in order to + * be properly comparable against other lists of Constraints. The specific order should not be relied upon beyond the + * execution of a single process. + */ +public abstract class Constraint implements Comparable { + + private final String typename; + private final boolean isWildcard; + private final Target target; + + /** + * Constructs a new constraint. + * + * @param t The code target. + * @param constraintName The name of the constraint, such as T or ? + */ + protected Constraint(Target t, String constraintName) { + this.typename = constraintName; + this.isWildcard = this.typename.equals("?"); + this.target = t; + } + + /** + * @return The name of the type, for instance T. If defined as a wildcard, this will be ?. + */ + public String getTypeName() { + return this.typename; + } + + /** + * @return A Set which contains the locations where this is valid to be defined at. + */ + public abstract Set validLocations(); + + /** + * @return The name of the type of constraint, for instance "lower bound constraint". This is useful for identifying + * the constraint type in error messages. + */ + public abstract String getConstraintName(); + + /** + * @return True if the type was defined as a wildcard. + */ + public boolean isWildcard() { + return isWildcard; + } + + /** + * @return The code target where this constraint was defined. + */ + public Target getTarget() { + return target; + } + + /** + * Returns true if the provided constraint is within the bounds defined by this constraint, false if it isn't. Given + * that class Z extends class Y which extends class X, and class Z has a public no arg constructor, consider the + * following class definition: class C<T extends X & new T()> and the LHS definition: + * C<? extends Y & new ?()> and the RHS new C<Z>(). + * + * This code is perfectly valid, however, when validating the LHS, we will compare the LHS constraint + * new ?() to the definition constraint ? extends Y. These don't compare at all, because + * we don't have enough information to compare these, however, comparing the RHS Z to both + * ? extends Y and new ?(), they both do apply. + *

+ * In cases where the comparison simply doesn't make sense, null is returned. However, this isn't necessarily + * sufficient to say that this is correct. Consider if the LHS had been defined as C<new ?()>. + * This is still lacking, because we need some constraint that matches the ? extends X in the + * definition. + *

+ * The key then, is to ensure that for each constraint in the class definition, at least one constraint on the LHS + * returns true, and none of them return false. + * + * @param lhs The constraint to determine if is in bounds of this constraint + * @param env The environment. + * @return True if the constraint is within the bounds, false otherwise. + */ + public final Boolean isWithinConstraint(Constraint lhs, Environment env) { + ConstraintToConstraintValidator validator = this.getConstraintToConstraintValidator(env); + Boolean baseResult = false; + if(lhs instanceof ConstructorConstraint c) { + baseResult = validator.isWithinBounds(c); + } else if(lhs instanceof ExactTypeConstraint c) { + baseResult = validator.isWithinBounds(c); + } else if(lhs instanceof LowerBoundConstraint c) { + baseResult = validator.isWithinBounds(c); + } else if(lhs instanceof UpperBoundConstraint c) { + baseResult = validator.isWithinBounds(c); + } else if(lhs instanceof UnboundedConstraint c) { + baseResult = validator.isWithinBounds(c); + } else if(lhs instanceof VariadicTypeConstraint c) { + baseResult = validator.isWithinBounds(c); + } else { + throw new Error("Unhandled constraint type"); + } + return baseResult; + + } + + /** + * Returns true if the type is within the bounds of this constraint. + * + * @param type The type to check + * @param env The environment. + * @return True if the type is within the bounds of this constraint. + */ + public abstract boolean isWithinConstraint(LeftHandSideType type, Environment env); + + /** + * Returns true if type unions can be used in the LHS of this value. Note that in no case can type unions be used on + * the RHS, since those require a concrete type. + * + * @return True if the constraint can support type unions. + */ + public abstract boolean supportsTypeUnions(); + + @ForceImplementation + protected abstract ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env); + + /** + * Given the Diamond Operator on the RHS, takes this LHS object and converts it to an ExactType object, which can be + * used on the RHS. Note that this only works with one constraint, multiple constraints cannot be inferred, by + * definition. + * + * @param t The code target + * @return The ExactTypeConstraint + * @throws CREGenericConstraintException If the type cannot be inferred from this Constraint + */ + public abstract ExactTypeConstraint convertFromDiamond(Target t) throws CREGenericConstraintException; + + /** + * Returns the appropriate auto type for this constraint. This is used as the inferred type for naked + * types, which is opposed to the diamond operator, which returns a non-auto type. + * @param t The code target + * @return The appropriate ExactTypeConstraint filled with an auto type value. + * @throws CREGenericConstraintException If an auto type cannot be used with this Constraint. + */ + public abstract ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException; + + /** + * Works like toString, but uses the class's simple name. + * + * @return The class's simple name. + */ + public abstract String toSimpleString(); + + @Override + public int compareTo(Constraint o) { + // Just compare against the toString, order doesn't *really* matter, it's just so that equality checks + // are deterministic. + return this.toString().compareTo(o.toString()); + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ConstructorConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ConstructorConstraint.java new file mode 100644 index 0000000000..15d5b78dc4 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ConstructorConstraint.java @@ -0,0 +1,119 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.PureUtilities.Common.StringUtils; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * A ConstructorConstraint is defined with new T(), optionally providing types such as + * new T(A, B, C) where A, B, C are concrete types, and T is the type variable. + * This requires that the class provided by the use-site contains a constructor of the specified type. + */ +public class ConstructorConstraint extends Constraint { + + private final List argTypes; + + public ConstructorConstraint(Target t, String typename, List argTypes) { + super(t, typename); + ConstraintValidator.ValidateTypename(typename, t); + this.argTypes = argTypes; + } + + public List getArgTypes() { + return new ArrayList<>(this.argTypes); + } + + @Override + public String toSimpleString() { + return "new " + getTypeName() + "(" + + StringUtils.Join(argTypes, + ", ", + ", ", + ", ", + "", + item -> item.getSimpleName()) + + ")"; + } + + @Override + public String toString() { + return "new " + getTypeName() + "(" + + StringUtils.Join(argTypes, ", ", item -> item.val()) + + ")"; + } + + @Override + public EnumSet validLocations() { + return EnumSet.allOf(ConstraintLocation.class); + } + + @Override + public String getConstraintName() { + return "constructor constraint"; + } + + @Override + public boolean isWithinConstraint(LeftHandSideType type, Environment env) { + // TODO: Nothing has constructors yet, this is always false + return false; + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return ConstructorConstraint.this.argTypes.equals(lhs.argTypes); + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + return ConstructorConstraint.this.isWithinConstraint(lhs.getType(), env); + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return null; + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return null; + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + return null; + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) { + throw new CREGenericConstraintException("Cannot infer generic parameter from new constraint.", t); + } + + @Override + public boolean supportsTypeUnions() { + return false; + } + + @Override + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + throw new CREGenericConstraintException("Generic type required, auto type cannot be used.", t); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ExactTypeConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ExactTypeConstraint.java new file mode 100644 index 0000000000..0fb7038f2d --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/ExactTypeConstraint.java @@ -0,0 +1,195 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.PureUtilities.ObjectHelpers; +import com.laytonsmith.PureUtilities.ObjectHelpers.StandardField; +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.InstanceofUtil; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConcreteGenericParameter; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.constructs.generics.Constraints; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import com.laytonsmith.core.exceptions.ConfigCompileException; + +import java.util.EnumSet; +import java.util.Objects; + +public class ExactTypeConstraint extends Constraint { + + @StandardField + private final LeftHandSideType type; + + private Constraints declarationBounds; + + /** + * Constructs a new unbounded wildcard instance of ExactTypeConstraint. + * + * @param t The code target + * @param declarationBounds The bounds of the declaration. This is used to check if the given concrete right hand + * side is within bounds of the declaration. Note that this will cause an exception to be thrown if the + * declarationBounds type is a type union, as unbounded wildcards cannot be used against a type union. + * @return + */ + public static ExactTypeConstraint AsUnboundedWildcard(Target t, Constraints declarationBounds) { + Objects.requireNonNull(declarationBounds); + ExactTypeConstraint type; + try { + type = new ExactTypeConstraint(t); + } catch(ConfigCompileException ex) { + throw new Error(ex); + } + type.declarationBounds = declarationBounds; + return type; + } + + private ExactTypeConstraint(Target t) throws ConfigCompileException { + super(t, "?"); + type = null; + } + + /** + * Constructs a new ExactType constraint. + * + * @param t The target where this is being defined. + * @param type The type. + */ + public ExactTypeConstraint(Target t, LeftHandSideType type) { + super(t, type.val()); + Objects.requireNonNull(type); + this.type = type; + } + + @Override + public EnumSet validLocations() { + return EnumSet.of(ConstraintLocation.LHS, ConstraintLocation.RHS); + } + + @Override + public String getConstraintName() { + return type == null ? "?" : type.val(); + } + + @Override + public boolean isWithinConstraint(LeftHandSideType type, Environment env) { + if(this.type == null) { + // It's an unbounded wildcard, we need the definition to determine. + return this.declarationBounds.withinBounds(type, env); + } + // Loop through each type in the union, and see if the top value type matches, and the + // other generics are in bounds. + if(this.type.getTypes().size() != type.getTypes().size()) { + return false; + } + // The order is deterministic across types that are equal, so given `A | B` and `B | A`, the order + // of each value will be reformatted into the same order for both types. So we can simply iterate + // each of these in order, and if any value isn't equal, then they aren't the same type. + for(int i = 0; i < this.type.getTypes().size(); i++) { + ConcreteGenericParameter theirs = type.getTypes().get(i); + ConcreteGenericParameter ours = this.type.getTypes().get(i); + if(!ours.getType().getNakedType(env).equals(theirs.getType().getNakedType(env)) + || (ours.getLeftHandGenericUse() == null && theirs.getLeftHandGenericUse() != null) + || (ours.getLeftHandGenericUse() != null && theirs.getLeftHandGenericUse() == null) + || (ours.getLeftHandGenericUse() != null + && !ours.getLeftHandGenericUse().isWithinBounds(env, theirs.getLeftHandGenericUse()))) { + return false; + } + } + return true; + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + if(ExactTypeConstraint.this.equals(lhs)) { + return true; + } + if(ExactTypeConstraint.this.getType().isAuto() || lhs.getType().isAuto()) { + return true; + } + if(!ExactTypeConstraint.this.getType().getNakedType(Target.UNKNOWN, env) + .equals(lhs.getType().getNakedType(Target.UNKNOWN, env))) { + return false; + } + // Upper type is the same now, for instance `array` and `array`, but + // now we have to ensure that the is in bounds of . + return InstanceofUtil.isInstanceof(lhs.getType(), ExactTypeConstraint.this.getType(), env); + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) { + return this; + } + + /** + * Returns the exact type for this constraint.Note that in the case of a wildcard "?", an ExactTypeConstraint + * constraint will be used, however, this value will be null. + * + * @return The type, or null if it was defined as a wildcard. + */ + public LeftHandSideType getType() { + return type; + } + + @Override + public String toSimpleString() { + return type == null ? "?" : type.getSimpleName(); + } + + @Override + public String toString() { + return type == null ? "?" : type.val(); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(Object that) { + return ObjectHelpers.DoEquals(this, that); + } + + @Override + public int hashCode() { + return ObjectHelpers.DoHashCode(this); + } + + @Override + public boolean supportsTypeUnions() { + return false; + } + + @Override + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, Auto.LHSTYPE); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/LowerBoundConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/LowerBoundConstraint.java new file mode 100644 index 0000000000..317872aeb2 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/LowerBoundConstraint.java @@ -0,0 +1,106 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import com.laytonsmith.core.natives.interfaces.Mixed; + +import java.util.EnumSet; + +/** + * A LowerBoundConstraint is defined with the super keyword, such as T super number. + * In this case, number is the lower bound. + */ +public class LowerBoundConstraint extends BoundaryConstraint { + + /** + * Constructs a new upper bound constraint. + * @param t Code target + * @param typename The name of this parameter, may be ? for LHS constraints + * @param lowerBound The concrete lower bound + */ + public LowerBoundConstraint(Target t, String typename, LeftHandSideType lowerBound) { + super(t, typename, lowerBound); + if(lowerBound.equals(Mixed.TYPE.asLeftHandSideType())) { + throw new CREGenericConstraintException("Cannot create a lower bound on mixed", t); + } + } + + @Override + protected boolean isConcreteClassWithinConstraint(LeftHandSideType type, Environment env) { + return this.bound.doesExtend(type, env); + } + + public LeftHandSideType getLowerBound() { + return this.bound; + } + + @Override + public String toSimpleString() { + return getTypeName() + " super " + getLowerBound().getSimpleName(); + } + + @Override + public String toString() { + return getTypeName() + " super " + getLowerBound(); + } + + @Override + public EnumSet validLocations() { + return EnumSet.of(ConstraintLocation.LHS); + } + + @Override + public String getConstraintName() { + return "lower bound"; + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return null; + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + return isWithinConstraint(lhs.getType(), env); + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return isWithinConstraint(lhs.getLowerBound(), env); + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) { + return new ExactTypeConstraint(t, this.bound); + } + + @Override + public boolean supportsTypeUnions() { + return true; + } + +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UnboundedConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UnboundedConstraint.java new file mode 100644 index 0000000000..d6725d5098 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UnboundedConstraint.java @@ -0,0 +1,101 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import com.laytonsmith.core.natives.interfaces.Mixed; + +import java.util.EnumSet; + +/** + * An UnboundedConstraint is a generic constraint which is a simple and single type. For instance, + * class M<T>, the constraint T is an UnboundedConstraint. Note that + * UnboundedConstraint isn't used in use-time, it's for declare time. + */ +public class UnboundedConstraint extends Constraint { + + public UnboundedConstraint(Target t, String typename) { + super(t, typename); + ConstraintValidator.ValidateTypename(typename, t); + } + + @Override + public EnumSet validLocations() { + return EnumSet.of(ConstraintLocation.DEFINITION); + } + + @Override + public String getConstraintName() { + return "unbounded constraint"; + } + + @Override + public boolean isWithinConstraint(LeftHandSideType type, Environment env) { + return true; + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, Mixed.TYPE.asLeftHandSideType()); + } + + @Override + public String toSimpleString() { + return toString(); + } + + @Override + public String toString() { + return getTypeName(); + } + + @Override + public boolean supportsTypeUnions() { + return true; + } + + @Override + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, Auto.LHSTYPE); + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UpperBoundConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UpperBoundConstraint.java new file mode 100644 index 0000000000..570236760c --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/UpperBoundConstraint.java @@ -0,0 +1,115 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import com.laytonsmith.core.objects.ObjectModifier; + +import java.util.EnumSet; +import java.util.Set; + +/** + * An UpperBoundConstraint is defined with the extends keyword, such as T extends Number. + * In this case, Number is the upper bound. + */ +public class UpperBoundConstraint extends BoundaryConstraint { + + /** + * Constructs a new upper bound constraint. + * @param t Code target + * @param typename The name of this parameter, may be ? for LHS constraints + * @param upperBound The concrete upper bound + */ + public UpperBoundConstraint(Target t, String typename, LeftHandSideType upperBound) { + super(t, typename, upperBound); + ConstraintValidator.ValidateTypename(typename, t); + for(Set upperBoundComponent : upperBound.getTypeObjectModifiers()) { + if(upperBoundComponent.contains(ObjectModifier.FINAL)) { + throw new CREGenericConstraintException(upperBound.val() + " is marked as final, and so" + + " cannot be used in an upper bound constraint.", t); + } + } + } + + public LeftHandSideType getUpperBound() { + return this.bound; + } + + @Override + public String toSimpleString() { + return getTypeName() + " extends " + getUpperBound().getSimpleName(); + } + + @Override + public String toString() { + return getTypeName() + " extends " + getUpperBound(); + } + + @Override + public EnumSet validLocations() { + return EnumSet.allOf(ConstraintLocation.class); + } + + @Override + public String getConstraintName() { + return "upper bound"; + } + + @Override + protected boolean isConcreteClassWithinConstraint(LeftHandSideType type, Environment env) { + return type.doesExtend(bound, env); + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return null; + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + if(lhs.getType() == null) { + // Wildcard + return true; + } + return isWithinConstraint(lhs.getType(), env); + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return lhs.getLowerBound().doesExtend(UpperBoundConstraint.this.getUpperBound(), env); + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return lhs.getUpperBound().doesExtend(UpperBoundConstraint.this.getUpperBound(), env); + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + throw new Error("Unexpected constraint combination."); + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) { + return new ExactTypeConstraint(t, this.bound); + } + + @Override + public boolean supportsTypeUnions() { + return true; + } +} diff --git a/src/main/java/com/laytonsmith/core/constructs/generics/constraints/VariadicTypeConstraint.java b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/VariadicTypeConstraint.java new file mode 100644 index 0000000000..531392dd9c --- /dev/null +++ b/src/main/java/com/laytonsmith/core/constructs/generics/constraints/VariadicTypeConstraint.java @@ -0,0 +1,94 @@ +package com.laytonsmith.core.constructs.generics.constraints; + +import com.laytonsmith.core.constructs.Auto; +import com.laytonsmith.core.constructs.LeftHandSideType; +import com.laytonsmith.core.constructs.Target; +import com.laytonsmith.core.constructs.generics.ConstraintLocation; +import com.laytonsmith.core.constructs.generics.ConstraintToConstraintValidator; +import com.laytonsmith.core.constructs.generics.ConstraintValidator; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.exceptions.CRE.CREGenericConstraintException; +import java.util.EnumSet; +import java.util.Set; + +/** + * + */ +public class VariadicTypeConstraint extends Constraint { + + public VariadicTypeConstraint(Target t, String typename) { + super(t, typename); + ConstraintValidator.ValidateTypename(typename, t); + } + + @Override + public Set validLocations() { + return EnumSet.of(ConstraintLocation.DEFINITION); + } + + @Override + public String getConstraintName() { + return "variadic generic type"; + } + + @Override + public boolean isWithinConstraint(LeftHandSideType type, Environment env) { + return true; + } + + @Override + public boolean supportsTypeUnions() { + return true; + } + + @Override + protected ConstraintToConstraintValidator getConstraintToConstraintValidator(Environment env) { + return new ConstraintToConstraintValidator() { + @Override + public Boolean isWithinBounds(ConstructorConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(ExactTypeConstraint lhs) { + return true; + } + + @Override + public Boolean isWithinBounds(LowerBoundConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(UpperBoundConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(UnboundedConstraint lhs) { + return false; + } + + @Override + public Boolean isWithinBounds(VariadicTypeConstraint lhs) { + return true; + } + }; + } + + @Override + public ExactTypeConstraint convertFromDiamond(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, Auto.LHSTYPE); + } + + @Override + public String toSimpleString() { + return getTypeName() + "..."; + } + + @Override + public ExactTypeConstraint convertFromNull(Target t) throws CREGenericConstraintException { + return new ExactTypeConstraint(t, (LeftHandSideType) Auto.LHSTYPE.asVariadicType(null)); + } + +} diff --git a/src/main/java/com/laytonsmith/core/exceptions/CRE/CREGenericConstraintException.java b/src/main/java/com/laytonsmith/core/exceptions/CRE/CREGenericConstraintException.java new file mode 100644 index 0000000000..4afdf03bd4 --- /dev/null +++ b/src/main/java/com/laytonsmith/core/exceptions/CRE/CREGenericConstraintException.java @@ -0,0 +1,49 @@ +package com.laytonsmith.core.exceptions.CRE; + +import com.laytonsmith.PureUtilities.Version; +import com.laytonsmith.annotations.typeof; +import com.laytonsmith.core.MSVersion; +import com.laytonsmith.core.constructs.CClassType; +import com.laytonsmith.core.constructs.Target; + +/** + * + */ +@typeof("ms.lang.CastException") +public class CREGenericConstraintException extends CREException { + + @SuppressWarnings("FieldNameHidesFieldInSuperclass") + public static final CClassType TYPE = CClassType.get(CREGenericConstraintException.class); + + public CREGenericConstraintException(String msg, Target t) { + super(msg, t); + } + + public CREGenericConstraintException(String msg, Target t, Throwable cause) { + super(msg, t, cause); + } + + @Override + public String docs() { + return "This exception is thrown if a generic definition has an invalid constraint combination. For instance," + + " in the generic definition , the class would need to be both" + + " a superclass of mixed and a subclass of number, a state which is impossible to be in."; + } + + @Override + public Version since() { + return MSVersion.V3_3_5; + } + + @Override + public CClassType[] getSuperclasses() { + return super.getSuperclasses(); + } + + @Override + public CClassType[] getInterfaces() { + return super.getInterfaces(); + } + +} + diff --git a/src/main/java/com/laytonsmith/core/functions/DataHandling.java b/src/main/java/com/laytonsmith/core/functions/DataHandling.java index fea745ffa0..bc4eb359aa 100644 --- a/src/main/java/com/laytonsmith/core/functions/DataHandling.java +++ b/src/main/java/com/laytonsmith/core/functions/DataHandling.java @@ -367,7 +367,7 @@ public Mixed exec(Target t, Environment env, Mixed... args) throws CancelCommand if(varArgsAllowed == null) { varArgsAllowed = false; } - if(type.isVarargs() && !varArgsAllowed) { + if(type.isVariadicType() && !varArgsAllowed) { throw new CRECastException("Cannot use varargs type in this context", t); } if(type.equals(CVoid.TYPE)) { diff --git a/src/test/java/com/laytonsmith/core/constructs/LeftHandSideTypeTest.java b/src/test/java/com/laytonsmith/core/constructs/LeftHandSideTypeTest.java new file mode 100644 index 0000000000..fdf6190c13 --- /dev/null +++ b/src/test/java/com/laytonsmith/core/constructs/LeftHandSideTypeTest.java @@ -0,0 +1,48 @@ +package com.laytonsmith.core.constructs; + +import com.laytonsmith.core.Static; +import com.laytonsmith.core.environments.Environment; +import com.laytonsmith.core.natives.interfaces.Booleanish; +import com.laytonsmith.testing.AbstractIntegrationTest; +import com.laytonsmith.testing.StaticTest; +import java.util.Arrays; +import java.util.List; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class LeftHandSideTypeTest extends AbstractIntegrationTest { + + Environment env; + + @Before + public void setup() throws Exception { + StaticTest.InstallFakeServerFrontend(); + env = Static.GenerateStandaloneEnvironment(); + } + + @Test + public void testInterfacesAreCorrect() throws Exception { + LeftHandSideType type = LeftHandSideType.fromCClassTypeUnion(Target.UNKNOWN, null, CString.TYPE, CArray.TYPE); + List interfaces = Arrays.asList(type.getTypeInterfaces(env)); + assertTrue(interfaces.contains(com.laytonsmith.core.natives.interfaces.Iterable.TYPE)); + assertFalse(interfaces.contains(Booleanish.TYPE)); + } + + @Test + public void testUnionsAreNormalized() throws Exception { + assertTrue(LeftHandSideType.fromCClassTypeUnion(Target.UNKNOWN, null, CInt.TYPE, CString.TYPE).equals( + LeftHandSideType.fromCClassTypeUnion(Target.UNKNOWN, null, CString.TYPE, CInt.TYPE))); + } + + @Test + public void testUnionsWithSupertypesAreNormalized() throws Exception { + assertEquals(CPrimitive.TYPE.asLeftHandSideType(), + LeftHandSideType.fromCClassTypeUnion(Target.UNKNOWN, null, CInt.TYPE, CPrimitive.TYPE)); + assertEquals(CPrimitive.TYPE.asLeftHandSideType(), + LeftHandSideType.fromCClassTypeUnion(Target.UNKNOWN, null, CPrimitive.TYPE, CInt.TYPE)); + } +}