From e08deaea1d342fd01975a494ab5a0c32533f4b12 Mon Sep 17 00:00:00 2001 From: Bobby Grayson Date: Thu, 8 Oct 2026 21:37:42 -0400 Subject: [PATCH] be: convert a List of a subtype where a List of its supertype is wanted `List` is declared covariant (`class List<@out T>`), so the frontend accepts a `List` where a `List` is wanted. java.util.List, Rust's `Arc>` and C++'s `shared_ptr>` are invariant, and each backend converted in some positions and not others: java [ERROR] ShapesGlobal.java:[27,60] incompatible types: java.util.List cannot be converted to java.util.List rust error[E0277]: the trait bound `Arc>: ToList` is not satisfied cpp error: no viable conversion from returned value of type 'shared_ptr>>' to function return type 'shared_ptr>>' Each backend now converts wherever a value meets a declared type: arguments, returns, local and module-level initializers, assignments, and property sets, with the exceptions listed at the end. java wraps the value in a new `Core.listUpcast`, an unchecked view of the same list. Nothing writes through a Temper `List`, so the view cannot let a wrong element in, and it costs nothing at run time. The element type is passed explicitly, `Core. listUpcast(squares)`, and a wanted element type that mentions a type parameter is left alone, since that parameter is not in scope at the call site. The parameter is `List` rather than `List` because Java wildcards cannot say that `List>` fits `List>`. rust copies, since a `Vec` and a `Vec` differ in layout: `maybeWrap` now converts each element with the same rule it applies to a single value, as in total(temper_core::listed::map(&squares__7.clone(), &|it___105| Shape::new(it___105))) Because `maybeWrap` already runs at every one of those positions, this is the only rust change. A list literal passed as an argument is left alone, since `translateActual` already builds it with the wanted element type. Three lines of RustBackendTest expectations change accordingly. Each recorded output that did not compile: a `List` assigned to a `List`, a `List` to a `List`, and a list of `temper_core::MapKey`, which temper_core does not define. cpp already had `wrapWithListUpcastIfNeeded` for arguments, local initializers and assignments. It is now also applied to returns, module-level initializers and property sets, compares whole element types so nested lists convert, and `List::upcast` converts elements the way `List::make` does (implicitly, or by `any_box` into AnyValue), recursing into nested lists and passing null through. The new functional test TypesListCovariance covers each position plus nested lists, nullable lists and `List` as `List`. Before this change it fails on java17, rust and cpp; it passes on js, py, lua, java17, rust and cpp after. The issue's shapes, literal and regex cases print `13`, `13` and `true` on js, py, lua, java and rust; on cpp the regex case still segfaults because nothing calls the std/regex init function (a separate bug), and the other two print `13`. What this does not cover: a formal whose type mentions a type parameter. java leaves those alone, so `both(squares, circles)` against `both(a: List, b: List)` still fails javac (the explicit type argument is not emitted either), and `ListBuilder>.add(squares)` still fails on java and cpp; rust compiles both. A `Map>` built from `new Pair("a", squares)` fails on java, rust and cpp, since the conversion would have to reach inside the `Pair`, and `Map` values get no conversion here at all. `Listed` is invariant, so `List` as `Listed` is a frontend error and stays one. csharp was not run (no dotnet); its `IReadOnlyList` is covariant for reference types, so it may not need this. Fixes #548. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Bobby Grayson --- .../lang/temper/be/cpp/CppTranslator.kt | 56 ++++++++++-- .../lang/temper/be/cpp/core/list.hpp | 31 +++++-- .../lang/temper/be/java/JavaTranslator.kt | 85 +++++++++++++++++-- .../lang/temper/be/java/StandardNames.kt | 1 + .../src/main/java/temper/core/Core.java | 17 ++++ .../kotlin/lang/temper/be/rust/RustExt.kt | 37 ++++++++ .../lang/temper/be/rust/RustBackendTest.kt | 6 +- functional-test-matrix.md | 2 + .../lang/temper/tests/FunctionalTestSuiteI.kt | 4 + .../lang/temper/tests/FunctionalTests.kt | 1 + .../src/commonMain/resources/config.temper.md | 1 + .../list/covariance/covariance.temper.md | 85 +++++++++++++++++++ 12 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 functional-test-suite/src/commonMain/resources/types/list/covariance/covariance.temper.md diff --git a/be-cpp/src/commonMain/kotlin/lang/temper/be/cpp/CppTranslator.kt b/be-cpp/src/commonMain/kotlin/lang/temper/be/cpp/CppTranslator.kt index 7f365f7c5..f353eba0d 100644 --- a/be-cpp/src/commonMain/kotlin/lang/temper/be/cpp/CppTranslator.kt +++ b/be-cpp/src/commonMain/kotlin/lang/temper/be/cpp/CppTranslator.kt @@ -30,7 +30,9 @@ import lang.temper.type.MethodShape import lang.temper.type.PropertyShape import lang.temper.type.TypeDefinition import lang.temper.type.TypeFormal +import lang.temper.type.TypeShape import lang.temper.type.WellKnownTypes +import lang.temper.type.excludeNullAndBubble import lang.temper.type2.NullableType import lang.temper.type2.Nullity import lang.temper.type2.Type2 @@ -329,10 +331,11 @@ class CppTranslator( WellKnownTypes.listedTypeDefinition, ) if (declDef !in listUpcastDefs || initDef !in listUpcastDefs) return translatedExpr - // Check if element types differ + // Check if element types differ. Compare whole types, not just definitions, + // so that a List> is converted to a List> too. val declElem = declaredTmpLType.bindings.firstOrNull() ?: return translatedExpr val initElem = initExprType.bindings.firstOrNull() ?: return translatedExpr - if (declElem.definition == initElem.definition) return translatedExpr + if (declElem.withNullity(Nullity.NonNull) == initElem.withNullity(Nullity.NonNull)) return translatedExpr // Element types differ — wrap with List::upcast val declElemCpp = translateType2(declElem) return cpp.callExpr( @@ -1671,7 +1674,13 @@ class CppTranslator( listOf(translateExpression(retExpr)), ) } - else -> translateExpressionOrNull(retExpr) + else -> translateExpressionOrNull(retExpr)?.let { translated -> + wrapWithListUpcastIfNeeded( + translated, + retExpr?.type, + enclosingReturnType(stmt)?.let { excludeNullAndBubble(it) }, + ) + } } listOf(cpp.returnStmt(translatedRet)) } @@ -1723,20 +1732,25 @@ class CppTranslator( } } val setterName = if (useSetterMethod) setterMethodNames[propDotName] else null + val translatedRight = wrapWithListUpcastIfNeeded( + translateExpression(right), + right.type, + propertyType(lval), + ) if (setterName != null) { // Call setter method instead of direct assignment val call: Cpp.Expr = when (val subj = lval.subject) { is TmpL.Expression -> cpp.callExpr( cpp.op("->", translateExpression(subj), setterName), - translateExpression(right), + translatedRight, ) is TmpL.ConnectedToTypeName -> cpp.callExpr( cpp.scopedName(translateTypeName(subj), setterName), - translateExpression(right), + translatedRight, ) is TmpL.TemperTypeName -> cpp.callExpr( cpp.scopedName(translateTypeName(subj), setterName), - translateExpression(right), + translatedRight, ) is TmpL.SuperSubject -> error("super property handled elsewhere") } @@ -1750,10 +1764,34 @@ class CppTranslator( is TmpL.SuperSubject -> error("illegal super call for backed property") } return listOf( - cpp.exprStmt(cpp.op("=", lhs, translateExpression(right))), + cpp.exprStmt(cpp.op("=", lhs, translatedRight)), ) } + /** The declared type of the property [lval] assigns, when its subject's type is known here. */ + private fun propertyType(lval: TmpL.PropertyLValue): Type2? { + val shape = when (val subject = lval.subject) { + is TmpL.Expression -> subject.type.definition as? TypeShape + is TmpL.TypeSubject -> subject.typeName.sourceDefinition as? TypeShape + } ?: return null + val symbol = when (val property = lval.property) { + is TmpL.ExternalPropertyId -> property.name.dotNameText + is TmpL.InternalPropertyId -> property.name.name.toSymbol()?.text + } + return shape.properties.find { it.symbol.text == symbol }?.descriptor as? Type2 + } + + private fun enclosingReturnType(stmt: TmpL.Tree): Type2? { + var ancestor = stmt.parent + while (ancestor != null) { + if (ancestor is TmpL.FunctionLike) { + return ancestor.sig.returnType2 + } + ancestor = ancestor.parent + } + return null + } + /** True if the block's control unconditionally leaves (so a trailing `break` is dead). */ private fun blockEndsWithControlLeave(block: TmpL.BlockStatement): Boolean = when (block.statements.lastOrNull()) { @@ -3025,7 +3063,9 @@ class CppTranslator( listOf(translateExpression(initExpr)), ) } else { - translateExpressionOrNull(initExpr) + translateExpressionOrNull(initExpr)?.let { translated -> + wrapWithListUpcastIfNeeded(translated, initExpr?.type, topLevel.descriptor) + } } // Always declare with null init and defer assignment // to the init function to avoid SIOF. diff --git a/be-cpp/src/commonMain/resources/lang/temper/be/cpp/core/list.hpp b/be-cpp/src/commonMain/resources/lang/temper/be/cpp/core/list.hpp index 64c83c698..c06c4fefc 100644 --- a/be-cpp/src/commonMain/resources/lang/temper/be/cpp/core/list.hpp +++ b/be-cpp/src/commonMain/resources/lang/temper/be/cpp/core/list.hpp @@ -48,19 +48,32 @@ namespace temper { return list; } + // Converts one element for upcast the way make converts one: implicitly, + // or by boxing a value into an AnyValue. An element that is itself a + // list, as in List> to List>, is upcast in turn. + template + struct UpcastElem { + static Base apply(const Derived& elem) { return convert_elem(elem); } + }; + + template + struct UpcastElem>, std::shared_ptr>> { + static std::shared_ptr> apply(const std::shared_ptr>& elem); + }; + template< class Base, class Derived, - class = typename std::enable_if< - !std::is_same::value - && std::is_convertible::value - >::type + class = typename std::enable_if::value>::type > std::shared_ptr> upcast(const std::shared_ptr>& src) { + if (src == nullptr) { + return nullptr; + } std::shared_ptr> result = std::make_shared>(); result->reserve(src->size()); for (const Derived& elem : *src) { - result->push_back(elem); + result->push_back(UpcastElem::apply(elem)); } return result; } @@ -70,6 +83,14 @@ namespace temper { return src; } + template + std::shared_ptr> + UpcastElem>, std::shared_ptr>>::apply( + const std::shared_ptr>& elem + ) { + return upcast(elem); + } + template bool isEmpty(const std::shared_ptr>& list) { return list->empty(); diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt index 14833796a..e2da1c868 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaTranslator.kt @@ -34,12 +34,18 @@ import lang.temper.name.ResolvedName import lang.temper.type.Abstractness import lang.temper.type.MethodKind import lang.temper.type.MethodShape +import lang.temper.type.TypeFormal +import lang.temper.type.TypeShape import lang.temper.type.WellKnownTypes import lang.temper.type.excludeNullAndBubble import lang.temper.type.isVoidLike import lang.temper.type.mentionsInvalid import lang.temper.type.simplify +import lang.temper.type2.DefinedNonNullType +import lang.temper.type2.Nullity import lang.temper.type2.Signature2 +import lang.temper.type2.Type2 +import lang.temper.type2.withNullity import lang.temper.type2.withType import lang.temper.value.TBoolean import lang.temper.value.TClass @@ -640,7 +646,7 @@ class JavaTranslator( activeInit().add( Assign.assign( left = fieldName.asNameExpr(t.pos), - right = expr(init), + right = upcastList(expr(init), init.type, t.descriptor), ).exprStatement(t.pos), ) } @@ -1617,7 +1623,10 @@ class JavaTranslator( is TmpL.LocalFunctionDeclaration -> LocalFunctionDecl(t).toLocalVarDecl() is TmpL.ModuleInitFailed -> moduleInitFailed(t) is TmpL.YieldStatement -> TODO() // JavaSupportNetwork opts into TranslateToRegularFunction - is TmpL.ReturnStatement -> J.ReturnStatement(t.pos, t.expression?.let(::expr)) + is TmpL.ReturnStatement -> J.ReturnStatement( + t.pos, + t.expression?.let { upcastList(expr(it), it.type, enclosingReturnType(t)) }, + ) is TmpL.SetProperty -> setProperty(t) is TmpL.ThrowStatement -> throwStmt(t) is TmpL.TryStatement -> tryStmt(t) @@ -1640,7 +1649,7 @@ class JavaTranslator( t.pos, type = varType(t), name = names.lookupRegularLocalNameObj(t.name).outName.toIdentifier(t.name.pos), - expr = t.init?.let(::expr), + expr = t.init?.let { upcastList(expr(it), it.type, t.descriptor) }, ) private fun ifStmt(t: TmpL.IfStatement) = J.IfStatement( @@ -1759,7 +1768,7 @@ class JavaTranslator( private fun assignment(t: TmpL.Assignment): J.BlockLevelStatement = Assign.assign( leftHandSide(t.left), - expr(t.right), + upcastList(expr(t.right), t.right.type, t.type), pos = t.pos, ).exprStatement(t.pos) @@ -1767,17 +1776,69 @@ class JavaTranslator( val left = t.left // Static properties are not assignable. val leftSubject = left.subject as TmpL.Expression + val right = upcastList(expr(t.right), t.right.type, propertyType(leftSubject, left.property)) return when (val prop = left.property) { is TmpL.ExternalPropertyId -> J.InstanceMethodInvocationExpr( t.pos, expr(leftSubject), method = names.setterName(prop.name), - args = listOf(expr(t.right).asArgument(t.pos)), + args = listOf(right.asArgument(t.pos)), ).exprStatement() is TmpL.InternalPropertyId -> - Assign.assign(expr(leftSubject).field(names.field(prop)), expr(t.right)).exprStatement(t.pos) + Assign.assign(expr(leftSubject).field(names.field(prop)), right).exprStatement(t.pos) + } + } + + private fun propertyType(subject: TmpL.Expression, property: TmpL.PropertyId): Type2? { + val shape = (subject.type as? DefinedNonNullType)?.definition as? TypeShape ?: return null + val symbol = when (property) { + is TmpL.ExternalPropertyId -> property.name.dotNameText + is TmpL.InternalPropertyId -> property.name.name.toSymbol()?.text + } + return shape.properties.find { it.symbol.text == symbol }?.descriptor as? Type2 + } + + private fun enclosingReturnType(t: TmpL.Tree): Type2? { + var ancestor = t.parent + while (ancestor != null) { + if (ancestor is TmpL.FunctionLike) { + return ancestor.sig.returnType2 + } + ancestor = ancestor.parent + } + return null + } + + /** + * `List` is covariant in Temper, so a `List` may be used where a + * `List` is wanted, but `java.util.List` is invariant and javac + * rejects the assignment. Wraps [translated] in + * `Core.listUpcast(...)`, an unchecked view of the same list, when + * the element types differ. + * + * The element type is given explicitly so that javac does not infer a + * narrower one from a generic context. A wanted element type that + * mentions a type parameter is left alone because that parameter may + * not be in scope at the use site. + */ + private fun upcastList(translated: J.Expression, given: Type2?, wanted: Type2?): J.Expression { + val givenElement = listElementType(given) ?: return translated + val wantedElement = listElementType(wanted) ?: return translated + if (givenElement.withNullity(Nullity.NonNull) == wantedElement.withNullity(Nullity.NonNull)) { + return translated } + if (mentionsTypeFormal(wantedElement)) { return translated } + val pos = translated.pos + val elementType = JavaType.fromFrontend(wantedElement, names).asReferenceType() + val (type, method) = temperListUpcast.split() + return J.StaticMethodInvocationExpr( + pos, + type = type.toQualIdent(pos.leftEdge), + typeArgs = J.TypeArguments(pos.leftEdge, listOf(elementType.toTypeArgAst(pos.leftEdge))), + method = J.Identifier(pos.leftEdge, method), + args = listOf(translated.asArgument()), + ) } fun expr(x: TmpL.Expression): J.Expression = when (x) { @@ -1935,7 +1996,7 @@ class JavaTranslator( else -> calleeType?.valueFormalsExceptThis } return actuals.mapGenericIndexed { idx, actual -> - var expr = expr(actual) + var expr = upcastList(expr(actual), actual.type, calleeFormals?.getOrNull(idx)?.type) if (calleeFormals != null && validInstanceMethodReferenceSubject(expr)) { val actualSig = withType( actual.passType, @@ -2499,3 +2560,13 @@ internal fun potentiallyLongStringToJavaExpression(pos: Position, s: String): J. args = emptyList(), ) } + +/** The element type of a `List`, ignoring whether the list itself is nullable or bubbly. */ +private fun listElementType(type: Type2?): Type2? { + val list = type?.let { excludeNullAndBubble(it) } as? DefinedNonNullType ?: return null + if (list.definition != WellKnownTypes.listTypeDefinition) { return null } + return list.bindings.getOrNull(0) +} + +private fun mentionsTypeFormal(type: Type2): Boolean = + type.definition is TypeFormal || type.bindings.any { mentionsTypeFormal(it) } diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt index 6e5e120cd..75b437a7a 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt @@ -157,6 +157,7 @@ val temperBoxedEqRev = temperCore.qualifyKnownSafe("boxedEqRev") val temperBubble = javaLangRuntimeException val temperBubbleMethod = temperCore.qualifyKnownSafe("bubble") val temperCast = temperCore.qualifyKnownSafe("cast") +val temperListUpcast = temperCore.qualifyKnownSafe("listUpcast") val temperCastToNonNull = temperCore.qualifyKnownSafe("castToNonNull") val temperConsoleClass = temperCore.qualifyKnownSafe("Console") val temperGetConsoleMethod = temperCore.qualifyKnownSafe("getConsole") diff --git a/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java b/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java index 5656c67ae..dfdfbda8e 100644 --- a/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java +++ b/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java @@ -722,6 +722,23 @@ private static List listCopyOfTrusted(List elems) { } } + /** + * Temper's List is covariant, so a List of Square may be used as a List + * of Shape, but java.util.List is invariant. Nothing adds to or sets + * elements of a list through a Temper List, so viewing the same list + * with a wider element type cannot let a non-Square into it. The Temper + * frontend has checked that the element types are related, including + * for nested lists like a List of List of Square, which Java's + * {@code ? extends} cannot express, hence the unchecked parameter type. + * @param list a list, or null + * @return the same list, or null + * @param the wider element type + */ + @SuppressWarnings("unchecked") + public static List listUpcast(List list) { + return (List) list; + } + /** * @param target a ListBuilder instance * @param elem the element to add diff --git a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustExt.kt b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustExt.kt index 560766860..1a5dec884 100644 --- a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustExt.kt +++ b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustExt.kt @@ -265,6 +265,8 @@ internal fun Rust.Expr.maybeWrap( return (mapParam ?: this).let { result -> when { wanted is FnDescription && !givenNone && !translator.isClosure(this) -> result.wrapArc() + wantedDefinition == givenDefinition && wantedDefinition == WellKnownTypes.listTypeDefinition -> + result.convertListElements(given = given, wanted = wanted, translator = translator) wantedDefinition == givenDefinition -> result wanted.isInterface() && !wantStringIndexOption -> when { wantedDefinition == WellKnownTypes.anyValueTypeDefinition -> result.methodCall("as_any_value") @@ -311,6 +313,41 @@ internal fun Rust.Expr.maybeWrap( } } +/** + * `List` is covariant in Temper, so a `List` can stand where a + * `List` is wanted, but a Rust `Arc>` is not an + * `Arc>`, so copy the elements through the same per-value + * conversion [maybeWrap] would apply to one of them, as in `temper_core::listed::map(&squares, &|it| Shape::new(it))`. + * Leaves the list alone when its elements need no conversion. + */ +private fun Rust.Expr.convertListElements( + given: Description, + wanted: Description, + translator: RustTranslator, +): Rust.Expr { + // A list literal passed as an argument is translated to an array whose + // elements already have the wanted type. See `translateActual`. + if (this is Rust.Array) { return this } + val givenElement = (given.type as? DefinedNonNullType)?.bindings?.getOrNull(0) ?: return this + val wantedElement = (wanted.type as? DefinedNonNullType)?.bindings?.getOrNull(0) ?: return this + if (givenElement == wantedElement) { return this } + val param = translator.unusedTemporaryName(pos, "it") + val element = param.maybeWrap(given = givenElement, wanted = wantedElement, translator = translator) + if (element === param) { return this } + val closure = Rust.Closure( + pos, + params = listOf(Rust.FunctionParam(pos, param.deepCopy(), null)), + value = element, + ) + return Rust.Call( + pos, + callee = LISTED_MAP_NAME.toId(pos), + args = listOf(this.ref(), closure.ref()), + ) +} + +private const val LISTED_MAP_NAME = "temper_core::listed::map" + internal fun Rust.Expr.maybeWrap(given: Type2, wanted: Description?, translator: RustTranslator) = maybeWrap(given = given.described(), wanted = wanted, translator = translator) diff --git a/be-rust/src/commonTest/kotlin/lang/temper/be/rust/RustBackendTest.kt b/be-rust/src/commonTest/kotlin/lang/temper/be/rust/RustBackendTest.kt index 5d90ea6b0..982d74b9e 100644 --- a/be-rust/src/commonTest/kotlin/lang/temper/be/rust/RustBackendTest.kt +++ b/be-rust/src/commonTest/kotlin/lang/temper/be/rust/RustBackendTest.kt @@ -941,9 +941,9 @@ class RustBackendTest { | let thing__0: temper_core::AnyValue = 1.as_any_value(); | let things__0: temper_core::List = std::sync::Arc::new(vec![1, 2]).unwrap(); | let more__0: temper_core::List = std::sync::Arc::new(vec![1, 2]); - | let still__0: temper_core::List = std::sync::Arc::new(vec![1, 2]); + | let still__0: temper_core::List = temper_core::listed::map( & std::sync::Arc::new(vec![1, 2]), & | it___0 | it___0.as_any_value()); | let yet__0: temper_core::List> = std::sync::Arc::new(vec![std::sync::Arc::new("one".to_string()), std::sync::Arc::new("two".to_string())]); - | let yetAgain__0: temper_core::List>> = yet__0.clone(); + | let yetAgain__0: temper_core::List>> = temper_core::listed::map( & yet__0.clone(), & | it___1 | temper_core::MapKey::new(it___1)); | Ok(()) | }).clone() |} @@ -2700,7 +2700,7 @@ class RustBackendTest { | INIT_ONCE.get_or_init(| |{ | let a__0: A = A::new(B::new()); | let a2__0: A = a__0.clone(); - | let things__0: temper_core::List = std::sync::Arc::new(vec![B::new()]); + | let things__0: temper_core::List = temper_core::listed::map( & std::sync::Arc::new(vec![B::new()]), & | it___0 | A::new(it___0)); | let more__0: temper_core::List = std::sync::Arc::new(vec![B::new()]).unwrap(); | println!("{}", a2__0.adjust(std::sync::Arc::new("hi".to_string()))); | Ok(()) diff --git a/functional-test-matrix.md b/functional-test-matrix.md index 2b7cb73b0..9b4d5be97 100644 --- a/functional-test-matrix.md +++ b/functional-test-matrix.md @@ -56,6 +56,7 @@ | [TypesIntLimits][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [TypesIntShifty][] | ✅ | ❌[198][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [TypesJsonSyntaxTree][] | ✅ | ❌[198][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [TypesListCovariance][] | ✅ | ❌[198][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [TypesListEmpty][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [TypesListOperations][] | ✅ | ❌[198][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [TypesListReduce][] | ✅ | ❌[198][] | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | @@ -128,6 +129,7 @@ [TypesIntLimits]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/int/limits/limits.temper.md [TypesIntShifty]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/int/shifty/shifty.temper.md [TypesJsonSyntaxTree]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/json-syntax-tree/json-syntax-tree.temper.md +[TypesListCovariance]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/list/covariance/covariance.temper.md [TypesListEmpty]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/list/empty/empty.temper.md [TypesListOperations]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/list/operations/operations.temper.md [TypesListReduce]: https://github.com/temperlang/temper/blob/main/functional-test-suite/src/commonMain/resources/types/list/reduce/reduce.temper.md diff --git a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt index 4b537a241..439398582 100644 --- a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt +++ b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt @@ -280,6 +280,10 @@ interface FunctionalTestSuiteI { fun typesJsonSyntaxTree() = runFunctionalTest(Ft.TypesJsonSyntaxTree) + @Test + fun typesListCovariance() = + runFunctionalTest(Ft.TypesListCovariance) + @Test fun typesListEmpty() = runFunctionalTest(Ft.TypesListEmpty) diff --git a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt index 75836f6c9..e5b6520c7 100644 --- a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt +++ b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt @@ -118,6 +118,7 @@ enum class FunctionalTests(val test: FunctionalTestBase) { TypesIntLimits(markdown("types/int/limits/limits.temper.md", "TypesIntLimits")), TypesIntShifty(markdown("types/int/shifty/shifty.temper.md", "TypesIntShifty")), TypesJsonSyntaxTree(markdown("types/json-syntax-tree/json-syntax-tree.temper.md", "TypesJsonSyntaxTree")), + TypesListCovariance(markdown("types/list/covariance/covariance.temper.md", "TypesListCovariance")), TypesListEmpty(markdown("types/list/empty/empty.temper.md", "TypesListEmpty")), TypesListOperations(markdown("types/list/operations/operations.temper.md", "TypesListOperations")), TypesListReduce(markdown("types/list/reduce/reduce.temper.md", "TypesListReduce")), diff --git a/functional-test-suite/src/commonMain/resources/config.temper.md b/functional-test-suite/src/commonMain/resources/config.temper.md index 1e3298642..4b923f162 100644 --- a/functional-test-suite/src/commonMain/resources/config.temper.md +++ b/functional-test-suite/src/commonMain/resources/config.temper.md @@ -59,6 +59,7 @@ Autogenerated file, to update use `./gradlew kcodegen:up`, to update this header import("./types/int/limits"); import("./types/int/shifty"); import("./types/json-syntax-tree"); + import("./types/list/covariance"); import("./types/list/empty"); import("./types/list/operations"); import("./types/list/reduce"); diff --git a/functional-test-suite/src/commonMain/resources/types/list/covariance/covariance.temper.md b/functional-test-suite/src/commonMain/resources/types/list/covariance/covariance.temper.md new file mode 100644 index 000000000..3bf6765a0 --- /dev/null +++ b/functional-test-suite/src/commonMain/resources/types/list/covariance/covariance.temper.md @@ -0,0 +1,85 @@ +# List Covariance Functional Test + +`List` is covariant, so a `List` can be used where a `List` +is wanted. Backends whose list types are invariant have to convert at each +place that can happen. + + export interface Shape { area(): Int; } + export class Square(public side: Int) extends Shape { + public area(): Int { side * side } + } + + export let total(shapes: List): Int { + var sum = 0; + for (var i = 0; i < shapes.length; ++i) { sum += shapes[i].area(); } + sum + } + export let totalOrMinusOne(shapes: List?): Int { + if (shapes == null) { -1 } else { total(shapes) } + } + export let totalNested(lists: List>): Int { + var sum = 0; + for (var i = 0; i < lists.length; ++i) { sum += total(lists[i]); } + sum + } + + let squares: List = [new Square(1), new Square(2)]; + console.log("argument ${total(squares)}"); + + export let literalReturned(n: Int): List { [new Square(n), new Square(n + 1)] } + console.log("returned literal ${total(literalReturned(2))}"); + + export let passedThrough(sq: List): List { sq } + console.log("returned variable ${total(passedThrough(squares))}"); + + let topLevel: List = [new Square(3)]; + console.log("top-level let ${total(topLevel)}"); + + let topLevelFromVariable: List = squares; + console.log("top-level let from variable ${total(topLevelFromVariable)}"); + + export let reassigned(): Int { + var ls: List = []; + ls = squares; + total(ls) + } + console.log("local assignment ${reassigned()}"); + + console.log("nullable parameter ${totalOrMinusOne(squares)}"); + + export let maybeSquares(b: Boolean): List? { if (b) { squares } else { null } } + console.log("nullable to nullable ${totalOrMinusOne(maybeSquares(true))} ${totalOrMinusOne(maybeSquares(false))}"); + + let nested: List> = [squares, squares]; + console.log("nested ${totalNested(nested)}"); + + export class Holder(public var shapes: List) { + public set(sq: List): Void { shapes = sq; } + } + let holder = new Holder(squares); + console.log("constructor argument ${total(holder.shapes)}"); + holder.set([new Square(4)]); + console.log("property assignment ${total(holder.shapes)}"); + +The element type may also widen to `AnyValue`, which some backends box. + + export let countAll(values: List): Int { values.length } + let ints: List = [1, 2, 3]; + console.log("any value ${countAll(ints)}"); + +Expected output: + +```log +argument 5 +returned literal 13 +returned variable 5 +top-level let 9 +top-level let from variable 5 +local assignment 5 +nullable parameter 5 +nullable to nullable 5 -1 +nested 10 +constructor argument 5 +property assignment 16 +any value 3 +```