From 019611a89c31618985303d4984ed581eadad11f5 Mon Sep 17 00:00:00 2001 From: Dima Date: Fri, 18 Aug 2023 15:15:20 +0200 Subject: [PATCH] feat(compiler): Structural typing for data and abilities [fixes LNG-215] (#843) --- aqua-src/antithesis.aqua | 40 ++++++++++++++++--- .../aqua/examples/structuraltyping.aqua | 35 ++++++++++++++++ .../src/__test__/examples.spec.ts | 6 +++ .../src/examples/structuralTypingCall.ts | 5 +++ .../aqua/model/inline/ArrowInliner.scala | 34 +++++++++------- .../inline/raw/CallArrowRawInliner.scala | 2 +- .../aqua/model/inline/state/Exports.scala | 28 ++++++++----- .../main/scala/aqua/types/CompareTypes.scala | 30 ++++++++------ 8 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 integration-tests/aqua/examples/structuraltyping.aqua create mode 100644 integration-tests/src/examples/structuralTypingCall.ts diff --git a/aqua-src/antithesis.aqua b/aqua-src/antithesis.aqua index cbeefc63..4bf24584 100644 --- a/aqua-src/antithesis.aqua +++ b/aqua-src/antithesis.aqua @@ -1,7 +1,35 @@ -service Srv("srv"): - call(x: i32) -> i32 +aqua Aaa -func main() -> i32: - arr = [1, 2, 3] - a <- Srv.call(0) - <- arr[Srv.call(1)] \ No newline at end of file +import "builtin.aqua" + +export structuralTypingTest + +data WideData: + s: string + n: u32 + +data ExactData: + s: string + +ability ExactAbility: + s: string + arr(s: string, s2: string, s3: string, s4: string) -> string + exact: ExactData + +ability WideAbility: + s: string + arr(s: string, s2: string, s3: string, s4: string) -> string + g: string + exact: WideData + +func ss(s1: string, s2: string, s3: string, s4: string) -> string: + <- Op.concat_strings(Op.concat_strings(Op.concat_strings(s1, s2), s3), s4) + +func main{ExactAbility}(someData: ExactData, secondData: ExactData) -> string: + <- ExactAbility.arr(someData.s, ExactAbility.exact.s, secondData.s, ExactAbility.s) + +func structuralTypingTest() -> string: + wd = WideData(s = "some_string", n = 32) + + WAbility = WideAbility(s = "ab_string", g = "", arr = ss, exact = wd) + <- main{WAbility}(wd, WAbility.exact) diff --git a/integration-tests/aqua/examples/structuraltyping.aqua b/integration-tests/aqua/examples/structuraltyping.aqua new file mode 100644 index 00000000..0d7aa54e --- /dev/null +++ b/integration-tests/aqua/examples/structuraltyping.aqua @@ -0,0 +1,35 @@ +aqua Aaa + +import "@fluencelabs/aqua-lib/builtin.aqua" + +export structuralTypingTest + +data WideData: + s: string + n: u32 + +data ExactData: + s: string + +ability ExactAbility: + s: string + arr(s: string, s2: string, s3: string, s4: string) -> string + exact: ExactData + +ability WideAbility: + s: string + arr(s: string, s2: string, s3: string, s4: string) -> string + g: string + exact: WideData + +func ss(s1: string, s2: string, s3: string, s4: string) -> string: + <- Op.concat_strings(Op.concat_strings(Op.concat_strings(s1, s2), s3), s4) + +func main{ExactAbility}(someData: ExactData, secondData: ExactData) -> string: + <- ExactAbility.arr(someData.s, ExactAbility.exact.s, secondData.s, ExactAbility.s) + +func structuralTypingTest() -> string: + wd = WideData(s = "some_string", n = 32) + + WAbility = WideAbility(s = "ab_string", g = "", arr = ss, exact = wd) + <- main{WAbility}(wd, WAbility.exact) diff --git a/integration-tests/src/__test__/examples.spec.ts b/integration-tests/src/__test__/examples.spec.ts index 237c094e..e78bd0f4 100644 --- a/integration-tests/src/__test__/examples.spec.ts +++ b/integration-tests/src/__test__/examples.spec.ts @@ -80,6 +80,7 @@ export const relay2 = config.relays[1]; const relayPeerId2 = relay2.peerId; import log from 'loglevel'; +import {structuralTypingCall} from "../examples/structuralTypingCall"; // log.setDefaultLevel("debug") async function start() { @@ -244,6 +245,11 @@ describe('Testing examples', () => { expect(result).toEqual([]); }); + it('structuraltyping.aqua', async () => { + let result = await structuralTypingCall(); + expect(result).toEqual("some_stringsome_stringsome_stringab_string"); + }); + it('collectionSugar array', async () => { let result = await arraySugarCall(); expect(result).toEqual([ diff --git a/integration-tests/src/examples/structuralTypingCall.ts b/integration-tests/src/examples/structuralTypingCall.ts new file mode 100644 index 00000000..2bb175d9 --- /dev/null +++ b/integration-tests/src/examples/structuralTypingCall.ts @@ -0,0 +1,5 @@ +import {structuralTypingTest} from "../compiled/examples/structuraltyping"; + +export async function structuralTypingCall(): Promise { + return await structuralTypingTest(); +} diff --git a/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala b/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala index 3a49a479..2e7ce8d9 100644 --- a/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/ArrowInliner.scala @@ -1,16 +1,16 @@ package aqua.model.inline import aqua.model -import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.model.* +import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.raw.ops.RawTag -import aqua.types.{AbilityType, ArrowType, BoxType, StreamType} import aqua.raw.value.{ValueRaw, VarRaw} -import cats.{Eval, Monoid} +import aqua.types.{AbilityType, ArrowType, BoxType, StreamType} import cats.data.{Chain, IndexedStateT, State} -import cats.syntax.traverse.* import cats.syntax.bifunctor.* import cats.syntax.foldable.* +import cats.syntax.traverse.* +import cats.{Eval, Monoid} import scribe.Logging /** @@ -267,7 +267,7 @@ object ArrowInliner extends Logging { ): State[S, AbilityResolvingResult] = { for { newName <- Mangler[S].findNewName(name) - newFieldsName = t.fields.mapBoth { case (n, t) => + newFieldsName = t.fields.mapBoth { case (n, _) => AbilityType.fullName(name, n) -> AbilityType.fullName(newName, n) } allNewNames = newFieldsName.add((name, newName)).toSortedMap @@ -313,22 +313,28 @@ object ArrowInliner extends Logging { case ArrowType(_, _) => Exports .getLastValue(currentOldName, exports) - .flatMap { case vm @ VarModel(name, _, _) => - arrows - .get(name) - .map(fa => - ( - Map(currentNewName.getOrElse(currentOldName) -> vm), - Map(name -> fa) + .flatMap { + case vm @ VarModel(name, _, _) => + arrows + .get(name) + .map(fa => + ( + Map(currentNewName.getOrElse(currentOldName) -> vm), + Map(name -> fa) + ) ) - ) + case lm @ LiteralModel(_, _) => + logger.error(s"Unexpected. Literal '$lm' cannot be an arrow") + None } .getOrElse((Map.empty, Map.empty)) case _ => Exports .getLastValue(currentOldName, exports) - .map(vm => (Map(currentNewName.getOrElse(currentOldName) -> vm), Map.empty)) + .map { vm => + (Map(currentNewName.getOrElse(currentOldName) -> vm), Map.empty) + } .getOrElse((Map.empty, Map.empty)) } }.foldMapA(_.bimap(_.toList, _.toList)).bimap(_.toMap, _.toMap) diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/CallArrowRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/CallArrowRawInliner.scala index 59e78691..4abe6679 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/CallArrowRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/CallArrowRawInliner.scala @@ -76,7 +76,7 @@ object CallArrowRawInliner extends RawInliner[CallArrowRaw] with Logging { ): State[S, (List[ValueModel], Inline)] = for { arrows <- Arrows[S].arrows exports <- Exports[S].exports - lastArrow <- Exports[S].getLast(funcName) + lastArrow <- Exports[S].getLastVarName(funcName) arrow = arrows .get(funcName) .orElse( diff --git a/model/inline/src/main/scala/aqua/model/inline/state/Exports.scala b/model/inline/src/main/scala/aqua/model/inline/state/Exports.scala index 024498d8..f524dc06 100644 --- a/model/inline/src/main/scala/aqua/model/inline/state/Exports.scala +++ b/model/inline/src/main/scala/aqua/model/inline/state/Exports.scala @@ -1,6 +1,6 @@ package aqua.model.inline.state -import aqua.model.{ValueModel, VarModel} +import aqua.model.{LiteralModel, ValueModel, VarModel} import aqua.model.ValueModel.Ability import aqua.types.AbilityType import cats.data.{NonEmptyList, State} @@ -44,9 +44,9 @@ trait Exports[S] extends Scoped[S] { def copyWithAbilityPrefix(prefix: String, newPrefix: String): State[S, Unit] /** - * Get name of last linked VarModel + * Get name of last linked VarModel. If the last element is not VarModel, return None */ - def getLast(name: String): State[S, Option[String]] + def getLastVarName(name: String): State[S, Option[String]] /** * Rename names in variables @@ -98,8 +98,8 @@ trait Exports[S] extends Scoped[S] { override def copyWithAbilityPrefix(prefix: String, newPrefix: String): State[R, Unit] = self.copyWithAbilityPrefix(prefix, newPrefix).transformS(f, g) - override def getLast(name: String): State[R, Option[String]] = - self.getLast(name).transformS(f, g) + override def getLastVarName(name: String): State[R, Option[String]] = + self.getLastVarName(name).transformS(f, g) override def renameVariables(renames: Map[String, String]): State[R, Unit] = self.renameVariables(renames).transformS(f, g) @@ -125,12 +125,14 @@ object Exports { def apply[S](implicit exports: Exports[S]): Exports[S] = exports // Get last linked VarModel - def getLastValue(name: String, state: Map[String, ValueModel]): Option[VarModel] = { + def getLastValue(name: String, state: Map[String, ValueModel]): Option[ValueModel] = { state.get(name) match { case Some(vm@VarModel(n, _, _)) => if (name == n) Option(vm) else getLastValue(n, state).orElse(Option(vm)) - case n => + case lm@Some(LiteralModel(_, _)) => + lm + case _ => None } } @@ -138,7 +140,7 @@ object Exports { object Simple extends Exports[Map[String, ValueModel]] { // Make links from one set of abilities to another (for ability assignment) - private def getAbilityPairs(oldName: String, newName: String, at: AbilityType, state: Map[String, ValueModel]): NonEmptyList[(String, VarModel)] = { + private def getAbilityPairs(oldName: String, newName: String, at: AbilityType, state: Map[String, ValueModel]): NonEmptyList[(String, ValueModel)] = { at.fields.toNel.flatMap { case (n, at@AbilityType(_, _)) => val newFullName = AbilityType.fullName(newName, n) @@ -158,15 +160,19 @@ object Exports { value: ValueModel ): State[Map[String, ValueModel], Unit] = State.modify { state => value match { - case vm@Ability(name, at, property) if property.isEmpty => + case Ability(name, at, property) if property.isEmpty => val pairs = getAbilityPairs(name, exportName, at, state) state ++ pairs.toList.toMap case _ => state + (exportName -> value) } } - override def getLast(name: String): State[Map[String, ValueModel], Option[String]] = - State.get.map(st => getLastValue(name, st).map(_.name)) + override def getLastVarName(name: String): State[Map[String, ValueModel], Option[String]] = + State.get.map(st => getLastValue(name, st).flatMap { + case VarModel(name, _, _) => Option(name) + case LiteralModel(_, _) => + None + }) override def resolved(exports: Map[String, ValueModel]): State[Map[String, ValueModel], Unit] = State.modify(_ ++ exports) diff --git a/types/src/main/scala/aqua/types/CompareTypes.scala b/types/src/main/scala/aqua/types/CompareTypes.scala index cbd70672..3dc276b7 100644 --- a/types/src/main/scala/aqua/types/CompareTypes.scala +++ b/types/src/main/scala/aqua/types/CompareTypes.scala @@ -63,19 +63,23 @@ object CompareTypes { val lfView = lf.view val rfView = rf.view if (lf == rf) 0.0 - else if ( - lf.keys.forall(rf.contains) && compareTypesList( - lfView.values.toList, - rfView.filterKeys(lfNEM.keys.contains).values.toList - ) == 1.0 - ) 1.0 - else if ( - rf.keys.forall(lf.contains) && compareTypesList( - lfView.filterKeys(rfNEM.keys.contains).values.toList, - rfView.values.toList - ) == -1.0 - ) -1.0 - else NaN + else if (lf.keys.forall(rf.contains)) { + if ( + compareTypesList( + lfView.values.toList, + rfView.filterKeys(lfNEM.keys.contains).values.toList + ) >= 0.0 + ) 1.0 + else NaN + } else if (rf.keys.forall(lf.contains)) { + if ( + compareTypesList( + lfView.filterKeys(rfNEM.keys.contains).values.toList, + rfView.values.toList + ) <= 0 + ) -1.0 + else NaN + } else NaN } private def compareProducts(l: ProductType, r: ProductType): Double = ((l, r): @unchecked) match {