From 1437a671e14542dc69185a2593d99aec9ce1668a Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Fri, 1 Sep 2023 08:05:48 +0200 Subject: [PATCH] Introducing generic Any.to type conversion method (#7704) --- CHANGELOG.md | 2 + .../lib/Standard/Base/0.0.0-dev/src/Any.enso | 45 +++++++++ .../IndirectInvokeConversionNode.java | 9 +- .../node/callable/InvokeConversionNode.java | 91 ++++++++++--------- test/Tests/src/Semantic/Conversion_Spec.enso | 28 +++++- test/Tests/src/Semantic/Conversion_Use.enso | 6 ++ 6 files changed, 129 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1cc8cb5c59..64fa4585575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -560,6 +560,7 @@ - [Expose `Text.normalize`.][7425] - [Implemented new value types (various sizes of `Integer` type, fixed-length and length-limited `Char` type) for the in-memory `Table` backend.][7557] +- [Introducing generic `Any.to` conversion method][7704] - [Added `take` and `drop` to database tables.][7615] - [Added ability to specify expected value type in `Column.from_vector`, `Column.map` and `Column.zip`.][7637] @@ -797,6 +798,7 @@ [7297]: https://github.com/enso-org/enso/pull/7297 [7425]: https://github.com/enso-org/enso/pull/7425 [7557]: https://github.com/enso-org/enso/pull/7557 +[7704]: https://github.com/enso-org/enso/pull/7704 [7615]: https://github.com/enso-org/enso/pull/7615 [7637]: https://github.com/enso-org/enso/pull/7637 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso index 867f0ccbf26..610b5771d39 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso @@ -20,6 +20,51 @@ from project.Function import const be used in that position. @Builtin_Type type Any + ## GROUP Conversions + Generic conversion of an arbitrary Enso value to requested type. + Delegates to appropriate `.from` conversion method, if it exists. + If such method doesn't exist, `No_Such_Conversion` panic is raised. + + Arguments: + - typ: the requested type. + + > Example + Following code defines conversion of a `Complex` type to a `Number` + by computing absolute distance from `0`. The code yields `5.0`: + + type Complex + Value re:Number im:Number + + Number.from (that:Complex) = that.re*that.re+that.im*that.im . sqrt + + Complex.Value 3 4 . to Number + + > Example + `.from` conversion methods may have additional arguments + with default values. Thus the conversion from `Complex` to + `Number` may take additional argument: + + type Complex + Value re:Number im:Number + + Number.from (that:Complex) = that.re*that.re+that.im*that.im . sqrt + + Complex.Value 3 4 . to Number + + type Complex + Value re:Number im:Number + + Number.from (that:Complex) (ignore_im:Boolean=False) = case ignore_im of + False -> that.re*that.re+that.im*that.im . sqrt + True -> that.re + + yields_3 = Complex.Value 3 4 . to Number ignore_im=True + yields_5 = Complex.Value 3 4 . to Number ignore_im=False + default5 = Complex.Value 3 4 . to Number + + to : Any -> Any ! No_Such_Conversion + to self typ = typ.from self ... + ## GROUP Conversions Generic conversion of an arbitrary Enso value to a corresponding textual representation. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/IndirectInvokeConversionNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/IndirectInvokeConversionNode.java index 6f117eea487..3f559883bd8 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/IndirectInvokeConversionNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/IndirectInvokeConversionNode.java @@ -62,10 +62,7 @@ public abstract class IndirectInvokeConversionNode extends Node { IndirectInvokeFunctionNode indirectInvokeFunctionNode) { Function function = conversionResolverNode.expectNonNull( - that, - InvokeConversionNode.extractConstructor(this, self), - typesLib.getType(that), - conversion); + that, InvokeConversionNode.extractType(this, self), typesLib.getType(that), conversion); return indirectInvokeFunctionNode.execute( function, frame, @@ -95,7 +92,7 @@ public abstract class IndirectInvokeConversionNode extends Node { @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = conversionResolverNode.execute( - InvokeConversionNode.extractConstructor(this, self), + InvokeConversionNode.extractType(this, self), EnsoContext.get(this).getBuiltins().dataflowError(), conversion); if (function != null) { @@ -184,7 +181,7 @@ public abstract class IndirectInvokeConversionNode extends Node { Function function = conversionResolverNode.expectNonNull( txt, - InvokeConversionNode.extractConstructor(this, self), + InvokeConversionNode.extractType(this, self), EnsoContext.get(this).getBuiltins().text(), conversion); arguments[0] = txt; diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java index 9433ad33efd..55cc8241a16 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InvokeConversionNode.java @@ -1,16 +1,8 @@ package org.enso.interpreter.node.callable; -import com.oracle.truffle.api.CompilerDirectives; -import com.oracle.truffle.api.dsl.*; -import com.oracle.truffle.api.dsl.Cached.Shared; -import com.oracle.truffle.api.frame.VirtualFrame; -import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.UnsupportedMessageException; -import com.oracle.truffle.api.library.CachedLibrary; -import com.oracle.truffle.api.nodes.Node; -import com.oracle.truffle.api.source.SourceSection; import java.util.UUID; import java.util.concurrent.locks.Lock; + import org.enso.interpreter.node.BaseNode; import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode; import org.enso.interpreter.node.callable.resolver.ConversionResolverNode; @@ -22,10 +14,25 @@ import org.enso.interpreter.runtime.control.TailCallException; import org.enso.interpreter.runtime.data.ArrayRope; import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.text.Text; -import org.enso.interpreter.runtime.error.*; +import org.enso.interpreter.runtime.error.DataflowError; +import org.enso.interpreter.runtime.error.PanicException; +import org.enso.interpreter.runtime.error.PanicSentinel; +import org.enso.interpreter.runtime.error.Warning; +import org.enso.interpreter.runtime.error.WithWarnings; import org.enso.interpreter.runtime.library.dispatch.TypesLibrary; import org.enso.interpreter.runtime.state.State; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Cached.Shared; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.nodes.Node; +import com.oracle.truffle.api.source.SourceSection; + public abstract class InvokeConversionNode extends BaseNode { private @Child InvokeFunctionNode invokeFunctionNode; private @Child InvokeConversionNode childDispatch; @@ -79,18 +86,18 @@ public abstract class InvokeConversionNode extends BaseNode { Object that, Object[] arguments); - static Type extractConstructor(Node thisNode, Object self) { - if (self instanceof Type) { - return (Type) self; + static Type extractType(Node thisNode, Object self) { + if (self instanceof Type type) { + return type; } else { - throw new PanicException( - EnsoContext.get(thisNode).getBuiltins().error().makeInvalidConversionTarget(self), - thisNode); + var ctx = EnsoContext.get(thisNode); + var err = ctx.getBuiltins().error().makeInvalidConversionTarget(self); + throw new PanicException(err, thisNode); } } - Type extractConstructor(Object self) { - return extractConstructor(this, self); + private Type extractType(Object self) { + return extractType(this, self); } @Specialization(guards = {"dispatch.hasType(that)", "!dispatch.hasSpecialDispatch(that)"}) @@ -102,11 +109,15 @@ public abstract class InvokeConversionNode extends BaseNode { Object that, Object[] arguments, @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary dispatch, - @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { - Function function = - conversionResolverNode.expectNonNull( - that, extractConstructor(self), dispatch.getType(that), conversion); - return invokeFunctionNode.execute(function, frame, state, arguments); + @Shared("conversionResolverNode") @Cached ConversionResolverNode resolveNode) { + var thatType = dispatch.getType(that); + if (thatType == self) { + return that; + } else { + var selfType = extractType(self); + var function = resolveNode.expectNonNull(that, selfType, thatType, conversion); + return invokeFunctionNode.execute(function, frame, state, arguments); + } } @Specialization @@ -120,8 +131,7 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary dispatch, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.execute( - extractConstructor(self), + conversionResolverNode.execute(extractType(self), EnsoContext.get(this).getBuiltins().dataflowError(), conversion); if (function != null) { @@ -197,9 +207,8 @@ public abstract class InvokeConversionNode extends BaseNode { String str = interop.asString(that); Text txt = Text.create(str); Function function = - conversionResolverNode.expectNonNull( - txt, - extractConstructor(self), + conversionResolverNode.expectNonNull(txt, + extractType(self), EnsoContext.get(this).getBuiltins().text(), conversion); arguments[0] = txt; @@ -227,8 +236,7 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.expectNonNull( - that, extractConstructor(self), EnsoContext.get(this).getBuiltins().date(), conversion); + conversionResolverNode.expectNonNull(that, extractType(self), EnsoContext.get(this).getBuiltins().date(), conversion); return invokeFunctionNode.execute(function, frame, state, arguments); } @@ -250,9 +258,8 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.expectNonNull( - that, - extractConstructor(self), + conversionResolverNode.expectNonNull(that, + extractType(self), EnsoContext.get(this).getBuiltins().timeOfDay(), conversion); return invokeFunctionNode.execute(function, frame, state, arguments); @@ -276,9 +283,8 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.expectNonNull( - that, - extractConstructor(self), + conversionResolverNode.expectNonNull(that, + extractType(self), EnsoContext.get(this).getBuiltins().dateTime(), conversion); return invokeFunctionNode.execute(function, frame, state, arguments); @@ -301,9 +307,8 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.expectNonNull( - that, - extractConstructor(self), + conversionResolverNode.expectNonNull(that, + extractType(self), EnsoContext.get(this).getBuiltins().duration(), conversion); return invokeFunctionNode.execute(function, frame, state, arguments); @@ -326,9 +331,8 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("typesLib") @CachedLibrary(limit = "10") TypesLibrary typesLib, @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { Function function = - conversionResolverNode.expectNonNull( - thatMap, - extractConstructor(self), + conversionResolverNode.expectNonNull(thatMap, + extractType(self), EnsoContext.get(this).getBuiltins().map(), conversion); return invokeFunctionNode.execute(function, frame, state, arguments); @@ -352,8 +356,7 @@ public abstract class InvokeConversionNode extends BaseNode { @Shared("conversionResolverNode") @Cached ConversionResolverNode conversionResolverNode) { var ctx = EnsoContext.get(this); var function = - conversionResolverNode.execute( - extractConstructor(self), ctx.getBuiltins().any(), conversion); + conversionResolverNode.execute(extractType(self), ctx.getBuiltins().any(), conversion); if (function == null) { throw new PanicException( ctx.getBuiltins().error().makeNoSuchConversion(self, that, conversion), this); diff --git a/test/Tests/src/Semantic/Conversion_Spec.enso b/test/Tests/src/Semantic/Conversion_Spec.enso index 33e5c4ef6cd..ef7991668ea 100644 --- a/test/Tests/src/Semantic/Conversion_Spec.enso +++ b/test/Tests/src/Semantic/Conversion_Spec.enso @@ -1,4 +1,5 @@ from Standard.Base import all +import Standard.Base.Errors.Common.No_Such_Conversion import project.Semantic.Conversion.Methods import project.Semantic.Conversion.Types @@ -120,7 +121,30 @@ spec = Hello.formulate [ Hello.Say "Proper", Hello.Say "Type" ] . should_equal "ProperType" Hello.formulate [ Foo.Value "Perform", Bar.Value "Conversion" ] . should_equal "PERFORM conversion!" -Hello.from (that:Foo) = Hello.Say <| (that.foo.to_case Case.Upper) + " " -Hello.from (that:Bar) = Hello.Say <| (that.bar.to_case Case.Lower) + "!" + Test.specify "Convert Foo.to Hello" <| + hello = Foo.Value "Perform" . to Hello + hello . msg . should_equal "PERFORM " + + Test.specify "Convert Bar.to Hello" <| + hello = Bar.Value "Conversion" . to Hello + hello . msg . should_equal "conversion!" + + Test.specify "Convert Bar.to Hello with other suffix" <| + hello = Bar.Value "Conversion" . to Hello suffix="?" + hello . msg . should_equal "conversion?" + + Test.specify "Idempotent convert Hello.to Hello" <| + Hello.Say "Hi there!" . to Hello . msg . should_equal "Hi there!" + + Test.specify "Unknown convertion Text.to Hello" <| + h = Panic.recover No_Such_Conversion <| "Hi there!" . to Hello + h . should_fail_with No_Such_Conversion + + Test.specify "Use Any.to in Conversion_Use module" <| + Hello.formulate_with_to [ Hello.Say "Proper", Hello.Say "Type" ] . should_equal "ProperType" + Hello.formulate_with_to [ Foo.Value "Perform", Bar.Value "Conversion" ] . should_equal "PERFORM conversion!" + +Hello.from (that:Foo) suffix=" " = Hello.Say <| (that.foo.to_case Case.Upper) + suffix +Hello.from (that:Bar) suffix="!" = Hello.Say <| (that.bar.to_case Case.Lower) + suffix main = Test_Suite.run_main spec diff --git a/test/Tests/src/Semantic/Conversion_Use.enso b/test/Tests/src/Semantic/Conversion_Use.enso index 427cc48a51b..1272977a805 100644 --- a/test/Tests/src/Semantic/Conversion_Use.enso +++ b/test/Tests/src/Semantic/Conversion_Use.enso @@ -7,3 +7,9 @@ type Hello formulate arr = process (t:Text) (h:Hello) = t + h.msg arr.fold "" process + + formulate_with_to : Vector Hello -> Text + formulate_with_to arr = + arr.fold "" t-> h-> + m = h.to Hello . msg + t + m