Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersection types & type checks #11600

Merged
merged 43 commits into from
Dec 12, 2024
Merged

Conversation

JaroslavTulach
Copy link
Member

@JaroslavTulach JaroslavTulach commented Nov 20, 2024

Pull Request Description

Implementation of type checks for intersection types. The idea is to split the list of types[] in EnsoMultiValue into two parts:

  • first methodDispatchTypes represent the types the value "has been cast to"
  • the rest of the types represent the types the value "can be cast to"

By performing this separation we address the #10882 requirements. After a type check only methods available on the methodDispatchTypes can be invoked. However the value can still be cast to all the possible types.

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

Followup Issues

The goal of this PR is to get dispatch/cast logic right. There is a lot of additional work to do:

@JaroslavTulach JaroslavTulach self-assigned this Nov 20, 2024
@JaroslavTulach JaroslavTulach changed the title EnsoMultiValue to pass intersection type thru the -> Complex check Pass intersection type thru the -> Complex check Nov 20, 2024
@Akirathan
Copy link
Member

Idea for increasing test coverage of EnsoMultiValue: Manually create some Truffle nodes and explicitly call them. Inspiration from EqualsTest and HashCodeTest.

Copy link
Contributor

@GregoryTravis GregoryTravis left a comment

Choose a reason for hiding this comment

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

Approve libs change.

@JaroslavTulach
Copy link
Member Author

Idea for increasing test coverage of EnsoMultiValue: Manually create some Truffle nodes and explicitly call them. Inspiration from EqualsTest and HashCodeTest.

@JaroslavTulach JaroslavTulach force-pushed the wip/jtulach/ComplexAndFloat11482 branch from afc04b2 to c780c37 Compare November 22, 2024 08:14
@JaroslavTulach JaroslavTulach force-pushed the wip/jtulach/ComplexAndFloat11482 branch 3 times, most recently from 5f2c78d to 2f72704 Compare December 10, 2024 05:28
@JaroslavTulach JaroslavTulach force-pushed the wip/jtulach/ComplexAndFloat11482 branch from 980bbf2 to 52038f7 Compare December 11, 2024 07:00

group_builder.specify "case of third downcasted" <|
abc = 3.14 : A&B&C
c1 = case abc:C of
Copy link
Member Author

Choose a reason for hiding this comment

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

Pattern matching on type on first value of a EnsoMultiValue works, but we want to match on other values as well:

diff --git test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso
index ca768daaa0..2e54642532 100644
--- test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso
+++ test/Base_Tests/src/Semantic/Multi_Value_Convert_Spec.enso
@@ -84,6 +84,41 @@ add_specs suite_builder =
                 _ -> "what?"
             c1 . should_equal "c"
 
+        group_builder.specify "case of second" <|
+            abc = 3.14 : A&B&C
+            c1 = case abc of
+                b:B -> b.b
+                a:A -> a.a
+                c:C -> c.c
+                _ -> "what?"
+            c1 . should_equal "b"
+
+        group_builder.specify "case of third" <|
+            abc = 3.14 : A&B&C
+            c1 = case abc of
+                c:C -> c.c
+                b:B -> b.b
+                a:A -> a.a
+                _ -> "what?"
+            c1 . should_equal "c"
+
+        group_builder.specify "case of second from extra types" <|
+            abc = 3.14 : A&B&C
+            c1 = case abc:A of
+                b:B -> b.b
+                a:A -> a.a
+                c:C -> c.c
+                _ -> "what?"
+            c1 . should_equal "b"
+
+        group_builder.specify "case of third  from extra types" <|
+            abc = 3.14 : A&B&C
+            c1 = case abc:A of
+                c:C -> c.c
+                b:B -> b.b
+                a:A -> a.a
+                _ -> "what?"
+            c1 . should_equal "c"
 
 main filter=Nothing =
     suite = Test.build suite_builder->

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

These tests look great

Copy link
Member

@radeusgd radeusgd left a comment

Choose a reason for hiding this comment

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

I've created a PR-to-your-PR #11838 that suggests some changes to the documentation. Please let me know what you think of them and if you are happy, you can just merge that PR into yours.

Comment on lines 34 to 46
group_builder.specify "performs conversion to U&V from B and C" <|
abc = 3.14 : A&B&C
uv = abc : U&V

uv.u . should_equal "u"
uv.v . should_equal 3.14

Test.expect_panic No_Such_Method <| uv.a
Test.expect_panic No_Such_Method <| uv.b
Test.expect_panic No_Such_Method <| uv.c
Test.expect_panic Type_Error (uv:A)
Test.expect_panic Type_Error (uv:B)
Test.expect_panic Type_Error (uv:C)
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused about this test.

Isn't a type-asserting expression (uv:A) supposed to work as the 'explicit' instanceof-like conversion? Then it probably should be able to extract the A type hidden from the multi-value abc upon the uv = abc : U&V conversion, no?

If that was a method call, e.g.:

f (a:A) = ...
...
f uv

then I agree this should yield a type error.

But here it is a 'type-asserting expression' which I thought according to our spec was supposed to act like instanceof and uncover any 'hidden' part of the type. What am I missing here?

Copy link
Member

Choose a reason for hiding this comment

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

This test is great and it tests both method dispatch and type asserting expressions (although as noted above I'm not sure if the behaviour is correct?).

But the test seems to be lacking any argument-type check tests. Can we add these? These would be similar to the expressions uv:A but instead we'd pass the uv value to a function with checked argument type:

f_a a:A = a.a

And see that if the type is 'hidden' then it cannot be uncovered in the argument type check.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

@JaroslavTulach JaroslavTulach Dec 12, 2024

Choose a reason for hiding this comment

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

it is a 'type-asserting expression' which I thought according to our spec was supposed to act like instanceof and uncover any 'hidden' part of the type

This test has been integrated in 1167b52 together with documentation for "converting type check". The test checks the behavior exactly as documented.

Remember, we are implementing variant 1. E.g. when we have to built new intersection type by using conversions, then we start from scratch. As such uv has no traces of abc.

Copy link
Member

Choose a reason for hiding this comment

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

So we are in disagreement over what variant 1 constitutes. I know you don't like my pseudocode but I used it as it allowed me to precisely describe what I mean, perhaps I could try using normal words but I'm sure I wouldn't be as precise.

In my variant 1 the explicit cast removes the 'unused' types from the visible type (Set<T> resultTypes = Set.empty();) but it actually still keeps them in the hidden part (List<T> remainingTypes = allInputTypes.removeAll(resultTypes); return RuntimeType(resultTypes.toList(), remainingTypes);. But your solution instead removes them altogether, 'clearing' the hidden part.

Are you sure that is right? How does that relate to the subclass metaphor? If a conversion is used, does that mean that we are losing the 'subclass' altogether and getting a new fresh class?

If we did uv = abc:(A&B&C&U&V) we'd have kept all the types, right?

Copy link
Member

Choose a reason for hiding this comment

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

I've added some more scenarios that test my understanding of multi value types with conversions and casts. All seems good.

types-tests-extended.zip

We could consider turning them into tests to serve as illustration + ensure the behaviour stay as we expect it to be.

Copy link
Member

Choose a reason for hiding this comment

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

These experiments led me to one concern - the meaning of : is heavily overloaded and I'm worried that it may be a source of confusion for developers and possibly end-users.

I think we tried to do a great job in leveraging the various meanings (argument type check, cast, conversion), but I'm worried if there isn't too much going on at the same time.

Especially in expressions, the type ascription x:T may mean different things - it can either be an instanceof-like 'assertion' that checks if the value of x has type T OR it can actually be a conversion that invokes some T.from (x:?) function. While similar, we have noted above that there are non-trivial differences in behaviour of these two.

IF the x:T was a cast, then the x will keep any additional multi value types it had before (although they become "hidden"). However, if it turned out to need to be a conversion, then any additional multi value types are removed.

Functionally, this seems a good choice - a conversion is applying a new function and its result has not that much to do with the input, so it makes sense to not inherit the inputs unless explicitly determined in the expression. And it prevents the powerset problem as @JaroslavTulach noted above.

However, that means that both the user or the type checker - when they see the type ascription x:T they cannot really tell if the additional multi value types (if there were any) will be kept on x (only hidden) or will be completely removed. That is something that is only decided at runtime because it depends on the actual precise type of x.

While I don't find any critical problems this could cause for type inference - there we are interested mostly in argument type checks and method dispatch - both of which rely on the visible part of the type that is fortunately the same both in a cast and conversion scenario. So we should be okay.

But conceptually - it doesn't feel right that by just looking at the code I cannot really tell if the additional types are kept or removed.

Thus I'm wondering if maybe we should distinguish a cast from a conversion syntactically and make them into two separate constructs. If we want to do this we probably should sooner rather than later.

BUT maybe we are alright after all - because we do have separate constructs:

  • we have T.from for 'just conversion'
  • and we have case of that allows us to do the instanceof-like check.

So I guess the problem is only with the : ascription used in expressions. Perhaps we should only use it when we have to - when we want to specify multiple types at once - like in x:(A&B). And in case we actually want to perform an instanceof-like check - we should probably prefer the case of statement as it will not trigger conversions accidentally and will ensure that any additional multi value types are always kept.

So to sum up all this experimentation - I think the current state is OK. The : is a bit overloaded so maybe we should prefer to use case of for instanceof checks (it even sounds similar 😀) and be careful when using x:T as expression ascriptions.

Copy link
Member Author

Choose a reason for hiding this comment

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

what I meant by the so called 'variant 1' after all

I am glad you are OK with current behavior, I am fine with it as well.

turning them into tests to serve as illustration + ensure the behaviour stay as we expect

Sure, go on!

Right now I am aware only of following differences from expected behavior:

Otherwise, whatever behavior works right now is (according to our knowledge) the desired behavior and deserves to be recorded as a test.

Copy link
Member Author

@JaroslavTulach JaroslavTulach Jan 6, 2025

Choose a reason for hiding this comment

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

the problem is only with the : ascription used in expressions. Perhaps we should only use it when we have to - when we want to specify multiple types at once - like in x:(A&B). And in case we actually want to perform an instanceof-like check - we should probably prefer the case of statement as it will not trigger conversions accidentally and will ensure that any additional multi value types are always kept.

In this case we are facing an end (GUI) user problem: We want the IDE to generate human readable code when invoking methods on hidden types of a multi value. So far the IDE can use mv:Hidden_Type . method. Should we allow unrevealing the hidden types via case of only, the generated code would be way more complicated...

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I guess for the GUI we need to keep the 'simpler' mv:Hidden_Type approach. Perhaps there the case of losing 'hidden types' when a conversion happens will not be as harmful? Maybe in practical GUI use-cases it won't be a big issue - I guess we need to see how this works in practice.

@JaroslavTulach JaroslavTulach added the CI: Ready to merge This PR is eligible for automatic merge label Dec 12, 2024
@mergify mergify bot merged commit 2964457 into develop Dec 12, 2024
42 of 43 checks passed
@mergify mergify bot deleted the wip/jtulach/ComplexAndFloat11482 branch December 12, 2024 07:29
jdunkerley pushed a commit that referenced this pull request Dec 13, 2024
Implementation of **type checks** for **intersection types**. The idea is to split the list of `types[]` in `EnsoMultiValue` into two parts:
- first `methodDispatchTypes` represent the types the value _"has been cast to"_
- the rest of the types represent the types the value _"can be cast to"_

By performing this separation we address the #10882 requirements. After a type check only methods available on the `methodDispatchTypes` can be invoked. However the value can still be cast to all the possible types.

(cherry picked from commit 2964457)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow (multi_value : Table) : Column to succeed
4 participants