diff --git a/AGENTS.md b/AGENTS.md index 50c442536..54284409f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -330,8 +330,9 @@ of save formats, `ChunkedString`, hashes, or `Serializable`. * Do not promise Lua support for a method which combines type parameters from its owning generic class with independent method type parameters. Serialization loaders should be free generic functions, or class methods parameterized only by their owning class. -* Do not require Lua specialization of generic-construction methods invoked directly on a freshly constructed - generic receiver. Use the free generic loader shape, or bind the receiver to a typed local first. +* A method invoked directly on a freshly constructed generic receiver is supported on Lua. The receiver's + declared type is still generic at that point, so specialization takes the instantiation from the construction. + Binding the receiver to a typed local first is no longer required. * Lua generic-construction dispatch through multi-parameter generic interfaces is outside the supported loader shape. The supported generic loader has a single construction type parameter. * Do not call `wurstNewInstance()` from the constructor of a generic class. Construct the simple state object in the diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d3ff7bf..f6d6b5c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ ## 1.9 (in progress) +- Added type class bounds for `T:` generics. A bound requires operations of the type it is bound to, so a + generic can do more than store and return values, without giving up static dispatch: + + public interface Indexable + function toIndex(T x) returns int + function fromIndex(int i) returns T + + implements Indexable + function toIndex(vec2 v) returns int + ... + function fromIndex(int i) returns vec2 + ... + + class HashMap + function get(K key) returns V + return V.fromIndex(loadInt(K.toIndex(key))) + + A bound names the interface unapplied, so `` means "there is an instance of `Indexable`", + and several combine with `and`. Requirements are called on the type parameter (`K.toIndex(key)`), which + keeps operations that produce a value of the type, such as `fromIndex`, in the same form as the rest. + + Unlike an interface used as a supertype, a bound is satisfiable by `int`, `real`, `string`, tuples and + handle types, and costs nothing at runtime: after specialisation each requirement is a direct call to the + instance function, on both Jass and Lua. + + An instance of `I` for type `X` may only be declared in the package declaring `I` or the one declaring + `X`, and only once, so `I` for `X` means the same thing throughout a program regardless of imports. + - Added new pseudo-natives for debugging memory leaks: // returns the maximum type id, can be usd to diff --git a/de.peeeq.wurstscript/parserspec/wurstscript.parseq b/de.peeeq.wurstscript/parserspec/wurstscript.parseq index 3ef44f5b7..8d6c33bce 100644 --- a/de.peeeq.wurstscript/parserspec/wurstscript.parseq +++ b/de.peeeq.wurstscript/parserspec/wurstscript.parseq @@ -36,6 +36,8 @@ WEntity = | ModuleDef(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Modifiers modifiers, Identifier nameId, TypeParamDefs typeParameters, ClassDefs innerClasses, FuncDefs methods, GlobalVarDefs vars, ConstructorDefs constructors, ModuleInstanciations p_moduleInstanciations, ModuleUses moduleUses, OnDestroyDef onDestroy) + // A type class instance: binds one interface to one concrete type. + | InstanceDecl(@ignoreForEquality de.peeeq.wurstscript.parser.WPos source, Modifiers modifiers, TypeExpr implementedInterface, FuncDefs methods) @@ -287,6 +289,7 @@ WScope = | WBlock | WEntities | ExprClosure + | InstanceDecl PackageOrGlobal = WPackage | CompilationUnit @@ -315,7 +318,7 @@ Modifier = // ElementWithBody = FunctionImplementation | InitBlock | ConstructorDef | OnDestroyDef //ElementWithModifier = NameDef | TypeDef | ModuleDef | ConstructorDef | GlobalVarDef | FunctionDefinition -HasModifier = NameDef | TypeDef | ModuleDef | ConstructorDef | GlobalVarDef | FunctionDefinition +HasModifier = NameDef | TypeDef | ModuleDef | ConstructorDef | GlobalVarDef | FunctionDefinition | InstanceDecl HasTypeArgs = ExprNewObject | FunctionCall | ModuleUse | StmtCall | TypeExprSimple AstElementWithFuncName = ExprFunctionCall | ExprMemberMethod | ExprFuncRef diff --git a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 index a3583c040..6b749b979 100644 --- a/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 +++ b/de.peeeq.wurstscript/src/main/antlr/de/peeeq/wurstscript/antlr/Wurst.g4 @@ -112,6 +112,7 @@ entity: | interfaceDef | tupleDef | extensionFuncDef + | instanceDef ; interfaceDef: @@ -132,11 +133,16 @@ classDef: ENDBLOCK)? ; -typeclassDef: - modifiersWithDoc 'typeclass' name=ID typeParams - ('extends' implemented+=typeExpr (',' implemented+=typeExpr)*)? +// A type class instance: binds an interface to one concrete type, e.g. +// implements Indexable +// function toIndex(vec2 v) returns int +// ... +// This reuses the existing 'implements' keyword deliberately. Introducing a new one would +// reserve a plausible identifier ('instance' is used as a local in the standard library). +instanceDef: + modifiersWithDoc 'implements' implemented=typeExpr NL (STARTBLOCK - classSlots + methods+=funcDef* ENDBLOCK)? ; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java index ab10a27d8..f15fc9d12 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java @@ -877,8 +877,11 @@ public LuaCompilationUnit transformProgToLua() { ImAttrType.setWurstClassType(null); int stage; - if (containsGenericNewCall()) { - beginPhase(2, "Specialize generics for generic construction"); + if (containsGenericNewCall() || containsTypeClassDispatch()) { + // Both operations need the concrete type argument, which erasure does not keep. Only + // the paths reaching them are specialised: the full elimination used for Jass is + // followed there by class elimination, and leaves state this backend cannot consume. + beginPhase(2, "Specialize generics for generic construction and type class dispatch"); new EliminateGenerics(getImTranslator(), getImProg()).transformGenericNewOnly(); timeTaker.endPhase(); } @@ -969,6 +972,7 @@ public LuaCompilationUnit transformProgToLua() { return luaCode; } + /** Whether the program constructs a value of a type parameter, which needs its concrete type. */ private boolean containsGenericNewCall() { boolean[] found = {false}; getImProg().accept(new de.peeeq.wurstscript.jassIm.Element.DefaultVisitor() { @@ -983,4 +987,16 @@ public void visit(ImFunctionCall call) { }); return found[0]; } + + /** Whether the program dispatches on a type class bound anywhere. */ + private boolean containsTypeClassDispatch() { + boolean[] found = {false}; + getImProg().accept(new de.peeeq.wurstscript.jassIm.Element.DefaultVisitor() { + @Override + public void visit(ImTypeVarDispatch dispatch) { + found[0] = true; + } + }); + return found[0]; + } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java index 48cabaf77..48183309a 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/DocumentSymbolRequest.java @@ -114,6 +114,15 @@ public void case_ClassDef(ClassDef classDef) { case_ClassOrModule(classDef); } + @Override + public void case_InstanceDecl(InstanceDecl instanceDecl) { + List children = new ArrayList<>(); + add("implements " + instanceDecl.getImplementedInterface(), SymbolKind.Object, children); + for (FuncDef f : instanceDecl.getMethods()) { + addSymbolsForEntity(children, f); + } + } + @Override public void case_InterfaceDef(InterfaceDef interfaceDef) { String name = interfaceDef.getName(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java index edc94658f..041c3846e 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/HoverInfo.java @@ -7,6 +7,7 @@ import de.peeeq.wurstscript.WLogger; import de.peeeq.wurstscript.ast.*; import de.peeeq.wurstscript.attributes.AttrWurstDoc; +import de.peeeq.wurstscript.attributes.DescriptionHtml; import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.attributes.names.NameLink; import de.peeeq.wurstscript.parser.TriviaIndex; @@ -403,6 +404,11 @@ public List> case_InterfaceDef(InterfaceDef interfa return description(interfaceDef); } + @Override + public List> case_InstanceDecl(InstanceDecl instanceDecl) { + return string(DescriptionHtml.description(instanceDecl)); + } + @Override public List> case_VisibilityProtected(VisibilityProtected visibilityProtected) { return string("protected: can be used in subclasses and in the same package"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/SymbolInformationRequest.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/SymbolInformationRequest.java index c64ab1b4f..3e9d3aa45 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/SymbolInformationRequest.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/requests/SymbolInformationRequest.java @@ -86,6 +86,15 @@ public void case_ClassDef(ClassDef classDef) { } } + @Override + public void case_InstanceDecl(InstanceDecl instanceDecl) { + String name = "implements " + instanceDecl.getImplementedInterface(); + add(name, SymbolKind.Object); + for (FuncDef f : instanceDecl.getMethods()) { + addSymbolsForEntity(result, containerName + "." + name, f); + } + } + @Override public void case_InterfaceDef(InterfaceDef interfaceDef) { String name = interfaceDef.getName(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java index 014bd7a1c..9f514656d 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrImplicitParameter.java @@ -5,6 +5,7 @@ import de.peeeq.wurstscript.attributes.names.NameLink; import de.peeeq.wurstscript.attributes.names.OtherLink; import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.types.WurstTypeTypeParam; import org.eclipse.jdt.annotation.Nullable; public class AttrImplicitParameter { @@ -86,6 +87,21 @@ private static OptExpr getImplicitParameterCaseNormalFunctionCall(FunctionCall e return getFunctionCallImplicitParameter(e, calledFunc, true); } + /** + * True for a call whose receiver is a bounded type parameter standing for itself, as in + * {@code T.toIndex(x)}. Such a call resolves through the type class instance chosen for T and + * therefore takes no implicit {@code this}. + */ + public static boolean isTypeClassDispatch(Element e) { + if (!(e instanceof HasReceiver hasReceiver)) { + return false; + } + Expr left = hasReceiver.getLeft(); + return left != null + && left.attrTyp() instanceof WurstTypeTypeParam tp + && tp.isStaticRef(); + } + static OptExpr getFunctionCallImplicitParameter(FunctionCall e, FuncLink calledFunc, boolean showError) { if (e instanceof HasReceiver) { HasReceiver hasReceiver = (HasReceiver) e; @@ -100,6 +116,11 @@ static OptExpr getFunctionCallImplicitParameter(FunctionCall e, FuncLink calledF if (calledFunc == null) { return Ast.NoExpr(); } + if (isTypeClassDispatch(e)) { + // T.f(x): the bound supplies the implementation and the value is an ordinary + // argument, so there is no receiver to pass even though f is declared as a method. + return Ast.NoExpr(); + } if (calledFunc.getDef().attrIsDynamicClassMember()) { // dynamic function call if (e.attrIsDynamicContext()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrNameDef.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrNameDef.java index e30419418..63c2d489c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrNameDef.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/AttrNameDef.java @@ -5,8 +5,12 @@ import de.peeeq.wurstscript.attributes.names.NameLink; import de.peeeq.wurstscript.attributes.names.OtherLink; import de.peeeq.wurstscript.attributes.names.Visibility; +import de.peeeq.wurstscript.jassIm.ImFunction; +import de.peeeq.wurstscript.translation.imtranslation.ImTranslator; +import de.peeeq.wurstscript.types.TypeClassConstraints; import de.peeeq.wurstscript.types.WurstType; import de.peeeq.wurstscript.types.WurstTypeClassOrInterface; +import de.peeeq.wurstscript.types.WurstTypeTypeParam; import de.peeeq.wurstscript.types.WurstTypeUnknown; import de.peeeq.wurstscript.types.WurstTypeEnum; import de.peeeq.wurstscript.types.WurstTypeModule; @@ -79,6 +83,16 @@ public static NameLink calculate(ExprMemberVar term) { protected static NameLink searchNameInScope(String varName, NameRef node) { boolean showErrors = !varName.startsWith("gg_"); if (!"it".equals(varName)) { + // A bounded type parameter can stand in receiver position, as in T.toIndex(x). The + // syntactic test comes first so ordinary lookups keep their original cost, and the + // probe below uses the cached form. Anything else falls through to the normal lookup, + // which must stay the last word so that it still reports ambiguity and unknown names. + if (isMethodCallReceiver(node) && node.lookupVar(varName, false) == null) { + NameLink typeParamRef = lookupBoundedTypeParam(varName, node); + if (typeParamRef != null) { + return typeParamRef; + } + } return node.lookupVar(varName, showErrors); } @@ -105,6 +119,33 @@ protected static NameLink searchNameInScope(String varName, NameRef node) { return node.lookupVar(varName, true); } + /** True when this reference is the receiver of a method call, as {@code T} is in {@code T.f(x)}. */ + private static boolean isMethodCallReceiver(NameRef node) { + return node.getParent() instanceof ExprMemberMethod call && call.getLeft() == node; + } + + /** + * Resolves a name which refers to a type parameter carrying type class bounds, so that the + * parameter can be used as the receiver of a required method: {@code T.toIndex(x)}. + *

+ * A bare type parameter is not a value, so this is deliberately limited to receiver position; + * everywhere else the ordinary "unknown variable" error is the right answer. + */ + private static @Nullable NameLink lookupBoundedTypeParam(String varName, NameRef node) { + TypeDef typeDef = node.lookupType(varName, false); + if (!(typeDef instanceof TypeParamDef tp) || !TypeClassConstraints.hasBounds(tp)) { + return null; + } + WurstTypeTypeParam typ = new WurstTypeTypeParam(tp).asStaticRef(); + return new OtherLink(Visibility.LOCAL, varName, typ) { + @Override + public de.peeeq.wurstscript.jassIm.ImExpr translate(NameRef e, ImTranslator t, ImFunction f) { + throw new CompileError(e.attrSource(), + "Type parameter " + varName + " is not a value; it can only be used to call a method required by its bounds."); + } + }; + } + private static @Nullable NameLink lookupImplicitClosureSelf(NameRef node, boolean showErrors) { ExprClosure closure = node.attrNearestExprClosure(); if (closure == null) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java index c30c5a104..749ef34a7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/DescriptionHtml.java @@ -230,6 +230,11 @@ public static String description(InitBlock initBlock) { return "An init block: This block is executed at map start"; } + public static String description(InstanceDecl instanceDecl) { + return "A type class instance for " + instanceDecl.getImplementedInterface() + + ": it lets this type be used where that interface is required as a type bound."; + } + public static @Nullable String description( IdentifierWithTypeParamDefs identifierWithTypeParamDefs) { return null; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ModifiersUtil.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ModifiersUtil.java index 3c388a57d..c3ba7fad9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ModifiersUtil.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ModifiersUtil.java @@ -19,6 +19,7 @@ public static Modifiers get(HasModifier h) { case TupleDef t -> t.getModifiers(); case ExtensionFuncDef e -> e.getModifiers(); case TypeParamDef tp -> tp.getModifiers(); + case InstanceDecl id -> id.getModifiers(); // If HasModifier ever expands, the compiler will force you to handle new cases here. case EnumDef enumDef -> enumDef.getModifiers(); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ReadVariables.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ReadVariables.java index 46c32add5..7dfeb3894 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ReadVariables.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/ReadVariables.java @@ -142,6 +142,10 @@ public static ImmutableList calculate(ClassDef e) { return generic(e); } + public static ImmutableList calculate(InstanceDecl e) { + return generic(e); + } + public static ImmutableList calculate(CompilationUnit e) { return generic(e); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java index 5622ed2f9..2eff16b07 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/FuncLink.java @@ -209,6 +209,21 @@ public FuncLink withTypeArgBinding(Element context, VariableBinding binding) { } } + /** + * Re-points this link at a different receiver type. + *

+ * Used for type class dispatch: a requirement is declared as a method of the interface, but a + * bound exposes it on the constrained type parameter itself, so {@code T.f(x)} resolves with + * {@code T} as the receiver rather than an interface instance. + */ + public FuncLink withReceiverType(@Nullable WurstType newReceiverType) { + if (newReceiverType == getReceiverType()) { + return this; + } + return new FuncLink(getVisibility(), getDefinedIn(), getTypeParams(), newReceiverType, def, + parameterNames, parameterTypes, returnType, mapping); + } + @Override public DefLink withGenericTypeParams(List typeParams) { if (typeParams.isEmpty()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java index 9fd9b602b..4793e0d22 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/NameLinks.java @@ -96,6 +96,16 @@ private static void reportOverrideErrors(Map calculate(InstanceDecl i) { + Multimap result = HashMultimap.create(); + addDefinedNames(result, i, i.getMethods()); + return ImmutableMultimap.copyOf(result); + } + public static ImmutableMultimap calculate(InterfaceDef i) { Multimap result = HashMultimap.create(); addDefinedNames(result, i, i.getMethods()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/TypeNameLinks.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/TypeNameLinks.java index f734ff836..341ac30b6 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/TypeNameLinks.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/names/TypeNameLinks.java @@ -39,6 +39,11 @@ public static ImmutableMultimap calculate(EnumDef e) { return ImmutableMultimap.of(); } + /** v1 type class instances have no type parameters of their own. */ + public static ImmutableMultimap calculate(InstanceDecl i) { + return ImmutableMultimap.of(); + } + public static ImmutableMultimap calculate(InterfaceDef i) { ImmutableMultimap.Builder result = ImmutableSetMultimap.builder(); addTypeParametersIfAny(result, i); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java index a0a477b11..23a65b403 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/attributes/prettyPrint/PrettyPrinter.java @@ -637,6 +637,20 @@ public static void prettyPrint(InitBlock e, Spacer spacer, StringBuilder sb, int e.getBody().prettyPrint(spacer, sb, indent + 1); } + public static void prettyPrint(InstanceDecl e, Spacer spacer, StringBuilder sb, int indent) { + printFirstNewline(e, sb, indent); + // InstanceDecl is not a Documentable (it has no name), so inline the printStuff steps: + printCommentsBefore(sb, e, indent); + printHotDoc(e.getModifiers(), spacer, sb, indent); + printIndent(sb, indent); + e.getModifiers().prettyPrint(spacer, sb, indent); + sb.append("implements"); + spacer.addSpace(sb); + e.getImplementedInterface().prettyPrint(spacer, sb, indent); + e.getMethods().prettyPrint(spacer, sb, indent + 1); + printCommentsAfter(sb, e, indent); + } + public static void prettyPrint(InterfaceDef e, Spacer spacer, StringBuilder sb, int indent) { printFirstNewline(e, sb, indent); printStuff(e, spacer, sb, indent); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java index 7f07951df..b4ef93be9 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/EvaluateExpr.java @@ -13,6 +13,7 @@ import de.peeeq.wurstscript.translation.imtranslation.ImPrinter; import de.peeeq.wurstscript.types.TypesHelper; import de.peeeq.wurstscript.utils.Utils; +import io.vavr.control.Either; import org.eclipse.jdt.annotation.Nullable; import java.util.ArrayList; @@ -489,8 +490,27 @@ public ILconst get() { public static ILconst eval(ImTypeVarDispatch e, ProgramState globalState, LocalState localState) { - // TODO store type arguments in localState with the required dispatch functions - throw new InterpreterException(e.attrTrace(), "Cannot evaluate " + e); + mark(e, globalState); + ImTypeArgument typeArgument = localState.getTypeArgument(e.getTypeVariable()); + if (typeArgument == null) { + throw new InterpreterException(e.attrTrace(), + "No type argument bound for " + e.getTypeVariable().getName() + + ", so " + e.getTypeClassFunc().getName() + " cannot be dispatched."); + } + Either impl = typeArgument.getTypeClassBinding().get(e.getTypeClassFunc()); + if (impl == null) { + throw new InterpreterException(e.attrTrace(), + "No type class instance bound for " + e.getTypeClassFunc().getName() + + " on type argument " + typeArgument.getType() + "."); + } + ImFunction target = impl.isRight() ? impl.get() : impl.getLeft().getImplementation(); + + ImExprs arguments = e.getArguments(); + ILconst[] args = new ILconst[arguments.size()]; + for (int i = 0; i < arguments.size(); i++) { + args[i] = arguments.get(i).evaluate(globalState, localState); + } + return ILInterpreter.runFunc(globalState, target, e, args).getReturnVal(); } public static ILconst eval(ImCast imCast, ProgramState globalState, LocalState localState) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java index 1f78165cc..467af1c5c 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ILInterpreter.java @@ -48,6 +48,132 @@ public ILInterpreter(ImProg prog, WurstGui gui, Optional mapFile, boolean this(prog, gui, mapFile, new ProgramState(gui, prog, isCompiletime)); } + /** + * Records the call's type arguments against the callee's type variables, so that a type class + * dispatch inside the body can find the instance chosen at the call site. Only relevant when + * interpreting a program which still has generics. + */ + private static void bindTypeArguments(ProgramState globalState, LocalState localState, ImFunction f, + @Nullable Element caller, ILconst[] args) { + Map binding = new HashMap<>(); + + // A bound may belong to the owning class rather than to the method, as in + // class Box. The receiver carries the arguments the class was created with. + if (args.length > 0 && args[0] instanceof ILconstObject receiver) { + bindClassTypeArguments(globalState, binding, receiver.getType(), + Collections.newSetFromMap(new IdentityHashMap<>())); + } + + ImTypeArguments typeArgs = null; + if (caller instanceof ImFunctionCall call) { + typeArgs = call.getTypeArguments(); + } else if (caller instanceof ImMethodCall call) { + typeArgs = call.getTypeArguments(); + } + if (typeArgs != null) { + List typeVars = f.getTypeVariables(); + for (int i = 0; i < Math.min(typeVars.size(), typeArgs.size()); i++) { + binding.putIfAbsent(typeVars.get(i), inheritIfStillAbstract(globalState, typeArgs.get(i))); + } + // A generic class holds its type variables on the class, not on its functions, so a + // constructor is called with arguments it has no variable of its own to bind. Only do + // this when the function has no type parameters, because then the arguments are the + // class's; a method with its own parameters is called with those instead, and the + // class's mapping already came from the receiver, which must not be overwritten. + if (typeVars.isEmpty()) { + ImClass owner = owningClass(f); + if (owner != null) { + ImTypeVars classVars = owner.getTypeVariables(); + for (int i = 0; i < Math.min(classVars.size(), typeArgs.size()); i++) { + binding.putIfAbsent(classVars.get(i), + inheritIfStillAbstract(globalState, typeArgs.get(i))); + } + } + } + } + + if (!binding.isEmpty()) { + localState.setTypeArguments(binding); + } + } + + /** + * Binds the type variables of the receiver's class and of every class it inherits from. + *

+ * A subclass may fix its parent's parameter, as in {@code class Child extends Parent}. + * The bound then belongs to {@code Parent}, while the receiver is a {@code Child} carrying no + * arguments of its own, so the supertype chain is where the concrete type is recorded. + */ + private static void bindClassTypeArguments(ProgramState globalState, + Map binding, + ImClassType classType, Set visited) { + bindClassTypeArguments(globalState, binding, classType.getClassDef(), + new ArrayList<>(classType.getTypeArguments()), visited); + } + + private static void bindClassTypeArguments(ProgramState globalState, + Map binding, + ImClass classDef, List classArgs, + Set visited) { + if (!visited.add(classDef)) { + return; + } + ImTypeVars classVars = classDef.getTypeVariables(); + for (int i = 0; i < Math.min(classVars.size(), classArgs.size()); i++) { + binding.putIfAbsent(classVars.get(i), inheritIfStillAbstract(globalState, classArgs.get(i))); + } + for (ImClassType superType : classDef.getSuperClasses()) { + // A subclass may forward its own parameter, as in class Child extends + // Parent. The supertype is written in terms of the subclass's variables, so resolve + // them against what this class was instantiated with before descending. + List superArgs = new ArrayList<>(); + for (ImTypeArgument superArg : superType.getTypeArguments()) { + superArgs.add(resolveAgainst(binding, superArg)); + } + bindClassTypeArguments(globalState, binding, superType.getClassDef(), superArgs, visited); + } + } + + /** Replaces a type argument that is still a variable by whatever that variable is bound to. */ + private static ImTypeArgument resolveAgainst(Map binding, + ImTypeArgument arg) { + if (!(arg.getType() instanceof ImTypeVarRef ref)) { + return arg; + } + ImTypeArgument known = binding.get(ref.getTypeVariable()); + if (known == null) { + // One source type parameter can be several nodes, so fall back to the name. + for (Map.Entry e : binding.entrySet()) { + if (e.getKey().getName().equals(ref.getTypeVariable().getName())) { + known = e.getValue(); + break; + } + } + } + return known != null ? known : arg; + } + + private static @Nullable ImClass owningClass(ImFunction f) { + Element owner = f.getParent(); + while (owner != null && !(owner instanceof ImClass)) { + owner = owner.getParent(); + } + return (ImClass) owner; + } + + /** + * When a bounded generic passes its own type parameter to another one, the inner call site + * carries no instance because the parameter is still abstract there. The caller's frame knows + * what it was called with, so take the argument from there. + */ + private static ImTypeArgument inheritIfStillAbstract(ProgramState globalState, ImTypeArgument arg) { + if (!arg.getTypeClassBinding().isEmpty() || !(arg.getType() instanceof ImTypeVarRef ref)) { + return arg; + } + ImTypeArgument fromCaller = globalState.getCurrentTypeArgument(ref.getTypeVariable()); + return fromCaller != null ? fromCaller : arg; + } + public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullable Element caller, ILconst... args) { if (Thread.currentThread().isInterrupted()) { @@ -94,6 +220,7 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab for (int i = 0; i < f.getParameters().size(); i++) { localState.setVal(f.getParameters().get(i), args[i]); } + bindTypeArguments(globalState, localState, f, caller, args); // --- stacktrace bookkeeping --- if (f.getBody().isEmpty()) { @@ -217,6 +344,7 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab } WPos pos = (caller != null) ? caller.attrTrace().attrErrorPos() : f.attrTrace().attrErrorPos(); globalState.pushStackframeWithTypes(f, receiverObj, args, pos, normalized); + globalState.pushTypeArguments(localState.getTypeArguments()); ILconst retVal = null; boolean didReturn = false; @@ -233,6 +361,7 @@ public static LocalState runFunc(ProgramState globalState, ImFunction f, @Nullab retVal = adjustTypeOfConstant(e.getVal(), f.getReturnType()); didReturn = true; } finally { + globalState.popTypeArguments(); globalState.popStackframe(); } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java index 4f6f450f7..cd6171d26 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/LocalState.java @@ -1,14 +1,55 @@ package de.peeeq.wurstscript.intermediatelang.interpreter; import de.peeeq.wurstscript.intermediatelang.ILconst; +import de.peeeq.wurstscript.jassIm.ImTypeArgument; +import de.peeeq.wurstscript.jassIm.ImTypeVar; import org.eclipse.jdt.annotation.Nullable; +import java.util.Map; + /** * Unchanged API. No eager map allocations unless you actually set/get vars/arrays. */ public class LocalState extends State { private @Nullable ILconst returnVal; + /** + * The type arguments this invocation was called with, kept only when the program still + * contains generics, i.e. when running before generic elimination. Type class dispatch reads + * the instance bound to a type argument from here. + */ + private @Nullable Map typeArguments; + + public void setTypeArguments(Map typeArguments) { + this.typeArguments = typeArguments; + } + + /** + * The argument bound to this type variable. + *

+ * One source type parameter can be represented by several {@code ImTypeVar} nodes — a class and + * its constructor hold separate ones — so fall back to matching by name, as generic elimination + * does. + */ + public @Nullable ImTypeArgument getTypeArgument(ImTypeVar typeVar) { + if (typeArguments == null) { + return null; + } + ImTypeArgument exact = typeArguments.get(typeVar); + if (exact != null) { + return exact; + } + for (Map.Entry e : typeArguments.entrySet()) { + if (e.getKey().getName().equals(typeVar.getName())) { + return e.getValue(); + } + } + return null; + } + + public Map getTypeArguments() { + return typeArguments == null ? java.util.Collections.emptyMap() : typeArguments; + } public LocalState() { // no eager allocations diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java index 0772d8224..357019bdb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/interpreter/ProgramState.java @@ -420,7 +420,7 @@ public ImType case_ImClassType(ImClassType ct) { boolean changed = false; for (ImTypeArgument ta : ct.getTypeArguments()) { ImType rt = resolveTypeDeep(ta.getType(), budget - 1); - newArgs.add(JassIm.ImTypeArgument(rt, ta.getTypeClassBinding())); + newArgs.add(JassIm.ImTypeArgument(rt, typeClassBindingFor(ta))); changed |= (rt != ta.getType()); } return changed ? JassIm.ImClassType(ct.getClassDef(), newArgs) : ct; @@ -476,7 +476,7 @@ public ImType case_ImClassType(ImClassType classType) { ImTypeArguments newArgs = JassIm.ImTypeArguments(); for (ImTypeArgument arg : classType.getTypeArguments()) { ImType substituted = substituteTypeVars(arg.getType(), substitutions); - newArgs.add(JassIm.ImTypeArgument(substituted, arg.getTypeClassBinding())); + newArgs.add(JassIm.ImTypeArgument(substituted, typeClassBindingFor(arg))); } return JassIm.ImClassType(classType.getClassDef(), newArgs); } @@ -524,6 +524,51 @@ public void pushStackframe(ImCompiletimeExpr f, WPos trace) { lastStatements.push(stmt); } + /** + * Type arguments of the frames currently on the stack. A type class dispatch on a parameter + * which the current frame received abstractly is answered from the frame which supplied it. + */ + private final Deque> typeArgumentFrames = new ArrayDeque<>(); + + public void pushTypeArguments(Map typeArguments) { + typeArgumentFrames.push(typeArguments); + } + + public void popTypeArguments() { + if (!typeArgumentFrames.isEmpty()) { + typeArgumentFrames.pop(); + } + } + + public @Nullable ImTypeArgument getCurrentTypeArgument(ImTypeVar typeVar) { + for (Map frame : typeArgumentFrames) { + for (Map.Entry e : frame.entrySet()) { + // A class and its constructor hold separate nodes for the same source type + // parameter, so identity alone is not enough to find the binding. + boolean sameVar = e.getKey() == typeVar || e.getKey().getName().equals(typeVar.getName()); + if (sameVar && !e.getValue().getTypeClassBinding().isEmpty()) { + return e.getValue(); + } + } + } + return null; + } + + /** + * The type class binding for a type argument being resolved. + *

+ * A class body refers to its own type variables, so the argument written there carries no + * binding. The frame which created the object was called with one, so take it from there; + * otherwise a bound on a generic class would have nothing to dispatch through. + */ + private Map> typeClassBindingFor(ImTypeArgument arg) { + if (!arg.getTypeClassBinding().isEmpty() || !(arg.getType() instanceof ImTypeVarRef ref)) { + return arg.getTypeClassBinding(); + } + ImTypeArgument fromFrame = getCurrentTypeArgument(ref.getTypeVariable()); + return fromFrame != null ? fromFrame.getTypeClassBinding() : arg.getTypeClassBinding(); + } + public void popStackframe() { // new Exception().printStackTrace(System.out); WLogger.trace(() -> "popStackframe " + (stackFrames.isEmpty() ? "empty" : stackFrames.peek().f)); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java index ee2565781..ed6660987 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/parser/antlr/AntlrWurstParseTreeTransformer.java @@ -384,6 +384,8 @@ private WPackage transformPackage(WpackageContext p) { return transformTupleDef(e.tupleDef()); } else if (e.extensionFuncDef() != null) { return transformExtensionFuncDef(e.extensionFuncDef()); + } else if (e.instanceDef() != null) { + return transformInstanceDef(e.instanceDef()); } if (e.exception != null) { @@ -397,6 +399,17 @@ private WPackage transformPackage(WpackageContext p) { } } + private WEntity transformInstanceDef(InstanceDefContext i) { + WPos src = source(i); + Modifiers modifiers = transformModifiers(i.modifiersWithDoc()); + TypeExpr implemented = transformTypeExpr(i.implemented); + FuncDefs methods = Ast.FuncDefs(); + for (FuncDefContext m : i.methods) { + methods.add(transformFuncDef(m)); + } + return Ast.InstanceDecl(src, modifiers, implemented, methods); + } + private WEntity transformExtensionFuncDef(ExtensionFuncDefContext f) { WPos src = source(f); Modifiers modifiers = transformModifiers(f.modifiersWithDoc()); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java index a24fbc516..885fa6f47 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/EliminateGenerics.java @@ -13,6 +13,7 @@ import de.peeeq.wurstscript.translation.imtojass.ImAttrType; import de.peeeq.wurstscript.translation.imtojass.TypeRewriteMatcher; import de.peeeq.wurstscript.translation.lua.translation.RemoveGarbage; +import io.vavr.control.Either; import org.eclipse.jdt.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -28,6 +29,14 @@ public class EliminateGenerics { private final ImProg prog; private boolean genericNewOnly; private final Deque genericsUses = new ArrayDeque<>(); + /** + * Call sites already rewritten to a specialisation. + *

+ * Collection has to be repeatable, because specialising one call is what makes the next one + * concrete. It is not naturally idempotent: a member call whose type arguments were consumed + * has them re-derived from its receiver, which would collect and specialise it again forever. + */ + private final Set specializedCallSites = Collections.newSetFromMap(new IdentityHashMap<>()); private final Table specializedFunctions = HashBasedTable.create(); private final Table specializedMethods = HashBasedTable.create(); private final Table specializedClasses = HashBasedTable.create(); @@ -107,12 +116,21 @@ public void transform() { public void transformGenericNewOnly() { genericNewOnly = true; collectUnspecializedGenericClassMethods(); - collectGenericNewRoots(); - eliminateGenericUses(); + // Specialising a constructor makes its result type concrete, which is what lets a method + // call on that result resolve. Repeat until a pass finds nothing new; collection is + // idempotent, so this terminates once every reachable site has been rewritten. + while (true) { + collectGenericNewRoots(); + if (genericsUses.isEmpty()) { + break; + } + eliminateGenericUses(); + } eliminateRemainingGenericNewCalls(); assertNoReachableGenericNewMarkers(); } + private void collectGenericNewRoots() { prog.accept(new Element.DefaultVisitor() { @Override @@ -155,6 +173,9 @@ public void visit(ImMethodCall call) { } private void collectGenericNewUse(ImFunctionCall call) { + if (specializedCallSites.contains(call)) { + return; + } if (translator.isGenericNewMarker(call.getFunc())) { if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericNewCall(call)); @@ -162,7 +183,7 @@ private void collectGenericNewUse(ImFunctionCall call) { return; } if (!call.getTypeArguments().isEmpty() - && functionContainsGenericNew(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>()))) { + && functionNeedsSpecialization(call.getFunc(), Collections.newSetFromMap(new IdentityHashMap<>()))) { if (!typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericImFunctionCall(call)); } @@ -170,8 +191,11 @@ && functionContainsGenericNew(call.getFunc(), Collections.newSetFromMap(new Iden } private void collectGenericNewUse(ImMethodCall call) { + if (specializedCallSites.contains(call)) { + return; + } ImMethod method = call.getMethod(); - if (!methodContainsGenericNew(method, + if (!methodNeedsSpecialization(method, Collections.newSetFromMap(new IdentityHashMap<>()), Collections.newSetFromMap(new IdentityHashMap<>()))) { return; @@ -179,12 +203,36 @@ private void collectGenericNewUse(ImMethodCall call) { if (call.getTypeArguments().isEmpty()) { addMemberTypeArguments(call, method.attrClass()); } + if (typeArgumentsContainTypeVariable(call.getTypeArguments())) { + // The receiver's declared type is still generic, which happens when the method is + // called straight on a freshly constructed value. The construction states the + // instantiation, so take the arguments from it. + useConstructionTypeArguments(call); + } if (!call.getTypeArguments().isEmpty() && !typeArgumentsContainTypeVariable(call.getTypeArguments())) { genericsUses.add(new GenericMethodCall(call)); } } + /** + * Replaces a member call's still-generic type arguments with those of the constructor call that + * produced its receiver, as in {@code new Box().render(x)}. + */ + private void useConstructionTypeArguments(ImMethodCall call) { + if (!(call.getReceiver() instanceof ImFunctionCall construction) + || construction.getTypeArguments().isEmpty() + || typeArgumentsContainTypeVariable(construction.getTypeArguments())) { + return; + } + List fromConstruction = new ArrayList<>(); + for (ImTypeArgument ta : construction.getTypeArguments()) { + fromConstruction.add(ta.copy()); + } + call.getTypeArguments().removeAll(); + call.getTypeArguments().addAll(fromConstruction); + } + private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) { for (ImTypeArgument typeArgument : typeArguments) { if (containsTypeVariable(typeArgument.getType())) { @@ -194,22 +242,46 @@ private boolean typeArgumentsContainTypeVariable(ImTypeArguments typeArguments) return false; } - private boolean functionContainsGenericNew(ImFunction function, Set visited) { - return functionContainsGenericNew(function, visited, + private boolean functionNeedsSpecialization(ImFunction function, Set visited) { + return functionNeedsSpecialization(function, visited, Collections.newSetFromMap(new IdentityHashMap<>())); } - private boolean functionContainsGenericNew(ImFunction function, Set visitedFunctions, + /** + * Whether a function must be specialised even on Lua, which otherwise keeps generics erased. + *

+ * Two operations need the concrete type argument: constructing a value of it, and dispatching + * on a type class bound. Specialising these paths keeps a bounded generic as cheap on Lua as it + * is on Jass, at the cost of one copy per instantiation actually used. + */ + private boolean functionNeedsSpecialization(ImFunction function, Set visitedFunctions, Set visitedMethods) { if (!visitedFunctions.add(function)) { return false; } boolean[] found = {false}; function.accept(new Element.DefaultVisitor() { + @Override + public void visit(ImTypeVarDispatch dispatch) { + found[0] = true; + } + + @Override + public void visit(ImAlloc alloc) { + // Constructing a class whose methods dispatch has to be specialised as well: + // otherwise the constructor keeps a generic result type, and a method call on that + // result never becomes concrete enough to resolve. + if (classNeedsSpecialization(alloc.getClazz().getClassDef(), visitedFunctions, visitedMethods)) { + found[0] = true; + return; + } + super.visit(alloc); + } + @Override public void visit(ImFunctionCall call) { if (translator.isGenericNewMarker(call.getFunc()) - || functionContainsGenericNew(call.getFunc(), visitedFunctions, visitedMethods)) { + || functionNeedsSpecialization(call.getFunc(), visitedFunctions, visitedMethods)) { found[0] = true; return; } @@ -218,7 +290,7 @@ public void visit(ImFunctionCall call) { @Override public void visit(ImMethodCall call) { - if (methodContainsGenericNew(call.getMethod(), visitedFunctions, visitedMethods)) { + if (methodNeedsSpecialization(call.getMethod(), visitedFunctions, visitedMethods)) { found[0] = true; return; } @@ -228,23 +300,65 @@ public void visit(ImMethodCall call) { return found[0]; } - private boolean methodContainsGenericNew(ImMethod method, Set visitedFunctions, + private boolean methodNeedsSpecialization(ImMethod method, Set visitedFunctions, Set visitedMethods) { if (!visitedMethods.add(method)) { return false; } if (method.getImplementation() != null - && functionContainsGenericNew(method.getImplementation(), visitedFunctions, visitedMethods)) { + && functionNeedsSpecialization(method.getImplementation(), visitedFunctions, visitedMethods)) { return true; } for (ImMethod subMethod : method.getSubMethods()) { - if (methodContainsGenericNew(subMethod, visitedFunctions, visitedMethods)) { + if (methodNeedsSpecialization(subMethod, visitedFunctions, visitedMethods)) { return true; } } return false; } + /** + * Whether constructing this class requires the concrete type argument, because one of its own + * or inherited members dispatches on a type class bound. + */ + private boolean classNeedsSpecialization(ImClass classDef, Set visitedFunctions, + Set visitedMethods) { + Boolean cached = classNeedsSpecialization.get(classDef); + if (cached != null) { + // Already answered, or currently being answered: a class reached through its own + // members contributes nothing new to the decision. + return cached; + } + classNeedsSpecialization.put(classDef, false); + boolean result = false; + for (ImFunction f : classDef.getFunctions()) { + if (functionNeedsSpecialization(f, visitedFunctions, visitedMethods)) { + result = true; + break; + } + } + if (!result) { + for (ImMethod m : classDef.getMethods()) { + if (methodNeedsSpecialization(m, visitedFunctions, visitedMethods)) { + result = true; + break; + } + } + } + if (!result) { + for (ImClassType superType : classDef.getSuperClasses()) { + if (classNeedsSpecialization(superType.getClassDef(), visitedFunctions, visitedMethods)) { + result = true; + break; + } + } + } + classNeedsSpecialization.put(classDef, result); + return result; + } + + private final Map classNeedsSpecialization = new IdentityHashMap<>(); + private void assertNoReachableGenericNewMarkers() { prog.accept(new Element.DefaultVisitor() { @Override @@ -949,7 +1063,9 @@ public void visit(ImClass c) { @Override public void visit(ImTypeArgument ta) { - ta.setType(transformType(ta.getType(), generics, typeVars)); + ImType original = ta.getType(); + ta.setType(transformType(original, generics, typeVars)); + inheritTypeClassBinding(ta, original, generics, typeVars); } @Override @@ -1007,9 +1123,115 @@ public void visit(ImDealloc e) { super.visit(e); } + @Override + public void visit(ImTypeVarDispatch e) { + super.visit(e); + resolveTypeClassDispatch(e, generics, typeVars); + } + }); } + /** + * Replaces a type class dispatch by a direct call once the type variable it dispatches on has + * been substituted by a concrete type argument. + *

+ * This is what keeps bounded generics free of runtime cost: after specialisation the call is an + * ordinary static call to the instance function, with no lookup and no indirection left. + */ + /** + * Carries a type class binding down into a nested call. + *

+ * When a bounded generic passes its own type parameter on to another bounded generic, the inner + * call site cannot know the instance: the parameter is still abstract there. Substituting the + * outer parameter also supplies the instance it was specialised with, which is what makes a + * chain of bounded generics resolve without any runtime dictionary. + */ + private static void inheritTypeClassBinding(ImTypeArgument ta, ImType original, + GenericTypes generics, List typeVars) { + if (!ta.getTypeClassBinding().isEmpty() || !(original instanceof ImTypeVarRef ref)) { + return; + } + int index = indexOfTypeVar(typeVars, ref.getTypeVariable()); + if (index < 0 || index >= generics.getTypeArguments().size()) { + return; + } + Map> outer = + generics.getTypeArguments().get(index).getTypeClassBinding(); + if (!outer.isEmpty()) { + ta.setTypeClassBinding(new LinkedHashMap<>(outer)); + } + } + + /** + * A type variable can be represented by more than one node for the same source type parameter, + * so match on the name as the rest of this pass does. + */ + private static String enclosingFunctionName(Element e) { + Element cur = e; + while (cur != null && !(cur instanceof ImFunction)) { + cur = cur.getParent(); + } + return cur == null ? "?" : ((ImFunction) cur).getName(); + } + + private static int indexOfTypeVar(List typeVars, ImTypeVar target) { + for (int i = 0; i < typeVars.size(); i++) { + ImTypeVar tv = typeVars.get(i); + if (tv == target || tv.getName().equals(target.getName())) { + return i; + } + } + return -1; + } + + private void resolveTypeClassDispatch(ImTypeVarDispatch e, GenericTypes generics, List typeVars) { + int index = indexOfTypeVar(typeVars, e.getTypeVariable()); + if (index >= 0 && index >= generics.getTypeArguments().size()) { + // Fewer arguments than variables: the variables and the arguments are not in + // correspondence here, so position says nothing. Reading one anyway would dispatch + // through whichever type happened to sit at that index. + return; + } + if (index < 0) { + // dispatching on a variable of some enclosing generic; it is resolved when that one is + // specialised. + return; + } + ImTypeArgument typeArgument = generics.getTypeArguments().get(index); + Either impl = typeArgument.getTypeClassBinding().get(e.getTypeClassFunc()); + if (impl == null) { + ImFunction fromRegistry = translator.lookupTypeClassImpl(e.getTypeClassFunc(), typeArgument.getType()); + if (fromRegistry != null) { + impl = Either.right(fromRegistry); + } + } + if (impl == null) { + if (containsTypeVariable(typeArgument.getType())) { + // Not an instantiation: passes which only rename or move type variables, such as + // lifting a class's variables onto its functions, substitute one variable for + // another. The dispatch is resolved once a concrete type argument arrives. + return; + } + throw new CompileError(e.attrTrace().attrSource(), + "No type class instance bound for " + e.getTypeClassFunc().getName() + + " on type argument " + typeArgument.getType() + + " (type variable " + e.getTypeVariable().getName() + + ", index " + index + " of " + typeVars.size() + + ", in " + enclosingFunctionName(e) + ")."); + } + ImExprs args = e.getArguments(); + args.setParent(null); + if (impl.isRight()) { + e.replaceBy(JassIm.ImFunctionCall(e.getTrace(), impl.get(), JassIm.ImTypeArguments(), args, + false, CallType.NORMAL)); + } else { + ImMethod method = impl.getLeft(); + e.replaceBy(JassIm.ImFunctionCall(e.getTrace(), method.getImplementation(), JassIm.ImTypeArguments(), + args, false, CallType.NORMAL)); + } + } + private static ImType transformType(ImType type, GenericTypes generics, List typeVars) { return ImAttrType.substituteType(type, generics.getTypeArguments(), typeVars); } @@ -1564,6 +1786,7 @@ public void eliminate() { } fc.setFunc(specializedFunc); fc.getTypeArguments().removeAll(); + specializedCallSites.add(fc); } } @@ -1661,6 +1884,7 @@ public void eliminate() { mc.setMethod(specializedMethod); mc.getTypeArguments().removeAll(); + specializedCallSites.add(mc); } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java index ea3a2580b..6d30244e8 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ExprTranslation.java @@ -8,6 +8,7 @@ import de.peeeq.wurstscript.ast.Element; import de.peeeq.wurstscript.attributes.AttrFuncDef; import de.peeeq.wurstscript.attributes.CompileError; +import de.peeeq.wurstscript.attributes.AttrImplicitParameter; import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.attributes.names.NameLink; import de.peeeq.wurstscript.attributes.names.OtherLink; @@ -512,6 +513,27 @@ public static ImExpr translateIntern(FunctionCall e, ImTranslator t, ImFunction } } + /** + * Translates {@code T.f(args)} into a dispatch through T's type class binding. + *

+ * The concrete implementation is not known here, because T is still abstract inside the + * generic. Generic elimination substitutes T and rewrites this node into a direct call to the + * function supplied by the instance chosen for the substituted type. + */ + private static ImExpr translateTypeClassDispatch(FunctionCall e, ImTranslator t, ImFunction f) { + WurstTypeTypeParam receiver = (WurstTypeTypeParam) ((HasReceiver) e).getLeft().attrTyp(); + FunctionDefinition called = e.attrFuncDef(); + if (!(called instanceof FuncDef method)) { + throw new CompileError(e.attrSource(), + "Type class requirement " + e.getFuncName() + " must be a function of the bound interface."); + } + ImExprs args = JassIm.ImExprs(); + for (Expr arg : e.getArgs()) { + args.add(arg.imTranslateExpr(t, f)); + } + return JassIm.ImTypeVarDispatch(e, t.getTypeClassFunc(method), args, t.getTypeVar(receiver.getDef())); + } + private static ImExpr translateFunctionCall(FunctionCall e, ImTranslator t, ImFunction f, boolean returnReveiver, boolean nullSafe) { if (e instanceof ExprFunctionCall call && CompilerIntrinsics.isNew(call)) { @@ -522,6 +544,10 @@ private static ImExpr translateFunctionCall(FunctionCall e, ImTranslator t, ImFu JassIm.ImExprs(), false, CallType.NORMAL); } + if (AttrImplicitParameter.isTypeClassDispatch(e)) { + return translateTypeClassDispatch(e, t, f); + } + if (e.getFuncName().equals("getStackTraceString") && e.attrImplicitParameter() instanceof NoExpr && e.getArgs().size() == 0) { // special built-in error function @@ -712,10 +738,7 @@ private static ImTypeArguments getFunctionCallTypeArguments(ImTranslator tr, Fun continue; } - ImType type = t.imTranslateType(tr); - // TODO handle constraints - Map> typeClassBinding = new HashMap<>(); - res.add(ImTypeArgument(type, typeClassBinding)); + res.add(t.imTranslateToTypeArgument(tr)); } return res; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java index df7f8a41d..f2becc2ef 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/Flatten.java @@ -70,8 +70,14 @@ private static String getTupleTempVarName() { return TUPLE_TEMP_VAR_NAMES[count % TUPLE_TEMP_VAR_NAMES.length]; } - public static Result flatten(ImTypeVarDispatch imTypeVarDispatch, ImTranslator translator, ImFunction f) { - throw new RuntimeException("called too early"); + /** + * A type class dispatch behaves like a call: only its arguments need flattening. The dispatch + * itself survives until generic elimination replaces it with a direct call. + */ + public static Result flatten(ImTypeVarDispatch e, ImTranslator t, ImFunction f) { + MultiResult r = flattenExprs(t, f, e.getArguments()); + return new Result(r.stmts, + ImTypeVarDispatch(e.getTrace(), e.getTypeClassFunc(), ImExprs(r.exprs), e.getTypeVariable())); } public static Result flatten(ImCast imCast, ImTranslator translator, ImFunction f) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java index 0f9f674cd..5c54dafb4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/GenericTypes.java @@ -39,9 +39,11 @@ public boolean equals(Object o) { if (!t1.getType().equalsType(t2.getType())) { return false; } - if (!t1.getTypeClassBinding().equals(t2.getTypeClassBinding())) { - return false; - } + // Deliberately not comparing the type class binding. It is only a fast path for + // resolving a dispatch; the implementation for a type is looked up by that type, so + // two arguments with the same type denote the same specialisation whether or not + // the binding survived. Comparing it also contradicted hashCode, which has always + // hashed the types alone, and produced two specialisations of the same class. } return true; } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java index c4afd7391..bb8b87843 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImPrinter.java @@ -3,6 +3,8 @@ import de.peeeq.wurstscript.jassIm.*; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.StringJoiner; import java.util.stream.Collectors; @@ -542,6 +544,21 @@ private static void printTypeArguments(ImTypeArguments typeArguments, int indent append(sb, ", "); } ta.getType().print(sb, indent); + // Show the type class instances travelling with the argument: whether they are + // present is the whole question when a bound fails to dispatch. + if (!ta.getTypeClassBinding().isEmpty()) { + // Sorted: this printing reaches specialization names and so the emitted symbols, + // which have to be identical for identical input. The binding is a hash map, so + // its iteration order is not. + List requirements = new ArrayList<>(); + for (ImTypeClassFunc requirement : ta.getTypeClassBinding().keySet()) { + requirements.add(requirement.getName()); + } + Collections.sort(requirements); + append(sb, "{"); + append(sb, String.join(", ", requirements)); + append(sb, "}"); + } first = false; } append(sb, ">"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java index 8797b7cc3..18cf87807 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/ImTranslator.java @@ -1694,6 +1694,67 @@ public TypeParamDef getTypeParamDef(ImTypeVar tv) { return typeVariableReverse.get(tv); } + /** + * The signature node standing for one type class requirement. + *

+ * Requirements are shared across every use of the bound, so each interface method maps to + * exactly one node. Dispatch sites reference it, and each type argument carries the concrete + * implementation bound to it, which is what lets generic elimination turn a dispatch into a + * direct call. + */ + public ImTypeClassFunc getTypeClassFunc(FuncDef method) { + return typeClassFuncs.computeIfAbsent(method, m -> { + ImTypeClassFunc result = JassIm.ImTypeClassFunc(m, m.getName(), JassIm.ImTypeVars(), + JassIm.ImVars(), m.attrReturnTyp().imTranslateType(this)); + imProg.getTypeClassFunctions().add(result); + return result; + }); + } + + private final Map typeClassFuncs = new LinkedHashMap<>(); + + /** + * Every type class implementation in the program, held per requirement against the type it is + * for. + *

+ * A type argument can carry its instances directly, but that binding lives on the argument + * position and is lost as soon as a type variable is substituted into a plain type, which is + * what happens when a generic class type travels through a return type or a receiver. Instance + * selection is static, so recording the type is enough to recover it. + *

+ * Matched by structural type equality rather than by printed name: a class prints as its simple + * name, so two classes of the same name in different packages would otherwise collide and the + * second would silently dispatch through the first. The lists hold one entry per instance of a + * requirement, so scanning them is cheaper than the printing it replaces. + */ + private final Map> typeClassImpls = new LinkedHashMap<>(); + + private record TypeClassImpl(ImType instanceType, ImFunction impl) { + } + + public void registerTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType, ImFunction impl) { + List impls = typeClassImpls.computeIfAbsent(requirement, r -> new ArrayList<>()); + for (TypeClassImpl existing : impls) { + if (existing.instanceType().equalsType(instanceType)) { + return; + } + } + impls.add(new TypeClassImpl(instanceType, impl)); + } + + public @Nullable ImFunction lookupTypeClassImpl(ImTypeClassFunc requirement, ImType instanceType) { + List impls = typeClassImpls.get(requirement); + if (impls == null) { + return null; + } + for (TypeClassImpl candidate : impls) { + if (candidate.instanceType().equalsType(instanceType)) { + return candidate.impl(); + } + } + return null; + } + public ImTypeVar getTypeVar(TypeParamDef tp) { // If we're translating inside a captured class (Iterator), prefer its override for (Map m : typeVarOverrideStack) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java index 987624959..e00f3a7e4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/imtranslation/TLDTranslation.java @@ -1,6 +1,9 @@ package de.peeeq.wurstscript.translation.imtranslation; import de.peeeq.wurstscript.ast.*; +import de.peeeq.wurstscript.types.TypeClassInstances; +import de.peeeq.wurstscript.types.WurstType; +import de.peeeq.wurstscript.jassIm.ImType; import de.peeeq.wurstscript.jassIm.ImFunction; import de.peeeq.wurstscript.jassIm.ImStmt; import de.peeeq.wurstscript.jassIm.ImVar; @@ -149,6 +152,38 @@ public static void translate(ModuleDef moduleDef, ImTranslator translator) { // nothing to do, only translate module instantiations } + /** + * A type class instance contributes its methods as ordinary global functions. Constraint + * resolution happens in the frontend, so every use site already knows which function it + * needs and can call it directly. + */ + public static void translate(InstanceDecl instanceDecl, ImTranslator translator) { + for (FuncDef method : instanceDecl.getMethods()) { + translate(method, translator); + } + registerInstance(instanceDecl, translator); + } + + /** + * Records which function implements which requirement for which type, so that a dispatch can be + * resolved from the concrete type it ends up with. + */ + private static void registerInstance(InstanceDecl decl, ImTranslator translator) { + InterfaceDef iface = TypeClassInstances.declaredInterface(decl); + WurstType instanceType = TypeClassInstances.instanceType(decl); + if (iface == null || instanceType == null || iface.getTypeParameters().size() != 1) { + return; // reported by the validator + } + ImType imInstanceType = instanceType.imTranslateType(translator); + for (FuncDef requirement : iface.getMethods()) { + FuncDef impl = TypeClassInstances.findImplementation(decl, requirement, instanceType); + if (impl != null) { + translator.registerTypeClassImpl(translator.getTypeClassFunc(requirement), + imInstanceType, translator.getFuncFor(impl)); + } + } + } + public static void translate(TypeParamDef typeParamDef, ImTranslator translator) { // not possible throw new Error("invalid AST"); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java index 225802d4d..4e729a863 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/translation/lua/translation/LuaTranslator.java @@ -196,7 +196,28 @@ public LuaTranslator(ImProg prog, ImTranslator imTr) { luaModel = LuaAst.LuaCompilationUnit(); } - protected String uniqueName(String name) { + /** + * Makes an intermediate-language name usable as a Lua identifier. + *

+ * Names from the IM are not constrained to Lua's identifier syntax; specialised generics, for + * example, are named after their type arguments. Sanitising here keeps that rule where it + * belongs, in the backend, rather than requiring every earlier pass to know about Lua. Any + * collisions the mapping introduces are resolved by the usual uniquing. + */ + private static String toLuaIdentifier(String name) { + StringBuilder sb = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + sb.append(c == '_' || Character.isLetterOrDigit(c) && c < 128 ? c : '_'); + } + if (sb.length() == 0 || Character.isDigit(sb.charAt(0))) { + sb.insert(0, '_'); + } + return sb.toString(); + } + + protected String uniqueName(String rawName) { + String name = toLuaIdentifier(rawName); Integer nextIndex = uniqueNameCounters.get(name); if (nextIndex == null) { uniqueNameCounters.put(name, 1); diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassConstraints.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassConstraints.java new file mode 100644 index 000000000..acca9f546 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassConstraints.java @@ -0,0 +1,138 @@ +package de.peeeq.wurstscript.types; + +import de.peeeq.wurstscript.ast.*; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Reads the type class bounds written on a new-style type parameter. + *

+ * A bound names the interface without applying it, so {@code } means + * "there is an instance of {@code Indexable}". v1 only supports interfaces with exactly one + * type parameter, which keeps the bound unambiguous and instance search a direct lookup. + */ +public final class TypeClassConstraints { + + private TypeClassConstraints() { + } + + /** + * The interfaces named as bounds of the given type parameter, in source order. + * Unusable bounds are skipped; {@link #invalidBoundReason} explains why, and the validator + * reports it at the bound itself so a bad bound does not cascade into every use site. + */ + public static List boundInterfaces(TypeParamDef tp) { + List exprs = boundExprs(tp); + if (exprs.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(exprs.size()); + for (TypeExpr e : exprs) { + InterfaceDef def = resolveBound(e); + if (def != null) { + result.add(def); + } + } + return result; + } + + /** The raw bound type expressions, so callers can attach diagnostics to the right source range. */ + public static List boundExprs(TypeParamDef tp) { + TypeParamConstraints constraints = tp.getTypeParamConstraints(); + if (!(constraints instanceof TypeExprList list) || list.isEmpty()) { + return Collections.emptyList(); + } + return new ArrayList<>(list); + } + + /** True if this type parameter carries at least one bound, i.e. it is more than a bare {@code }. */ + public static boolean hasBounds(TypeParamDef tp) { + return !boundExprs(tp).isEmpty(); + } + + /** + * Resolves one bound expression to the interface it names, or null when the bound is not a + * plain reference to a single-parameter interface. + */ + public static @Nullable InterfaceDef resolveBound(TypeExpr boundExpr) { + return invalidBoundReason(boundExpr) == null ? namedInterface(boundExpr) : null; + } + + /** + * Explains why a bound cannot be used as a type class, or null when it is usable. + *

+ * A bound must name an interface, unapplied, with exactly one type parameter, and that + * interface must not extend another. The last restriction keeps the set of requirements equal + * to the interface's own methods, so an instance cannot silently miss an inherited one. + */ + public static @Nullable String invalidBoundReason(TypeExpr boundExpr) { + if (!(boundExpr instanceof TypeExprSimple simple)) { + return "A bound must name an interface."; + } + // A bound names the interface unapplied: writing the type argument would be redundant, + // because it is always the type parameter being constrained. + if (!simple.getTypeArgs().isEmpty()) { + return "A bound must name the interface without type arguments, because the argument is" + + " always the type parameter being constrained."; + } + TypeDef def = boundExpr.lookupType(simple.getTypeName(), false); + if (def == null) { + return "Could not find " + simple.getTypeName() + "."; + } + if (!(def instanceof InterfaceDef i)) { + return simple.getTypeName() + " is not an interface, so it cannot be used as a bound."; + } + if (i.getTypeParameters().size() != 1) { + return i.getName() + " must have exactly one type parameter to be used as a bound, but has " + + i.getTypeParameters().size() + "."; + } + if (!i.getExtendsList().isEmpty()) { + return i.getName() + " extends another interface, which is not supported for bounds:" + + " the requirements of a bound are the interface's own functions."; + } + FuncDef generic = firstGenericMethod(i); + if (generic != null) { + // Matching such a requirement means pairing the interface's method type parameters with + // the implementation's, which this version does not do. Say so, rather than comparing + // parameters that only look identical and reporting a mismatch between a name and itself. + return i.getName() + "." + generic.getName() + " has its own type parameters, which is not" + + " supported for a bound: a requirement may only use the interface's type parameter."; + } + return null; + } + + /** The first requirement declaring type parameters of its own, or null when none does. */ + public static @Nullable FuncDef firstGenericMethod(InterfaceDef iface) { + for (FuncDef method : iface.getMethods()) { + if (!method.getTypeParameters().isEmpty()) { + return method; + } + } + return null; + } + + private static @Nullable InterfaceDef namedInterface(TypeExpr boundExpr) { + if (!(boundExpr instanceof TypeExprSimple simple)) { + return null; + } + return boundExpr.lookupType(simple.getTypeName(), false) instanceof InterfaceDef i ? i : null; + } + + /** + * Looks up a method required by any bound of the given type parameter. + * Earlier bounds win, matching the left-to-right order the bounds were written in. + */ + public static @Nullable FuncDef findRequiredMethod(TypeParamDef tp, String name) { + for (InterfaceDef bound : boundInterfaces(tp)) { + for (FuncDef m : bound.getMethods()) { + if (m.getName().equals(name)) { + return m; + } + } + } + return null; + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassInstances.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassInstances.java new file mode 100644 index 000000000..43ba5f981 --- /dev/null +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/TypeClassInstances.java @@ -0,0 +1,178 @@ +package de.peeeq.wurstscript.types; + +import de.peeeq.wurstscript.ast.*; +import org.eclipse.jdt.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Finds the type class instance which satisfies a bound. + *

+ * Resolution deliberately does not search the import graph. An instance may only be declared in + * the package that declares the interface or in the package that declares the type, so a lookup + * only ever has to inspect those two packages. That keeps instance selection independent of which + * packages happen to be imported at the use site, which in turn guarantees that a given + * {@code (interface, type)} pair means the same thing everywhere in the program. + */ +public final class TypeClassInstances { + + private TypeClassInstances() { + } + + /** + * The single instance binding {@code iface} to {@code type}, or null when there is none. + * Ambiguity is impossible by construction here; the validator rejects duplicate instances at + * their declaration sites instead, where the error can point at both offenders. + */ + public static @Nullable InstanceDecl find(InterfaceDef iface, WurstType type) { + for (WPackage p : candidatePackages(iface, type)) { + InstanceDecl found = findIn(p, iface, type); + if (found != null) { + return found; + } + } + return null; + } + + /** The only two packages an instance for this pair is permitted to live in. */ + public static List candidatePackages(InterfaceDef iface, WurstType type) { + List result = new ArrayList<>(2); + WPackage ifacePackage = packageOf(iface); + if (ifacePackage != null) { + result.add(ifacePackage); + } + WPackage typePackage = packageOfType(type); + if (typePackage != null && !result.contains(typePackage)) { + result.add(typePackage); + } + return result; + } + + private static @Nullable InstanceDecl findIn(WPackage p, InterfaceDef iface, WurstType type) { + for (InstanceDecl decl : declaredIn(p)) { + if (matches(decl, iface, type)) { + return decl; + } + } + return null; + } + + /** The instance declarations written directly in the given package. */ + public static List declaredIn(WPackage p) { + List result = new ArrayList<>(); + for (WEntity e : p.getElements()) { + if (e instanceof InstanceDecl decl) { + result.add(decl); + } + } + return result; + } + + /** True when this declaration is the instance for exactly {@code iface} applied to {@code type}. */ + public static boolean matches(InstanceDecl decl, InterfaceDef iface, WurstType type) { + InterfaceDef declared = declaredInterface(decl); + if (declared != iface) { + return false; + } + WurstType declaredFor = instanceType(decl); + return declaredFor != null && declaredFor.equalsType(type, decl); + } + + /** The interface named by an instance declaration, e.g. {@code Indexable} in {@code instance Indexable}. */ + public static @Nullable InterfaceDef declaredInterface(InstanceDecl decl) { + if (!(decl.getImplementedInterface() instanceof TypeExprSimple simple)) { + return null; + } + TypeDef def = decl.lookupType(simple.getTypeName(), false); + return def instanceof InterfaceDef i ? i : null; + } + + /** The concrete type an instance is declared for, e.g. {@code vec2} in {@code instance Indexable}. */ + public static @Nullable WurstType instanceType(InstanceDecl decl) { + if (!(decl.getImplementedInterface() instanceof TypeExprSimple simple)) { + return null; + } + if (simple.getTypeArgs().size() != 1) { + return null; + } + return simple.getTypeArgs().get(0).attrTyp(); + } + + /** + * The instance method implementing the given requirement, or null when none matches. + *

+ * An interface may overload a requirement name, so the match is on the substituted signature + * rather than the name alone. Validation and lowering both go through here, so a rejected + * instance and a selected implementation can never disagree. + */ + public static @Nullable FuncDef findImplementation(InstanceDecl decl, FuncDef requirement, + WurstType instanceType) { + List sameName = new ArrayList<>(); + for (FuncDef provided : decl.getMethods()) { + if (provided.getName().equals(requirement.getName())) { + sameName.add(provided); + } + } + if (sameName.size() == 1) { + // The common case: one candidate, so let the signature check report any mismatch + // against this one rather than silently finding nothing. + return sameName.get(0); + } + for (FuncDef provided : sameName) { + if (signatureMatches(provided, requirement, instanceType, decl)) { + return provided; + } + } + return null; + } + + /** True when the implementation matches the requirement with the interface parameter substituted. */ + public static boolean signatureMatches(FuncDef provided, FuncDef requirement, WurstType instanceType, + Element context) { + VariableBinding binding = requirementBinding(requirement, instanceType, context); + if (binding == null || provided.getParameters().size() != requirement.getParameters().size()) { + return false; + } + for (int i = 0; i < requirement.getParameters().size(); i++) { + WurstType expected = requirement.getParameters().get(i).attrTyp().setTypeArgs(binding); + if (!provided.getParameters().get(i).attrTyp().equalsType(expected, context)) { + return false; + } + } + WurstType expectedReturn = requirement.attrReturnTyp().setTypeArgs(binding); + return provided.attrReturnTyp().equalsType(expectedReturn, context); + } + + /** Replaces the interface's own type parameter by the type an instance is declared for. */ + public static @Nullable VariableBinding requirementBinding(FuncDef requirement, WurstType instanceType, + Element context) { + ClassOrInterface owner = requirement.attrNearestClassOrInterface(); + if (!(owner instanceof InterfaceDef iface) || iface.getTypeParameters().size() != 1) { + return null; + } + TypeParamDef ifaceParam = iface.getTypeParameters().get(0); + return VariableBinding.emptyMapping() + .set(ifaceParam, new WurstTypeBoundTypeParam(ifaceParam, instanceType, context)); + } + + private static @Nullable WPackage packageOf(Element e) { + PackageOrGlobal p = e.attrNearestPackage(); + return p instanceof WPackage w ? w : null; + } + + /** + * The package which declares the given type, if it has one. Primitives and Jass handle types + * have no declaring Wurst package, so instances for those must live with the interface. + */ + private static @Nullable WPackage packageOfType(WurstType type) { + WurstType t = type.normalize(); + if (t instanceof WurstTypeNamedScope ns && ns.getDef() != null) { + return packageOf(ns.getDef()); + } + if (t instanceof WurstTypeTuple tuple && tuple.getTupleDef() != null) { + return packageOf(tuple.getTupleDef()); + } + return null; + } +} diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeBoundTypeParam.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeBoundTypeParam.java index fe60894ad..97e6a4ca7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeBoundTypeParam.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeBoundTypeParam.java @@ -199,9 +199,40 @@ public boolean isTemplateTypeParameter() { } public ImTypeArgument imTranslateToTypeArgument(ImTranslator tr) { - ImType t = imTranslateType(tr); - Map> typeClassBinding = new HashMap<>(); - // TODO add type class binding - return JassIm.ImTypeArgument(t, typeClassBinding); + return JassIm.ImTypeArgument(imTranslateType(tr), imTypeClassBinding(tr)); + } + + /** + * Binds every requirement of this type parameter's bounds to the function supplied by the + * instance chosen for the type it is bound to. + *

+ * Used for both function and class type arguments, so that a bound on a generic class works the + * same way as one on a generic function. Stays empty while the bound type is itself abstract: + * the enclosing generic is specialised first, and the binding is inherited at that point. + */ + public Map> imTypeClassBinding(ImTranslator tr) { + Map> binding = new HashMap<>(); + List bounds = TypeClassConstraints.boundInterfaces(typeParamDef); + if (bounds.isEmpty()) { + return binding; + } + WurstType concrete = baseType.normalize(); + if (concrete instanceof WurstTypeTypeParam || concrete instanceof WurstTypeBoundTypeParam) { + return binding; + } + for (InterfaceDef iface : bounds) { + InstanceDecl instance = TypeClassInstances.find(iface, concrete); + if (instance == null) { + // the validator reports the unsatisfied bound; do not fail translation as well + continue; + } + for (FuncDef requirement : iface.getMethods()) { + FuncDef impl = TypeClassInstances.findImplementation(instance, requirement, concrete); + if (impl != null) { + binding.put(tr.getTypeClassFunc(requirement), Either.right(tr.getFuncFor(impl))); + } + } + } + return binding; } } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeTypeParam.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeTypeParam.java index 10f44b84c..927668c38 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeTypeParam.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/types/WurstTypeTypeParam.java @@ -1,8 +1,11 @@ package de.peeeq.wurstscript.types; import de.peeeq.wurstscript.ast.Element; +import de.peeeq.wurstscript.ast.FuncDef; +import de.peeeq.wurstscript.ast.InterfaceDef; import de.peeeq.wurstscript.ast.TypeExprList; import de.peeeq.wurstscript.ast.TypeParamDef; +import de.peeeq.wurstscript.attributes.names.FuncLink; import de.peeeq.wurstscript.jassIm.ImExprOpt; import de.peeeq.wurstscript.jassIm.ImType; import de.peeeq.wurstscript.jassIm.JassIm; @@ -10,12 +13,25 @@ import io.vavr.control.Option; import org.eclipse.jdt.annotation.Nullable; +import java.util.List; +import java.util.stream.Stream; + public class WurstTypeTypeParam extends WurstType { private final TypeParamDef def; + /** + * True when this stands for the type parameter itself rather than a value of it, as in the + * receiver of {@code T.toIndex(x)}. Only this form exposes the methods required by the bounds. + */ + private final boolean staticRef; public WurstTypeTypeParam(TypeParamDef t) { + this(t, false); + } + + public WurstTypeTypeParam(TypeParamDef t, boolean staticRef) { this.def = t; + this.staticRef = staticRef; } @Override @@ -54,6 +70,16 @@ public TypeParamDef getDef() { return def; } + @Override + public boolean isStaticRef() { + return staticRef; + } + + /** The same type parameter, seen as the type itself rather than as a value of it. */ + public WurstTypeTypeParam asStaticRef() { + return staticRef ? this : new WurstTypeTypeParam(def, true); + } + @Override public VariableBinding getTypeArgBinding() { return VariableBinding.emptyMapping(); @@ -67,6 +93,44 @@ public WurstType setTypeArgs(VariableBinding typeParamBounds) { return this; } + @Override + public void addMemberMethods(Element node, String name, List result) { + if (!staticRef) { + return; + } + for (InterfaceDef bound : TypeClassConstraints.boundInterfaces(def)) { + for (FuncDef method : bound.getMethods()) { + if (method.getName().equals(name)) { + result.add(requirementLink(node, bound, method)); + } + } + } + } + + @Override + public Stream getMemberMethods(Element node) { + if (!staticRef) { + return Stream.empty(); + } + return TypeClassConstraints.boundInterfaces(def).stream() + .flatMap(bound -> bound.getMethods().stream() + .map(method -> requirementLink(node, bound, method))); + } + + /** + * Exposes one interface method as a requirement of this type parameter: the interface's own + * type parameter is substituted by this one, and the receiver becomes the type parameter, so + * the call reads {@code T.f(args)} with the arguments exactly as declared. + */ + private FuncLink requirementLink(Element node, InterfaceDef bound, FuncDef method) { + TypeParamDef ifaceParam = bound.getTypeParameters().get(0); + VariableBinding binding = VariableBinding.emptyMapping() + .set(ifaceParam, new WurstTypeBoundTypeParam(ifaceParam, new WurstTypeTypeParam(def), node)); + return FuncLink.create(method, bound) + .withTypeArgBinding(node, binding) + .withReceiverType(this); + } + @Override public ImType imTranslateType(ImTranslator tr) { if (hasTypeConstraints()) { diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java index 6a65f1544..39c0d0e1f 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/validation/WurstValidator.java @@ -403,6 +403,12 @@ private void check(Element e) { } if (e instanceof ClassOrModule) checkConstructorsUnique((ClassOrModule) e); + if (e instanceof InstanceDecl) + checkInstanceDecl((InstanceDecl) e); + if (e instanceof TypeParamDef) + checkTypeParamBounds((TypeParamDef) e); + if (e instanceof StmtCall) + checkCallBounds((StmtCall) e); if (e instanceof CompilationUnit) checkPackageName((CompilationUnit) e); if (e instanceof ConstructorDef) { @@ -2501,7 +2507,7 @@ public VariableBinding case_ExprMemberMethodQuestionDot(ExprMemberMethodQuestion TypeParamDef tp = t._1(); if (isTypeParamNewGeneric(tp)) { - // new style generics + checkBoundsSatisfied(e, tp, typ); } else { // old style generics if (!typ.isTranslatedToInt() && !(e instanceof ModuleUse)) { @@ -2572,6 +2578,241 @@ public static boolean isTypeParamNewGeneric(TypeParamDef tp) { return tp.getTypeParamConstraints() instanceof TypeExprList; } + /** + * Checks the bounds of the callee's type parameters against the type arguments this call + * inferred. The call signature is the only place both are known, since the return type alone + * usually mentions none of them. + */ + private void checkCallBounds(StmtCall call) { + FunctionSignature sig = call.attrFunctionSignature(); + if (sig == null) { + return; + } + VariableBinding mapping = sig.getMapping(); + if (mapping == null) { + return; + } + for (Tuple2 t : mapping) { + checkBoundsSatisfied(call, t._1(), t._2().getBaseType()); + } + } + + /** + * Every bound on a type parameter must have an instance for the type it was bound to, checked + * where the type argument is chosen so the message can name both. + */ + private void checkBoundsSatisfied(Element location, TypeParamDef tp, WurstType typ) { + List bounds = TypeClassConstraints.boundInterfaces(tp); + if (bounds.isEmpty() || typ instanceof WurstTypeUnknown) { + return; + } + WurstType normalized = typ.normalize(); + TypeParamDef abstractArg = asTypeParam(normalized); + if (abstractArg != null) { + // The argument is another type parameter, so no instance exists yet. It can only supply + // the bound if it declares it itself; otherwise the call is unsatisfiable no matter what + // the outer generic is later instantiated with. + for (InterfaceDef bound : bounds) { + if (!TypeClassConstraints.boundInterfaces(abstractArg).contains(bound)) { + location.addError("Type parameter " + abstractArg.getName() + " does not satisfy the bound " + + tp.getName() + ": " + bound.getName() + ".\nAdd the bound to " + abstractArg.getName() + + ", as in <" + abstractArg.getName() + ": " + bound.getName() + ">."); + } + } + return; + } + for (InterfaceDef bound : bounds) { + if (TypeClassInstances.find(bound, normalized) == null) { + location.addError("Type " + normalized + " does not satisfy the bound " + tp.getName() + ": " + + bound.getName() + ".\nDeclare 'implements " + bound.getName() + "<" + normalized + + ">' in the package of " + bound.getName() + " or of " + normalized + "."); + } + } + } + + /** The type parameter this type stands for, when it is still abstract. */ + private static @Nullable TypeParamDef asTypeParam(WurstType typ) { + if (typ instanceof WurstTypeTypeParam tp) { + return tp.getDef(); + } + if (typ instanceof WurstTypeBoundTypeParam bound) { + return asTypeParam(bound.getBaseType().normalize()); + } + return null; + } + + /** Every bound written on a type parameter must be usable as a type class. */ + private void checkTypeParamBounds(TypeParamDef tp) { + if (TypeClassConstraints.hasBounds(tp) && tp.attrNearestStructureDef() instanceof ModuleDef) { + // Using a module copies its body into the class, replacing the module's type parameters + // in type positions. A requirement is called on the parameter itself, which is an + // expression, so it survives the copy and no longer resolves. Reject that here rather + // than let it fail later as an unknown name. + tp.addError("Type class bounds are not supported on a module type parameter." + + "\nMove the bounded generic into a class, or use the module without a bound."); + return; + } + for (TypeExpr boundExpr : TypeClassConstraints.boundExprs(tp)) { + String reason = TypeClassConstraints.invalidBoundReason(boundExpr); + if (reason != null) { + boundExpr.addError("Invalid bound on type parameter " + tp.getName() + ": " + reason); + } + } + } + + /** + * Checks one instance declaration: it must name a usable interface and type, live in a package + * permitted by the orphan rule, be the only instance for its pair, and implement every + * requirement exactly once. + */ + private void checkInstanceDecl(InstanceDecl decl) { + InterfaceDef iface = TypeClassInstances.declaredInterface(decl); + if (iface == null) { + decl.addError("An instance must name an interface applied to one type, as in " + + "'implements Indexable'."); + return; + } + if (iface.getTypeParameters().size() != 1) { + decl.addError("Interface " + iface.getName() + " cannot be used as a type class: a bound requires " + + "exactly one type parameter, but it has " + iface.getTypeParameters().size() + "."); + return; + } + WurstType instanceType = TypeClassInstances.instanceType(decl); + if (instanceType == null || instanceType instanceof WurstTypeUnknown) { + decl.addError("Could not resolve the type this instance is declared for."); + return; + } + + if (!iface.getExtendsList().isEmpty()) { + decl.addError("Interface " + iface.getName() + " extends another interface, which is not supported" + + " for type classes: the requirements of a bound are the interface's own functions."); + return; + } + FuncDef genericRequirement = TypeClassConstraints.firstGenericMethod(iface); + if (genericRequirement != null) { + decl.addError(iface.getName() + "." + genericRequirement.getName() + " has its own type parameters," + + " which is not supported for a type class: a requirement may only use the interface's" + + " type parameter."); + return; + } + + checkInstanceIsNotOrphan(decl, iface, instanceType); + checkInstanceIsUnique(decl, iface, instanceType); + checkInstanceIsComplete(decl, iface, instanceType); + } + + private void checkInstanceIsNotOrphan(InstanceDecl decl, InterfaceDef iface, WurstType instanceType) { + List allowed = TypeClassInstances.candidatePackages(iface, instanceType); + PackageOrGlobal declaredIn = decl.attrNearestPackage(); + if (!(declaredIn instanceof WPackage p) || allowed.contains(p)) { + return; + } + StringBuilder allowedNames = new StringBuilder(); + for (WPackage a : allowed) { + if (allowedNames.length() > 0) { + allowedNames.append(" or "); + } + allowedNames.append(a.getName()); + } + decl.addError("An instance must be declared with its interface or with its type, but this one is in " + + p.getName() + ".\nMove it to " + allowedNames + ".\nThis keeps a type class instance the same " + + "everywhere, independent of which packages happen to be imported."); + } + + private void checkInstanceIsUnique(InstanceDecl decl, InterfaceDef iface, WurstType instanceType) { + for (WPackage p : TypeClassInstances.candidatePackages(iface, instanceType)) { + for (InstanceDecl other : TypeClassInstances.declaredIn(p)) { + if (other != decl && TypeClassInstances.matches(other, iface, instanceType)) { + decl.addError("There is already an instance of " + iface.getName() + " for " + instanceType + + ", declared in " + p.getName() + " at line " + other.getSource().getLine() + + ".\nA type may implement an interface as a type class only once."); + return; + } + } + } + } + + private void checkInstanceIsComplete(InstanceDecl decl, InterfaceDef iface, WurstType instanceType) { + // The requirement is written in terms of the interface's type parameter, so compare against + // it with that parameter replaced by the type this instance is for. + TypeParamDef ifaceParam = iface.getTypeParameters().get(0); + VariableBinding binding = VariableBinding.emptyMapping() + .set(ifaceParam, new WurstTypeBoundTypeParam(ifaceParam, instanceType, decl)); + + // An interface may overload a requirement name, so pair each requirement with the + // implementation that matches its signature; the lowering selects the same one. + StringBuilder missing = new StringBuilder(); + Set used = Collections.newSetFromMap(new IdentityHashMap<>()); + for (FuncDef requirement : iface.getMethods()) { + FuncDef impl = TypeClassInstances.findImplementation(decl, requirement, instanceType); + if (impl == null || !used.add(impl)) { + missing.append("\n ").append(signatureText(requirement, binding)); + continue; + } + checkInstanceMethodSignature(impl, requirement, binding, iface); + } + if (missing.length() > 0) { + decl.addError("This instance of " + iface.getName() + " must implement:" + missing); + } + for (FuncDef provided : decl.getMethods()) { + if (!used.contains(provided)) { + provided.addError(provided.getName() + " does not implement any requirement of " + + iface.getName() + ", so it cannot be defined in this instance."); + } + } + } + + /** + * An instance method must match its requirement exactly once the interface's type parameter is + * replaced by the instance type. Matching on the name alone would let a wrongly typed + * implementation be selected and emitted, which the backend cannot catch. + */ + private void checkInstanceMethodSignature(FuncDef provided, FuncDef requirement, + VariableBinding binding, InterfaceDef iface) { + List expectedParams = new ArrayList<>(); + for (WParameter p : requirement.getParameters()) { + expectedParams.add(p.attrTyp().setTypeArgs(binding)); + } + if (provided.getParameters().size() != expectedParams.size()) { + provided.addError(provided.getName() + " must take " + expectedParams.size() + + " parameter(s) to implement " + iface.getName() + "." + + "\nExpected: " + signatureText(requirement, binding)); + return; + } + for (int i = 0; i < expectedParams.size(); i++) { + WParameter actual = provided.getParameters().get(i); + WurstType expected = expectedParams.get(i); + if (!actual.attrTyp().equalsType(expected, actual)) { + actual.addError("Parameter " + actual.getName() + " should have type " + expected + + " to implement " + iface.getName() + "." + provided.getName() + ", but has " + + actual.attrTyp() + "."); + } + } + WurstType expectedReturn = requirement.attrReturnTyp().setTypeArgs(binding); + if (!provided.attrReturnTyp().equalsType(expectedReturn, provided)) { + provided.addError(provided.getName() + " should return " + expectedReturn + " to implement " + + iface.getName() + ", but returns " + provided.attrReturnTyp() + "."); + } + } + + private String signatureText(FuncDef requirement, VariableBinding binding) { + StringBuilder sb = new StringBuilder("function ").append(requirement.getName()).append("("); + boolean first = true; + for (WParameter p : requirement.getParameters()) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(p.attrTyp().setTypeArgs(binding)).append(" ").append(p.getName()); + } + sb.append(")"); + WurstType returnType = requirement.attrReturnTyp().setTypeArgs(binding); + if (!(returnType instanceof WurstTypeVoid)) { + sb.append(" returns ").append(returnType); + } + return sb.toString(); + } + private void checkFuncRef(FuncRef ref) { if (isConstructorThisCall(ref)) { return; @@ -2812,6 +3053,11 @@ public void case_InterfaceDef(InterfaceDef interfaceDef) { check(VisibilityPublic.class); } + @Override + public void case_InstanceDecl(InstanceDecl instanceDecl) { + check(VisibilityPublic.class); + } + @Override public void case_TupleDef(TupleDef tupleDef) { check(VisibilityPublic.class); diff --git a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md index 1ea90edb9..3b8b2f538 100644 --- a/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md +++ b/de.peeeq.wurstscript/src/main/resources/agent-docs/WURST_LANGUAGE.md @@ -1,4 +1,4 @@ - + # WurstScript language digest This is the compact, agent-oriented language reference shipped with the WurstScript compiler. It covers language semantics and compiler-facing syntax; standard-library APIs, dependency conventions, UI rules, and object-editor policies belong to the project or dependency documentation. @@ -154,6 +154,48 @@ class Box The older unconstrained `T` form erases through integer casts and can share storage in surprising ways. +## Type class bounds + +A `T:` type parameter can require operations on the type it is bound to. The requirements are an ordinary generic `interface` with exactly one type parameter, and a top-level `implements` block binds them to one concrete type: + +```wurst +public interface Indexable + function toIndex(T x) returns int + function fromIndex(int i) returns T + +implements Indexable + function toIndex(vec2 v) returns int + return ... + function fromIndex(int i) returns vec2 + return ... + +class HashMap + function put(K key, V value) + saveInt(K.toIndex(key), V.toIndex(value)) + function get(K key) returns V + return V.fromIndex(loadInt(K.toIndex(key))) +``` + +A bound names the interface without type arguments; `` means "there is an instance of `Indexable`". Combine several with `and`: ``. + +Call a requirement on the type parameter, not on the value: `K.toIndex(key)`, never `key.toIndex()`. The value is an ordinary argument, so requirements which produce a value rather than consume one (`fromIndex`) need no special form. + +Unlike an interface used as a supertype, a bound works for `int`, `real`, `string`, tuples and handle types, and it costs nothing at runtime: after specialisation each requirement is a direct call to the instance function, on both Jass and Lua. + +Instances are unique and must be declared next to what they relate. An instance of `I` for type `X` may only live in the package declaring `I` or the package declaring `X`, and there may be only one. This makes `I` for `X` mean the same thing everywhere, independent of imports. Instances have no type parameters of their own in this version, so there is no way to write "every `List` is `Indexable` when `T` is". + +An instance must implement each requirement with the signature it has after the interface's type parameter is replaced by the instance type; a matching name is not enough. An interface used as a bound must not extend another interface, because the requirements of a bound are the interface's own functions. + +A generic which passes its own type parameter to another bounded generic must declare that bound itself: + +```wurst +function inner(Q x) returns string + return Q.show(x) + +function outer(R x) returns string // alone would not compile + return inner(x) +``` + ## Lua and Jass targets The target is selected by the project `wurst.build` `scriptMode` field. `wc3Patch` separately selects the compatible core Jass and standard-library era. diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java index 9e6ef9af2..c52a2fc01 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/FieldIterationTests.java @@ -1218,4 +1218,35 @@ public void tupleFieldIterationDiagnosticsAreActionable() { "endpackage" ); } + + /** + * A generic-construction method called straight on a freshly constructed receiver. The + * receiver's declared type is still generic at that point, so specialization takes the + * instantiation from the construction rather than requiring a typed local. + */ + @Test + public void genericConstructionOnFreshlyConstructedReceiver() { + test().testLua(true).executeProg().lines( + "package MagicFunctions", + " @compilerintrinsic function wurstNewInstance() returns T", + " return null", + "endpackage", + "", + "package FieldIterationTest", + " import MagicFunctions", + " native testSuccess()", + "", + " class State", + " int value = 7", + "", + " class Loader", + " function load() returns T", + " return wurstNewInstance()", + "", + " init", + " if new Loader().load().value == 7", + " testSuccess()", + "endpackage" + ); + } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java new file mode 100644 index 000000000..18aadaafc --- /dev/null +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/TypeClassTests.java @@ -0,0 +1,862 @@ +package tests.wurstscript.tests; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * Tests for type class bounds on new-style generics: + *

+ * - an ordinary generic {@code interface} declares the requirements, + * - a top level {@code implements} block binds those requirements to one concrete type, + * - a bound {@code } makes the requirements available inside the generic body, + * - {@code T.method(args)} dispatches through the instance chosen for {@code T}. + */ +public class TypeClassTests extends WurstScriptTest { + + @Test + public void parseInstanceDecl() { + testAssertOkLines(false, + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 42" + ); + } + + @Test + public void dispatchThroughBoundTypeChecks() { + testAssertOkLines(false, + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 42", + "function foo(Q x) returns int", + " return Q.toIndex(x)" + ); + } + + @Test + public void dispatchRuntime() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 42", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " if foo(new A) == 42", + " testSuccess()" + ); + } + + /** Each type argument picks its own instance, so one generic serves several types. */ + @Test + public void twoInstancesOfOneClass() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "class B", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 1", + "implements ToIndex", + " function toIndex(B x) returns int", + " return 2", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " if foo(new A) == 1 and foo(new B) == 2", + " testSuccess()" + ); + } + + /** A bound is satisfiable by a primitive, which is the whole point of not using subtyping. */ + @Test + public void instanceForPrimitive() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x) returns int", + " return x * 2", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " if foo(21) == 42", + " testSuccess()" + ); + } + + /** Several requirements combine with 'and', and each resolves independently. */ + @Test + public void multipleBounds() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Plus", + " function plus(T x, T y) returns T", + "interface Times", + " function times(T x, T y) returns T", + "implements Plus", + " function plus(int x, int y) returns int", + " return x + y", + "implements Times", + " function times(int x, int y) returns int", + " return x * y", + "function calc(Q x) returns Q", + " return Q.plus(x, Q.times(x, x))", + "init", + " if calc(6) == 42", + " testSuccess()" + ); + } + + /** An interface may require more than one function. */ + @Test + public void roundTripThroughTwoRequirements() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Indexable", + " function toIndex(T x) returns int", + " function fromIndex(int i) returns T", + "implements Indexable", + " function toIndex(int x) returns int", + " return x + 1", + " function fromIndex(int i) returns int", + " return i - 1", + "function roundTrip(Q x) returns Q", + " return Q.fromIndex(Q.toIndex(x))", + "init", + " if roundTrip(7) == 7", + " testSuccess()" + ); + } + + /** A bounded generic may call another one, passing its own still-abstract type parameter on. */ + @Test + public void transitiveBound() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x) returns int", + " return x", + "function inner(Q x) returns int", + " return Q.toIndex(x)", + "function outer(R x) returns int", + " return inner(x)", + "init", + " if outer(42) == 42", + " testSuccess()" + ); + } + + /** The same generic used at two types must not collapse to one specialisation. */ + @Test + public void distinctSpecialisationsPerType() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "implements Show", + " function show(string x) returns string", + " return \"s\"", + "function render(Q x) returns string", + " return Q.show(x)", + "init", + " if render(1) == \"i\" and render(\"a\") == \"s\"", + " testSuccess()" + ); + } + + @Test + public void dispatchRuntimeLua() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x) returns int", + " return x * 2", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " if foo(21) == 42", + " testSuccess()" + ); + } + + /** + * A bound must cost nothing at runtime: after specialisation the requirement is an ordinary + * call to the instance function, with no dispatch, lookup or table left behind. + */ + @Test + public void dispatchLowersToDirectCallLua() throws IOException { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x) returns int", + " return x * 2", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " if foo(21) == 42", + " testSuccess()" + ); + String compiled = Files.toString( + new File("test-output/lua/TypeClassTests_dispatchLowersToDirectCallLua.lua"), Charsets.UTF_8); + assertTrue(compiled.contains("toIndex"), "the instance function should survive as a real function"); + assertFalse(compiled.contains("TypeVarDispatch"), "dispatch must not reach the backend"); + assertFalse(compiled.contains("typeClassBinding"), "no dictionary should be emitted"); + } + + // --- diagnostics ------------------------------------------------------------------------- + + @Test + public void unsatisfiedBoundIsRejected() { + testAssertErrorsLines(false, "does not satisfy the bound", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "function foo(Q x) returns int", + " return Q.toIndex(x)", + "init", + " foo(new A)" + ); + } + + @Test + public void duplicateInstanceIsRejected() { + testAssertErrorsLines(false, "already an instance", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 1", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 2" + ); + } + + @Test + public void incompleteInstanceIsRejected() { + testAssertErrorsLines(false, "must implement", + "package test", + "interface Indexable", + " function toIndex(T x) returns int", + " function fromIndex(int i) returns T", + "class A", + "implements Indexable", + " function toIndex(A x) returns int", + " return 1" + ); + } + + @Test + public void methodNotRequiredByInterfaceIsRejected() { + testAssertErrorsLines(false, "does not implement any requirement of", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 1", + " function somethingElse(A x) returns int", + " return 2" + ); + } + + /** An instance must live with its interface or with its type, never anywhere else. */ + @Test + public void orphanInstanceIsRejected() { + testAssertErrorsLines(false, "must be declared with its interface or with its type", + "package Iface", + "public interface ToIndex", + " function toIndex(T x) returns int", + "endpackage", + "package Types", + "public class A", + "endpackage", + "package Orphan", + "import Iface", + "import Types", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 1", + "endpackage" + ); + } + + /** The instance may live with the type rather than with the interface. */ + @Test + public void instanceWithTypeIsAccepted() { + testAssertOkLines(false, + "package Iface", + "public interface ToIndex", + " function toIndex(T x) returns int", + "endpackage", + "package Types", + "import public Iface", + "public class A", + "implements ToIndex", + " function toIndex(A x) returns int", + " return 1", + "endpackage" + ); + } + + /** A bound must name an interface with exactly one type parameter. */ + @Test + public void multiParameterInterfaceIsRejectedAsBound() { + testAssertErrorsLines(false, "cannot be used as a type class", + "package test", + "interface Convert", + " function convert(A a) returns B", + "class C", + "implements Convert", + " function convert(C a) returns int", + " return 1" + ); + } + + /** + * An instance method must match its requirement, not merely its name. Matching on the name + * alone let a wrongly typed implementation be selected and emitted, which pjass then rejected. + */ + @Test + public void instanceMethodWithWrongParameterTypeIsRejected() { + testAssertErrorsLines(false, "should have type int", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(string x) returns int", + " return 1" + ); + } + + @Test + public void instanceMethodWithWrongParameterCountIsRejected() { + testAssertErrorsLines(false, "must take 1 parameter", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x, int y) returns int", + " return 1" + ); + } + + @Test + public void instanceMethodWithWrongReturnTypeIsRejected() { + testAssertErrorsLines(false, "should return int", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "implements ToIndex", + " function toIndex(int x) returns string", + " return \"a\"" + ); + } + + /** The substituted requirement is what must be matched, so T becomes the instance type. */ + @Test + public void instanceMethodMatchingSubstitutedRequirementIsAccepted() { + testAssertOkLines(false, + "package test", + "interface Indexable", + " function toIndex(T x) returns int", + " function fromIndex(int i) returns T", + "implements Indexable", + " function toIndex(int x) returns int", + " return x", + " function fromIndex(int i) returns int", + " return i" + ); + } + + /** + * An abstract type argument can only supply a bound it declares itself. Accepting it silently + * produced a program that type checked but failed at runtime with no instance to dispatch to. + */ + @Test + public void unboundedTypeParameterCannotSatisfyBound() { + testAssertErrorsLines(false, "does not satisfy the bound", + "package test", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "function inner(Q x) returns string", + " return Q.show(x)", + "function outer(R x) returns string", + " return inner(x)", + "init", + " outer(42)" + ); + } + + /** The same shape is fine once the outer parameter declares the bound it passes on. */ + @Test + public void boundedTypeParameterSatisfiesBound() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "function inner(Q x) returns string", + " return Q.show(x)", + "function outer(R x) returns string", + " return inner(x)", + "init", + " if outer(42) == \"i\"", + " testSuccess()" + ); + } + + /** A bound naming something that is not a usable type class must be reported, not ignored. */ + @Test + public void classAsBoundIsRejected() { + testAssertErrorsLines(false, "is not an interface", + "package test", + "class Marker", + "function foo(Q x) returns int", + " return 0", + "init", + " foo(1)" + ); + } + + @Test + public void appliedInterfaceAsBoundIsRejected() { + testAssertErrorsLines(false, "without type arguments", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "function foo>(Q x) returns int", + " return 0" + ); + } + + @Test + public void multiParameterInterfaceAsBoundIsRejected() { + testAssertErrorsLines(false, "exactly one type parameter", + "package test", + "interface Convert", + " function convert(A a) returns B", + "function foo(Q x) returns int", + " return 0" + ); + } + + /** Requirements are the interface's own functions, so an extending interface is not a bound. */ + @Test + public void extendingInterfaceAsBoundIsRejected() { + testAssertErrorsLines(false, "extends another interface", + "package test", + "interface Base", + " function base(T x) returns int", + "interface Derived extends Base", + " function derived(T x) returns int", + "function foo(Q x) returns int", + " return 0" + ); + } + + /** + * A bound may belong to a generic class rather than to the method, which is the shape the + * documentation uses for HashMap. The receiver carries the arguments the class was made with. + */ + @Test + public void boundOnGenericClass() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Box", + " function render(T x) returns string", + " return T.show(x)", + "init", + " if new Box().render(42) == \"i\"", + " testSuccess()" + ); + } + + /** The same, with the class holding the value, as a container actually would. */ + @Test + public void boundOnGenericClassWithField() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Indexable", + " function toIndex(T x) returns int", + " function fromIndex(int i) returns T", + "implements Indexable", + " function toIndex(int x) returns int", + " return x + 1", + " function fromIndex(int i) returns int", + " return i - 1", + "class Cell", + " private int stored", + " function put(T value)", + " stored = T.toIndex(value)", + " function get() returns T", + " return T.fromIndex(stored)", + "init", + " let c = new Cell()", + " c.put(7)", + " if c.get() == 7", + " testSuccess()" + ); + } + + @Test + public void boundOnGenericClassLua() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Box", + " function render(T x) returns string", + " return T.show(x)", + "init", + " if new Box().render(42) == \"i\"", + " testSuccess()" + ); + } + + /** Two classes at different types must each pick their own instance. */ + @Test + public void boundOnGenericClassTwoTypes() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "implements Show", + " function show(string x) returns string", + " return \"s\"", + "class Box", + " function render(T x) returns string", + " return T.show(x)", + "init", + " if new Box().render(1) == \"i\" and new Box().render(\"a\") == \"s\"", + " testSuccess()" + ); + } + + /** + * An interface may overload a requirement name. Each requirement pairs with the implementation + * matching its signature, and the lowering must select the same one. + */ + @Test + public void overloadedRequirementsAreMatchedBySignature() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Convert", + " function convert(T x) returns int", + " function convert(int scale, T x) returns int", + "implements Convert", + " function convert(string x) returns int", + " return 1", + " function convert(int scale, string x) returns int", + " return scale * 2", + "function useBoth(Q x) returns int", + " return Q.convert(x) + Q.convert(20, x)", + "init", + " if useBoth(\"a\") == 41", + " testSuccess()" + ); + } + + /** A missing overload is still reported, even when the name is present. */ + @Test + public void missingOverloadIsRejected() { + testAssertErrorsLines(false, "must implement", + "package test", + "interface Convert", + " function convert(T x) returns int", + " function convert(int scale, T x) returns int", + "implements Convert", + " function convert(string x) returns int", + " return 1" + ); + } + + /** A subclass may fix its parent's bounded type parameter. */ + @Test + public void boundInheritedFromGenericParent() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Parent", + " function render(T x) returns string", + " return T.show(x)", + "class Child extends Parent", + "init", + " if new Child().render(1) == \"i\"", + " testSuccess()" + ); + } + + @Test + public void boundInheritedFromGenericParentLua() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Parent", + " function render(T x) returns string", + " return T.show(x)", + "class Child extends Parent", + "init", + " if new Child().render(1) == \"i\"", + " testSuccess()" + ); + } + + /** A generic subclass may forward its own parameter to its parent's bound. */ + @Test + public void boundForwardedByGenericSubclass() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Parent", + " function render(T x) returns string", + " return T.show(x)", + "class Child extends Parent", + "init", + " if new Child().render(1) == \"i\"", + " testSuccess()" + ); + } + + @Test + public void boundForwardedByGenericSubclassLua() { + test().testLua(true).executeProg().lines( + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Parent", + " function render(T x) returns string", + " return T.show(x)", + "class Child extends Parent", + "init", + " if new Child().render(1) == \"i\"", + " testSuccess()" + ); + } + + /** Two levels of forwarding, so the substitution has to compose rather than apply once. */ + @Test + public void boundForwardedThroughTwoLevels() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "class Top", + " function render(T x) returns string", + " return T.show(x)", + "class Middle extends Top", + "class Bottom extends Middle", + "init", + " if new Bottom().render(1) == \"i\"", + " testSuccess()" + ); + } + + /** A bound on a module type parameter is rejected: using a module copies its body out of scope. */ + @Test + public void boundOnGenericModule() { + testAssertErrorsLines(false, "not supported on a module type parameter", + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"i\"", + "module M", + " function render(T x) returns string", + " return T.show(x)", + "class C", + " use M", + "init", + " if new C().render(1) == \"i\"", + " testSuccess()" + ); + } + + /** A method with its own type parameters must not disturb the class binding taken from the receiver. */ + @Test + public void classBoundWithIndependentMethodTypeParam() { + testAssertOkLines(true, + "package test", + "native testSuccess()", + "interface Show", + " function show(T x) returns string", + "implements Show", + " function show(int x) returns string", + " return \"int\"", + "implements Show", + " function show(string x) returns string", + " return \"string\"", + "class Box", + " function describe(T x, U other) returns string", + " return T.show(x)", + "init", + " if new Box().describe(1, \"a\") == \"int\"", + " testSuccess()" + ); + } + + /** A requirement may only use the interface's type parameter, and says so plainly. */ + @Test + public void genericRequirement() { + testAssertErrorsLines(false, "has its own type parameters", + "package test", + "interface Pairing", + " function pair(T x, U y) returns U", + "implements Pairing", + " function pair(int x, U y) returns U", + " return y" + ); + } + + /** + * Two classes with the same simple name in different packages, dispatched through a bounded + * generic class so the lookup by type is the path used. Selecting by printed name made the + * second silently dispatch through the first's implementation. + */ + @Test + public void sameSimpleNameThroughRegistryFallback() { + testAssertOkLines(true, + "package Iface", + "public interface Show", + " function show(T x) returns string", + "public class Renderer", + " function render(Q x) returns string", + " return Q.show(x)", + "endpackage", + "", + "package First", + "import public Iface", + "public class Item", + "implements Show", + " function show(Item x) returns string", + " return \"first\"", + "public function firstResult() returns string", + " return new Renderer().render(new Item())", + "endpackage", + "", + "package Second", + "import public Iface", + "public class Item", + "implements Show", + " function show(Item x) returns string", + " return \"second\"", + "public function secondResult() returns string", + " return new Renderer().render(new Item())", + "endpackage", + "", + "package test", + "import First", + "import Second", + "native testSuccess()", + "init", + " if firstResult() == \"first\" and secondResult() == \"second\"", + " testSuccess()", + "endpackage" + ); + } + + /** A type parameter is not a value, so it may only appear as the receiver of a requirement. */ + @Test + public void typeParameterIsNotAValue() { + testAssertErrorsLines(false, "Could not find variable Q", + "package test", + "interface ToIndex", + " function toIndex(T x) returns int", + "function foo(Q x) returns int", + " let y = Q", + " return 0" + ); + } +}