Repository navigation
be: convert a List of a subtype where a List of its supertype is wanted - #607
notactuallytreyanastasio wants to merge 1 commit into
Conversation
`List` is declared covariant (`class List<@out T>`), so the frontend accepts a `List<Square>` where a `List<Shape>` is wanted. java.util.List, Rust's `Arc<Vec<T>>` and C++'s `shared_ptr<vector<T>>` are invariant, and each backend converted in some positions and not others: java [ERROR] ShapesGlobal.java:[27,60] incompatible types: java.util.List<shapes.Square> cannot be converted to java.util.List<shapes.Shape> rust error[E0277]: the trait bound `Arc<Vec<Square>>: ToList<Shape>` is not satisfied cpp error: no viable conversion from returned value of type 'shared_ptr<vector<shared_ptr<literal::Square>>>' to function return type 'shared_ptr<vector<shared_ptr<Shape>>>' 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.<Shape> 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<? extends E>` because Java wildcards cannot say that `List<List<Square>>` fits `List<List<Shape>>`. rust copies, since a `Vec<Square>` and a `Vec<Shape>` 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<i32>` assigned to a `List<AnyValue>`, a `List<B>` to a `List<A>`, 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<Int>` as `List<AnyValue>`. 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<Shape>(squares, circles)` against `both<T>(a: List<T>, b: List<T>)` still fails javac (the explicit type argument is not emitted either), and `ListBuilder<List<Shape>>.add(squares)` still fails on java and cpp; rust compiles both. A `Map<String, List<Shape>>` 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<Square>` as `Listed<Shape>` is a frontend error and stays one. csharp was not run (no dotnet); its `IReadOnlyList<out T>` is covariant for reference types, so it may not need this. Fixes temperlang#548. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Bobby Grayson <bobbbygrayson+github@gmail.com>
mikesamuel
left a comment
There was a problem hiding this comment.
@tjpalmer what do you think?
If list variance is going away, this is a non-issue.
The functional test looks good given the current givens.
I wonder though if casting should go through SupportNetwork.maybeInsertImplicitCast.
I think we should get rid of variance still, yeah, so this existing but broken thing in RustBackendTest should illegal: let yet = ["one", "two"];
let yetAgain: List<MapKey<String>> = yet; // MapKey only supported for constraint, not yet explicit value typeThis old translation doesn't look like it would compile to me, although I haven't freshly tested it: let yet__0: temper_core::List<std::sync::Arc<String>> = 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<temper_core::MapKey<std::sync::Arc<String>>> = yet__0.clone();And this new version from this PR for the second line would silently copy a full list just to meet semantics: let yetAgain__0: temper_core::List<temper_core::MapKey<std::sync::Arc<String>>> = temper_core::listed::map( & yet__0.clone(), & | it___1 | temper_core::MapKey::new(it___1));And I don't think we want silent list copies that just look like a simple reference assignment in Temper, so I think that contributes to the "we need to ditch variance" topic. |
|
So, yeah, I vote we close this PR without merging it. And maybe we should remove variance sooner than later. |
Listis declared covariant (class List<@out T>), so the frontendaccepts a
List<Square>where aList<Shape>is wanted. java.util.List,Rust's
Arc<Vec<T>>and C++'sshared_ptr<vector<T>>are invariant, andeach backend converted in some positions and not others:
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 ofthe same list. Nothing writes through a Temper
List, so the viewcannot let a wrong element in, and it costs nothing at run time. The
element type is passed explicitly,
Core.<Shape> 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 thanList<? extends E>because Java wildcards cannotsay that
List<List<Square>>fitsList<List<Shape>>.rust copies, since a
Vec<Square>and aVec<Shape>differ in layout:maybeWrapnow converts each element with the same rule it applies to asingle value, as in
Because
maybeWrapalready runs at every one of those positions, this isthe only rust change. A list literal passed as an argument is left alone,
since
translateActualalready builds it with the wanted element type.Three lines of RustBackendTest expectations change accordingly. Each
recorded output that did not compile: a
List<i32>assigned to aList<AnyValue>, aList<B>to aList<A>, and a list oftemper_core::MapKey, which temper_core does not define.cpp already had
wrapWithListUpcastIfNeededfor arguments, localinitializers 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::upcastconverts elements theway
List::makedoes (implicitly, or byany_boxinto AnyValue),recursing into nested lists and passing null through.
The new functional test TypesListCovariance covers each position plus
nested lists, nullable lists and
List<Int>asList<AnyValue>. Beforethis 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,13andtrueon js, py, lua, java and rust; on cpp theregex 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<Shape>(squares, circles)against
both<T>(a: List<T>, b: List<T>)still fails javac (theexplicit type argument is not emitted either), and
ListBuilder<List<Shape>>.add(squares)still fails on java and cpp;rust compiles both. A
Map<String, List<Shape>>built fromnew Pair("a", squares)fails on java, rust and cpp, since theconversion would have to reach inside the
Pair, andMapvalues getno conversion here at all.
Listedis invariant, soList<Square>asListed<Shape>is a frontend error and stays one. csharp was not run(no dotnet); its
IReadOnlyList<out T>is covariant for referencetypes, so it may not need this.
Fixes #548.
Repro: https://github.com/notactuallytreyanastasio/temper-issue-repros/tree/main/26-list-of-subtype-as-list-of-supertype
🤖 Generated with Claude Code