Intersection types & type checks (#11600)

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.
This commit is contained in:
Jaroslav Tulach 2024-12-12 08:29:00 +01:00 committed by GitHub
parent ffd0de4661
commit 2964457d48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1253 additions and 203 deletions

View File

@ -1,3 +1,11 @@
# Next Next Release
#### Enso Language & Runtime
- [Intersection types & type checks][11600]
[11600]: https://github.com/enso-org/enso/pull/11600
# Next Release
#### Enso IDE

View File

@ -43,6 +43,7 @@ The various components of Enso's syntax are described below:
- [**Functions:**](./functions.md) The syntax for writing functions in Enso.
- [**Function Arguments:**](./function-arguments.md) The syntax for function
arguments in Enso.
- [**Conversions:**](./conversions.md) The syntax of special _conversion functions_ in Enso.
- [**Field Access:**](./projections.md) The syntax for working with fields of
data types in Enso.
- [**Comments:**](./comments.md) The syntax for writing comments in Enso.

View File

@ -0,0 +1,35 @@
---
layout: developer-doc
title: Defining Functions
category: syntax
tags: [syntax, functions]
order: 10
---
# Conversions
Conversions are special [functions](./functions.md) associated with a [type](../types/hierarchy.md),
named `from` and taking single `that` argument. Following example:
```ruby
type Complex
Num re:Float im:Float
Complex.from (that:Float) = Complex.Num that 0
```
defines type `Complex` and a **conversion** from `Float` which uses the provided `Float` value as
real part of a complex number while setting the imaginary part to zero.
## Type Checks
Conversions are integral part of [type checking](../types/inference-and-checking.md#type-checking-algorithm)
during runtime. Having the right conversion in scope one can write:
```ruby
complex_pi = 3.14:Complex
```
to create a new instance of type `Complex`. The Enso runtime represents
the `3.14` literal as `Float` value and that would fail the `Complex` type check if there was no
conversion method available. However as there is one, the runtime uses `Complex.from` behind the
scene and `complex_pi` then becomes `Complex.Num 3.14 0` value.
Type checks may perform no, one or multiple conversions (like in case of
[intersection types](../types/intersection-types.md)).

View File

@ -48,6 +48,7 @@ Information on the type system is broken up into the following sections:
- [**Goals for the Type System:**](./goals.md) The goals for the Enso type
system, particularly around usability and user experience.
- [**The Type Hierarchy:**](./hierarchy.md) The type hierarchy in Enso.
- [**Intersection Types:**](./intersection-types.md) intersection types in Enso.
- [**Function Types:**](./function-types.md) Function types in Enso.
- [**Access Modification:**](./access-modifiers.md) Access modifiers in Enso
(e.g. `private` and `unsafe`),

View File

@ -142,7 +142,7 @@ They are as follows:
- **Union - `|`:** This operator creates a typeset that contains the members in
the union of its operands.
- **Intersection - `&`:** This operator creates a typeset that contains the
members in the intersection of its operands.
members in the [intersection of its operands](./intersection-types.md).
> [!WARNING]
> These operators _don't seem to be supported_. There is no plan to

View File

@ -0,0 +1,157 @@
---
layout: developer-doc
title: The Enso Type Hierarchy
category: types
tags: [types, hierarchy, typeset]
order: 2
---
# Intersection Types
Intersection types play an important role in Enso [type hierarchy](./hierarchy.md)
and its visual representation. Having a value that can play _multiple roles_
at once is essential for smooth _live programming_ manipulation of such a value.
Intersections types are created with the use of [`&` operator](./hierarchy.md#typeset-operators).
In an attempt to represent `Complex` numbers (with real and imaginary component)
one may decide to create a type that is both `Complex` and `Float` when the
imaginary part is `0`:
```ruby
type Complex
Num re:Float im:Float
plain_or_both self =
if self.im != 0 then self else
both = self.re : Complex&Float
both # value with both types: Complex and Float
```
Having a value with such _intersection type_ allows the IDE to offer operations
available on all individual types.
## Creating
Creating a value of _intersection types_ is as simple as performing a type check:
```ruby
self : Complex&Float
```
However such a _type check_ is closely associated with [conversions](../syntax/conversions.md).
If the value doesn't represent all the desired types yet, then the system looks
for [conversion methods](../syntax/conversions.md) being available in the scope like:
```
Complex.from (that:Float) = Complex.Num that 0
```
and uses them to create all the values the _intersection type_ represents.
> [!NOTE]
> Note that if a `Float.from (that:Complex)` conversion were available in the scope,
> any `Complex` instance would be convertible to `Float` regardless of how it was constructed.
> To ensure that such mix-ins are only available on values that opt-in to being
> an intersection type (like in the `Complex` example above where we include
> the `Float` mix-in only if `self.im == 0`), we need to ensure that the conversion
> used to create the intersection type is not available in the default
> conversion resolution scope. Thus it cannot be defined in the same module
> as `Complex` or `Float` types, but instead it should be defined in a separate
> module that is only imported in the place that will be constructing the multi-values.
<!--
Just as demonstrated at
https://github.com/enso-org/enso/commit/3d8a0e1b90b20cfdfe5da8d2d3950f644a4b45b8#diff-c6ef852899778b52ce6a11ebf9564d102c273021b212a4848b7678e120776287R23
-->
## Narrowing Type Check
When an _intersection type_ value is being downcast to _some of the types it already represents_,
these types become its _visible_ types. Any additional types it represents become _hidden_.
The following operations consider only the _visible_ part of the type:
- [dynamic dispatch](../types/dynamic-dispatch.md)
- cases when value is passed as an argument
However the value still keeps internal knowledge of all the types it represents.
Thus, after casting a value `cf:Complex&Float` to just `Complex`, e.g. `c = cf:Complex`:
- method calls on `c` will only consider methods defined on `Complex`
- the value `c` can only be passed as argument to methods expecting `Complex` type
- a type error is raised when a `Float` parameter is expected
As such a _static analysis_ knows the type a value _has been cast to_ (the _visible_ part)
and can deduce the set of operations that can be performed with it. Moreover, any
method calls will also only accept the value if it satisfies the type it
_has been cast to_. Any additional remaining _hidden_ types
can only be brought back through an _explicit_ cast.
To perform an explicit cast that can uncover the 'hidden' part of a type write
`f = c:Float`.
<!--
When #11828 is fixed.
- or inspect the types in a `case` expression, e.g.
```
case c of
f : Float -> f.sqrt
_ -> "Not a float"
```
-->
> [!WARNING]
> Keep in mind that while both argument type check in method definitions and a
> 'type asserting' expression look similarly, they have slightly different behaviour.
> ```
> f a:Float = a
> g a = a:Float
> ```
> These two functions, while very similar, will have different behaviour when
> passed a value like the value `c` above. The function `f` will fail with
> a type error, because the visible type of `c` is just `Complex` (assuming
> the conversion to `Float` is not available in the current scope).
> However, the function `g` will accept the same value and return it as
> a `Float` value, based on the 'hidden' part of its type.
> [!NOTE]
> In the **object oriented terminology** we can think of
> a type `Complex&Float` as being a subclass of `Complex` and subclass of `Float` types.
> As such a value of type `Complex&Float` may be used wherever `Complex` or `Float` types
> are used. Let there, for example, be a function:
> ```ruby
> use_complex c:Complex callback:(Any -> Any) = callback c
> ```
> that accepts `Complex` value and passes it back to a provided callback function.
> It is possible to pass a value of `Complex&Float` type to such a function. Only
> operations available on type `Complex` can be performed on value in variable `c`.
>
> However the `callback` function may still explicitly cast the value to `Float`.
> E.g. the following is valid:
> ```ruby
> both = v : Complex&Float
> use_complex both (v-> v:Float . sqrt)
> ```
> This behavior is often described as being **open to subclasses**. E.g. the `c:Complex`
> check allows values with _intersection types_ that include `Complex` to pass thru with
> all their runtime information available,
> but one has to perform an explicit cast to extract the other types associated with
> such a value.
This behavior allows creation of values with types like `Table&Column` to represent a table
with a single column - something the users of visual _live programming_ environment of Enso find
very useful.
```ruby
Table.join self right:Table -> Table = ...
```
Such a `Table&Column` value can be returned from the above `Table.join` function and while
having only `Table` behavior by default, still being able to be explicitly casted by the visual environment
to `Column`.
## Converting Type Check
When an _intersection type_ is being checked against a type it doesn't represent,
any of its component types can be used for [conversion](../syntax/conversions.md).
Should there be a `Float` to `Text` conversion:
```ruby
Text.from (that:Float) = Float.to_text
```
then `Complex&Float` value `cf` can be typed as `cf:Text`. The value can also
be converted to another _intersection type_ like `ct = cf:Complex&Text`. In such case
it looses its `Float` type and `ct:Float` would fail.
In short: when a [conversion](../syntax/conversions.md) is needed to satisfy a type check
a new value is created to satisfy just the types requested in the check.

View File

@ -89,15 +89,15 @@ public class TypeOfNodeMultiValueTest {
var rawValue = ContextUtils.unwrapValue(ctx(), polyValue);
var rawType = ContextUtils.unwrapValue(ctx(), t);
if (rawType instanceof Type type) {
var singleMultiValue = EnsoMultiValue.create(new Type[] {type}, new Object[] {rawValue});
var singleMultiValue = EnsoMultiValue.create(new Type[] {type}, 1, new Object[] {rawValue});
var n = t.getMetaSimpleName();
data.add(new Object[] {singleMultiValue, n, 0});
var rawInt = (Type) ContextUtils.unwrapValue(ctx(), g.typeInteger());
var secondMultiValue =
EnsoMultiValue.create(new Type[] {rawInt, type}, new Object[] {5L, rawValue});
EnsoMultiValue.create(new Type[] {rawInt, type}, 2, new Object[] {5L, rawValue});
data.add(new Object[] {secondMultiValue, n, 1});
var firstMultiValue =
EnsoMultiValue.create(new Type[] {type, rawInt}, new Object[] {rawValue, 6L});
EnsoMultiValue.create(new Type[] {type, rawInt}, 2, new Object[] {rawValue, 6L});
data.add(new Object[] {firstMultiValue, n, 0});
} else {
if (!t.isHostObject()) {

View File

@ -0,0 +1,79 @@
package org.enso.interpreter.node.typecheck;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.oracle.truffle.api.CallTarget;
import org.enso.interpreter.runtime.data.EnsoMultiValue;
import org.enso.interpreter.runtime.data.Type;
import org.enso.test.utils.ContextUtils;
import org.enso.test.utils.TestRootNode;
import org.graalvm.polyglot.Context;
import org.junit.AfterClass;
import org.junit.Test;
public class TypeCheckValueTest {
private static Context ctx;
private static Context ctx() {
if (ctx == null) {
ctx = ContextUtils.defaultContextBuilder().build();
}
return ctx;
}
@AfterClass
public static void closeCtx() {
if (ctx != null) {
ctx.close();
}
ctx = null;
}
@Test
public void avoidDoubleWrappingOfEnsoMultiValue() {
var convert = allOfIntegerAndText();
ContextUtils.executeInContext(
ctx(),
() -> {
var builtins = ContextUtils.leakContext(ctx).getBuiltins();
var m1 =
EnsoMultiValue.create(
new Type[] {builtins.text(), builtins.number().getInteger()},
2,
new Object[] {"Hi", 42});
assertEquals("Text & Integer", m1.toDisplayString(true));
var res = convert.call(m1);
assertTrue("Got multivalue again", res instanceof EnsoMultiValue);
var emv = (EnsoMultiValue) res;
assertEquals("Integer & Text", emv.toDisplayString(true));
return null;
});
}
private static CallTarget allOfIntegerAndText() {
var call = new CallTarget[1];
ContextUtils.executeInContext(
ctx(),
() -> {
var builtins = ContextUtils.leakContext(ctx).getBuiltins();
var intNode = TypeCheckValueNode.single("int", builtins.number().getInteger());
var textNode = TypeCheckValueNode.single("text", builtins.text());
var bothNode = TypeCheckValueNode.allOf("int&text", intNode, textNode);
var root =
new TestRootNode(
(frame) -> {
var arg = frame.getArguments()[0];
var res = bothNode.handleCheckOrConversion(frame, arg, null);
return res;
});
root.insertChildren(bothNode);
call[0] = root.getCallTarget();
return null;
});
return call[0];
}
}

View File

@ -0,0 +1,83 @@
package org.enso.interpreter.test;
import com.oracle.truffle.api.interop.InteropLibrary;
import java.util.ArrayList;
import org.enso.interpreter.runtime.data.EnsoMultiValue;
import org.enso.interpreter.runtime.data.Type;
import org.enso.test.utils.ContextUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public class EnsoMultiValueInteropTest {
@Parameterized.Parameter(0)
public Object value;
private static Context ctx;
private static Context ctx() {
if (ctx == null) {
ctx = ContextUtils.defaultContextBuilder().build();
}
return ctx;
}
@Parameterized.Parameters
public static Object[][] allEnsoMultiValuePairs() throws Exception {
var g = ValuesGenerator.create(ctx());
var typeOf =
ContextUtils.evalModule(
ctx(),
"""
from Standard.Base import all
typ obj = Meta.type_of obj
main = typ
""");
var data = new ArrayList<Object[]>();
for (var v1 : g.allValues()) {
for (var v2 : g.allValues()) {
registerValue(g, typeOf, v1, v2, data);
}
}
return data.toArray(new Object[0][]);
}
private static void registerValue(
ValuesGenerator g, Value typeOf, Value v1, Value v2, ArrayList<Object[]> data) {
var t1 = typeOf.execute(v1);
var t2 = typeOf.execute(v2);
if (!t1.isNull() && !t2.isNull()) {
var rawT1 = ContextUtils.unwrapValue(ctx(), t1);
var rawT2 = ContextUtils.unwrapValue(ctx(), t2);
if (rawT1 instanceof Type typ1 && rawT2 instanceof Type typ2) {
var r1 = ContextUtils.unwrapValue(ctx, v1);
var r2 = ContextUtils.unwrapValue(ctx, v2);
var both = EnsoMultiValue.create(new Type[] {typ1, typ2}, 2, new Object[] {r1, r2});
data.add(new Object[] {both});
}
}
}
@AfterClass
public static void disposeCtx() throws Exception {
if (ctx != null) {
ctx.close();
ctx = null;
}
}
@Test
public void isStringDoesntFail() {
ContextUtils.executeInContext(
ctx,
() -> {
return InteropLibrary.getUncached().isString(value);
});
}
}

View File

@ -0,0 +1,270 @@
package org.enso.interpreter.test;
import static org.enso.test.utils.ContextUtils.createDefaultContext;
import static org.enso.test.utils.ContextUtils.executeInContext;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.oracle.truffle.api.frame.VirtualFrame;
import org.enso.common.MethodNames;
import org.enso.interpreter.node.expression.builtin.meta.EqualsNode;
import org.enso.interpreter.node.expression.foreign.HostValueToEnsoNode;
import org.enso.interpreter.runtime.data.EnsoMultiValue;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.test.utils.ContextUtils;
import org.enso.test.utils.TestRootNode;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class EqualsMultiValueTest {
private static Context context;
private static EqualsNode equalsNode;
private static TestRootNode testRootNode;
private static HostValueToEnsoNode hostValueToEnsoNode;
@BeforeClass
public static void initContextAndData() {
context = createDefaultContext();
executeInContext(
context,
() -> {
testRootNode = new TestRootNode(EqualsMultiValueTest::equalityCheck);
equalsNode = EqualsNode.create();
hostValueToEnsoNode = HostValueToEnsoNode.build();
testRootNode.insertChildren(equalsNode, hostValueToEnsoNode);
return null;
});
}
@AfterClass
public static void disposeContext() {
context.close();
context = null;
}
@Test
public void testEqualityIntegerAndMultiValue() {
executeInContext(
context,
() -> {
var builtins = ContextUtils.leakContext(context).getBuiltins();
var intType = builtins.number().getInteger();
var textText = builtins.text();
var fourExtraText =
EnsoMultiValue.create(
new Type[] {intType, textText}, 1, new Object[] {4L, Text.create("Hi")});
assertTrue("4 == 4t", equalityCheck(4L, fourExtraText));
assertFalse("5 != 4t", equalityCheck(5L, fourExtraText));
assertTrue("4t == 4", equalityCheck(fourExtraText, 4L));
assertFalse("4t != 5", equalityCheck(fourExtraText, 5L));
return null;
});
}
@Test
public void testEqualityTextAndExtraIntegerMultiValue() {
executeInContext(
context,
() -> {
var builtins = ContextUtils.leakContext(context).getBuiltins();
var intType = builtins.number().getInteger();
var textType = builtins.text();
var bothTypes = new Type[] {textType, intType};
var text = Text.create("Hi");
var ahoj = Text.create("Ahoj");
var integer = 4L;
//
// following variable represents result of
// x = _ : (Text & Integer) : Text
// e.g. multi value with Text and Integer, casted to Text only
//
var multiV = EnsoMultiValue.create(bothTypes, 1, text, integer);
assertTrue("'Hi' == multiV", equalityCheck(text, multiV));
assertFalse("'Ahoj' != multiV", equalityCheck(ahoj, multiV));
assertFalse(
"Don't consider extra Integer type in equals", equalityCheck(integer, multiV));
assertFalse("5 != t4", equalityCheck(5L, multiV));
assertFalse(
"Don't consider extra Integer type in equals", equalityCheck(multiV, integer));
assertFalse("4 != t5", equalityCheck(multiV, 5L));
assertTrue("multiV == 'Hi'", equalityCheck(multiV, text));
assertFalse("multiV != 'Ahoj'", equalityCheck(multiV, ahoj));
return null;
});
}
@Test
public void testEqualityIntegerAndMultiValueWithBoth() {
executeInContext(
context,
() -> {
var builtins = ContextUtils.leakContext(context).getBuiltins();
var intType = builtins.number().getInteger();
var textText = builtins.text();
var hi = Text.create("Hi");
var fourExtraText =
EnsoMultiValue.create(new Type[] {textText, intType}, 2, new Object[] {hi, 4L});
assertTrue("4 == 4t", equalityCheck(4L, fourExtraText));
assertFalse("5 != 4t", equalityCheck(5L, fourExtraText));
assertTrue("4t == 4", equalityCheck(fourExtraText, 4L));
assertFalse("4t != 5", equalityCheck(fourExtraText, 5L));
assertTrue("4t == 'Hi'", equalityCheck(fourExtraText, hi));
assertTrue("'Hi' == 4t", equalityCheck(hi, fourExtraText));
return null;
});
}
@Test
public void testEqualityIntegerAndMultiValueWithIntText() {
executeInContext(
context,
() -> {
var builtins = ContextUtils.leakContext(context).getBuiltins();
var intType = builtins.number().getInteger();
var textText = builtins.text();
var fourExtraText =
EnsoMultiValue.create(
new Type[] {intType, textText}, 2, new Object[] {4L, Text.create("Hi")});
assertTrue("4 == 4t", equalityCheck(4L, fourExtraText));
assertFalse("5 != 4t", equalityCheck(5L, fourExtraText));
assertTrue("4t == 4", equalityCheck(fourExtraText, 4L));
assertFalse("4t != 5", equalityCheck(fourExtraText, 5L));
return null;
});
}
@Test
public void twoMultiValues() {
executeInContext(
context,
() -> {
var builtins = ContextUtils.leakContext(context).getBuiltins();
var intType = builtins.number().getInteger();
var textText = builtins.text();
var fourExtraText =
EnsoMultiValue.create(
new Type[] {intType, textText}, 1, new Object[] {4L, Text.create("Hi")});
var fourExtraText2 =
EnsoMultiValue.create(
new Type[] {intType, textText}, 1, new Object[] {4L, Text.create("Hi")});
var fiveExtraText =
EnsoMultiValue.create(
new Type[] {intType, textText}, 1, new Object[] {5L, Text.create("Hi")});
assertFalse("!= for sure #1", equalityCheck(fiveExtraText, fourExtraText));
assertFalse("!= for sure #2", equalityCheck(fourExtraText, fiveExtraText));
assertTrue("equals #1", equalityCheck(fourExtraText, fourExtraText2));
assertTrue("equals #2", equalityCheck(fourExtraText2, fourExtraText));
return null;
});
}
@Test
public void testEqualityIntegerNoMultiValueWithConversion() throws Exception {
assertEqualityIntegerWithConversion("c:Complex");
}
@Test
public void testEqualityIntegerAndMultiValueWithConversion() throws Exception {
assertEqualityIntegerWithConversion("c.as_complex_and_float");
}
private void assertEqualityIntegerWithConversion(String complexNew) throws Exception {
var code =
"""
import Standard.Base.Data.Numbers.Float
import Standard.Base.Data.Numbers.Number
import Standard.Base.Data.Ordering.Comparable
import Standard.Base.Data.Ordering.Ordering
import Standard.Base.Nothing
import Standard.Base.Error.Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
## Sample definition of a complex number with conversions
from Number and implementation of a comparator.
type Complex
private Value re:Float im:Float
new re=0:Float im=0:Float -> Complex =
c = Complex.Value re 0
if im != 0 then c:Complex else
${complexNew}
+ self (that:Complex) = Complex.new self.re+that.re self.im+that.im
< self (that:Complex) = Complex_Comparator.compare self that == Ordering.Less
> self (that:Complex) = Complex_Comparator.compare self that == Ordering.Greater
>= self (that:Complex) =
ordering = Complex_Comparator.compare self that
ordering == Ordering.Greater || ordering == Ordering.Equal
<= self (that:Complex) =
ordering = Complex_Comparator.compare self that
ordering == Ordering.Less || ordering == Ordering.Equal
Complex.from (that:Number) = Complex.new that
Comparable.from (that:Complex) = Comparable.new that Complex_Comparator
Comparable.from (that:Number) = Comparable.new that Complex_Comparator
type Complex_Comparator
compare x:Complex y:Complex = if x.re==y.re && x.im==y.im then Ordering.Equal else
if x.im==0 && y.im==0 then Ordering.compare x.re y.re else
Nothing
hash x:Complex = if x.im == 0 then Ordering.hash x.re else
7*x.re + 11*x.im
## uses the explicit conversion defined in this private module
Complex.as_complex_and_float self =
self : Complex&Float
## explicit "conversion" of `Complex` to `Float` in a private module
used in `as_complex_and_float`
Float.from (that:Complex) =
if that.im == 0 then that.re else
Error.throw <| Illegal_Argument.Error "Cannot convert Complex with imaginary part to Float"
"""
.replace("${complexNew}", complexNew);
var src = Source.newBuilder("enso", code, "complex.enso").build();
var complexModule = context.eval(src);
var complexFourValue =
complexModule.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "Complex.new 4");
executeInContext(
context,
() -> {
var complexFour = ContextUtils.unwrapValue(context, complexFourValue);
assertTrue("4 == 4t", equalityCheck(4L, complexFour));
assertFalse("5 != 4t", equalityCheck(5L, complexFour));
assertTrue("4t == 4", equalityCheck(complexFour, 4L));
assertFalse("4t != 5", equalityCheck(complexFour, 5L));
return null;
});
}
private static boolean equalityCheck(VirtualFrame frame) {
var args = frame.getArguments();
return equalsNode.execute(frame, args[0], args[1]).isTrue();
}
private boolean equalityCheck(Object first, Object second) {
return (Boolean) testRootNode.getCallTarget().call(first, second);
}
}

View File

@ -1027,7 +1027,20 @@ public class SignatureTest extends ContextTest {
}
@Test
public void returnTypeCheckByLastStatementOfMain() throws Exception {
public void returnTypeCheckByLastStatementOfMainTextFirst() throws Exception {
var main = assertTypeCheckByLastStatementOfMain("Text & Integer");
assertTrue(main.isString());
assertEquals("42", main.asString());
}
@Test
public void returnTypeCheckByLastStatementOfMainIntFirst() throws Exception {
var main = assertTypeCheckByLastStatementOfMain("Integer & Text");
assertTrue(main.fitsInInt());
assertEquals(42, main.asInt());
}
private Value assertTypeCheckByLastStatementOfMain(String cast) throws Exception {
final URI uri = new URI("memory://rts.enso");
final Source src =
Source.newBuilder(
@ -1036,18 +1049,17 @@ public class SignatureTest extends ContextTest {
from Standard.Base import all
fn =
(42 : Text & Integer)
(42 : ${cast})
Text.from (that:Integer) = that.to_text
""",
"""
.replace("${cast}", cast),
uri.getAuthority())
.uri(uri)
.buildLiteral();
var module = ctx.eval(src);
var main = module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "fn");
assertEquals(42, main.asInt());
assertEquals("42", main.asString());
return module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "fn");
}
/**

View File

@ -157,7 +157,12 @@ final class BinaryOperatorNode extends ExpressionNode {
for (var thatType : all) {
var fn = findSymbol(symbol, thatType);
if (fn != null) {
var thatCasted = EnsoMultiValue.CastToNode.getUncached().executeCast(thatType, multi);
var thatCasted =
EnsoMultiValue.CastToNode.getUncached()
.findTypeOrNull(thatType, multi, false, false);
if (thatCasted == null) {
continue;
}
var result =
doDispatch(
frame, self, thatCasted, selfType, thatType, fn, convertNode, invokeNode);

View File

@ -183,7 +183,7 @@ public abstract class InvokeConversionNode extends BaseNode {
Object[] arguments,
@Cached EnsoMultiValue.CastToNode castTo) {
var type = extractType(self);
var result = castTo.executeCast(type, that);
var result = castTo.findTypeOrNull(type, that, true, true);
if (result == null) {
throw new PanicException(
EnsoContext.get(this).getBuiltins().error().makeNoSuchConversion(type, self, conversion),

View File

@ -299,7 +299,7 @@ public abstract class InvokeMethodNode extends BaseNode {
@Cached EnsoMultiValue.CastToNode castTo) {
var fnAndType = self.resolveSymbol(methodResolverNode, symbol);
if (fnAndType != null) {
var unwrapSelf = castTo.executeCast(fnAndType.getRight(), self);
var unwrapSelf = castTo.findTypeOrNull(fnAndType.getRight(), self, false, false);
if (unwrapSelf != null) {
assert arguments[0] == self;
arguments[0] = unwrapSelf;

View File

@ -1,6 +1,7 @@
package org.enso.interpreter.node.expression.builtin.meta;
import com.oracle.truffle.api.Assumption;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.Truffle;
import org.enso.interpreter.runtime.data.hash.EnsoHashMap;
@ -76,4 +77,10 @@ public final class EqualsAndInfo {
return new EqualsAndInfo(result, warnings);
}
}
@Override
@CompilerDirectives.TruffleBoundary
public String toString() {
return "EqualsAndInfo{" + "equals=" + equals + ", hasWarnings=" + (getWarnings() != null) + '}';
}
}

View File

@ -18,6 +18,7 @@ import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.EnsoMultiValue;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.atom.Atom;
import org.enso.interpreter.runtime.data.atom.StructsLibrary;
@ -109,7 +110,7 @@ public final class EqualsNode extends Node {
}
private static boolean isDefinedIn(ModuleScope scope, Function fn) {
if (fn.getCallTarget().getRootNode() instanceof EnsoRootNode ensoRoot) {
if (fn != null && fn.getCallTarget().getRootNode() instanceof EnsoRootNode ensoRoot) {
return ensoRoot.getModuleScope().getModule() == scope.getModule();
} else {
return false;
@ -240,6 +241,13 @@ public final class EqualsNode extends Node {
var state = State.create(ctx);
try {
var thatAsSelf = convertNode.execute(convert, state, new Object[] {selfType, that});
if (thatAsSelf instanceof EnsoMultiValue emv) {
thatAsSelf =
EnsoMultiValue.CastToNode.getUncached().findTypeOrNull(selfType, emv, false, false);
}
if (thatAsSelf == null) {
return EqualsAndInfo.FALSE;
}
var withInfo = equalityNode.execute(frame, self, thatAsSelf);
var result = withInfo.isTrue();
assert !result || assertHashCodeIsTheSame(that, thatAsSelf);

View File

@ -21,6 +21,7 @@ import org.enso.interpreter.runtime.data.EnsoObject;
import org.enso.interpreter.runtime.data.atom.Atom;
import org.enso.interpreter.runtime.data.atom.AtomConstructor;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.library.dispatch.TypeOfNode;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
import org.enso.interpreter.runtime.warning.WarningsLibrary;
import org.enso.polyglot.common_utils.Core_Text_Utils;
@ -118,7 +119,7 @@ abstract class EqualsSimpleNode extends Node {
}
}
@Specialization
@Specialization(guards = "isNotMulti(other)")
EqualsAndInfo equalsLongInterop(
long self,
Object other,
@ -227,7 +228,8 @@ abstract class EqualsSimpleNode extends Node {
}
@TruffleBoundary
@Specialization(guards = {"isBigInteger(iop, self)", "!isPrimitiveValue(other)"})
@Specialization(
guards = {"isBigInteger(iop, self)", "!isPrimitiveValue(other)", "isNotMulti(other)"})
EqualsAndInfo equalsBigIntInterop(
Object self,
Object other,
@ -266,7 +268,11 @@ abstract class EqualsSimpleNode extends Node {
* lexicographical order, handling Unicode normalization. See {@code Text_Utils.compare_to}.
*/
@Specialization(
guards = {"selfInterop.isString(selfString)"},
guards = {
"selfInterop.isString(selfString)",
"isNotMulti(selfString)",
"isNotMulti(otherString)"
},
limit = "3")
EqualsAndInfo equalsStrings(
Object selfString,
@ -317,6 +323,40 @@ abstract class EqualsSimpleNode extends Node {
}
}
@Specialization
EqualsAndInfo equalsMultiValue(
VirtualFrame frame,
EnsoMultiValue self,
Object other,
@Shared("multiCast") @Cached EnsoMultiValue.CastToNode castNode,
@Shared("multiType") @Cached TypeOfNode typesNode,
@Shared("multiEquals") @Cached EqualsSimpleNode delegate) {
var types = typesNode.findAllTypesOrNull(self);
assert types != null;
for (var t : types) {
var value = castNode.findTypeOrNull(t, self, false, false);
if (value == null) {
continue;
}
var res = delegate.execute(frame, value, other);
if (res.isTrue()) {
return res;
}
}
return EqualsAndInfo.FALSE;
}
@Specialization
EqualsAndInfo equalsMultiValueReversed(
VirtualFrame frame,
Object self,
EnsoMultiValue other,
@Shared("multiCast") @Cached EnsoMultiValue.CastToNode castNode,
@Shared("multiType") @Cached TypeOfNode typesNode,
@Shared("multiEquals") @Cached EqualsSimpleNode delegate) {
return equalsMultiValue(frame, other, self, castNode, typesNode, delegate);
}
@Specialization
EqualsAndInfo equalsReverseLong(
VirtualFrame frame,
@ -375,7 +415,7 @@ abstract class EqualsSimpleNode extends Node {
return true;
}
if (a instanceof EnsoMultiValue || b instanceof EnsoMultiValue) {
return true;
return false;
}
return !isPrimitive(a, interop) && !isPrimitive(b, interop);
}
@ -400,6 +440,10 @@ abstract class EqualsSimpleNode extends Node {
return object instanceof Boolean || object instanceof Long || object instanceof Double;
}
static boolean isNotMulti(Object v) {
return !(v instanceof EnsoMultiValue);
}
static boolean isBigInteger(InteropLibrary iop, Object v) {
return switch (v) {
case EnsoBigInteger b -> true;

View File

@ -11,7 +11,7 @@ import org.enso.interpreter.runtime.error.DataflowError;
/**
* Root of hierarchy of nodes checking types. This class (and its subclasses) are an implementation
* detail. The API to perform the is in {@link TypeCheckNode}.
* detail. The API to perform the check or conversion is in {@link TypeCheckValueNode}.
*/
abstract sealed class AbstractTypeCheckNode extends Node
permits OneOfTypesCheckNode, AllOfTypesCheckNode, SingleTypeCheckNode, MetaTypeCheckNode {
@ -29,6 +29,16 @@ abstract sealed class AbstractTypeCheckNode extends Node
abstract String expectedTypeMessage();
final boolean isAllTypes() {
Node p = this;
for (; ; ) {
if (p instanceof TypeCheckValueNode vn) {
return vn.isAllTypes();
}
p = p.getParent();
}
}
/**
* The error message for this node's check. Ready for being used at "fast path".
*

View File

@ -40,11 +40,19 @@ final class AllOfTypesCheckNode extends AbstractTypeCheckNode {
if (result == null) {
return null;
}
values[at] = result;
valueTypes[at] = types.getType(result);
if (result instanceof EnsoMultiValue emv) {
result =
EnsoMultiValue.CastToNode.getUncached()
.findTypeOrNull(valueTypes[at], emv, false, true);
}
if (result == null) {
return null;
}
values[at] = result;
at++;
}
return EnsoMultiValue.create(valueTypes, values);
return EnsoMultiValue.create(valueTypes, valueTypes.length, values);
}
@Override

View File

@ -11,7 +11,7 @@ import org.enso.interpreter.runtime.util.CachingSupplier;
/**
* Node for checking {@code polyglot java import} types. This class (and its subclasses)
* are an implementation detail. The API to perform the is in {@link TypeCheckNode}.
* are an implementation detail. The API to perform the is in {@link TypeCheckValueNode}.
*/
non-sealed abstract class MetaTypeCheckNode extends AbstractTypeCheckNode {
private final CachingSupplier<? extends Object> expectedSupplier;

View File

@ -1,14 +1,7 @@
package org.enso.interpreter.node.typecheck;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.InvalidAssumptionException;
import com.oracle.truffle.api.nodes.Node;
import java.util.Arrays;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.node.EnsoRootNode;
import org.enso.interpreter.node.ExpressionNode;
@ -28,6 +21,16 @@ import org.enso.interpreter.runtime.error.PanicSentinel;
import org.enso.interpreter.runtime.library.dispatch.TypeOfNode;
import org.graalvm.collections.Pair;
import com.oracle.truffle.api.CompilerAsserts;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.MaterializedFrame;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.InvalidAssumptionException;
import com.oracle.truffle.api.nodes.Node;
non-sealed abstract class SingleTypeCheckNode extends AbstractTypeCheckNode {
private final Type expectedType;
@Node.Child IsValueOfTypeNode checkType;
@ -104,7 +107,7 @@ non-sealed abstract class SingleTypeCheckNode extends AbstractTypeCheckNode {
CompilerDirectives.transferToInterpreter();
var enso = EnsoLanguage.get(this);
var node = (AbstractTypeCheckNode) copy();
lazyCheck = new LazyCheckRootNode(enso, new TypeCheckValueNode(node));
lazyCheck = new LazyCheckRootNode(enso, new TypeCheckValueNode(node, isAllTypes()));
}
var lazyCheckFn = lazyCheck.wrapThunk(fn);
return lazyCheckFn;
@ -115,7 +118,7 @@ non-sealed abstract class SingleTypeCheckNode extends AbstractTypeCheckNode {
CompilerDirectives.transferToInterpreter();
castTo = insert(EnsoMultiValue.CastToNode.create());
}
var result = castTo.executeCast(expectedType, mv);
var result = castTo.findTypeOrNull(expectedType, mv, true, isAllTypes());
if (result != null) {
return result;
}
@ -175,7 +178,15 @@ non-sealed abstract class SingleTypeCheckNode extends AbstractTypeCheckNode {
if (v instanceof EnsoMultiValue multi) {
var all = typeOfNode.findAllTypesOrNull(multi);
assert all != null;
return all;
var to = 0;
// only consider methodDispatchTypes and not "all types" of the multi value
for (var i = 0; i < all.length; i++) {
var typeOrNull = EnsoMultiValue.CastToNode.getUncached().findTypeOrNull(all[i], multi, false, false);
if (typeOrNull != null) {
all[to++] = all[i];
}
}
return Arrays.copyOf(all, to);
}
if (v instanceof UnresolvedConstructor) {
return null;

View File

@ -19,8 +19,8 @@ final class TypeCheckExpressionNode extends ExpressionNode {
@Override
public Object executeGeneric(VirtualFrame frame) {
java.lang.Object value = original.executeGeneric(frame);
java.lang.Object result = check.handleCheckOrConversion(frame, value, original);
var value = original.executeGeneric(frame);
var result = check.handleCheckOrConversion(frame, value, original);
return result;
}

View File

@ -18,10 +18,12 @@ import org.enso.interpreter.runtime.util.CachingSupplier;
/** A node and a factory for nodes performing type checks (including necessary conversions). */
public final class TypeCheckValueNode extends Node {
private @Child AbstractTypeCheckNode check;
private final boolean allTypes;
TypeCheckValueNode(AbstractTypeCheckNode check) {
TypeCheckValueNode(AbstractTypeCheckNode check, boolean allTypes) {
assert check != null;
this.check = check;
this.allTypes = allTypes;
}
/**
@ -83,8 +85,8 @@ public final class TypeCheckValueNode extends Node {
var arr = toArray(flatten);
return switch (arr.length) {
case 0 -> null;
case 1 -> new TypeCheckValueNode(arr[0]);
default -> new TypeCheckValueNode(new AllOfTypesCheckNode(comment, arr));
case 1 -> new TypeCheckValueNode(arr[0], true);
default -> new TypeCheckValueNode(new AllOfTypesCheckNode(comment, arr), true);
};
}
@ -106,7 +108,7 @@ public final class TypeCheckValueNode extends Node {
default -> {
var abstractTypeCheckList = list.stream().map(n -> n.check).toList();
var abstractTypeCheckArr = toArray(abstractTypeCheckList);
yield new TypeCheckValueNode(new OneOfTypesCheckNode(comment, abstractTypeCheckArr));
yield new TypeCheckValueNode(new OneOfTypesCheckNode(comment, abstractTypeCheckArr), true);
}
};
}
@ -120,7 +122,7 @@ public final class TypeCheckValueNode extends Node {
*/
public static TypeCheckValueNode single(String comment, Type expectedType) {
var typeCheckNodeImpl = SingleTypeCheckNodeGen.create(comment, expectedType);
return new TypeCheckValueNode(typeCheckNodeImpl);
return new TypeCheckValueNode(typeCheckNodeImpl, true);
}
/**
@ -134,7 +136,25 @@ public final class TypeCheckValueNode extends Node {
String comment, Supplier<? extends Object> metaObjectSupplier) {
var cachingSupplier = CachingSupplier.wrap(metaObjectSupplier);
var typeCheckNodeImpl = MetaTypeCheckNodeGen.create(comment, cachingSupplier);
return new TypeCheckValueNode(typeCheckNodeImpl);
return new TypeCheckValueNode(typeCheckNodeImpl, true);
}
/**
* Creates new node with different {@code allTypes} state. Otherwise the behavior is the same as
* {@code node}. All types state influences the behavior of type checking {@link EnsoMultiValue} -
* should all types the value is convertible to be used or only those types that the value has
* already been converted to?
*
* <pre>
* method arg:Text = # this check has allTypes == false
* num = arg:Number # this check has allTypes == true
* </pre>
*
* @param allTypes the value of all types state for the new node
* @param node the previous node with type checking logic
*/
public static TypeCheckValueNode allTypes(boolean allTypes, TypeCheckValueNode node) {
return node == null ? null : new TypeCheckValueNode(node.check, allTypes);
}
/**
@ -179,4 +199,8 @@ public final class TypeCheckValueNode extends Node {
}
return arr;
}
final boolean isAllTypes() {
return allTypes;
}
}

View File

@ -2,7 +2,6 @@ package org.enso.interpreter.runtime.data;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.dsl.GenerateUncached;
import com.oracle.truffle.api.dsl.NeverDefault;
@ -41,19 +40,36 @@ public final class EnsoMultiValue extends EnsoObject {
@CompilationFinal(dimensions = 1)
private final Type[] types;
@CompilationFinal private final int methodDispatchTypes;
@CompilationFinal(dimensions = 1)
private final Object[] values;
private EnsoMultiValue(Type[] types, Object[] values) {
private EnsoMultiValue(Type[] types, int dispatchTypes, Object[] values) {
this.types = types;
this.methodDispatchTypes = dispatchTypes;
assert types.length == values.length;
this.values = values;
assert !Stream.of(values).filter(v -> v instanceof EnsoMultiValue).findAny().isPresent()
assert !Stream.of(values).anyMatch(v -> v instanceof EnsoMultiValue)
: "Avoid double wrapping " + Arrays.toString(values);
}
public static EnsoObject create(Type[] types, Object[] values) {
return new EnsoMultiValue(types, values);
/**
* Creates new instance of EnsoMultiValue from provided information.
*
* @param types all the types this value can be {@link CastToNode cast to}
* @param dispatchTypes the (subset of) types that the value is cast to currently - bigger than
* {@code 0} and at most {@code type.length}
* @param values value of each of the provided {@code types}
* @return non-{@code null} multi value instance
*/
@NeverDefault
public static EnsoMultiValue create(
@NeverDefault Type[] types, @NeverDefault int dispatchTypes, @NeverDefault Object... values) {
assert dispatchTypes > 0;
assert dispatchTypes <= types.length;
assert types.length == values.length;
return new EnsoMultiValue(types, dispatchTypes, values);
}
@ExportMessage
@ -83,125 +99,134 @@ public final class EnsoMultiValue extends EnsoObject {
return toString();
}
private enum InteropType {
NULL,
BOOLEAN,
DATE_TIME_ZONE,
DURATION,
STRING,
NUMBER,
POINTER,
META_OBJECT,
ITERATOR;
private record Value(InteropType type, Object value) {}
static Value find(Object[] values, int max, InteropLibrary iop) {
for (var i = 0; i < max; i++) {
var v = values[i];
if (iop.isNull(v)) {
return new Value(NULL, v);
}
if (iop.isBoolean(v)) {
return new Value(BOOLEAN, v);
}
if (iop.isDate(v) || iop.isTime(v) || iop.isTimeZone(v)) {
return new Value(DATE_TIME_ZONE, v);
}
if (iop.isDuration(v)) {
return new Value(DURATION, v);
}
if (iop.isString(v)) {
return new Value(STRING, v);
}
if (iop.isNumber(v)) {
return new Value(NUMBER, v);
}
if (iop.isPointer(v)) {
return new Value(POINTER, v);
}
if (iop.isMetaObject(v)) {
return new Value(META_OBJECT, v);
}
if (iop.isIterator(v)) {
return new Value(ITERATOR, v);
}
}
return new Value(null, null);
}
}
@ExportMessage
boolean isBoolean(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isBoolean(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.BOOLEAN;
}
@ExportMessage
boolean asBoolean(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.isBoolean(values[i])) {
return iop.asBoolean(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.BOOLEAN) {
return iop.asBoolean(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isString(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isString(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.STRING;
}
@ExportMessage
String asString(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (Object value : values) {
if (iop.isString(value)) {
return iop.asString(value);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.STRING) {
return iop.asString(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isNumber(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isNumber(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER;
}
@ExportMessage
boolean fitsInByte(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInByte(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInByte(both.value());
}
@ExportMessage
boolean fitsInShort(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInShort(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInShort(both.value());
}
@ExportMessage
boolean fitsInInt(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInShort(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInInt(both.value());
}
@ExportMessage
boolean fitsInLong(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInLong(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInLong(both.value());
}
@ExportMessage
boolean fitsInFloat(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInFloat(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInFloat(both.value());
}
@ExportMessage
boolean fitsInDouble(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInDouble(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInDouble(both.value());
}
@ExportMessage
byte asByte(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInByte(values[i])) {
return iop.asByte(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asByte(both.value());
}
throw UnsupportedMessageException.create();
}
@ -209,10 +234,9 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
short asShort(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInShort(values[i])) {
return iop.asShort(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asShort(both.value());
}
throw UnsupportedMessageException.create();
}
@ -220,10 +244,9 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
int asInt(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInInt(values[i])) {
return iop.asInt(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asInt(both.value());
}
throw UnsupportedMessageException.create();
}
@ -231,10 +254,9 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
long asLong(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInLong(values[i])) {
return iop.asLong(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asLong(both.value());
}
throw UnsupportedMessageException.create();
}
@ -242,10 +264,9 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
float asFloat(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInFloat(values[i])) {
return iop.asFloat(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asFloat(both.value());
}
throw UnsupportedMessageException.create();
}
@ -253,115 +274,89 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
double asDouble(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInDouble(values[i])) {
return iop.asDouble(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asDouble(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean fitsInBigInteger(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInBigInteger(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.NUMBER && iop.fitsInBigInteger(both.value());
}
@ExportMessage
BigInteger asBigInteger(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.fitsInBigInteger(values[i])) {
return iop.asBigInteger(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.NUMBER) {
return iop.asBigInteger(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isTime(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isTime(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.DATE_TIME_ZONE && iop.isTime(both.value());
}
@ExportMessage
LocalTime asTime(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.isTime(values[i])) {
return iop.asTime(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.DATE_TIME_ZONE) {
return iop.asTime(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isDate(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isDate(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.DATE_TIME_ZONE && iop.isDate(both.value());
}
@ExportMessage
LocalDate asDate(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.isDate(values[i])) {
return iop.asDate(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.DATE_TIME_ZONE) {
return iop.asDate(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isTimeZone(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isTimeZone(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.DATE_TIME_ZONE && iop.isTimeZone(both.value());
}
@ExportMessage
ZoneId asTimeZone(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.isTimeZone(values[i])) {
return iop.asTimeZone(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.DATE_TIME_ZONE) {
return iop.asTimeZone(both.value());
}
throw UnsupportedMessageException.create();
}
@ExportMessage
boolean isDuration(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
if (iop.isDuration(values[i])) {
return true;
}
}
return false;
var both = InteropType.find(values, methodDispatchTypes, iop);
return both.type() == InteropType.DURATION;
}
@ExportMessage
Duration asDuration(@Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop)
throws UnsupportedMessageException {
for (var i = 0; i < values.length; i++) {
if (iop.isDuration(values[i])) {
return iop.asDuration(values[i]);
}
var both = InteropType.find(values, methodDispatchTypes, iop);
if (both.type() == InteropType.DURATION) {
return iop.asDuration(both.value());
}
throw UnsupportedMessageException.create();
}
@ -376,7 +371,7 @@ public final class EnsoMultiValue extends EnsoObject {
Object getMembers(
boolean includeInternal, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
var names = new TreeSet<String>();
for (var i = 0; i < values.length; i++) {
for (var i = 0; i < methodDispatchTypes; i++) {
try {
var members = iop.getMembers(values[i]);
var len = iop.getArraySize(members);
@ -393,7 +388,7 @@ public final class EnsoMultiValue extends EnsoObject {
@ExportMessage
boolean isMemberInvocable(
String name, @Shared("interop") @CachedLibrary(limit = "10") InteropLibrary iop) {
for (var i = 0; i < values.length; i++) {
for (var i = 0; i < methodDispatchTypes; i++) {
if (iop.isMemberInvocable(values[i], name)) {
return true;
}
@ -410,7 +405,7 @@ public final class EnsoMultiValue extends EnsoObject {
ArityException,
UnsupportedTypeException,
UnknownIdentifierException {
for (var i = 0; i < values.length; i++) {
for (var i = 0; i < methodDispatchTypes; i++) {
if (iop.isMemberInvocable(values[i], name)) {
return iop.invokeMember(values[i], name, args);
}
@ -430,11 +425,20 @@ public final class EnsoMultiValue extends EnsoObject {
/**
* Casts value in a multi value into specific type.
*
* @param type the requested t
* @param type the requested type
* @param mv a multi value
* @return instance of the {@code t} or {@code null} if no suitable value was found
* @param reorderOnly allow (modified) {@link EnsoMultiValue} to be returned otherwise extract
* the value of {@code type} and return it directly
* @param allTypes should we search all types or just up to {@code methodDispatchTypes}
* @return instance of the {@code type} or {@code null} if no suitable value was found
*/
public abstract Object executeCast(Type type, EnsoMultiValue mv);
public final Object findTypeOrNull(
Type type, EnsoMultiValue mv, boolean reorderOnly, boolean allTypes) {
return executeCast(type, mv, reorderOnly, allTypes);
}
abstract Object executeCast(
Type type, EnsoMultiValue mv, boolean reorderOnly, boolean allTypes);
@NeverDefault
public static CastToNode create() {
@ -447,13 +451,21 @@ public final class EnsoMultiValue extends EnsoObject {
}
@Specialization
Object castsToAType(
Type type,
EnsoMultiValue mv,
@Cached(value = "type", allowUncached = true, neverDefault = true) Type cachedType) {
for (var i = 0; i < mv.types.length; i++) {
if (mv.types[i] == cachedType) {
return mv.values[i];
Object castsToAType(Type type, EnsoMultiValue mv, boolean reorderOnly, boolean allTypes) {
var max = allTypes ? mv.types.length : mv.methodDispatchTypes;
for (var i = 0; i < max; i++) {
if (mv.types[i] == type) {
if (reorderOnly) {
var copyTypes = mv.types.clone();
var copyValues = mv.values.clone();
copyTypes[i] = mv.types[0];
copyValues[i] = mv.values[0];
copyTypes[0] = mv.types[i];
copyValues[0] = mv.values[i];
return EnsoMultiValue.create(copyTypes, 1, copyValues);
} else {
return mv.values[i];
}
}
}
return null;
@ -471,10 +483,11 @@ public final class EnsoMultiValue extends EnsoObject {
MethodResolverNode node, UnresolvedSymbol symbol) {
var ctx = EnsoContext.get(node);
Pair<Function, Type> foundAnyMethod = null;
for (Type t : types) {
for (var i = 0; i < methodDispatchTypes; i++) {
var t = types[i];
var fnAndType = node.execute(t, symbol);
if (fnAndType != null) {
if (fnAndType.getRight() != ctx.getBuiltins().any()) {
if (methodDispatchTypes == 1 || fnAndType.getRight() != ctx.getBuiltins().any()) {
return Pair.create(fnAndType.getLeft(), t);
}
foundAnyMethod = fnAndType;

View File

@ -908,7 +908,11 @@ class IrToTruffle(
arg: DefinitionArgument
): TypeCheckValueNode = {
val comment = "`" + arg.name.name + "`"
arg.ascribedType.map(extractAscribedType(comment, _)).getOrElse(null)
arg.ascribedType
.map { t =>
TypeCheckValueNode.allTypes(false, extractAscribedType(comment, t))
}
.getOrElse(null)
}
/** Checks if the expression has a @Builtin_Method annotation

View File

@ -23,6 +23,7 @@ import org.graalvm.polyglot.proxy.ProxyExecutable;
/** A collection of classes and methods useful for testing {@link Context} related stuff. */
public final class ContextUtils {
private ContextUtils() {}
public static Context createDefaultContext() {
@ -60,12 +61,15 @@ public final class ContextUtils {
}
/**
* Executes the given callable in the given context. A necessity for executing artificially
* created Truffle ASTs.
* Executes the given callable in the given context.A necessity for executing artificially created
* Truffle ASTs.
*
* @param <T> type of the return value
* @param ctx context to execute at
* @param callable action to invoke with given return type
* @return Object returned from {@code callable} wrapped in {@link Value}.
*/
public static Value executeInContext(Context ctx, Callable<Object> callable) {
public static <T> Value executeInContext(Context ctx, Callable<T> callable) {
// Force initialization of the context
ctx.eval("enso", "value = 0");
var err = new Exception[1];
@ -208,6 +212,7 @@ public final class ContextUtils {
@ExportLibrary(InteropLibrary.class)
static final class Unwrapper implements TruffleObject {
Object[] args;
@ExportMessage

View File

@ -10,7 +10,7 @@ import project.Data.Complex_Helpers.Complex_Comparator
type Complex
private Value re:Float im:Float
new re=0:Float im=0:Float =
new re=0:Float im=0:Float -> Complex =
c = Complex.Value re im
if im != 0 then c:Complex else
c.as_complex_and_float

View File

@ -11,6 +11,7 @@ import project.Semantic.Import_Loop.Spec as Import_Loop_Spec
import project.Semantic.Meta_Spec
import project.Semantic.Instrumentor_Spec
import project.Semantic.Meta_Location_Spec
import project.Semantic.Multi_Value_Convert_Spec
import project.Semantic.Multi_Value_Spec
import project.Semantic.Names_Spec
import project.Semantic.Equals_Spec
@ -133,6 +134,7 @@ main filter=Nothing =
Meta_Spec.add_specs suite_builder
Instrumentor_Spec.add_specs suite_builder
Meta_Location_Spec.add_specs suite_builder
Multi_Value_Convert_Spec.add_specs suite_builder
Multi_Value_Spec.add_specs suite_builder
Names_Spec.add_specs suite_builder
Numbers_Spec.add_specs suite_builder

View File

@ -0,0 +1,132 @@
from Standard.Base import all
from Standard.Test import all
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Common.No_Such_Method
import project.Data.Complex.Complex
type A
A value
a self = "a"
type B
B value
b self = "b"
type C
C value
c self = "c"
A.from (that:Float) = A.A that
B.from (that:Float) = B.B that
C.from (that:Float) = C.C that
type U
U value
u self = "u"
type V
V v
U.from (that:B) = U.U that.value
V.from (that:C) = V.V that.value
add_specs suite_builder =
suite_builder.group "Convert Multi Value" group_builder->
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)
uv:U . u . should_equal "u"
uv:V . v . should_equal 3.14
Test.expect_panic No_Such_Method <| (uv:V).u
Test.expect_panic No_Such_Method <| (uv:U).v
group_builder.specify "fails conversion to U&V from A and C as B is missing" <|
ac = 3.14 : A&C
Test.expect_panic Type_Error <| ac:U&V
Test.expect_panic Type_Error <| ac:U
ac:V . v . should_equal 3.14
group_builder.specify "case of first" <|
abc = 3.14 : A&B&C
c1 = case abc of
a:A -> a.a
b:B -> b.b
c:C -> c.c
_ -> "what?"
c1 . should_equal "a"
group_builder.specify "case of second downcasted" <|
abc = 3.14 : A&B&C
c1 = case abc:B of
b:B -> b.b
a:A -> a.a
c:C -> c.c
_ -> "what?"
c1 . should_equal "b"
group_builder.specify "case of third downcasted" <|
abc = 3.14 : A&B&C
c1 = case abc:C of
c:C -> c.c
b:B -> b.b
a:A -> a.a
_ -> "what?"
c1 . should_equal "c"
suite_builder.group "Equals and hash" group_builder->
group_builder.specify "Dictionary with value and multi value" <|
pi = 3.14
a = pi : A
b = pi : B
c = pi : C
abc = pi : A&B&C
downcast_a = abc : A
downcast_ab = abc : A&B
downcast_ba = abc : B&A
downcast_b = abc : B
downcast_c = abc : C
Ordering.compare a b . catch Any e->
e.should_equal (Standard.Base.Errors.Common.Incomparable_Values.Error a b)
Ordering.compare a downcast_a . should_equal Ordering.Equal
Ordering.compare a downcast_ab . should_equal Ordering.Equal
Ordering.compare a abc . should_equal Ordering.Equal
Ordering.compare a downcast_ba . should_equal Ordering.Equal
Ordering.compare downcast_ba b . should_equal Ordering.Equal
# if a == downcast_ba && downcast_ba == b then # due to transitivity
# Ordering.compare a b . should_equal Ordering.Equal
dict = Dictionary.empty
. insert a "A"
. insert b "B"
. insert c "C"
. insert downcast_a "A_"
. insert downcast_b "B_"
. insert downcast_c "C_"
. insert abc "Multi"
# dict . get a . should_equal "A" # sometimes it is A and sometimes it is Multi
# dict . get b . should_equal "B"
# dict . get c . should_equal "C" # sometimes it is C and sometimes it is Multi
dict . get downcast_a . should_equal "Multi"
dict . get downcast_b . should_equal "Multi"
dict . get downcast_c . should_equal "Multi"
dict . get abc . should_equal "Multi"
main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder
suite.run_with_filter filter

View File

@ -1,9 +1,30 @@
from Standard.Base import all
from Standard.Test import all
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Common.No_Such_Method
import project.Data.Complex.Complex
type A
a = "a"
type B
B v
b self = "b"
type C
C v
c self = "c"
B.from (that:A) = B.B that
C.from (that:B) = C.C that
type X
x = "x"
X.from (_:A) = X
add_specs suite_builder =
suite_builder.group "Complex Multi Value" group_builder->
group_builder.specify "Cannot convert to Float if it has imaginary part" <|
@ -14,6 +35,116 @@ add_specs suite_builder =
(c:Complex).re . should_equal 1.5
(c:Float) . should_equal 1.5
suite_builder.group "Chain Multi Value" group_builder->
to_b_to_c obj =
b = obj : B
c = b : C
c
group_builder.specify "Multiple conversions of A work" <|
a = A
converted = to_b_to_c a
converted . to_text . should_equal "(C (B A))"
group_builder.specify "Multiple conversions of A & X work" <|
ax = A:A&X
converted = to_b_to_c ax
converted . to_text . should_equal "(C (B A & X))"
group_builder.specify "Multiple conversions of A & X work" <|
xa = A:X&A
converted = to_b_to_c xa
converted . to_text . should_equal "(C (B A & X))"
group_builder.specify "Multiple conversions of (A & X : A) work" <|
ax = A:A&X
a = ax : A
converted = to_b_to_c a
converted . to_text . should_equal "(C (B A & X))"
group_builder.specify "Multiple conversions of X fail" <|
x = X
# has to fail as X cannot be converted to B in to_b_to_c
Test.expect_panic Type_Error <|
to_b_to_c x
group_builder.specify "Multiple conversions of (A & X : X) work" <|
ax = A:A&X
ax.a . should_equal "a"
ax.x . should_equal "x"
x = ax : X
x.x . should_equal "x"
Test.expect_panic No_Such_Method <|
# method of A isn't visible for direct dispatch
x.a
# but the object can be converted to A and then
# method a is available
(x:A).a . should_equal "a"
call_a obj:A = obj.a
# A&X type has attribute a
call_a ax . should_equal "a"
# according to "static typing" discussion at
# https://github.com/enso-org/enso/pull/11600#discussion_r1867584107
# we want the same `call_a` with `x` to fail
Test.expect_panic Type_Error <|
call_a x . should_equal "a"
# multivalue ax restricted to X cannot be converted to B in to_b_to_c
Test.expect_panic Type_Error <|
to_b_to_c x
group_builder.specify "Intersection type of unrelated types is not possible" <|
Test.expect_panic Type_Error <|
_ = X:X&B
Test.expect_panic Type_Error <|
_ = B:X&B
Test.expect_panic Type_Error <|
_ = X:B&X
Test.expect_panic Type_Error <|
_ = B:B&X
group_builder.specify "X:(A|X) --> B is not possible" <|
a = X:(A|X)
Test.expect_panic Type_Error <|
_ = a : B
group_builder.specify "A:(A|X) --> B is possible" <|
a = A:(A|X)
b = a : B
b.to_text . should_equal "(B A)"
# This test is failing
group_builder.specify "A:(A|B) --> (A&B) is possible" <|
a = A:(A|B)
both = a : (A&B)
both.a . should_equal "a"
both.b . should_equal "b"
group_builder.specify "B:(A|B) --> (A&B) is not possible" <|
b = B.B 42
a_or_b = b : (A|B)
Test.expect_panic Type_Error <|
_ = a_or_b : (A&B)
# This test is failing
group_builder.specify "B --> (A|C) is possible" <|
b = B.B 42
a_or_c = b : (A|C)
a_or_c.c . should_equal "c"
group_builder.specify "no transitive conversions" <|
a = A
Test.expect_panic Type_Error <|
a : C
main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder