diff --git a/integration-tests/.prettierrc.js b/integration-tests/.prettierrc.cjs similarity index 100% rename from integration-tests/.prettierrc.js rename to integration-tests/.prettierrc.cjs diff --git a/integration-tests/aqua/examples/boolAlgebra.aqua b/integration-tests/aqua/examples/boolAlgebra.aqua new file mode 100644 index 000000000..91aa13c74 --- /dev/null +++ b/integration-tests/aqua/examples/boolAlgebra.aqua @@ -0,0 +1,40 @@ +aqua Bool + +export main, Effector + +service Effector("effector"): + effect(name: string) -> bool + +func foo(x: i8) -> bool: + y = x + 1 + <- y < 5 + +func bar(x: i8) -> i8: + y = x - 1 + <- y + +func main(peer: string) -> []bool: + res: *bool + + on peer: + a = 1 + 2 + b = 2 - 1 + res <<- true || false && true -- true + res <<- (true || false) && true -- true + res <<- foo(3) && b > 0 || a > 4 -- true + res <<- bar(a) > 2 || true -- true + res <<- foo(4) && bar(2) < 2 -- false + res <<- !foo(10) && !!true -- true + res <<- !(bar(2) < 1) || !!(a < 2) -- true + + -- Effector is only registered on init_peer + res <<- true || Effector.effect("impossible") -- true + res <<- !!false && Effector.effect("impossible") -- false + res <<- foo(0) || Effector.effect("impossible") -- true + res <<- foo(10) && Effector.effect("impossible") -- false + res <<- Effector.effect("true") || true -- true + res <<- Effector.effect("true") && false -- false + res <<- !foo(10) || Effector.effect("impossible") -- true + res <<- !(1 < 2) && !Effector.effect("impossible") -- false + + <- res \ No newline at end of file diff --git a/integration-tests/src/__test__/examples.spec.ts b/integration-tests/src/__test__/examples.spec.ts index e72f8eadd..bf49fc09d 100644 --- a/integration-tests/src/__test__/examples.spec.ts +++ b/integration-tests/src/__test__/examples.spec.ts @@ -1,103 +1,108 @@ -import {Fluence, IFluenceClient, createClient} from '@fluencelabs/js-client.api'; -import "@fluencelabs/js-client.node" -import {getObjAssignCall, getObjCall, getObjRelayCall} from "../examples/objectCall.js"; -import {callArrowCall, reproArgsBug426Call} from '../examples/callArrowCall.js'; -import {dataAliasCall} from '../examples/dataAliasCall.js'; -import {onCall} from '../examples/onCall.js'; -import {onPropagateCall, nestedOnPropagateCall, seqOnPropagateCall} from '../examples/onErrorPropagation.js'; -import {funcCall} from '../examples/funcCall.js'; -import {registerPrintln} from '../compiled/examples/println.js'; -import {helloWorldCall} from '../examples/helloWorldCall.js'; -import {foldBug499Call, foldCall} from '../examples/foldCall.js'; -import {bugNG69Call, ifCall, ifWrapCall} from '../examples/ifCall.js'; -import {parCall, testTimeoutCall} from '../examples/parCall.js'; -import {complexCall} from '../examples/complex.js'; -import {constantsCall, particleTtlAndTimestampCall} from '../examples/constantsCall.js'; +import { Fluence, IFluenceClient, createClient } from '@fluencelabs/js-client.api'; +import '@fluencelabs/js-client.node'; +import { getObjAssignCall, getObjCall, getObjRelayCall } from '../examples/objectCall.js'; +import { callArrowCall, reproArgsBug426Call } from '../examples/callArrowCall.js'; +import { dataAliasCall } from '../examples/dataAliasCall.js'; +import { onCall } from '../examples/onCall.js'; +import { onPropagateCall, nestedOnPropagateCall, seqOnPropagateCall } from '../examples/onErrorPropagation.js'; +import { funcCall } from '../examples/funcCall.js'; +import { registerPrintln } from '../compiled/examples/println.js'; +import { helloWorldCall } from '../examples/helloWorldCall.js'; +import { foldBug499Call, foldCall } from '../examples/foldCall.js'; +import { bugNG69Call, ifCall, ifWrapCall } from '../examples/ifCall.js'; +import { parCall, testTimeoutCall } from '../examples/parCall.js'; +import { complexCall } from '../examples/complex.js'; +import { constantsCall, particleTtlAndTimestampCall } from '../examples/constantsCall.js'; import { nilLengthCall, nilLiteralCall, returnNilCall, - returnNoneCall, streamAssignmentCall, + returnNoneCall, + streamAssignmentCall, streamCall, - streamFunctorCall, streamIntFunctorCall, streamJoinCall, - streamReturnFromInnerFunc + streamFunctorCall, + streamIntFunctorCall, + streamJoinCall, + streamReturnFromInnerFunc, } from '../examples/streamCall.js'; -import {topologyBug205Call, topologyBug394Call, topologyBug427Call, topologyCall} from '../examples/topologyCall.js'; -import {foldJoinCall} from '../examples/foldJoinCall.js'; -import {registerHandlers, returnNull, returnOptionalCall, useOptionalCall} from '../examples/useOptionalCall.js'; -import {viaArrCall, viaOptCall, viaOptNullCall, viaStreamCall} from '../examples/viaCall.js'; -import {nestedFuncsCall} from '../examples/nestedFuncsCall.js'; -import {assignmentCall} from '../examples/assignment.js'; -import {tryCatchCall} from '../examples/tryCatchCall.js'; -import {tryOtherwiseCall} from '../examples/tryOtherwiseCall.js'; -import {coCall} from '../examples/coCall.js'; -import {bugLNG60Call, passArgsCall} from '../examples/passArgsCall.js'; -import {streamArgsCall} from '../examples/streamArgsCall.js'; -import {streamResultsCall} from '../examples/streamResultsCall.js'; -import {pushToStreamCall} from '../examples/pushToStreamCall.js'; -import {literalCall} from '../examples/returnLiteralCall.js'; -import {multiReturnCall} from '../examples/multiReturnCall.js'; -import {declareCall} from '../examples/declareCall.js'; -import {genOptions, genOptionsEmptyString} from '../examples/optionsCall.js'; +import { topologyBug205Call, topologyBug394Call, topologyBug427Call, topologyCall } from '../examples/topologyCall.js'; +import { foldJoinCall } from '../examples/foldJoinCall.js'; +import { registerHandlers, returnNull, returnOptionalCall, useOptionalCall } from '../examples/useOptionalCall.js'; +import { viaArrCall, viaOptCall, viaOptNullCall, viaStreamCall } from '../examples/viaCall.js'; +import { nestedFuncsCall } from '../examples/nestedFuncsCall.js'; +import { assignmentCall } from '../examples/assignment.js'; +import { boolAlgebraCall } from '../examples/boolAlgebra.js'; +import { tryCatchCall } from '../examples/tryCatchCall.js'; +import { tryOtherwiseCall } from '../examples/tryOtherwiseCall.js'; +import { coCall } from '../examples/coCall.js'; +import { bugLNG60Call, passArgsCall } from '../examples/passArgsCall.js'; +import { streamArgsCall } from '../examples/streamArgsCall.js'; +import { streamResultsCall } from '../examples/streamResultsCall.js'; +import { pushToStreamCall } from '../examples/pushToStreamCall.js'; +import { literalCall } from '../examples/returnLiteralCall.js'; +import { multiReturnCall } from '../examples/multiReturnCall.js'; +import { declareCall } from '../examples/declareCall.js'; +import { genOptions, genOptionsEmptyString } from '../examples/optionsCall.js'; import { lng193BugCall } from '../examples/closureReturnRename.js'; -import {closuresCall} from '../examples/closures.js'; -import {bugLNG63_2Call, bugLNG63_3Call, bugLNG63Call, streamCanCall} from '../examples/streamCanCall.js'; -import {streamCallbackCall} from '../examples/streamCallback.js'; -import {streamResCall} from '../examples/streamRestrictionsCall.js'; -import {joinIdxCall, joinIdxLocalCall, joinIdxRelayCall} from '../examples/joinCall.js'; -import {recursiveStreamsCall} from '../examples/recursiveStreamsCall.js'; +import { closuresCall } from '../examples/closures.js'; +import { bugLNG63_2Call, bugLNG63_3Call, bugLNG63Call, streamCanCall } from '../examples/streamCanCall.js'; +import { streamCallbackCall } from '../examples/streamCallback.js'; +import { streamResCall } from '../examples/streamRestrictionsCall.js'; +import { joinIdxCall, joinIdxLocalCall, joinIdxRelayCall } from '../examples/joinCall.js'; +import { recursiveStreamsCall } from '../examples/recursiveStreamsCall.js'; +import { arraySugarCall, bugLNG59Call, optionSugarCall, streamSugarCall } from '../examples/collectionSugarCall.js'; +import { funcsCall } from '../examples/funcsCall.js'; +import { nestedDataCall } from '../examples/nestedDataCall.js'; import { - arraySugarCall, - bugLNG59Call, - optionSugarCall, - streamSugarCall, -} from '../examples/collectionSugarCall.js'; -import {funcsCall} from '../examples/funcsCall.js'; -import {nestedDataCall} from '../examples/nestedDataCall.js'; -import {mathTest1Call, mathTest2Call, mathTestI16Call, mathTestI32Call, mathTestI64Call, mathTestU64Call} from '../examples/mathCall.js'; -import {lng58Bug} from '../compiled/examples/closures.js'; -import {config, isEphemeral} from '../config.js'; -import {bugLng79Call} from "../examples/canonCall.js"; -import {bugLng119Call} from "../examples/functorsCall.js"; -import {returnArrowCall, returnArrowChainCall} from "../examples/returnArrowCall.js"; + mathTest1Call, + mathTest2Call, + mathTestI16Call, + mathTestI32Call, + mathTestI64Call, + mathTestU64Call, +} from '../examples/mathCall.js'; +import { lng58Bug } from '../compiled/examples/closures.js'; +import { config, isEphemeral } from '../config.js'; +import { bugLng79Call } from '../examples/canonCall.js'; +import { bugLng119Call } from '../examples/functorsCall.js'; +import { returnArrowCall, returnArrowChainCall } from '../examples/returnArrowCall.js'; var selfPeerId: string; var peer1: IFluenceClient; var peer2: IFluenceClient; -export const relay1 = config.relays[0] -const relayPeerId1 = relay1.peerId -export const relay2 = config.relays[1] -const relayPeerId2 = relay2.peerId +export const relay1 = config.relays[0]; +const relayPeerId1 = relay1.peerId; +export const relay2 = config.relays[1]; +const relayPeerId2 = relay2.peerId; import log from 'loglevel'; -import {abilityCall} from "../examples/abilityCall"; +import { abilityCall } from '../examples/abilityCall'; // log.setDefaultLevel("debug") async function start() { - console.log("CONNECTING TO FIRST:") + console.log('CONNECTING TO FIRST:'); Fluence.onConnectionStateChange((s) => { - console.log(s) - }) - await Fluence.connect(relay1) + console.log(s); + }); + await Fluence.connect(relay1); const cl = await Fluence.getClient(); peer1 = cl; - selfPeerId = cl.getPeerId() - console.log("CONNECTED") + selfPeerId = cl.getPeerId(); + console.log('CONNECTED'); - peer2 = await createClient(relay2) - console.log("CONNECTING TO SECOND:") + peer2 = await createClient(relay2); + console.log('CONNECTING TO SECOND:'); peer2.onConnectionStateChange((s) => { - console.log(s) - }) - await peer2.connect() - console.log("CONNECTED") + console.log(s); + }); + await peer2.connect(); + console.log('CONNECTED'); } async function stop() { await Fluence.disconnect(); if (peer2) { - await peer2.disconnect(); } } @@ -116,7 +121,6 @@ describe('Testing examples', () => { afterAll(async () => { await stop(); - }); it('callArrow.aqua args bug 426', async () => { @@ -135,12 +139,16 @@ describe('Testing examples', () => { it('returnArrow.aqua chain', async () => { let argResult = await returnArrowChainCall(); - expect(argResult).toStrictEqual( - ["first", "firstarg for func1 literal", - "second", "secondarg for func2 second literal", - "third", "thirdarg for func2 second literal", - "fourth", "fourth from second literal" - ]); + expect(argResult).toStrictEqual([ + 'first', + 'firstarg for func1 literal', + 'second', + 'secondarg for func2 second literal', + 'third', + 'thirdarg for func2 second literal', + 'fourth', + 'fourth from second literal', + ]); }); it('streamRestrictions.aqua', async () => { @@ -191,24 +199,24 @@ describe('Testing examples', () => { }); it('stream.aqua return stream from inner func', async () => { - let streamResult = await streamReturnFromInnerFunc() + let streamResult = await streamReturnFromInnerFunc(); expect(streamResult).toEqual([1, 2, 3, 4]); - }) + }); it('stream.aqua functor', async () => { - let streamResult = await streamFunctorCall() - expect(streamResult).toEqual("123"); - }) + let streamResult = await streamFunctorCall(); + expect(streamResult).toEqual('123'); + }); it('stream.aqua assignment', async () => { - let streamResult = await streamAssignmentCall() - expect(streamResult).toEqual("333"); - }) + let streamResult = await streamAssignmentCall(); + expect(streamResult).toEqual('333'); + }); it('stream.aqua nil literal', async () => { - let result = await nilLiteralCall() + let result = await nilLiteralCall(); expect(result).toEqual([]); - }) + }); it('collectionSugar array', async () => { let result = await arraySugarCall(); @@ -219,35 +227,38 @@ describe('Testing examples', () => { }); it('object creation getObj', async () => { - let result = await getObjCall() + let result = await getObjCall(); expect(result).toEqual({ - str: "some str", + str: 'some str', num: 5, inner: { - arr: ["a", "b", "c"], - num: 6 - } + arr: ['a', 'b', 'c'], + num: 6, + }, }); }); it('object creation getObjAssign', async () => { - let result = await getObjAssignCall() - expect(result).toEqual([{ - str: "first str", - num: 5, - inner: { - arr: ["d", "e", "f"], - num: 7 - } - }, { - str: "some str", - num: 6, - inner: { - arr: ["a", "b", "c"], - num: 7 - } - }, - 1]); + let result = await getObjAssignCall(); + expect(result).toEqual([ + { + str: 'first str', + num: 5, + inner: { + arr: ['d', 'e', 'f'], + num: 7, + }, + }, + { + str: 'some str', + num: 6, + inner: { + arr: ['a', 'b', 'c'], + num: 7, + }, + }, + 1, + ]); }); it('collectionSugar stream', async () => { @@ -340,7 +351,7 @@ describe('Testing examples', () => { it('ability.aqua', async () => { let result = await abilityCall(); - expect(result).toStrictEqual(['declare_const123', "efre123", "declare_const123", 12]); + expect(result).toStrictEqual(['declare_const123', 'efre123', 'declare_const123', 12]); }); it('functors.aqua LNG-119 bug', async () => { @@ -373,6 +384,27 @@ describe('Testing examples', () => { expect(assignmentResult).toEqual(['abc', 'hello']); }); + it('boolAlgebra.aqua', async () => { + let boolAlgebraResult = await boolAlgebraCall(relayPeerId1); + expect(boolAlgebraResult).toEqual([ + true, + true, + true, + true, + false, + true, + true, + true, + false, + true, + false, + true, + false, + true, + false, + ]); + }); + it('join.aqua local', async () => { let joinLocalCallResult = await joinIdxLocalCall(relayPeerId1); expect(joinLocalCallResult.length).toBeGreaterThanOrEqual(2); @@ -389,14 +421,14 @@ describe('Testing examples', () => { }); it('stream.aqua nil length', async () => { - let result = await nilLengthCall() + let result = await nilLengthCall(); expect(result).toEqual(0); - }) + }); it('stream.aqua int functor', async () => { - let streamResult = await streamIntFunctorCall() - expect(streamResult).toEqual("123"); - }) + let streamResult = await streamIntFunctorCall(); + expect(streamResult).toEqual('123'); + }); it('streamCan.aqua LNG-63', async () => { let result = await bugLNG63Call(); @@ -444,9 +476,9 @@ describe('Testing examples', () => { }); it('stream.aqua join', async () => { - let streamResult = await streamJoinCall() - expect(streamResult).toEqual("444"); - }) + let streamResult = await streamJoinCall(); + expect(streamResult).toEqual('444'); + }); it('funcs.aqua', async () => { let result = await funcsCall(); @@ -501,22 +533,28 @@ describe('Testing examples', () => { it('onErrorPropagate.aqua', async () => { let call = onPropagateCall(peer2, relay2.peerId); expect(call).rejects.toMatchObject({ - message: expect.stringContaining("propagated error") - }) + message: expect.stringContaining('propagated error'), + }); }); it('onErrorPropagate.aqua nested', async () => { - let call = nestedOnPropagateCall(peer2, relay2.peerId, config.relays[3].peerId, config.relays[4].peerId, config.relays[5].peerId); + let call = nestedOnPropagateCall( + peer2, + relay2.peerId, + config.relays[3].peerId, + config.relays[4].peerId, + config.relays[5].peerId, + ); expect(call).rejects.toMatchObject({ - message: expect.stringContaining("propagated error") - }) + message: expect.stringContaining('propagated error'), + }); }); it('onErrorPropagate.aqua sequential', async () => { let call = seqOnPropagateCall(peer2, relay2.peerId, config.relays[3].peerId, config.relays[4].peerId); expect(call).rejects.toMatchObject({ - message: expect.stringContaining("propagated error") - }) + message: expect.stringContaining('propagated error'), + }); }); it('complex.aqua', async () => { @@ -525,14 +563,14 @@ describe('Testing examples', () => { }); it('object creation getObjRelay', async () => { - let result = await getObjRelayCall() + let result = await getObjRelayCall(); expect(result).toEqual({ - str: "some str", + str: 'some str', num: 5, inner: { - arr: ["a", "b", "c"], - num: 6 - } + arr: ['a', 'b', 'c'], + num: 6, + }, }); }); @@ -560,7 +598,12 @@ describe('Testing examples', () => { }); it('topology.aqua bug 394', async () => { - let topologyResult = await topologyBug394Call(peer1.getPeerId(), relay1.peerId, peer2.getPeerId(), relay2.peerId); + let topologyResult = await topologyBug394Call( + peer1.getPeerId(), + relay1.peerId, + peer2.getPeerId(), + relay2.peerId, + ); expect(topologyResult).toEqual(selfPeerId); }); @@ -582,8 +625,8 @@ describe('Testing examples', () => { it('closureReturnRename.aqua bug LNG-193', async () => { let result = await lng193BugCall(); - expect(result).toEqual((1 + 42) + (2 + 42) + (3 + 42) + (4 + 42)) - }, 20000) + expect(result).toEqual(1 + 42 + (2 + 42) + (3 + 42) + (4 + 42)); + }, 20000); it('closures.aqua', async () => { let closuresResult = await closuresCall(); @@ -618,6 +661,4 @@ describe('Testing examples', () => { let joinCallResult = await joinIdxCall(relayPeerId1); expect(joinCallResult.length).toBeGreaterThanOrEqual(2); }, 10000); - - }); diff --git a/integration-tests/src/config.ts b/integration-tests/src/config.ts index 756477fb6..627c84f0e 100644 --- a/integration-tests/src/config.ts +++ b/integration-tests/src/config.ts @@ -1,73 +1,51 @@ -import { - krasnodar, - stage, - testNet, -} from "@fluencelabs/fluence-network-environment"; -import { local } from "./local-nodes.js"; +import { krasnodar, stage, testNet } from '@fluencelabs/fluence-network-environment'; +import { local } from './local-nodes.js'; declare global { - namespace NodeJS { - interface ProcessEnv { - FLUENCE_ENV?: string; + namespace NodeJS { + interface ProcessEnv { + FLUENCE_ENV?: string; + } } - } } function setConfig(env) { - switch (env) { - case "krasnodar": - return { config: krasnodarConfig, isEphemeral: false }; - case "testnet": - return { config: testNetConfig, isEphemeral: false }; - case "ephemeral": - return { config: null, isEphemeral: true }; - case "local": - return { config: localConfig, isEphemeral: false }; - default: - return { config: stageConfig, isEphemeral: false }; - } + switch (env) { + case 'krasnodar': + return { config: krasnodarConfig, isEphemeral: false }; + case 'testnet': + return { config: testNetConfig, isEphemeral: false }; + case 'ephemeral': + return { config: null, isEphemeral: true }; + case 'local': + return { config: localConfig, isEphemeral: false }; + default: + return { config: stageConfig, isEphemeral: false }; + } } export const krasnodarConfig = { - relays: krasnodar, - externalAddressesRelay1: [ - "/ip4/164.90.171.139/tcp/7770", - "/ip4/164.90.171.139/tcp/9990/ws", - ], - externalAddressesRelay2: [ - "/ip4/164.90.164.229/tcp/7001", - "/ip4/164.90.164.229/tcp/9001/ws", - ], - tryCatchError: - "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", + relays: krasnodar, + externalAddressesRelay1: ['/ip4/164.90.171.139/tcp/7770', '/ip4/164.90.171.139/tcp/9990/ws'], + externalAddressesRelay2: ['/ip4/164.90.164.229/tcp/7001', '/ip4/164.90.164.229/tcp/9001/ws'], + tryCatchError: + "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", }; export const stageConfig = { - relays: stage, - externalAddressesRelay1: [ - "/ip4/134.209.186.43/tcp/7001", - "/ip4/134.209.186.43/tcp/9001/ws", - ], - externalAddressesRelay2: [ - "/ip4/134.209.186.43/tcp/7770", - "/ip4/134.209.186.43/tcp/9990/ws", - ], - tryCatchError: - "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", + relays: stage, + externalAddressesRelay1: ['/ip4/134.209.186.43/tcp/7001', '/ip4/134.209.186.43/tcp/9001/ws'], + externalAddressesRelay2: ['/ip4/134.209.186.43/tcp/7770', '/ip4/134.209.186.43/tcp/9990/ws'], + tryCatchError: + "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", }; export const testNetConfig = { - relays: testNet, - externalAddressesRelay1: [ - "/ip4/165.227.164.206/tcp/7001", - "/ip4/165.227.164.206/tcp/9001/ws", - ], - externalAddressesRelay2: [ - "/ip4/142.93.169.49/tcp/7001", - "/ip4/142.93.169.49/tcp/9001/ws", - ], - tryCatchError: - "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", + relays: testNet, + externalAddressesRelay1: ['/ip4/165.227.164.206/tcp/7001', '/ip4/165.227.164.206/tcp/9001/ws'], + externalAddressesRelay2: ['/ip4/142.93.169.49/tcp/7001', '/ip4/142.93.169.49/tcp/9001/ws'], + tryCatchError: + "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", }; // export const ephemeralConfig = { @@ -82,21 +60,21 @@ export const testNetConfig = { // }; export const localConfig = { - relays: local, - externalAddressesRelay1: [ - "/ip4/10.50.10.10/tcp/7771", - "/ip4/10.50.10.10/tcp/9991/ws", - "/dns4/nox-1/tcp/7771", - "/dns4/nox-1/tcp/9991/ws", - ], - externalAddressesRelay2: [ - "/ip4/10.50.10.60/tcp/7776", - "/ip4/10.50.10.60/tcp/9996/ws", - "/dns4/nox-6/tcp/7776", - "/dns4/nox-6/tcp/9996/ws", - ], - tryCatchError: - "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", + relays: local, + externalAddressesRelay1: [ + '/ip4/10.50.10.10/tcp/7771', + '/ip4/10.50.10.10/tcp/9991/ws', + '/dns4/nox-1/tcp/7771', + '/dns4/nox-1/tcp/9991/ws', + ], + externalAddressesRelay2: [ + '/ip4/10.50.10.60/tcp/7776', + '/ip4/10.50.10.60/tcp/9996/ws', + '/dns4/nox-6/tcp/7776', + '/dns4/nox-6/tcp/9996/ws', + ], + tryCatchError: + "Local service error, ret_code is 1, error message is '\"Service with id 'unex' not found (function getStr)\"'", }; -export const { config, isEphemeral } = setConfig("local"); +export const { config, isEphemeral } = setConfig('local'); diff --git a/integration-tests/src/examples/boolAlgebra.ts b/integration-tests/src/examples/boolAlgebra.ts new file mode 100644 index 000000000..22534f2ff --- /dev/null +++ b/integration-tests/src/examples/boolAlgebra.ts @@ -0,0 +1,12 @@ +import { main, registerEffector } from '../compiled/examples/boolAlgebra.js'; + +export async function boolAlgebraCall(relay: string): Promise { + registerEffector({ + effect(name, _) { + if (name == 'true') return Promise.resolve(true); + else return Promise.reject(`unknown effect: ${name}`); + }, + }); + + return await main(relay); +} diff --git a/model/inline/src/main/scala/aqua/model/inline/Inline.scala b/model/inline/src/main/scala/aqua/model/inline/Inline.scala index 349174a86..bec723297 100644 --- a/model/inline/src/main/scala/aqua/model/inline/Inline.scala +++ b/model/inline/src/main/scala/aqua/model/inline/Inline.scala @@ -3,76 +3,76 @@ package aqua.model.inline import aqua.model.{EmptyModel, OpModel, ParModel, SeqModel} import aqua.raw.ops.RawTag import aqua.raw.value.ValueRaw +import aqua.model.inline.Inline.MergeMode +import aqua.model.inline.Inline.MergeMode.* + import cats.Monoid import cats.data.Chain +import cats.data.Chain.* +import cats.syntax.option.* import scala.collection.immutable.ListMap -sealed trait MergeMode -object SeqMode extends MergeMode -object ParMode extends MergeMode - /** - * @param flattenValues values that need to be resolved before `predo`. - * ListMap for keeping order of values (mostly for debugging purposes) + * Inlining result + * * @param predo operations tree - * @param mergeMode how `flattenValues` and `predo` must be merged + * @param mergeMode how `predo` must be merged */ private[inline] case class Inline( - flattenValues: ListMap[String, ValueRaw] = ListMap.empty, predo: Chain[OpModel.Tree] = Chain.empty, mergeMode: MergeMode = ParMode ) { def desugar: Inline = { - val desugaredPredo = - predo.toList match { - case Nil => Chain.empty - case x :: Nil => - Chain.one(x) - case l => - mergeMode match - case SeqMode => - val wrapped = SeqModel.wrap(l: _*) - wrapped match - case EmptyModel.leaf => Chain.empty - case _ => Chain.one(wrapped) - case ParMode => Chain.one(ParModel.wrap(l: _*)) - } - - Inline( - flattenValues, - desugaredPredo - ) + val desugaredPredo = predo match { + case Chain.nil | _ ==: Chain.nil => predo + case chain => + mergeMode match + case SeqMode => + val wrapped = SeqModel.wrap(chain) + wrapped match + case EmptyModel.leaf => Chain.empty + case _ => Chain.one(wrapped) + case ParMode => Chain.one(ParModel.wrap(chain)) + } + + Inline(desugaredPredo) } def mergeWith(inline: Inline, mode: MergeMode): Inline = { val left = desugar val right = inline.desugar - Inline(left.flattenValues ++ right.flattenValues, left.predo ++ right.predo, mode) + Inline(left.predo ++ right.predo, mode) } } // TODO may not be needed there private[inline] object Inline { - val empty: Inline = Inline() - def preload(pairs: (String, ValueRaw)*): Inline = Inline(ListMap.from(pairs)) + enum MergeMode { + case SeqMode + case ParMode + } + + val empty: Inline = Inline() def tree(tr: OpModel.Tree): Inline = Inline(predo = Chain.one(tr)) given Monoid[Inline] with - override val empty: Inline = Inline() + override val empty: Inline = Inline.empty override def combine(a: Inline, b: Inline): Inline = - Inline(a.flattenValues ++ b.flattenValues, a.predo ++ b.predo) + // TODO: Is it ok to ignore merge mode? + Inline(a.predo ++ b.predo) - def parDesugarPrefix(ops: List[OpModel.Tree]): Option[OpModel.Tree] = ops match { - case Nil => None - case x :: Nil => Option(x) - case _ => Option(ParModel.wrap(ops: _*)) - } + def parDesugarPrefix(ops: List[OpModel.Tree]): Option[OpModel.Tree] = + ops match { + case Nil => none + case x :: Nil => x.some + case _ => ParModel.wrap(ops).some + } def parDesugarPrefixOpt(ops: Option[OpModel.Tree]*): Option[OpModel.Tree] = parDesugarPrefix(ops.toList.flatten) diff --git a/model/inline/src/main/scala/aqua/model/inline/MakeStructRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/MakeStructRawInliner.scala index cceda60d7..81c76bfe6 100644 --- a/model/inline/src/main/scala/aqua/model/inline/MakeStructRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/MakeStructRawInliner.scala @@ -1,19 +1,29 @@ package aqua.model.inline -import aqua.model.{CallModel, CallServiceModel, LiteralModel, OpModel, SeqModel, ValueModel, VarModel} +import aqua.model.{ + CallModel, + CallServiceModel, + LiteralModel, + OpModel, + SeqModel, + ValueModel, + VarModel +} import aqua.model.inline.raw.RawInliner -import cats.data.Chain import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.raw.value.{LiteralRaw, MakeStructRaw} -import cats.data.{NonEmptyMap, State} import aqua.model.inline.Inline import aqua.model.inline.RawValueInliner.{unfold, valueToModel} import aqua.types.ScalarType + +import cats.data.Chain +import cats.data.{NonEmptyMap, State} import cats.syntax.traverse.* import cats.syntax.monoid.* import cats.syntax.functor.* import cats.syntax.flatMap.* import cats.syntax.apply.* +import cats.syntax.foldable.* object MakeStructRawInliner extends RawInliner[MakeStructRaw] { @@ -45,15 +55,14 @@ object MakeStructRawInliner extends RawInliner[MakeStructRaw] { name <- Mangler[S].findAndForbidName(raw.structType.name + "_obj") foldedFields <- raw.fields.nonEmptyTraverse(unfold(_)) varModel = VarModel(name, raw.baseType) - valsInline = foldedFields.toSortedMap.values.map(_._2).fold(Inline.empty)(_ |+| _).desugar - fields = foldedFields.map(_._1) + valsInline = foldedFields.foldMap { case (_, inline) => inline }.desugar + fields = foldedFields.map { case (vm, _) => vm } objCreation <- createObj(fields, varModel) } yield { ( varModel, Inline( - valsInline.flattenValues, - Chain.one(SeqModel.wrap((valsInline.predo :+ objCreation).toList: _*)) + Chain.one(SeqModel.wrap(valsInline.predo :+ objCreation)) ) ) } diff --git a/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala b/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala index 69f12d85e..8195209fb 100644 --- a/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/RawValueInliner.scala @@ -1,11 +1,22 @@ package aqua.model.inline import aqua.model.inline.state.{Arrows, Counter, Exports, Mangler} +import aqua.model.inline.Inline.MergeMode.* import aqua.model.* -import aqua.model.inline.raw.{ApplyFunctorRawInliner, ApplyGateRawInliner, ApplyPropertiesRawInliner, CallArrowRawInliner, CollectionRawInliner, MakeAbilityRawInliner} +import aqua.model.inline.raw.{ + ApplyBinaryOpRawInliner, + ApplyFunctorRawInliner, + ApplyGateRawInliner, + ApplyPropertiesRawInliner, + ApplyUnaryOpRawInliner, + CallArrowRawInliner, + CollectionRawInliner, + MakeAbilityRawInliner +} import aqua.raw.ops.* import aqua.raw.value.* import aqua.types.{ArrayType, LiteralType, OptionType, StreamType} + import cats.syntax.traverse.* import cats.syntax.monoid.* import cats.syntax.functor.* @@ -13,6 +24,7 @@ import cats.syntax.flatMap.* import cats.syntax.apply.* import cats.instances.list.* import cats.data.{Chain, State, StateT} +import cats.syntax.applicative.* import scribe.Logging object RawValueInliner extends Logging { @@ -42,8 +54,14 @@ object RawValueInliner extends Logging { case dr: MakeStructRaw => MakeStructRawInliner(dr, propertiesAllowed) - case sr: AbilityRaw => - MakeAbilityRawInliner(sr, propertiesAllowed) + case ar: AbilityRaw => + MakeAbilityRawInliner(ar, propertiesAllowed) + + case auor: ApplyUnaryOpRaw => + ApplyUnaryOpRawInliner(auor, propertiesAllowed) + + case abbor: ApplyBinaryOpRaw => + ApplyBinaryOpRawInliner(abbor, propertiesAllowed) case cr: CallArrowRaw => CallArrowRawInliner(cr, propertiesAllowed) @@ -51,22 +69,11 @@ object RawValueInliner extends Logging { private[inline] def inlineToTree[S: Mangler: Exports: Arrows]( inline: Inline - ): State[S, List[OpModel.Tree]] = { - inline.flattenValues.toList.traverse { case (name, v) => - valueToModel(v).map { - case (vv, Some(op)) => - SeqModel.wrap(op, FlattenModel(vv, name).leaf) - - case (vv, _) => - FlattenModel(vv, name).leaf - } - }.map { predo => - inline.mergeMode match - case SeqMode => - SeqModel.wrap((inline.predo.toList ++ predo): _*) :: Nil - case ParMode => inline.predo.toList ::: predo - } - } + ): State[S, List[OpModel.Tree]] = + (inline.mergeMode match { + case SeqMode => SeqModel.wrap(inline.predo) :: Nil + case ParMode => inline.predo.toList + }).pure private[inline] def toModel[S: Mangler: Exports: Arrows]( unfoldF: State[S, (ValueModel, Inline)] diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyBinaryOpRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyBinaryOpRawInliner.scala new file mode 100644 index 000000000..16e51a4a9 --- /dev/null +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyBinaryOpRawInliner.scala @@ -0,0 +1,111 @@ +package aqua.model.inline.raw + +import aqua.model.* +import aqua.model.inline.raw.RawInliner +import aqua.model.inline.state.{Arrows, Exports, Mangler} +import aqua.raw.value.{AbilityRaw, LiteralRaw, MakeStructRaw} +import cats.data.{NonEmptyList, NonEmptyMap, State} +import aqua.model.inline.Inline +import aqua.model.inline.RawValueInliner.{unfold, valueToModel} +import aqua.types.{ArrowType, ScalarType} +import aqua.raw.value.ApplyBinaryOpRaw +import aqua.raw.value.ApplyBinaryOpRaw.Op.* + +import cats.data.Chain +import cats.syntax.traverse.* +import cats.syntax.monoid.* +import cats.syntax.functor.* +import cats.syntax.flatMap.* +import cats.syntax.apply.* +import cats.syntax.foldable.* +import cats.syntax.applicative.* + +object ApplyBinaryOpRawInliner extends RawInliner[ApplyBinaryOpRaw] { + + override def apply[S: Mangler: Exports: Arrows]( + raw: ApplyBinaryOpRaw, + propertiesAllowed: Boolean + ): State[S, (ValueModel, Inline)] = for { + left <- unfold(raw.left) + (lmodel, linline) = left + right <- unfold(raw.right) + (rmodel, rinline) = right + + result <- (lmodel, rmodel) match { + // Optimize in case of left value is known at compile time + case (LiteralModel.Bool(lvalue), _) => + (raw.op match { + case And if !lvalue => (LiteralModel.bool(false), linline) + case Or if lvalue => (LiteralModel.bool(true), linline) + case _ => (rmodel, Inline(linline.predo ++ rinline.predo)) + }).pure[State[S, *]] + // Optimize in case of right value is known at compile time and it has no computation + case (_, LiteralModel.Bool(rvalue)) if rinline.predo.isEmpty => + (raw.op match { + case And if !rvalue => (LiteralModel.bool(false), linline) + case Or if rvalue => (LiteralModel.bool(true), linline) + case _ => (lmodel, linline) + }).pure[State[S, *]] + // Produce unoptimized inline + case _ => fullInline(lmodel, rmodel, linline, rinline, raw.op) + } + } yield result + + private def fullInline[S: Mangler: Exports: Arrows]( + lmodel: ValueModel, + rmodel: ValueModel, + linline: Inline, + rinline: Inline, + op: ApplyBinaryOpRaw.Op + ): State[S, (ValueModel, Inline)] = { + val (name, compareWith) = op match { + case And => ("and", false) + case Or => ("or", true) + } + + /** + * (seq + * + * (xor + * (match + * (ap ) + * ) + * (seq + * + * (ap ) + * ) + * ) + * ) + */ + val predo = (resName: String) => + SeqModel.wrap( + linline.predo :+ XorModel.wrap( + MatchMismatchModel( + lmodel, + LiteralModel.bool(compareWith), + shouldMatch = true + ).wrap( + FlattenModel( + lmodel, + resName + ).leaf + ), + SeqModel.wrap( + rinline.predo :+ FlattenModel( + rmodel, + resName + ).leaf + ) + ) + ) + + Mangler[S] + .findAndForbidName(name) + .map(resName => + ( + VarModel(resName, ScalarType.bool), + Inline(Chain.one(predo(resName))) + ) + ) + } +} diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyFunctorRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyFunctorRawInliner.scala index 57b0cb0aa..2691d1565 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyFunctorRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyFunctorRawInliner.scala @@ -10,7 +10,8 @@ import aqua.model.{ ValueModel, VarModel } -import aqua.model.inline.{Inline, SeqMode} +import aqua.model.inline.Inline.MergeMode.* +import aqua.model.inline.Inline import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.raw.value.{FunctorRaw, ValueRaw} import cats.data.State @@ -53,10 +54,12 @@ object ApplyFunctorRawInliner extends Logging { } } yield { val tree = Inline( - predo = Chain.one(SeqModel.wrap( - flat, - FlattenModel(apVar, resultName).leaf - )), + predo = Chain.one( + SeqModel.wrap( + flat, + FlattenModel(apVar, resultName).leaf + ) + ), mergeMode = SeqMode ) diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyGateRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyGateRawInliner.scala index 2830ee864..61ee9f86f 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyGateRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyGateRawInliner.scala @@ -121,8 +121,7 @@ object ApplyGateRawInliner extends RawInliner[ApplyGateRaw] with Logging { val tree = SeqModel.wrap(idxInline.predo.toList :+ gate) - val treeInline = - Inline(idxInline.flattenValues, predo = Chain.one(tree)) + val treeInline = Inline(predo = Chain.one(tree)) ( VarModel(uniqueResultName, ArrayType(afr.streamType.element)), diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyIntoCopyRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyIntoCopyRawInliner.scala index 690b1155e..0dabe90cf 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyIntoCopyRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyIntoCopyRawInliner.scala @@ -1,12 +1,22 @@ package aqua.model.inline.raw -import aqua.model.{CallModel, CallServiceModel, LiteralModel, OpModel, SeqModel, ValueModel, VarModel} -import aqua.model.inline.{Inline, SeqMode, TagInliner} +import aqua.model.{ + CallModel, + CallServiceModel, + LiteralModel, + OpModel, + SeqModel, + ValueModel, + VarModel +} +import aqua.model.inline.Inline.MergeMode.* +import aqua.model.inline.{Inline, TagInliner} import aqua.model.inline.MakeStructRawInliner.createObj import aqua.model.inline.RawValueInliner.unfold import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.raw.value.{IntoCopyRaw, LiteralRaw} import aqua.types.ScalarType + import cats.data.{Chain, NonEmptyMap, State} import scribe.Logging import cats.syntax.traverse.* @@ -14,6 +24,7 @@ import cats.syntax.monoid.* import cats.syntax.functor.* import cats.syntax.flatMap.* import cats.syntax.apply.* +import cats.syntax.foldable.* object ApplyIntoCopyRawInliner extends Logging { @@ -45,15 +56,14 @@ object ApplyIntoCopyRawInliner extends Logging { name <- Mangler[S].findAndForbidName(value.name + "_obj_copy") foldedFields <- intoCopy.fields.nonEmptyTraverse(unfold(_)) varModel = VarModel(name, value.baseType) - valsInline = foldedFields.toSortedMap.values.map(_._2).fold(Inline.empty)(_ |+| _).desugar + valsInline = foldedFields.toList.foldMap { case (_, inline) => inline }.desugar fields = foldedFields.map(_._1) objCopy <- copyObj(value, fields, varModel) } yield { ( varModel, Inline( - valsInline.flattenValues, - Chain.one(SeqModel.wrap((valsInline.predo :+ objCopy).toList: _*)), + Chain.one(SeqModel.wrap(valsInline.predo :+ objCopy)), SeqMode ) ) diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyPropertiesRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyPropertiesRawInliner.scala index 55c16259b..863bc7971 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyPropertiesRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyPropertiesRawInliner.scala @@ -20,7 +20,7 @@ import aqua.model.{ XorModel } import aqua.model.inline.Inline -import aqua.model.inline.{ParMode, SeqMode} +import aqua.model.inline.Inline.MergeMode.* import aqua.model.inline.RawValueInliner.unfold import aqua.model.inline.state.{Arrows, Exports, Mangler} import aqua.raw.value.{ @@ -48,10 +48,14 @@ import aqua.types.{ StreamType, Type } + import cats.Eval +import cats.syntax.bifunctor.* import cats.data.{Chain, IndexedStateT, State} import cats.syntax.monoid.* import cats.syntax.traverse.* +import cats.syntax.foldable.* +import cats.Id import cats.instances.list.* import scribe.Logging @@ -183,7 +187,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi newVI <- ApplyFunctorRawInliner(flatten, f) } yield { newVI._1 -> Inline( - inline.flattenValues ++ newVI._2.flattenValues, inline.predo ++ newVI._2.predo, mergeMode = SeqMode ) @@ -198,7 +201,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi newVI <- ApplyIntoCopyRawInliner(varModel, ic) } yield { newVI._1 -> Inline( - inline.flattenValues ++ newVI._2.flattenValues, inline.predo ++ newVI._2.predo, mergeMode = SeqMode ) @@ -218,7 +220,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi case (vm @ VarModel(_, _, _), inline) if vm.properties.nonEmpty => removeProperties(vm).map { case (vf, inlf) => PropertyRawWithModel(iir, Option(IntoIndexModel(vf.name, t))) -> Inline( - inline.flattenValues ++ inlf.flattenValues, inline.predo ++ inlf.predo, mergeMode = SeqMode ) @@ -230,11 +231,7 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi } case p => State.pure(PropertyRawWithModel(p, None) -> Inline.empty) - }.sequence.map { (propsWithInline: Chain[(PropertyRawWithModel, Inline)]) => - val fullInline = propsWithInline.map(_._2).foldLeft(Inline.empty)(_ |+| _) - val props = propsWithInline.map(_._1) - (props, fullInline) - } + }.sequence.map(_.toList.unzip.bimap(Chain.fromSeq, _.combineAll)) } private def unfoldProperties[S: Mangler: Exports: Arrows]( @@ -254,7 +251,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi ( vm, Inline( - leftInline.flattenValues ++ inl.flattenValues, leftInline.predo ++ inl.predo, mergeMode = SeqMode ) @@ -269,7 +265,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi case (v, i) if !propertiesAllowed && v.properties.nonEmpty => removeProperties(v).map { case (vf, inlf) => vf -> Inline( - leftInline.flattenValues ++ i.flattenValues ++ inlf.flattenValues, leftInline.predo ++ i.predo ++ inlf.predo, mergeMode = SeqMode ) @@ -277,7 +272,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi case (v, i) => State.pure( v -> Inline( - leftInline.flattenValues ++ i.flattenValues, leftInline.predo ++ i.predo, mergeMode = SeqMode ) @@ -304,7 +298,6 @@ object ApplyPropertiesRawInliner extends RawInliner[ApplyPropertyRaw] with Loggi unfoldProperties(gateResInline, gateResVal, properties, propertiesAllowed).map { case (v, i) => v -> Inline( - inl.flattenValues ++ i.flattenValues, inl.predo ++ i.predo, mergeMode = SeqMode ) diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/ApplyUnaryOpRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyUnaryOpRawInliner.scala new file mode 100644 index 000000000..8d77845b6 --- /dev/null +++ b/model/inline/src/main/scala/aqua/model/inline/raw/ApplyUnaryOpRawInliner.scala @@ -0,0 +1,92 @@ +package aqua.model.inline.raw + +import aqua.model.* +import aqua.model.inline.raw.RawInliner +import aqua.model.inline.state.{Arrows, Exports, Mangler} +import aqua.raw.value.{AbilityRaw, LiteralRaw, MakeStructRaw} +import cats.data.{NonEmptyList, NonEmptyMap, State} +import aqua.model.inline.Inline +import aqua.model.inline.RawValueInliner.{unfold, valueToModel} +import aqua.types.{ArrowType, ScalarType} +import aqua.raw.value.ApplyUnaryOpRaw +import aqua.raw.value.ApplyUnaryOpRaw.Op.* + +import cats.data.Chain +import cats.syntax.traverse.* +import cats.syntax.monoid.* +import cats.syntax.functor.* +import cats.syntax.flatMap.* +import cats.syntax.apply.* +import cats.syntax.foldable.* +import cats.syntax.applicative.* + +object ApplyUnaryOpRawInliner extends RawInliner[ApplyUnaryOpRaw] { + + override def apply[S: Mangler: Exports: Arrows]( + raw: ApplyUnaryOpRaw, + propertiesAllowed: Boolean + ): State[S, (ValueModel, Inline)] = for { + value <- unfold(raw.value) + (vm, vinline) = value + + result <- vm match { + // Optimize in case of value is known at compile time + case LiteralModel.Bool(bvalue) => + (raw.op match { + case Not => (LiteralModel.bool(!bvalue), vinline) + }).pure[State[S, *]] + // Produce unoptimized inline + case _ => fullInline(vm, vinline, raw.op) + } + } yield result + + private def fullInline[S: Mangler: Exports: Arrows]( + vm: ValueModel, + vinline: Inline, + op: ApplyUnaryOpRaw.Op + ): State[S, (ValueModel, Inline)] = { + val name = op match { + case Not => "not" + } + + /* + * (seq + * + * (xor + * (match true + * (ap false ) + * ) + * (ap true ) + * ) + * ) + */ + val predo = (resName: String) => + SeqModel.wrap( + vinline.predo :+ XorModel.wrap( + MatchMismatchModel( + vm, + LiteralModel.bool(true), + shouldMatch = true + ).wrap( + FlattenModel( + LiteralModel.bool(false), + resName + ).leaf + ), + FlattenModel( + LiteralModel.bool(true), + resName + ).leaf + ) + ) + + Mangler[S] + .findAndForbidName(name) + .map(resName => + ( + VarModel(resName, ScalarType.bool), + Inline(Chain.one(predo(resName))) + ) + ) + } +} 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 4b63fe118..da46b0d5a 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 @@ -29,11 +29,10 @@ object CallArrowRawInliner extends RawInliner[CallArrowRaw] with Logging { cd <- callToModel(call, true) sd <- valueToModel(serviceId) } yield cd._1.exportTo.map(_.asVar.resolveWith(exports)) -> Inline( - ListMap.empty, Chain( SeqModel.wrap( sd._2.toList ++ - cd._2.toList :+ CallServiceModel(sd._1, value.name, cd._1).leaf: _* + cd._2.toList :+ CallServiceModel(sd._1, value.name, cd._1).leaf ) ) ) @@ -58,7 +57,6 @@ object CallArrowRawInliner extends RawInliner[CallArrowRaw] with Logging { .callArrowRet(fn, cm) .map { case (body, vars) => vars -> Inline( - ListMap.empty, Chain.one( // Leave meta information in tree after inlining MetaModel diff --git a/model/inline/src/main/scala/aqua/model/inline/raw/MakeAbilityRawInliner.scala b/model/inline/src/main/scala/aqua/model/inline/raw/MakeAbilityRawInliner.scala index 49336b490..affc1a387 100644 --- a/model/inline/src/main/scala/aqua/model/inline/raw/MakeAbilityRawInliner.scala +++ b/model/inline/src/main/scala/aqua/model/inline/raw/MakeAbilityRawInliner.scala @@ -22,6 +22,7 @@ import cats.syntax.monoid.* import cats.syntax.functor.* import cats.syntax.flatMap.* import cats.syntax.apply.* +import cats.syntax.foldable.* object MakeAbilityRawInliner extends RawInliner[AbilityRaw] { @@ -33,16 +34,14 @@ object MakeAbilityRawInliner extends RawInliner[AbilityRaw] { name <- Mangler[S].findAndForbidName(raw.abilityType.name + "_ab") foldedFields <- raw.fieldsAndArrows.nonEmptyTraverse(unfold(_)) varModel = VarModel(name, raw.baseType) - valsInline = foldedFields.toSortedMap.values.map(_._2).fold(Inline.empty)(_ |+| _).desugar - _ <- foldedFields.map(_._1).toNel.toList.traverse { case (n, vm) => - val namef = s"$name.$n" - Exports[S].resolved(namef, vm) + valsInline = foldedFields.toList.foldMap { case (_, inline) => inline }.desugar + _ <- foldedFields.toNel.traverse { case (n, (vm, _)) => + Exports[S].resolved(s"$name.$n", vm) } } yield { ( varModel, Inline( - valsInline.flattenValues, Chain.one(SeqModel.wrap(valsInline.predo)) ) ) diff --git a/model/inline/src/test/scala/aqua/model/inline/RawValueInlinerSpec.scala b/model/inline/src/test/scala/aqua/model/inline/RawValueInlinerSpec.scala index da49ede7c..021cdaddd 100644 --- a/model/inline/src/test/scala/aqua/model/inline/RawValueInlinerSpec.scala +++ b/model/inline/src/test/scala/aqua/model/inline/RawValueInlinerSpec.scala @@ -22,6 +22,8 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import scala.collection.immutable.SortedMap +import aqua.raw.value.ApplyBinaryOpRaw +import aqua.raw.value.CallArrowRaw class RawValueInlinerSpec extends AnyFlatSpec with Matchers { @@ -127,9 +129,8 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { "raw value inliner" should "desugarize a single non-recursive raw value" in { // x[y] valueToModel[InliningState](`raw x[y]`) - .run(InliningState(noNames = Set("x", "y"))) - .value - ._2 should be( + .runA(InliningState(noNames = Set("x", "y"))) + .value shouldBe ( VarModel( "x", ArrayType(ScalarType.string), @@ -143,13 +144,12 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { import aqua.model.inline.state.Mangler.Simple // a.field1.field2 valueToModel[InliningState](`raw res.c`) - .run( + .runA( InliningState(resolvedExports = Map("res" -> VarModel("a", aType, Chain.one(IntoFieldModel("b", bType)))) ) ) - .value - ._2 should be( + .value shouldBe ( VarModel( "a", aType, @@ -158,14 +158,13 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) } - "raw value inliner" should "desugarize a single recursive raw value" in { + it should "desugarize a single recursive raw value" in { // x[ys!] val (resVal, resTree) = valueToModel[InliningState]( `raw x[ys[0]]` ) - .run(InliningState(noNames = Set("x", "ys"))) + .runA(InliningState(noNames = Set("x", "ys"))) .value - ._2 resVal should be( VarModel( @@ -189,10 +188,10 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) should be(true) } - "raw value inliner" should "desugarize properties with functors x[ys[ys.length]][2] and make proper flattener tags" in { + it should "desugarize properties with functors x[ys[ys.length]][2] and make proper flattener tags" in { val (resVal, resTree) = valueToModel[InliningState]( `x[xs[ys.length]][xss[yss.length]]` - ).run(InliningState(noNames = Set("x", "ys", "xs", "yss", "xss"))).value._2 + ).runA(InliningState(noNames = Set("x", "ys", "xs", "yss", "xss"))).value resVal should be( VarModel( @@ -263,13 +262,12 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) should be(true) } - "raw value inliner" should "desugarize x[ys[0]][ys[1]] and make proper flattener tags" in { + it should "desugarize x[ys[0]][ys[1]] and make proper flattener tags" in { val (resVal, resTree) = valueToModel[InliningState]( `raw x[ys[0]][ys[1]]` ) - .run(InliningState(noNames = Set("x", "ys"))) + .runA(InliningState(noNames = Set("x", "ys"))) .value - ._2 resVal should be( VarModel( @@ -306,16 +304,15 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) should be(true) } - "raw value inliner" should "desugarize stream with gate" in { + it should "desugarize stream with gate" in { val streamWithProps = VarRaw("x", StreamType(ScalarType.string)).withProperty( IntoIndexRaw(ysVarRaw(1), ScalarType.string) ) val (resVal, resTree) = valueToModel[InliningState](streamWithProps) - .run(InliningState(noNames = Set("x", "ys"))) + .runA(InliningState(noNames = Set("x", "ys"))) .value - ._2 resVal should be( VarModel( @@ -328,28 +325,23 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) } - "raw value inliner" should "desugarize stream with length" in { + it should "desugarize stream with length" in { val streamWithProps = VarRaw("x", StreamType(ScalarType.string)).withProperty( FunctorRaw("length", ScalarType.u32) ) val (resVal, resTree) = valueToModel[InliningState](streamWithProps) - .run(InliningState(noNames = Set("x", "ys"))) + .runA(InliningState(noNames = Set("x", "ys"))) .value - ._2 - - // println(resVal) - // println(resTree) } - "raw value inliner" should "desugarize a recursive lambda value" in { + it should "desugarize a recursive lambda value" in { val (resVal, resTree) = valueToModel[InliningState]( `raw x[zs[ys[0]]][ys[1]]` ) - .run(InliningState(noNames = Set("x", "ys", "zs"))) + .runA(InliningState(noNames = Set("x", "ys", "zs"))) .value - ._2 // This is x[zs-0][ys-0] // zs-0 should be zs[ys[0]], which should be already flattened @@ -396,5 +388,4 @@ class RawValueInlinerSpec extends AnyFlatSpec with Matchers { ) ) should be(true) } - } diff --git a/model/raw/src/main/scala/aqua/raw/value/ValueRaw.scala b/model/raw/src/main/scala/aqua/raw/value/ValueRaw.scala index 3d463013f..9a5ebcc1f 100644 --- a/model/raw/src/main/scala/aqua/raw/value/ValueRaw.scala +++ b/model/raw/src/main/scala/aqua/raw/value/ValueRaw.scala @@ -179,6 +179,56 @@ case class AbilityRaw(fieldsAndArrows: NonEmptyMap[String, ValueRaw], abilityTyp copy(fieldsAndArrows = fieldsAndArrows.map(_.renameVars(map))) } +case class ApplyBinaryOpRaw( + op: ApplyBinaryOpRaw.Op, + left: ValueRaw, + right: ValueRaw +) extends ValueRaw { + + // Only boolean operations are supported for now + override def baseType: Type = ScalarType.bool + + override def map(f: ValueRaw => ValueRaw): ValueRaw = + f(copy(left = f(left), right = f(right))) + + override def varNames: Set[String] = left.varNames ++ right.varNames + + override def renameVars(map: Map[String, String]): ValueRaw = + copy(left = left.renameVars(map), right = right.renameVars(map)) +} + +object ApplyBinaryOpRaw { + + enum Op { + case And + case Or + } +} + +case class ApplyUnaryOpRaw( + op: ApplyUnaryOpRaw.Op, + value: ValueRaw +) extends ValueRaw { + + // Only boolean operations are supported for now + override def baseType: Type = ScalarType.bool + + override def map(f: ValueRaw => ValueRaw): ValueRaw = + f(copy(value = f(value))) + + override def varNames: Set[String] = value.varNames + + override def renameVars(map: Map[String, String]): ValueRaw = + copy(value = value.renameVars(map)) +} + +object ApplyUnaryOpRaw { + + enum Op { + case Not + } +} + case class CallArrowRaw( // TODO: ability should hold a type, not name ability: Option[String], diff --git a/model/src/main/scala/aqua/model/ValueModel.scala b/model/src/main/scala/aqua/model/ValueModel.scala index 547a0fccd..b904853b9 100644 --- a/model/src/main/scala/aqua/model/ValueModel.scala +++ b/model/src/main/scala/aqua/model/ValueModel.scala @@ -62,6 +62,19 @@ case class LiteralModel(value: String, `type`: Type) extends ValueModel { object LiteralModel { + /** + * Used to match bool literals in pattern matching + */ + object Bool { + + def unapply(lm: LiteralModel): Option[Boolean] = + lm match { + case LiteralModel("true", ScalarType.bool | LiteralType.bool) => true.some + case LiteralModel("false", ScalarType.bool | LiteralType.bool) => false.some + case _ => none + } + } + // AquaVM will return empty string for // %last_error%.$.error_code if there is no %last_error% val emptyErrorCode = quote("") @@ -71,6 +84,8 @@ object LiteralModel { def quote(str: String): LiteralModel = LiteralModel(s"\"$str\"", LiteralType.string) def number(n: Int): LiteralModel = LiteralModel(n.toString, LiteralType.forInt(n)) + + def bool(b: Boolean): LiteralModel = LiteralModel(b.toString.toLowerCase, LiteralType.bool) } sealed trait PropertyModel { diff --git a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala index d9deb344b..de36ad730 100644 --- a/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala +++ b/parser/src/main/scala/aqua/parser/lexer/ValueToken.scala @@ -3,17 +3,19 @@ package aqua.parser.lexer import aqua.parser.Expr import aqua.parser.head.FilenameExpr import aqua.parser.lexer.Token.* -import aqua.parser.lexer.ValueToken.{initPeerId, literal} import aqua.parser.lift.LiftParser import aqua.parser.lift.LiftParser.* import aqua.types.LiteralType +import aqua.parser.lift.Span +import aqua.parser.lift.Span.{P0ToSpan, PToSpan, S} + import cats.parse.{Numbers, Parser as P, Parser0 as P0} import cats.syntax.comonad.* import cats.syntax.functor.* import cats.{~>, Comonad, Functor} import cats.data.{NonEmptyList, NonEmptyMap} -import aqua.parser.lift.Span -import aqua.parser.lift.Span.{P0ToSpan, PToSpan, S} +import cats.syntax.foldable.* +import cats.arrow.FunctionK sealed trait ValueToken[F[_]] extends Token[F] { def mapK[K[_]: Comonad](fk: F ~> K): ValueToken[K] @@ -156,29 +158,60 @@ case class InfixToken[F[_]: Comonad]( object InfixToken { - import ValueToken._ + enum BoolOp(val symbol: String): + case Or extends BoolOp("||") + case And extends BoolOp("&&") + + enum MathOp(val symbol: String): + case Pow extends MathOp("**") + case Mul extends MathOp("*") + case Div extends MathOp("/") + case Rem extends MathOp("%") + case Add extends MathOp("+") + case Sub extends MathOp("-") + + enum CmpOp(val symbol: String): + case Gt extends CmpOp(">") + case Gte extends CmpOp(">=") + case Lt extends CmpOp("<") + case Lte extends CmpOp("<=") enum Op(val symbol: String): - case Pow extends Op("**") - case Mul extends Op("*") - case Div extends Op("/") - case Rem extends Op("%") - case Add extends Op("+") - case Sub extends Op("-") - case Gt extends Op(">") - case Gte extends Op(">=") - case Lt extends Op("<") - case Lte extends Op("<=") + /** + * Scala3 does not support nested enums with fields + * so this type acrobatics is used to enable exhaustive matching check + */ + case Math(mathOp: MathOp) extends Op(mathOp.symbol) + case Cmp(cmpOp: CmpOp) extends Op(cmpOp.symbol) + case Bool(boolOp: BoolOp) extends Op(boolOp.symbol) def p: P[Unit] = P.string(symbol) object Op { - val math: List[Op] = List(Pow, Mul, Div, Rem, Add, Sub) - val compare: List[Op] = List(Gt, Gte, Lt, Lte) + val Pow = Math(MathOp.Pow) + val Mul = Math(MathOp.Mul) + val Div = Math(MathOp.Div) + val Rem = Math(MathOp.Rem) + val Add = Math(MathOp.Add) + val Sub = Math(MathOp.Sub) + + val math = MathOp.values.map(Math(_)).toList + + val Gt = Cmp(CmpOp.Gt) + val Gte = Cmp(CmpOp.Gte) + val Lt = Cmp(CmpOp.Lt) + val Lte = Cmp(CmpOp.Lte) + + val cmp = CmpOp.values.map(Cmp(_)).toList + + val And = Bool(BoolOp.And) + val Or = Bool(BoolOp.Or) + + val bool = BoolOp.values.map(Bool(_)).toList } private def opsParser(ops: List[Op]): P[(Span, Op)] = - P.oneOf(ops.map(op => op.p.lift.map(s => s.as(op)))) + P.oneOf(ops.map(op => op.p.lift.map(_.as(op)))) // Parse left-associative operations `basic (OP basic)*`. // We use this form to avoid left recursion. @@ -209,26 +242,8 @@ object InfixToken { vt } - def brackets(basic: P[ValueToken[Span.S]]): P[ValueToken[Span.S]] = - basic.between(`(`, `)`).backtrack - - // One element of math expression - val atom: P[ValueToken[S]] = P.oneOf( - literal.backtrack :: - initPeerId.backtrack :: - P.defer( - CollectionToken.collection - ) :: - P.defer(NamedValueToken.dataValue).backtrack :: - P.defer(CallArrowToken.callArrow).backtrack :: - P.defer(abProperty).backtrack :: - P.defer(brackets(InfixToken.mathExpr)).backtrack :: - varProperty :: - Nil - ) - private val pow: P[ValueToken[Span.S]] = - infixParserRight(atom, Op.Pow :: Nil) + infixParserRight(P.defer(ValueToken.atom), Op.Pow :: Nil) private val mult: P[ValueToken[Span.S]] = infixParserLeft(pow, Op.Mul :: Op.Div :: Op.Rem :: Nil) @@ -245,6 +260,12 @@ object InfixToken { Op.Gte :: Op.Lte :: Op.Gt :: Op.Lt :: Nil ) + private val and: P[ValueToken[Span.S]] = + infixParserLeft(compare, Op.And :: Nil) + + private val or: P[ValueToken[Span.S]] = + infixParserLeft(and, Op.Or :: Nil) + /** * The math expression parser. * @@ -291,9 +312,17 @@ object InfixToken { * * The grammar below expresses the operator precedence and associativity we expect from math expressions: * - * -- Comparison is the entry point because it has the lowest priority. + * -- Logical OR is the entry point because it has the lowest priority. * mathExpr - * -> cmpExpr + * -> orExpr + * + * -- Logical OR is left associative. + * orExpr + * -> andExpr OR_OP andExpr + * + * -- Logical AND is left associative. + * andExpr + * -> cmpExpr AND_OP cmpExpr * * -- Comparison isn't an associative operation so it's not a recursive definition. * cmpExpr @@ -326,19 +355,51 @@ object InfixToken { * | ... * | ( mathExpr ) */ - val mathExpr: P[ValueToken[Span.S]] = compare + val value: P[ValueToken[Span.S]] = or +} + +case class PrefixToken[F[_]: Comonad]( + operand: ValueToken[F], + prefix: F[PrefixToken.Op] +) extends ValueToken[F] { + + def op: PrefixToken.Op = prefix.extract + override def as[T](v: T): F[T] = prefix.as(v) + + override def mapK[K[_]: Comonad](fk: FunctionK[F, K]): ValueToken[K] = + copy(operand.mapK(fk), fk(prefix)) +} + +object PrefixToken { + + enum Op(val symbol: String) { + case Not extends Op("!") + + def p: P[Unit] = P.string(symbol) + } + + private def parseOps(ops: List[Op]): P[S[Op]] = + P.oneOf(ops.map(op => op.p.lift.map(_.as(op)))) + + private def parsePrefix(basic: P[ValueToken[S]], ops: List[Op]) = + (parseOps(ops).surroundedBy(`/s*`) ~ basic).map { case (op, vt) => + PrefixToken(vt, op) + } + + val value: P[ValueToken[Span.S]] = + parsePrefix(P.defer(ValueToken.atom), Op.Not :: Nil) } object ValueToken { val varProperty: P[VarToken[Span.S]] = (Name.dotted ~ PropertyOp.ops.?).map { case (n, l) ⇒ - VarToken(n, l.fold[List[PropertyOp[Span.S]]](Nil)(_.toList)) + VarToken(n, l.foldMap(_.toList)) } val abProperty: P[VarToken[Span.S]] = (Name.cl ~ PropertyOp.ops.?).map { case (n, l) ⇒ - VarToken(n, l.fold[List[PropertyOp[Span.S]]](Nil)(_.toList)) + VarToken(n, l.foldMap(_.toList)) } val bool: P[LiteralToken[Span.S]] = @@ -375,8 +436,25 @@ object ValueToken { val literal: P[LiteralToken[Span.S]] = P.oneOf(bool.backtrack :: float.backtrack :: num.backtrack :: string :: Nil) + private def brackets(basic: P[ValueToken[Span.S]]): P[ValueToken[Span.S]] = + basic.between(`(`, `)`).backtrack + + // Basic element of math expression + val atom: P[ValueToken[S]] = P.oneOf( + literal.backtrack :: + initPeerId.backtrack :: + P.defer(CollectionToken.collection).backtrack :: + P.defer(NamedValueToken.dataValue).backtrack :: + P.defer(CallArrowToken.callArrow).backtrack :: + P.defer(abProperty).backtrack :: + P.defer(PrefixToken.value).backtrack :: + P.defer(brackets(InfixToken.value)).backtrack :: + varProperty :: + Nil + ) + // One of entry points for parsing the whole math expression val `value`: P[ValueToken[Span.S]] = - P.defer(InfixToken.mathExpr) + P.defer(InfixToken.value) } diff --git a/parser/src/test/scala/aqua/parser/InfixTokenSpec.scala b/parser/src/test/scala/aqua/parser/ValueTokenComplexSpec.scala similarity index 55% rename from parser/src/test/scala/aqua/parser/InfixTokenSpec.scala rename to parser/src/test/scala/aqua/parser/ValueTokenComplexSpec.scala index 310aee473..d10d6df22 100644 --- a/parser/src/test/scala/aqua/parser/InfixTokenSpec.scala +++ b/parser/src/test/scala/aqua/parser/ValueTokenComplexSpec.scala @@ -2,18 +2,22 @@ package aqua.parser import aqua.AquaSpec import aqua.parser.expr.func.IfExpr -import aqua.parser.lexer.InfixToken.Op -import aqua.parser.lexer.{EqOp, InfixToken, LiteralToken, ValueToken} +import aqua.parser.lexer.InfixToken.Op as InfixOp +import aqua.parser.lexer.PrefixToken.Op as PrefixOp +import aqua.parser.lexer.* import aqua.parser.lexer.InfixToken.Op.* +import aqua.parser.lexer.PrefixToken.Op.* import aqua.parser.lift.Span import aqua.types.LiteralType + import cats.syntax.comonad.* import cats.{~>, Comonad, Id} +import cats.parse.{Numbers, Parser as P, Parser0 as P0} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import cats.parse.{Numbers, Parser as P, Parser0 as P0} +import org.scalatest.Inside -class InfixTokenSpec extends AnyFlatSpec with Matchers with AquaSpec { +class ValueTokenComplexSpec extends AnyFlatSpec with Matchers with Inside with AquaSpec { def spanToId: Span.S ~> Id = new (Span.S ~> Id) { @@ -24,9 +28,20 @@ class InfixTokenSpec extends AnyFlatSpec with Matchers with AquaSpec { import AquaSpec._ + private def variable(name: String): ValueToken[Id] = + VarToken(Name(name), Nil) + + private def func(name: String, args: List[ValueToken[Id]]): ValueToken[Id] = + CallArrowToken(None, Name(name), args) + private def literal(n: Int): ValueToken[Id] = toNumber(n) - private def infixToken(left: ValueToken[Id], right: ValueToken[Id], op: Op) = + private def literalBool(b: Boolean): ValueToken[Id] = toBool(b) + + private def prefixToken(value: ValueToken[Id], op: PrefixOp) = + PrefixToken[Id](value, op) + + private def infixToken(left: ValueToken[Id], right: ValueToken[Id], op: InfixOp) = InfixToken[Id](left, right, op) private def mul(left: ValueToken[Id], right: ValueToken[Id]): ValueToken[Id] = @@ -257,9 +272,14 @@ class InfixTokenSpec extends AnyFlatSpec with Matchers with AquaSpec { } "complex cmp math expression " should "be parsed" in { - val test = (op: Op) => { - val vt = - ValueToken.`value`.parseAll(s"(1 + 2) ** 3 ${op.symbol} 4 - 5 * 6").right.get.mapK(spanToId) + val test = (op: InfixOp) => { + val vt = ValueToken.`value` + .parseAll( + s"(1 + 2) ** 3 ${op.symbol} 4 - 5 * 6" + ) + .right + .get + .mapK(spanToId) val left = pow(add(literal(1), literal(2)), literal(3)) val right = sub(literal(4), mul(literal(5), literal(6))) val exp = infixToken(left, right, op) @@ -273,4 +293,220 @@ class InfixTokenSpec extends AnyFlatSpec with Matchers with AquaSpec { val vt = ValueToken.`value`.parseAll("(1 > 3)").right.get.mapK(spanToId) vt shouldBe InfixToken(literal(1), literal(3), Gt) } + + "simple logical expression" should "be parsed" in { + val vtAnd = ValueToken.`value`.parseAll("true && false").map(_.mapK(spanToId)) + + inside(vtAnd) { case Right(vt) => + vt shouldBe infixToken(literalBool(true), literalBool(false), And) + } + + val vtOr = ValueToken.`value`.parseAll("false || true").map(_.mapK(spanToId)) + + inside(vtOr) { case Right(vt) => + vt shouldBe infixToken(literalBool(false), literalBool(true), Or) + } + + val vtAndOr = ValueToken.`value`.parseAll("false && true || false").map(_.mapK(spanToId)) + + inside(vtAndOr) { case Right(vt) => + vt shouldBe infixToken( + infixToken(literalBool(false), literalBool(true), And), + literalBool(false), + Or + ) + } + + val vtOrAnd = ValueToken.`value`.parseAll("false || true && false").map(_.mapK(spanToId)) + + inside(vtOrAnd) { case Right(vt) => + vt shouldBe infixToken( + literalBool(false), + infixToken(literalBool(true), literalBool(false), And), + Or + ) + } + + val vtOrNotAnd = ValueToken.`value`.parseAll("false || !true && false").map(_.mapK(spanToId)) + + inside(vtOrNotAnd) { case Right(vt) => + vt shouldBe infixToken( + literalBool(false), + infixToken( + prefixToken(literalBool(true), Not), + literalBool(false), + And + ), + Or + ) + } + } + + "logical expression with brackets" should "be parsed" in { + val vtAndOr = ValueToken.`value`.parseAll("false && (true || false)").map(_.mapK(spanToId)) + + inside(vtAndOr) { case Right(vt) => + vt shouldBe infixToken( + literalBool(false), + infixToken(literalBool(true), literalBool(false), Or), + And + ) + } + + val vtOrAnd = ValueToken.`value`.parseAll("(false || true) && false").map(_.mapK(spanToId)) + + inside(vtOrAnd) { case Right(vt) => + vt shouldBe infixToken( + infixToken(literalBool(false), literalBool(true), Or), + literalBool(false), + And + ) + } + + val vtNotAndOr = ValueToken.`value`.parseAll("!false && (true || false)").map(_.mapK(spanToId)) + + inside(vtNotAndOr) { case Right(vt) => + vt shouldBe infixToken( + prefixToken(literalBool(false), Not), + infixToken(literalBool(true), literalBool(false), Or), + And + ) + } + + val vtNotOrAnd = ValueToken.`value`.parseAll("!(false || true) && false").map(_.mapK(spanToId)) + + inside(vtNotOrAnd) { case Right(vt) => + vt shouldBe infixToken( + prefixToken( + infixToken(literalBool(false), literalBool(true), Or), + Not + ), + literalBool(false), + And + ) + } + } + + "logical expression with math expressions" should "be parsed" in { + val vt1 = ValueToken.`value`.parseAll("1 < 2 + 3 || 3 % 2 > 1").map(_.mapK(spanToId)) + + inside(vt1) { case Right(vt) => + vt shouldBe infixToken( + infixToken( + literal(1), + infixToken(literal(2), literal(3), Add), + Lt + ), + infixToken( + infixToken(literal(3), literal(2), Rem), + literal(1), + Gt + ), + Or + ) + } + + val vt2 = ValueToken.`value`.parseAll("1 - 2 > 3 && 3 ** 2 <= 1").map(_.mapK(spanToId)) + + inside(vt2) { case Right(vt) => + vt shouldBe infixToken( + infixToken( + infixToken(literal(1), literal(2), Sub), + literal(3), + Gt + ), + infixToken( + infixToken(literal(3), literal(2), Pow), + literal(1), + Lte + ), + And + ) + } + + val vt3 = ValueToken.`value`.parseAll("!(1 - 2 > 3) && 3 ** 2 <= 1").map(_.mapK(spanToId)) + + inside(vt3) { case Right(vt) => + vt shouldBe infixToken( + prefixToken( + infixToken( + infixToken(literal(1), literal(2), Sub), + literal(3), + Gt + ), + Not + ), + infixToken( + infixToken(literal(3), literal(2), Pow), + literal(1), + Lte + ), + And + ) + } + } + + "logical expression with function calls and variables" should "be parsed" in { + val vt1 = ValueToken.`value`.parseAll("foo() || a + 1 < 2 && b").map(_.mapK(spanToId)) + + inside(vt1) { case Right(vt) => + vt shouldBe infixToken( + func("foo", Nil), + infixToken( + infixToken( + infixToken( + variable("a"), + literal(1), + Add + ), + literal(2), + Lt + ), + variable("b"), + And + ), + Or + ) + } + + val vt2 = ValueToken.`value`.parseAll("bar(a) < 2 && (b > 5 || c)").map(_.mapK(spanToId)) + + inside(vt2) { case Right(vt) => + vt shouldBe infixToken( + infixToken(func("bar", List(variable("a"))), literal(2), Lt), + infixToken( + infixToken( + variable("b"), + literal(5), + Gt + ), + variable("c"), + Or + ), + And + ) + } + + val vt3 = ValueToken.`value`.parseAll("!baz(a) && (!(b > 4) || !c)").map(_.mapK(spanToId)) + + inside(vt3) { case Right(vt) => + vt shouldBe infixToken( + prefixToken(func("baz", List(variable("a"))), Not), + infixToken( + prefixToken( + infixToken( + variable("b"), + literal(4), + Gt + ), + Not + ), + prefixToken(variable("c"), Not), + Or + ), + And + ) + } + } + } diff --git a/parser/src/test/scala/aqua/parser/lexer/VarLambdaSpec.scala b/parser/src/test/scala/aqua/parser/lexer/VarLambdaSpec.scala index f23e03f7e..42c2eb402 100644 --- a/parser/src/test/scala/aqua/parser/lexer/VarLambdaSpec.scala +++ b/parser/src/test/scala/aqua/parser/lexer/VarLambdaSpec.scala @@ -32,7 +32,7 @@ class VarLambdaSpec extends AnyFlatSpec with Matchers with EitherValues { } "var lambda in value" should "parse" in { - val opsP = (s: String) => InfixToken.atom.parseAll(s).value.mapK(spanToId) + val opsP = (s: String) => ValueToken.atom.parseAll(s).value.mapK(spanToId) opsP("some_val") should be(VarToken[Id](Name[Id]("some_val"))) opsP("SomeClass.SOME_CONST") should be(VarToken[Id](Name[Id]("SomeClass.SOME_CONST"))) } diff --git a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala index d5ddca72c..bb2d7ef9a 100644 --- a/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala +++ b/semantics/src/main/scala/aqua/semantics/rules/ValuesAlgebra.scala @@ -1,12 +1,14 @@ package aqua.semantics.rules import aqua.parser.lexer.* -import aqua.parser.lexer.InfixToken.Op +import aqua.parser.lexer.InfixToken.{BoolOp, CmpOp, MathOp, Op as InfOp} +import aqua.parser.lexer.PrefixToken.Op as PrefOp import aqua.raw.value.* import aqua.semantics.rules.abilities.AbilitiesAlgebra import aqua.semantics.rules.names.NamesAlgebra import aqua.semantics.rules.types.TypesAlgebra import aqua.types.* + import cats.Monad import cats.data.Chain import cats.syntax.applicative.* @@ -17,6 +19,7 @@ import cats.syntax.traverse.* import cats.syntax.option.* import cats.instances.list.* import cats.data.{NonEmptyList, NonEmptyMap} +import cats.data.OptionT import scribe.Logging import scala.collection.immutable.SortedMap @@ -174,82 +177,126 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](implicit case ca: CallArrowToken[S] => callArrowToRaw(ca).map(_.widen[ValueRaw]) + case pr @ PrefixToken(operand, _) => + (for { + raw <- OptionT( + valueToRaw(operand) + ) + typeCheck <- OptionT.liftF( + T.ensureTypeMatches(operand, ScalarType.bool, raw.`type`) + ) + result <- OptionT.when(typeCheck)( + ApplyUnaryOpRaw( + op = pr.op match { + case PrefOp.Not => ApplyUnaryOpRaw.Op.Not + }, + value = raw + ) + ) + } yield result).value + case it @ InfixToken(l, r, _) => (valueToRaw(l), valueToRaw(r)).flatMapN { case (Some(leftRaw), Some(rightRaw)) => val lType = leftRaw.`type` val rType = rightRaw.`type` lazy val uType = lType `∪` rType - val hasFloat = List(lType, rType).exists( - _ acceptsValueOf LiteralType.float - ) - // See https://github.com/fluencelabs/aqua-lib/blob/main/math.aqua - val (id, fn) = it.op match { - case Op.Add => ("math", "add") - case Op.Sub => ("math", "sub") - case Op.Mul if hasFloat => ("math", "fmul") - case Op.Mul => ("math", "mul") - case Op.Div => ("math", "div") - case Op.Rem => ("math", "rem") - case Op.Pow => ("math", "pow") - case Op.Gt => ("cmp", "gt") - case Op.Gte => ("cmp", "gte") - case Op.Lt => ("cmp", "lt") - case Op.Lte => ("cmp", "lte") - } + it.op match { + case InfOp.Bool(bop) => + for { + leftChecked <- T.ensureTypeMatches(l, ScalarType.bool, lType) + rightChecked <- T.ensureTypeMatches(r, ScalarType.bool, rType) + } yield Option.when( + leftChecked && rightChecked + )( + ApplyBinaryOpRaw( + op = bop match { + case BoolOp.And => ApplyBinaryOpRaw.Op.And + case BoolOp.Or => ApplyBinaryOpRaw.Op.Or + }, + left = leftRaw, + right = rightRaw + ) + ) + case op @ (InfOp.Math(_) | InfOp.Cmp(_)) => + // Some type acrobatics to make + // compiler check exhaustive pattern matching + val iop = op match { + case InfOp.Math(op) => op + case InfOp.Cmp(op) => op + } - /* - * If `uType == TopType`, it means that we don't - * have type big enough to hold the result of operation. - * e.g. We will use `i64` for result of `i32 * u64` - * TODO: Handle this more gracefully - * (use warning system when it is implemented) - */ - def uTypeBounded = if (uType == TopType) { - val bounded = ScalarType.i64 - logger.warn( - s"Result type of ($lType ${it.op} $rType) is $TopType, " + - s"using $bounded instead" - ) - bounded - } else uType + val hasFloat = List(lType, rType).exists( + _ acceptsValueOf LiteralType.float + ) - // Expected type sets of left and right operands, result type - val (leftExp, rightExp, resType) = it.op match { - case Op.Add | Op.Sub | Op.Div | Op.Rem => - (ScalarType.integer, ScalarType.integer, uTypeBounded) - case Op.Pow => - (ScalarType.integer, ScalarType.unsigned, uTypeBounded) - case Op.Mul if hasFloat => - (ScalarType.float, ScalarType.float, ScalarType.i64) - case Op.Mul => - (ScalarType.integer, ScalarType.integer, uTypeBounded) - case Op.Gt | Op.Lt | Op.Gte | Op.Lte => - (ScalarType.integer, ScalarType.integer, ScalarType.bool) - } + // See https://github.com/fluencelabs/aqua-lib/blob/main/math.aqua + val (id, fn) = iop match { + case MathOp.Add => ("math", "add") + case MathOp.Sub => ("math", "sub") + case MathOp.Mul if hasFloat => ("math", "fmul") + case MathOp.Mul => ("math", "mul") + case MathOp.Div => ("math", "div") + case MathOp.Rem => ("math", "rem") + case MathOp.Pow => ("math", "pow") + case CmpOp.Gt => ("cmp", "gt") + case CmpOp.Gte => ("cmp", "gte") + case CmpOp.Lt => ("cmp", "lt") + case CmpOp.Lte => ("cmp", "lte") + } - for { - leftChecked <- T.ensureTypeOneOf(l, leftExp, lType) - rightChecked <- T.ensureTypeOneOf(r, rightExp, rType) - } yield Option.when( - leftChecked.isDefined && rightChecked.isDefined - )( - CallArrowRaw( - ability = Some(id), - name = fn, - arguments = leftRaw :: rightRaw :: Nil, - baseType = ArrowType( - ProductType(lType :: rType :: Nil), - ProductType(resType :: Nil) - ), - serviceId = Some(LiteralRaw.quote(id)) - ) - ) + /* + * If `uType == TopType`, it means that we don't + * have type big enough to hold the result of operation. + * e.g. We will use `i64` for result of `i32 * u64` + * TODO: Handle this more gracefully + * (use warning system when it is implemented) + */ + def uTypeBounded = if (uType == TopType) { + val bounded = ScalarType.i64 + logger.warn( + s"Result type of ($lType ${it.op} $rType) is $TopType, " + + s"using $bounded instead" + ) + bounded + } else uType + + // Expected type sets of left and right operands, result type + val (leftExp, rightExp, resType) = iop match { + case MathOp.Add | MathOp.Sub | MathOp.Div | MathOp.Rem => + (ScalarType.integer, ScalarType.integer, uTypeBounded) + case MathOp.Pow => + (ScalarType.integer, ScalarType.unsigned, uTypeBounded) + case MathOp.Mul if hasFloat => + (ScalarType.float, ScalarType.float, ScalarType.i64) + case MathOp.Mul => + (ScalarType.integer, ScalarType.integer, uTypeBounded) + case CmpOp.Gt | CmpOp.Lt | CmpOp.Gte | CmpOp.Lte => + (ScalarType.integer, ScalarType.integer, ScalarType.bool) + } + for { + leftChecked <- T.ensureTypeOneOf(l, leftExp, lType) + rightChecked <- T.ensureTypeOneOf(r, rightExp, rType) + } yield Option.when( + leftChecked.isDefined && rightChecked.isDefined + )( + CallArrowRaw( + ability = Some(id), + name = fn, + arguments = leftRaw :: rightRaw :: Nil, + baseType = ArrowType( + ProductType(lType :: rType :: Nil), + ProductType(resType :: Nil) + ), + serviceId = Some(LiteralRaw.quote(id)) + ) + ) + + } case _ => None.pure[Alg] } - } // Generate CallArrowRaw for arrow in ability diff --git a/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala b/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala index b01814503..03fbcbfc8 100644 --- a/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala +++ b/semantics/src/test/scala/aqua/semantics/ValuesAlgebraSpec.scala @@ -14,10 +14,10 @@ import aqua.semantics.rules.definitions.DefinitionsInterpreter import aqua.semantics.rules.types.TypesInterpreter import aqua.semantics.rules.locations.LocationsAlgebra import aqua.semantics.rules.locations.DummyLocationsInterpreter -import aqua.raw.value.LiteralRaw +import aqua.raw.value.{ApplyBinaryOpRaw, LiteralRaw} import aqua.raw.RawContext import aqua.types.{LiteralType, ScalarType, TopType, Type} -import aqua.parser.lexer.{InfixToken, LiteralToken, Name, ValueToken, VarToken} +import aqua.parser.lexer.{InfixToken, LiteralToken, Name, PrefixToken, ValueToken, VarToken} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -27,6 +27,7 @@ import cats.data.State import cats.syntax.functor.* import cats.syntax.comonad.* import monocle.syntax.all.* +import aqua.raw.value.ApplyUnaryOpRaw class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside { @@ -203,4 +204,157 @@ class ValuesAlgebraSpec extends AnyFlatSpec with Matchers with Inside { } } } + + it should "handle ||, && on bool values" in { + val types = List(LiteralType.bool, ScalarType.bool) + + allPairs(types).foreach { case (lt, rt) => + InfixToken.BoolOp.values.foreach { op => + val left = lt match { + case lt: LiteralType => + literal("true", lt) + case _ => + variable("left") + } + val right = rt match { + case rt: LiteralType => + literal("false", rt) + case _ => + variable("right") + } + + val alg = algebra() + + val state = genState( + vars = ( + List("left" -> lt).filter(_ => lt != LiteralType.bool) ++ + List("right" -> rt).filter(_ => rt != LiteralType.bool) + ).toMap + ) + + val token = InfixToken[Id](left, right, InfixToken.Op.Bool(op)) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + inside(res) { case Some(ApplyBinaryOpRaw(bop, _, _)) => + bop shouldBe (op match { + case InfixToken.BoolOp.And => ApplyBinaryOpRaw.Op.And + case InfixToken.BoolOp.Or => ApplyBinaryOpRaw.Op.Or + }) + } + } + } + } + + it should "handle ! on bool values" in { + val types = List(LiteralType.bool, ScalarType.bool) + + types.foreach { t => + PrefixToken.Op.values.foreach { op => + val value = t match { + case lt: LiteralType => + literal("true", lt) + case _ => + variable("val") + } + + val alg = algebra() + + val state = genState( + vars = List("val" -> t).filter(_ => t != LiteralType.bool).toMap + ) + + val token = PrefixToken[Id](value, op) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + inside(res) { case Some(ApplyUnaryOpRaw(uop, _)) => + uop shouldBe (op match { + case PrefixToken.Op.Not => ApplyUnaryOpRaw.Op.Not + }) + } + } + } + } + + it should "check type of logical operands (binary)" in { + val types = List(LiteralType.bool, ScalarType.bool).flatMap(t => + List(t -> ScalarType.i8, ScalarType.i8 -> t) + ) + + types.foreach { case (lt, rt) => + InfixToken.BoolOp.values.foreach { op => + val left = lt match { + case lt: LiteralType => + literal("true", lt) + case _ => + variable("left") + } + val right = rt match { + case rt: LiteralType => + literal("false", rt) + case _ => + variable("right") + } + + val alg = algebra() + + val state = genState( + vars = ( + List("left" -> lt).filter(_ => lt != LiteralType.bool) ++ + List("right" -> rt).filter(_ => rt != LiteralType.bool) + ).toMap + ) + + val token = InfixToken[Id](left, right, InfixToken.Op.Bool(op)) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + res shouldBe None + st.errors.exists(_.isInstanceOf[RulesViolated[Id]]) shouldBe true + } + } + } + + it should "check type of logical operand (unary)" in { + val types = ScalarType.integer.toList :+ LiteralType.unsigned + + types.foreach { t => + PrefixToken.Op.values.foreach { op => + val value = t match { + case lt: LiteralType => + literal("42", lt) + case _ => + variable("val") + } + + val alg = algebra() + + val state = genState( + vars = Map( + "value" -> t + ).filter(_ => t != LiteralType.unsigned) + ) + + val token = PrefixToken[Id](value, op) + + val (st, res) = alg + .valueToRaw(token) + .run(state) + .value + + res shouldBe None + st.errors.exists(_.isInstanceOf[RulesViolated[Id]]) shouldBe true + } + } + } }