diff --git a/analysis/src/main/scala/org/polystat/odin/analysis/EOOdinAnalyzer.scala b/analysis/src/main/scala/org/polystat/odin/analysis/EOOdinAnalyzer.scala index 799b10ee..127c5438 100644 --- a/analysis/src/main/scala/org/polystat/odin/analysis/EOOdinAnalyzer.scala +++ b/analysis/src/main/scala/org/polystat/odin/analysis/EOOdinAnalyzer.scala @@ -1,7 +1,7 @@ package org.polystat.odin.analysis import cats._ -import cats.data.EitherNel +import cats.data.{EitherNel, NonEmptyList} import cats.effect.Sync import cats.syntax.all._ import fs2.Stream @@ -9,6 +9,7 @@ import org.polystat.odin.analysis.EOOdinAnalyzer._ import org.polystat.odin.analysis.liskov.Analyzer import org.polystat.odin.analysis.mutualrec.advanced.Analyzer.analyzeAst import org.polystat.odin.analysis.mutualrec.naive.findMutualRecursionFromAst +import org.polystat.odin.analysis.stateaccess.DetectStateAccess import org.polystat.odin.analysis.utils.inlining.Inliner import org.polystat.odin.core.ast.EOProg import org.polystat.odin.core.ast.astparams.EOExprOnly @@ -29,9 +30,9 @@ object EOOdinAnalyzer { final case class Ok(override val ruleId: String) extends OdinAnalysisResult - final case class DefectDetected( + final case class DefectsDetected( override val ruleId: String, - message: String + messages: NonEmptyList[String], ) extends OdinAnalysisResult final case class AnalyzerFailure( @@ -42,10 +43,10 @@ object EOOdinAnalyzer { def fromErrors( analyzer: String )(errors: List[String]): OdinAnalysisResult = - if (errors.isEmpty) - Ok(analyzer) - else - DefectDetected(analyzer, errors.mkString("\n")) + errors match { + case e :: es => DefectsDetected(analyzer, NonEmptyList(e, es)) + case Nil => Ok(analyzer) + } def fromThrow[F[_]: ApplicativeThrow]( analyzer: String @@ -99,7 +100,10 @@ object EOOdinAnalyzer { }) } yield odinError - stream.compile.toList.map(OdinAnalysisResult.fromErrors(name)) + stream + .compile + .toList + .map(OdinAnalysisResult.fromErrors(name)) } } @@ -127,14 +131,6 @@ object EOOdinAnalyzer { override val name: String = "Unjustified Assumption" - private def toThrow[A](eitherNel: EitherNel[String, A]): F[A] = { - MonadThrow[F].fromEither( - eitherNel - .leftMap(_.mkString_(util.Properties.lineSeparator)) - .leftMap(new Exception(_)) - ) - } - override def analyze( ast: EOProg[EOExprOnly] ): F[OdinAnalysisResult] = @@ -170,12 +166,33 @@ object EOOdinAnalyzer { } - def analyzeSourceCode[EORepr, F[_]: Monad]( + def directStateAccessAnalyzer[F[_]: MonadThrow]: ASTAnalyzer[F] = + new ASTAnalyzer[F] { + + override val name: String = "Direct Access to Superclass State" + + override def analyze( + ast: EOProg[EOExprOnly] + ): F[OdinAnalysisResult] = + OdinAnalysisResult.fromThrow[F](name) { + for { + tmpTree <- + toThrow(Inliner.createObjectTree(ast)) + tree <- toThrow(Inliner.resolveParents(tmpTree)) + errors <- + toThrow(DetectStateAccess.analyze(tree)) + } yield errors + } + + } + + def analyzeSourceCode[EORepr, F[_]]( analyzer: ASTAnalyzer[F] )( eoRepr: EORepr )(implicit - parser: EoParser[EORepr, F, EOProg[EOExprOnly]] + m: Monad[F], + parser: EoParser[EORepr, F, EOProg[EOExprOnly]], ): F[OdinAnalysisResult] = for { programAst <- parser.parse(eoRepr) mutualRecursionErrors <- diff --git a/analysis/src/main/scala/org/polystat/odin/analysis/stateaccess/DetectStateAccess.scala b/analysis/src/main/scala/org/polystat/odin/analysis/stateaccess/DetectStateAccess.scala new file mode 100644 index 00000000..2e4a7f2b --- /dev/null +++ b/analysis/src/main/scala/org/polystat/odin/analysis/stateaccess/DetectStateAccess.scala @@ -0,0 +1,178 @@ +package org.polystat.odin.analysis.stateaccess + +import cats.data.EitherNel +import higherkindness.droste.data.Fix +import org.polystat.odin.analysis.utils.Abstract +import org.polystat.odin.analysis.utils.inlining._ +import org.polystat.odin.core.ast._ +import org.polystat.odin.core.ast.astparams.EOExprOnly + +import scala.annotation.tailrec + +object DetectStateAccess { + + type ObjInfo = ObjectInfo[ParentInfo[MethodInfo, ObjectInfo], MethodInfo] + + case class State( + containerName: String, + statePath: List[String], + states: Vector[EONamedBnd] + ) + + case class StateChange( + method: EONamedBnd, + state: EONamedBnd, + statePath: List[String] + ) + + def collectNestedStates(mainParent: String)( + subTree: Inliner.CompleteObjectTree + ): Vector[State] = { + val currentLvlStateNames = subTree + .info + .bnds + .collect { + case BndItself( + EOBndExpr( + bndName, + EOSimpleAppWithLocator("memory" | "cage", _) + ) + ) => bndName + } + + Vector( + State(mainParent, subTree.info.fqn.names.tail, currentLvlStateNames) + ) ++ + subTree + .children + .flatMap(t => collectNestedStates(mainParent)(t._2)) + } + + def accumulateParentState(tree: Map[EONamedBnd, Inliner.CompleteObjectTree])( + currentParentLink: Option[ParentInfo[MethodInfo, ObjectInfo]], + existingStates: Vector[EONamedBnd] = Vector() + ): Vector[State] = { + currentParentLink match { + case Some(parentLink) => + val parentObj = parentLink.linkToParent.getOption(tree).get + val currentObjName = parentObj.info.name.name.name + val currentLvlStateNames = parentObj + .info + .bnds + .collect { + case BndItself( + EOBndExpr( + bndName, + EOSimpleAppWithLocator("memory" | "cage", _) + ) + ) if !existingStates.contains(bndName) => + bndName + } + val currentLvlState = + State(currentObjName, List(), currentLvlStateNames) + val nestedStates = parentObj + .children + .flatMap(c => + collectNestedStates(parentObj.info.name.name.name)(c._2) + ) + .toVector + + Vector(currentLvlState) ++ nestedStates ++ + accumulateParentState(tree)( + parentObj.info.parentInfo, + existingStates ++ currentLvlStateNames + ) + + case None => Vector() + } + } + + def getAccessedStates(method: (EONamedBnd, MethodInfo)): List[StateChange] = { + @tailrec + def hasSelfAsSource(dot: EODot[EOExprOnly]): Boolean = { + Fix.un(dot.src) match { + case EOSimpleAppWithLocator("self", x) if x == 0 => true + case innerDot @ EODot(_, _) => hasSelfAsSource(innerDot) + case _ => false + } + } + + def buildDotChain(dot: EODot[EOExprOnly]): List[String] = + Fix.un(dot.src) match { + case EOSimpleAppWithLocator("self", x) if x == 0 => List() + case innerDot @ EODot(_, _) => + buildDotChain(innerDot).appended(innerDot.name) + case _ => List() + } + + val binds = method._2.body.bndAttrs + + def processDot( + innerDot: EODot[Fix[EOExpr]], + state: String + ): List[StateChange] = { + val stateName = EOAnyNameBnd(LazyName(state)) + val containerChain = buildDotChain(innerDot) + + List(StateChange(method._1, stateName, containerChain)) + } + + Abstract.foldAst[List[StateChange]](binds) { + case EOCopy(Fix(dot @ EODot(Fix(innerDot @ EODot(_, state)), _)), _) + if hasSelfAsSource(dot) => + processDot(innerDot, state) + + case dot @ EODot(_, state) if hasSelfAsSource(dot) => + processDot(dot, state) + } + } + + def detectStateAccesses( + tree: Map[EONamedBnd, Inliner.CompleteObjectTree] + )(obj: (EONamedBnd, Inliner.CompleteObjectTree)): List[String] = { + val availableParentStates = + accumulateParentState(tree)(obj._2.info.parentInfo) + val accessedStates = obj._2.info.methods.flatMap(getAccessedStates) + val results = + for { + StateChange(targetMethod, state, accessedStatePath) <- accessedStates + State(baseClass, statePath, changedStates) <- availableParentStates + } yield + if (changedStates.contains(state) && statePath == accessedStatePath) { + val objName = obj._2.info.fqn.names.toList.mkString(".") + val stateName = state.name.name + val method = targetMethod.name.name + val container = statePath.prepended(baseClass).mkString(".") + + List( + f"Method '$method' of object '$objName' directly accesses state '$stateName' of base class '$container'" + ) + } else List() + + results.toList.flatten + } + + def analyze[F[_]]( + originalTree: Map[EONamedBnd, Inliner.CompleteObjectTree] + ): EitherNel[String, List[String]] = { + def helper( + tree: Map[EONamedBnd, Inliner.CompleteObjectTree] + ): List[String] = + tree + .filter(_._2.info.parentInfo.nonEmpty) + .flatMap(detectStateAccesses(originalTree)) + .toList + + def recurse( + tree: Map[EONamedBnd, Inliner.CompleteObjectTree] + ): List[String] = { + val currentRes = helper(tree) + val children = tree.values.map(_.children) + + currentRes ++ children.flatMap(recurse) + } + + Right(recurse(originalTree)) + } + +} diff --git a/analysis/src/test/scala/org/polystat/odin/analysis/DetectStateAccessTests.scala b/analysis/src/test/scala/org/polystat/odin/analysis/DetectStateAccessTests.scala new file mode 100644 index 00000000..4870c4d5 --- /dev/null +++ b/analysis/src/test/scala/org/polystat/odin/analysis/DetectStateAccessTests.scala @@ -0,0 +1,403 @@ +package org.polystat.odin.analysis + +import cats.effect._ +import org.scalatest.wordspec.AnyWordSpec +import org.polystat.odin.parser.EoParser.sourceCodeEoParser +import cats.effect.unsafe.implicits.global +import org.polystat.odin.analysis.EOOdinAnalyzer.directStateAccessAnalyzer +import EOOdinAnalyzer.OdinAnalysisResult._ + +class DetectStateAccessTests extends AnyWordSpec { + case class TestCase(label: String, code: String, expected: List[String]) + + def analyze(code: String): IO[List[String]] = EOOdinAnalyzer + .analyzeSourceCode[String, IO](directStateAccessAnalyzer)(code)( + cats.Monad[IO], + sourceCodeEoParser() + ) + .flatMap { + case Ok(_) => IO.pure(List.empty) + case DefectsDetected(_, errors) => IO.pure(errors.toList) + case AnalyzerFailure(_, e) => IO.raiseError(e) + } + + val testsWithDefect: List[TestCase] = List( + TestCase( + label = "Write to state", + code = """[] > a + | memory > state + | [self new_state] > update_state + | self.state.write new_state > @ + |[] > b + | a > @ + | [self new_state] > change_state_plus_two + | self.state.write (new_state.add 2) > @ + |""".stripMargin, + expected = List( + "Method 'change_state_plus_two' of object 'b' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "Access to state", + code = """[] > base + | memory > state + |[] > b + | base > @ + | [self var] > alter_var + | var.write 10 > @ + | [self] > change_state + | self.alter_var self self.state > @ + |""".stripMargin, + expected = List( + "Method 'change_state' of object 'b' directly accesses state 'state' of base class 'base'" + ) + ), + TestCase( + label = "calculation chain", + code = """[] > test + | [] > a + | memory > state + | [] > b + | a > @ + | [self x] > n + | add. > @ + | x + | mul. + | 100 + | add. + | 100 + | sub. + | 100 + | self.state + |""".stripMargin, + expected = List( + "Method 'n' of object 'test.b' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "read-in-calculation-chain", + code = """[] > test + | [] > a + | memory > state + | [] > b + | a > @ + | [self x] > n + | add. > @ + | self.state + | mul. + | 100 + | add. + | 100 + | sub. + | 100 + | x + |""".stripMargin, + expected = List( + "Method 'n' of object 'test.b' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "read-in-inheritance-chain", + code = """[] > test + | [] > a + | memory > state + | [] > b + | a > @ + | [] > c + | b > @ + | [self x] > n + | self.state.add x > @ + |""".stripMargin, + expected = List( + "Method 'n' of object 'test.c' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "access-read-nested-class-2", + code = """[] > test + | [] > very_outer + | [] > outer + | [] > a + | memory > state + | [] > b + | a > @ + | [self x] > n + | self.state.add x > @ + |""".stripMargin, + expected = List( + "Method 'n' of object 'test.very_outer.outer.b' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "write-through-another-method", + code = """[] > test + | [] > a + | memory > state + | [] > b + | a > @ + | [self x y] > m + | x.write y > @ + | [self y] > n + | self.m self (self.state) y > @ + |""".stripMargin, + expected = List( + "Method 'n' of object 'test.b' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "Access to cage", + code = """[] > a + | cage > state + |[] > second_obj + | a > @ + | [self] > func + | self.state > @ + |""".stripMargin, + expected = List( + "Method 'func' of object 'second_obj' directly accesses state 'state' of base class 'a'" + ) + ), + TestCase( + label = "Access to cage AND memory", + code = """[] > a + | cage > state + | memory > mem + |[] > second_obj + | a > @ + | [self] > func + | self.state > tmp + | self.mem > @ + |""".stripMargin, + expected = List( + "Method 'func' of object 'second_obj' directly accesses state 'state' of base class 'a'", + "Method 'func' of object 'second_obj' directly accesses state 'mem' of base class 'a'" + ) + ), + TestCase( + label = "Access to inner state", + code = """[] > base + | memory > plain_state + | [] > inner_state + | [] > very_inner_state + | memory > hidden_state + | memory > inner_mem + | cage > inner_cage + |[] > b + | base > @ + | [self] > func + | self.inner_state.very_inner_state.hidden_state > super_tmp + | self.inner_state.inner_cage > tmp + | seq > @ + | self.plain_state.write 10 + | self.inner_state.inner_mem + |""".stripMargin, + expected = List( + "Method 'func' of object 'b' directly accesses state 'hidden_state' of base class 'base.inner_state.very_inner_state'", + "Method 'func' of object 'b' directly accesses state 'inner_cage' of base class 'base.inner_state'", + "Method 'func' of object 'b' directly accesses state 'plain_state' of base class 'base'", + "Method 'func' of object 'b' directly accesses state 'inner_mem' of base class 'base.inner_state'" + ) + ), + TestCase( + label = "Access to state in inner hierarchy", + code = """ + |[] > superroot + | [] > root + | [] > parent + | memory > state + | + | [] > child + | parent > @ + | [self] > method + | self.state.write 10 > @ + |""".stripMargin, + expected = List( + "Method 'method' of object 'superroot.root.child' directly accesses state 'state' of base class 'parent'", + ) + ), + TestCase( + label = "Access to state that is high in the hierarchy", + code = """ + |[] > super_puper + | memory > omega_state + | + |[] > super + | super_puper > @ + | memory > super_state + | [self] > super_bad_func + | seq > @ + | self.omega_state.write 10 + | self.super_state.write 30 + | + |[] > parent + | super > @ + | memory > parent_state + | + |[] > child + | parent > @ + | memory > local_state + | [self] > bad_func + | seq > @ + | self.omega_state.write 10 + | self.super_state.write 10 + | self.parent_state.write 10 + | self.local_state.write 10 + |""".stripMargin, + expected = List( + "Method 'super_bad_func' of object 'super' directly accesses state 'omega_state' of base class 'super_puper'", + "Method 'bad_func' of object 'child' directly accesses state 'omega_state' of base class 'super_puper'", + "Method 'bad_func' of object 'child' directly accesses state 'super_state' of base class 'super'", + "Method 'bad_func' of object 'child' directly accesses state 'parent_state' of base class 'parent'", + ) + ), + TestCase( + label = + "Access to state that is high in the hierarchy | shadowing, attaching attributes during application", + code = """ + |[] > super_puper + | memory > omega_state + | memory > additional_state + | + |[] > super + | super_puper > @ + | super_puper.additional_state > omega_state + | [self] > super_bad_func + | seq > @ + | self.omega_state.write 10 + | + |[] > parent + | ((super)) > @ + | memory > parent_state + | + |[] > child + | parent > @ + | memory > local_state + | [self] > bad_func + | seq > @ + | self.omega_state.write 10 + | self.parent_state.write 10 + | self.local_state.write 10 + |""".stripMargin, + expected = List( + "Method 'super_bad_func' of object 'super' directly accesses state 'omega_state' of base class 'super_puper'", + "Method 'bad_func' of object 'child' directly accesses state 'omega_state' of base class 'super_puper'", + "Method 'bad_func' of object 'child' directly accesses state 'parent_state' of base class 'parent'" + ) + ), + TestCase( + label = "Access to state with further method call", + code = """ + |[] > parent + | memory > state + |[] > child + | parent > @ + | [self] > method + | self.state.add 10 > @ + |""".stripMargin, + expected = List( + "Method 'method' of object 'child' directly accesses state 'state' of base class 'parent'", + ) + ), + TestCase( + label = "Access to state with further method call | indirect", + code = """ + |[] > parent + | memory > state + |[] > child + | parent > @ + | [self] > method + | (self.state.add 10).write 4 > @ + |""".stripMargin, + expected = List( + "Method 'method' of object 'child' directly accesses state 'state' of base class 'parent'", + ) + ), + TestCase( + label = "Access to state with a funky method call", + code = """ + |[] > parent + | memory > state + |[] > child + | parent > @ + | [self] > method + | 3.sub ((self.state.add 10).add 10) > @ + |""".stripMargin, + expected = List( + "Method 'method' of object 'child' directly accesses state 'state' of base class 'parent'", + ) + ) + ) + + val testsWithoutDefect: List[TestCase] = List( + TestCase( + label = "Proper access to state", + code = """[] > a + | memory > state + | [self new_state] > update_state + | self.state.write new_state > @ + |[] > b + | a > @ + | [self new_state] > change_state_plus_two + | new_state.add 2 > tmp + | self.update_state self tmp > @ + |""".stripMargin, + expected = List() + ), + TestCase( + label = "Read on not inherited object", + code = """[] > test + | [] > a_factory + | [] > get_a + | memory > state + | [] > b + | a_factory.get_a > @ + | [self x] > n + | a_factory.get_a.state.add x > @ + |""".stripMargin, + expected = List() + ), + TestCase( + label = "State that is not accessed", + code = """[] > a + | memory > state + | memory > state_2 + | cage > obj_state + | [] > more_state + | memory > inner_state + |""".stripMargin, + expected = List() + ), + TestCase( + label = "Access to local state", + code = """[] > a + | memory > state + |[] > b + | a > @ + | memory > local_state + | [self] > func + | self.local_state.write 10 > @ + |""".stripMargin, + expected = List() + ) + ) + + def runTests(tests: List[TestCase]): Unit = + tests.foreach { case TestCase(label, code, expected) => + registerTest(label) { + val obtained = analyze(code).unsafeRunSync() + assert(obtained == expected) + } + } + + "analyzer" should { + "find errors" should { + runTests(testsWithDefect) + } + + "not find errors" should { + runTests(testsWithoutDefect) + } + + } + +} diff --git a/analysis/src/test/scala/org/polystat/odin/analysis/LiskovPrincipleTests.scala b/analysis/src/test/scala/org/polystat/odin/analysis/LiskovPrincipleTests.scala index 3e6790ed..810f5026 100644 --- a/analysis/src/test/scala/org/polystat/odin/analysis/LiskovPrincipleTests.scala +++ b/analysis/src/test/scala/org/polystat/odin/analysis/LiskovPrincipleTests.scala @@ -18,7 +18,7 @@ class LiskovPrincipleTests extends AnyWordSpec { ) .flatMap { case Ok(_) => IO.pure(List.empty) - case DefectDetected(_, message) => IO.pure(message.split("\n").toList) + case DefectsDetected(_, message) => IO.pure(message.toList) case AnalyzerFailure(_, e) => IO.raiseError(e) } diff --git a/analysis/src/test/scala/org/polystat/odin/analysis/UnjustifiedAssumptionTests.scala b/analysis/src/test/scala/org/polystat/odin/analysis/UnjustifiedAssumptionTests.scala index 28883200..5555ba8e 100644 --- a/analysis/src/test/scala/org/polystat/odin/analysis/UnjustifiedAssumptionTests.scala +++ b/analysis/src/test/scala/org/polystat/odin/analysis/UnjustifiedAssumptionTests.scala @@ -18,7 +18,7 @@ class UnjustifiedAssumptionTests extends AnyWordSpec { ) .flatMap { case Ok(_) => IO.pure(List.empty) - case DefectDetected(_, message) => IO.pure(message.split("\n").toList) + case DefectsDetected(_, message) => IO.pure(message.toList) case AnalyzerFailure(_, e) => IO.raiseError(e) } diff --git a/interop/src/main/scala/org/polystat/odin/interop/java/EOOdinAnalyzer.scala b/interop/src/main/scala/org/polystat/odin/interop/java/EOOdinAnalyzer.scala index ed078465..c5f62195 100644 --- a/interop/src/main/scala/org/polystat/odin/interop/java/EOOdinAnalyzer.scala +++ b/interop/src/main/scala/org/polystat/odin/interop/java/EOOdinAnalyzer.scala @@ -7,6 +7,7 @@ import org.polystat.odin.analysis import org.polystat.odin.analysis.ASTAnalyzer import org.polystat.odin.analysis.EOOdinAnalyzer.{ advancedMutualRecursionAnalyzer, + directStateAccessAnalyzer, liskovPrincipleViolationAnalyzer, unjustifiedAssumptionAnalyzer } @@ -14,10 +15,9 @@ import org.polystat.odin.core.ast.EOProg import org.polystat.odin.core.ast.astparams.EOExprOnly import org.polystat.odin.parser.EoParser import org.polystat.odin.parser.EoParser.sourceCodeEoParser -import org.polystat.odin.interop.java.OdinAnalysisResultInterop -import scala.jdk.CollectionConverters._ import java.util +import scala.jdk.CollectionConverters._ trait EOOdinAnalyzer[R] { @@ -34,6 +34,7 @@ object EOOdinAnalyzer { List( advancedMutualRecursionAnalyzer[IO], unjustifiedAssumptionAnalyzer[IO], + directStateAccessAnalyzer[IO], liskovPrincipleViolationAnalyzer[IO] ) diff --git a/interop/src/main/scala/org/polystat/odin/interop/java/OdinAnalysisResultInterop.scala b/interop/src/main/scala/org/polystat/odin/interop/java/OdinAnalysisResultInterop.scala index f10ac433..fb044b1d 100644 --- a/interop/src/main/scala/org/polystat/odin/interop/java/OdinAnalysisResultInterop.scala +++ b/interop/src/main/scala/org/polystat/odin/interop/java/OdinAnalysisResultInterop.scala @@ -2,6 +2,8 @@ package org.polystat.odin.interop.java import org.polystat.odin.analysis.EOOdinAnalyzer.OdinAnalysisResult import org.polystat.odin.analysis.EOOdinAnalyzer.OdinAnalysisResult._ +import cats.syntax.foldable._ +import scala.util.Properties class OdinAnalysisResultInterop( val ruleId: java.lang.String, @@ -32,11 +34,11 @@ object OdinAnalysisResultInterop { java.util.Optional.empty, ) ) - case DefectDetected(rule, message) => + case DefectsDetected(rule, messages) => List( new OdinAnalysisResultInterop( rule, - java.util.Optional.of(message), + java.util.Optional.of(messages.mkString_(Properties.lineSeparator)), java.util.Optional.empty, ) ) diff --git a/sandbox/src/main/scala/org/polystat/odin/sandbox/Sandbox.scala b/sandbox/src/main/scala/org/polystat/odin/sandbox/Sandbox.scala index 38314c7a..f8b69855 100644 --- a/sandbox/src/main/scala/org/polystat/odin/sandbox/Sandbox.scala +++ b/sandbox/src/main/scala/org/polystat/odin/sandbox/Sandbox.scala @@ -64,6 +64,15 @@ object Sandbox extends IOApp { | [self] > h | self.f self 1 2 3 > @ |""".stripMargin, + "eight" -> """[] > a + | memory > state + | [self new_state] > update_state + | self.state.write new_state > @ + |[] > b + | a > @ + | [self new_state] > change_state_plus_two + | self.state.write (new_state.add 2) > @ + |""".stripMargin, ) override def run(args: List[String]): IO[ExitCode] = for {