Skip to content

be: convert a List of a subtype where a List of its supertype is wanted - #607

Open
notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/list-covariant-backends
Open

notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/list-covariant-backends

Conversation

@notactuallytreyanastasio

Copy link
Copy Markdown
Contributor

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 #548.

Repro: https://github.com/notactuallytreyanastasio/temper-issue-repros/tree/main/26-list-of-subtype-as-list-of-supertype

🤖 Generated with Claude Code

`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 mikesamuel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@tjpalmer

Copy link
Copy Markdown
Contributor

@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 type

This 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.

@tjpalmer

Copy link
Copy Markdown
Contributor

So, yeah, I vote we close this PR without merging it. And maybe we should remove variance sooner than later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A List<Square> used where List<Shape> is expected passes the frontend, then fails to compile on java and rust, and on cpp when it is a returned literal

3 participants