diff --git a/effekt/jvm/src/main/scala/effekt/Runner.scala b/effekt/jvm/src/main/scala/effekt/Runner.scala index 5c0c3fe9c..a8ab10cf6 100644 --- a/effekt/jvm/src/main/scala/effekt/Runner.scala +++ b/effekt/jvm/src/main/scala/effekt/Runner.scala @@ -306,9 +306,15 @@ object LLVMRunner extends Runner[String] { def libuvArgs(using C: Context): Seq[String] = val OS = System.getProperty("os.name").toLowerCase + + // only relevant for macOS + val arch: String = System.getProperty("os.arch") + assert(!OS.contains("mac") || List("aarch64", "x86_64").contains(arch), "If you have a macbook -> It should have either an aarch64 or x86_64 architecture.") + val libraries = C.config.clangLibraries.toOption.map(file).orElse { OS match { - case os if os.contains("mac") => Some(file("/opt/homebrew/lib")) + case os if os.contains("mac") && arch.equals("aarch64") => Some(file("/opt/homebrew/lib")) + case os if os.contains("mac") => Some(file("/usr/local/lib")) case os if os.contains("win") => None case os if os.contains("linux") => Some(file("/usr/local/lib")) case os => None @@ -316,7 +322,8 @@ object LLVMRunner extends Runner[String] { } val includes = C.config.clangIncludes.toOption.map(file).orElse { OS match { - case os if os.contains("mac") => Some(file("/opt/homebrew/include")) + case os if os.contains("mac") && arch.equals("aarch64") => Some(file("/opt/homebrew/include")) + case os if os.contains("mac") => Some(file("/usr/local/include")) case os if os.contains("win") => None case os if os.contains("linux") => Some(file("/usr/local/include")) case os => None diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala index b7a572519..754e543d1 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Transformer.scala @@ -3,9 +3,9 @@ package generator package llvm import effekt.machine +import effekt.machine.analysis.freeVariables import effekt.util.intercalate import effekt.util.messages.ErrorReporter -import effekt.machine.analysis.* import scala.annotation.tailrec import scala.collection.mutable @@ -13,6 +13,7 @@ import scala.collection.mutable object Transformer { val llvmFeatureFlags: List[String] = List("llvm") + val slotSize: Int = 64 // the size of a heap slot in bytes def transform(program: machine.Program)(using ErrorReporter): List[Definition] = program match { case machine.Program(declarations, definitions, entry) => @@ -24,7 +25,9 @@ object Transformer { val entryInstructions = List( Call("stack", Ccc(), stackType, withEmptyStack, List()), - Call("_", Tailcc(false), VoidType(), transform(entry), List(LocalReference(stackType, "stack")))) + Call("", Ccc(), VoidType(), initializeMemory, List()), + Call("_", Tailcc(false), VoidType(), transform(entry), List(LocalReference(stackType, "stack"))), + ) val entryBlock = BasicBlock("entry", entryInstructions, RetVoid()) val entryFunction = Function(External(), Ccc(), VoidType(), "effektMain", List(), List(entryBlock)) @@ -32,9 +35,9 @@ object Transformer { } // context getters - private def MC(using MC: ModuleContext): ModuleContext = MC - private def FC(using FC: FunctionContext): FunctionContext = FC - private def BC(using BC: BlockContext): BlockContext = BC + def MC(using MC: ModuleContext): ModuleContext = MC + def FC(using FC: FunctionContext): FunctionContext = FC + def BC(using BC: BlockContext): BlockContext = BC def transform(declaration: machine.Declaration)(using ErrorReporter): Definition = declaration match { @@ -57,26 +60,26 @@ object Transformer { """call void @hole() |unreachable |""".stripMargin - } + } def transform(template: Template[machine.Variable]): String = "; variable\n " ++ intercalate(template.strings, template.args.map { case machine.Variable(name, tpe) => PrettyPrinter.localName(name) }).mkString def transform(definition: machine.Definition)(using ModuleContext): Unit = definition match { - case machine.Definition(machine.Label(name, environment), body) => - val parameters = environment.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } - defineLabel(name, parameters) { - emit(Comment(s"definition $name, environment length ${environment.length}")) - eraseValues(environment, freeVariables(body)) - transform(body) - } + case machine.Definition(machine.Label(name, environment), body) => + val parameters = environment.map { case machine.Variable(name, tpe) => Parameter(transform(tpe), name) } + defineLabel(name, parameters) { + emit(Comment(s"definition $name, environment length ${environment.length}")) + eraseValues(environment, freeVariables(body)) + transform(body) + } } def transform(statement: machine.Statement)(using ModuleContext, FunctionContext, BlockContext): Terminator = statement match { - case machine.Jump(label, arguments) => + case machine.Jump(label, arguments) => emit(Comment(s"jump ${label.name}")) shareValues(arguments, Set()) emit(callLabel(transform(label), arguments.map(transform))) @@ -84,7 +87,7 @@ object Transformer { case machine.Construct(variable, tag, values, rest) => emit(Comment(s"construct ${variable.name}, tag ${tag}, ${values.length} values")) - val fields = produceObject("fields", values, freeVariables(rest)) + val fields = produceObject(values, freeVariables(rest)) val temporaryName = freshName(variable.name + "_temporary") emit(InsertValue(temporaryName, ConstantAggregateZero(positiveType), ConstantInt(tag), 0)) emit(InsertValue(variable.name, LocalReference(positiveType, temporaryName), fields, 1)) @@ -154,7 +157,7 @@ object Transformer { val vtableName = freshName("vtable") emit(GlobalConstant(vtableName, ConstantArray(methodType, clauseNames))) - val vtable = produceObject("closure", closureEnvironment, freeVariables(rest)); + val vtable = produceObject(closureEnvironment, freeVariables(rest)); val temporaryName = freshName("vtable_temporary"); emit(InsertValue(temporaryName, ConstantAggregateZero(negativeType), ConstantGlobal(vtableName), 0)); emit(InsertValue(variable.name, LocalReference(negativeType, temporaryName), vtable, 1)); @@ -417,6 +420,7 @@ object Transformer { val returnAddressType = NamedType("ReturnAddress"); val sharerType = NamedType("Sharer"); val eraserType = NamedType("Eraser"); + val slotType = NamedType("Slot"); val frameHeaderType = NamedType("FrameHeader"); val environmentType = NamedType("Environment"); val objectType = NamedType("Object"); @@ -425,6 +429,8 @@ object Transformer { val resumptionType = NamedType("Resumption"); val promptType = NamedType("Prompt"); val referenceType = NamedType("Reference"); + val headerType = NamedType("Header"); + val referenceCountType = NamedType("ReferenceCount"); def transform(tpe: machine.Type): Type = tpe match { case machine.Positive() => positiveType @@ -437,6 +443,11 @@ object Transformer { case machine.Type.Reference(tpe) => referenceType } + def environmentType(environment: machine.Environment): Type = + StructureType(environment.map { + case machine.Variable(_, tpe) => transform(tpe) + }) + def environmentSize(environment: machine.Environment): Int = environment.map { case machine.Variable(_, typ) => typeSize(typ) }.sum @@ -488,158 +499,326 @@ object Transformer { def callLabelTransition(name: Operand, arguments: List[Operand])(using BlockContext): Instruction = Call("_", Tailcc(false), VoidType(), name, arguments :+ getStack()) - def initialEnvironmentPointer = LocalReference(environmentType, "environment") + def pushFrameOnto(stack: Operand, environment: machine.Environment, returnAddressName: String, sharer: Operand, eraser: Operand)(using ModuleContext, FunctionContext, BlockContext) = { + val size = environmentSize(environment); + + val newStack = LocalReference(stackType, freshName("stack")) + emit(Call(newStack.name, Ccc(), stackType, checkLimit, List(stack, ConstantInt(size + 24)))); + setStack(newStack); + + val environmentPointer = LocalReference(stackPointerType, freshName("environmentPointer")); + emit(Call(environmentPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(size)))); + + storeEnvironmentAt(environmentPointer, environment, StackPointer); + + val headerPointer = LocalReference(stackPointerType, freshName("headerPointer")); + emit(Call(headerPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(24)))); + + val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); + emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, headerPointer, List(0, 0))); + val sharerPointer = LocalReference(PointerType(), freshName("sharer_pointer")); + emit(GetElementPtr(sharerPointer.name, frameHeaderType, headerPointer, List(0, 1))); + val eraserPointer = LocalReference(PointerType(), freshName("eraser_pointer")); + emit(GetElementPtr(eraserPointer.name, frameHeaderType, headerPointer, List(0, 2))); - def loadEnvironment(environmentPointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { + emit(Store(returnAddressPointer, ConstantGlobal(returnAddressName), StackPointer)); + emit(Store(sharerPointer, sharer, StackPointer)); + emit(Store(eraserPointer, eraser, StackPointer)); + } + + def popEnvironmentFrom(stack: Operand, environment: machine.Environment)(using ModuleContext, FunctionContext, BlockContext): Unit = { if (environment.isEmpty) { () } else { - loadEnvironmentAt(environmentPointer, environment, alias); + val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); + val size = ConstantInt(environmentSize(environment)); + emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); + loadEnvironmentAt(stackPointer, environment, StackPointer) + } + } + + def popReturnAddressFrom(stack: Operand, returnAddressName: String)(using ModuleContext, FunctionContext, BlockContext): Unit = { + + val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); + // TODO properly find size + val size = ConstantInt(24); + emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); + + val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); + emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, stackPointer, List(0, 0))); + + emit(Load(returnAddressName, returnAddressType, returnAddressPointer, StackPointer)); + } + + /** + * if we can fit our object in one single saving block -> Easiest case + * Else we have to split our environment into multiple slots and reference in a linked list fashion + */ + def fitsInOneSlot(environment: machine.Environment): Boolean = { + environmentSize(environment) > 0 && environmentSize(environment) <= 48 + } + + /** + * Produces an Object. It is always ${slotSize} bytes long + */ + def produceObject(environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Operand = { + if (environment.isEmpty) { + ConstantNull(objectType) + } else if (fitsInOneSlot(environment)) { + produceSingleObject(environment, freeInBody) + } else { + produceShardedObject(environment, freeInBody) } } - def storeEnvironment(environmentPointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { + def consumeObject(`object`: LocalReference, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = { if (environment.isEmpty) { () + } else if (fitsInOneSlot(environment)) { + consumeSingleObject(`object`, environment, freeInBody) } else { - storeEnvironmentAt(environmentPointer, environment, alias); + consumeShardedObject(`object`, environment, freeInBody) } } - def getEraser(environment: machine.Environment, kind: EraserKind)(using C: ModuleContext): Operand = { - val types = environment.map{ _.tpe }; - val freshEnvironment = environment.map{ - case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) - }; + def produceSingleObject(environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): LocalReference = { + val objectReference = LocalReference(objectType, freshName("object")) + val environmentPointer = LocalReference(environmentType, freshName("environment")) + val size = ConstantInt(environmentSize(environment)) + val eraser = getEraser(environment, ObjectEraser) - C.erasers.getOrElseUpdate((types, kind), { - kind match { - case ObjectEraser => - val eraser = ConstantGlobal(freshName("eraser")); - defineFunction(eraser.name, List(Parameter(environmentType, "environment"))) { - emit(Comment(s"${kind} eraser, ${freshEnvironment.length} free variables")) + emit(Call(objectReference.name, Ccc(), objectType, newObject, List(eraser, size))); + emit(Call(environmentPointer.name, Ccc(), environmentType, objectEnvironment, List(objectReference))); + shareValues(environment, freeInBody); + storeEnvironmentAt(environmentPointer, environment, Object); - // TODO avoid unnecessary loads - loadEnvironmentAt(LocalReference(environmentType, "environment"), freshEnvironment, Object); - eraseValues(freshEnvironment, Set()); - RetVoid() - }; - eraser - case StackEraser | StackFrameEraser => - val eraser = ConstantGlobal(freshName("eraser")); - defineFunction(eraser.name, List(Parameter(stackPointerType, "stackPointer"))) { - emit(Comment(s"${kind} eraser, ${freshEnvironment.length} free variables")) + objectReference + } - val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); - loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer); + def consumeSingleObject(`object`: LocalReference, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = { + val environmentPointer = LocalReference(environmentType, freshName("environment")) + emit(Call(environmentPointer.name, Ccc(), environmentType, objectEnvironment, List(`object`))) + loadEnvironmentAt(environmentPointer, environment, Object) + val release = ConstantGlobal(freshName("release")) + defineFunction(release.name, List(Parameter(objectType, "object"))) { + createReleaseFunction(release.name, environment, freeInBody) + } + emit(Call("_", Ccc(), VoidType(), release, List(`object`))) + } - eraseValues(freshEnvironment, Set()); - val next = if (kind == StackEraser) freeStack else eraseFrames // TODO: improve this (in RTS?) - emit(Call("_", Ccc(), VoidType(), next, List(nextStackPointer))); - RetVoid() - }; - eraser - } - }) + def consumeShardedObject(`object`: LocalReference, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = { + val environmentPointer = LocalReference(environmentType, freshName("environment")) + emit(Call(environmentPointer.name, Ccc(), environmentType, objectEnvironment, List(`object`))) + loadObjectEnvironmentAt(environmentPointer, environment) + val release = ConstantGlobal(freshName("release")) + defineFunction(release.name, List(Parameter(objectType, "object"))) { + createReleaseFunction(release.name, environment, freeInBody) + } + emit(Call("_", Ccc(), VoidType(), release, List(`object`))) } - def getSharer(environment: machine.Environment, kind: SharerKind)(using C: ModuleContext): Operand = { - val types = environment.map{ _.tpe }; - val freshEnvironment = environment.map{ - case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) - }; + /** + * When you have a big object to produce, you shard it into multiple slots instead of one single big slot. + * The end of each slot references to the next one. + * Each slot is 64 byte long + */ + def produceShardedObject(environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Operand = { + // Split into multiple 64-byte blocks and chain them via a boxed link (%Pos with tag 0 pointing to next %Object) + val shardedEnvironments = splitEnvironment(environment) + + // Allocate the last block first. + val lastEnvironment = shardedEnvironments.last + val lastObjectReference = LocalReference(objectType, freshName("object")) + val lastEnvPtr = LocalReference(environmentType, freshName("environment")) + val lastSize = ConstantInt(environmentSize(lastEnvironment)) + val lastEraser = getEraser(lastEnvironment, ObjectEraser) + + emit(Call(lastObjectReference.name, Ccc(), objectType, newObject, List(lastEraser, lastSize))) + emit(Call(lastEnvPtr.name, Ccc(), environmentType, objectEnvironment, List(lastObjectReference))) + shareValues(lastEnvironment, freeInBody) + if (environment.isEmpty) { + () + } else { + storeEnvironmentAt(lastEnvPtr, lastEnvironment, Object); + } - C.sharers.getOrElseUpdate((types, kind), { - val sharer = ConstantGlobal(freshName("sharer")); - defineFunction(sharer.name, List(Parameter(stackPointerType, "stackPointer"))) { - emit(Comment(s"${kind} sharer, ${freshEnvironment.length} free variables")) + // Chain preceding blocks, each storing a boxed link to the next object + var headObject: LocalReference = lastObjectReference - val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); - loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer); + // loop through all shardedEnvironments except the last one in reversed order + shardedEnvironments.init.reverse.foreach { envChunk => + val temporaryName = freshName("link_temporary") + val linkValName = freshName("link") - shareValues(freshEnvironment, Set.from(freshEnvironment)); + // we create a new Positive Object with tag 0... + emit(InsertValue(temporaryName, ConstantAggregateZero(positiveType), ConstantInt(0), 0)) // we create a fake Link Tag 0 that we just use for store a tag in the Pos + emit(InsertValue(linkValName, LocalReference(positiveType, temporaryName), headObject, 1)) - if (kind == StackFrameSharer) // TODO: improve this (in RTS?) - emit(Call("_", Ccc(), VoidType(), shareFrames, List(nextStackPointer))); - RetVoid() - } - sharer - }) + // ... and inject it into our environment... + val extendedEnv = envChunk :+ machine.Variable(linkValName, machine.Positive()) + + // ...finally, we do the usual behavior when we produce an object + headObject = produceSingleObject(extendedEnv, freeInBody) + } + headObject } - def produceObject(role: String, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Operand = { - if (environment.isEmpty) { - ConstantNull(objectType) + def loadObjectEnvironmentAt(elementPointer: Operand, environment: machine.Environment)(using ModuleContext, FunctionContext, BlockContext): Unit = { + // if we have a sharded object + if !fitsInOneSlot(environment) then { + // Follow chained ${slotSize}-byte blocks created in produceObject + val chunkedEnvironments = splitEnvironment(environment) + var currentEnvironmentPtr: Operand = elementPointer + + // here we loop over all environments *in* order + chunkedEnvironments.zipWithIndex.foreach { case (chunk, idx) => + val isLast = idx == chunkedEnvironments.length - 1 + if (isLast) { + loadEnvironmentAt(currentEnvironmentPtr, chunk, Object) + } else { + val link = machine.Variable(freshName("link"), machine.Positive()) + val extendedChunk = chunk :+ link + loadEnvironmentAt(currentEnvironmentPtr, extendedChunk, Object) + + // Follow link to next block + val nextObj = LocalReference(objectType, freshName("next_object")) + emit(ExtractValue(nextObj.name, LocalReference(positiveType, link.name), 1)) + val nextEnv = LocalReference(environmentType, freshName("environment")) + emit(Call(nextEnv.name, Ccc(), environmentType, objectEnvironment, List(nextObj))) + currentEnvironmentPtr = nextEnv + } + } } else { - val objectReference = LocalReference(objectType, freshName(role)); - val environmentReference = LocalReference(environmentType, freshName("environment")); - val size = ConstantInt(environmentSize(environment)); - val eraser = getEraser(environment, ObjectEraser) - - emit(Call(objectReference.name, Ccc(), objectType, newObject, List(eraser, size))); - emit(Call(environmentReference.name, Ccc(), environmentType, objectEnvironment, List(objectReference))); - shareValues(environment, freeInBody); - storeEnvironment(environmentReference, environment, Object); - objectReference + loadEnvironmentAt(elementPointer, environment, Object) } } - def consumeObject(`object`: Operand, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - val environmentReference = LocalReference(environmentType, freshName("environment")); - emit(Call(environmentReference.name, Ccc(), environmentType, objectEnvironment, List(`object`))); - loadEnvironment(environmentReference, environment, Object); - shareValues(environment, freeInBody); - emit(Call("_", Ccc(), VoidType(), eraseObject, List(`object`))); + /** + * Splits the environment into multiple environment such that you can store one object in multiple slots. + * Is required to do fixed-sized-allocation. + */ + def splitEnvironment(environment: machine.Environment): List[machine.Environment] = { + val headerSize = 16 // we use 16 byte for the last saving block only, the others are 32 bytes + val linkSize = 16 // how much bytes we need to store the link to the next block (%Pos object) + + // How much bytes do we need to reserve to save a slot? + // The last block only needs 16 bytes, because here we only need to store the header. + // For all other blocks we need 32 bytes, because we need to store the header and the link to the next block. + var reservedSpaceForSlot = headerSize + + var currentEnvironment = List[machine.Variable]() + var result = List[machine.Environment]() + var isLast = true + for (variable <- environment.reverse) { + val variableSize = typeSize(variable.tpe) + + if (reservedSpaceForSlot + (environmentSize(currentEnvironment) + variableSize) <= slotSize) { + currentEnvironment = currentEnvironment :+ variable + } + else { + if (isLast) { + reservedSpaceForSlot = headerSize + linkSize // normal header size + size of the positive type which is the reference to the next object + isLast = false + } + result = result :+ currentEnvironment.reverse + currentEnvironment = List(variable) + } } + result = result :+ currentEnvironment.reverse + + result.reverse } - def pushFrameOnto(stack: Operand, environment: machine.Environment, returnAddressName: String, sharer: Operand, eraser: Operand)(using ModuleContext, FunctionContext, BlockContext) = { - val size = environmentSize(environment); - val newStack = LocalReference(stackType, freshName("stack")) - emit(Call(newStack.name, Ccc(), stackType, checkLimit, List(stack, ConstantInt(size + 24)))); - setStack(newStack); + def createReleaseFunction(releaseLabel: String, environment: machine.Environment, freeInBody: Set[machine.Variable])(using ModuleContext, FunctionContext, BlockContext): Terminator = { + val pushOntoFreeListLabel = freshName("pushOntoFreeList") + val shareChildrenAndEraseLabel = freshName("shareChildrenAndErase") + val objectReference = LocalReference(objectType, "object") + val environmentRef = LocalReference(environmentType, freshName("environment")) - val environmentPointer = LocalReference(stackPointerType, freshName("environmentPointer")); - emit(Call(environmentPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(size)))); + val referenceCountPtr = LocalReference(PointerType(), freshName("referenceCount_pointer")) + val referenceCount: LocalReference = LocalReference(referenceCountType, freshName("referenceCount")) - storeEnvironmentAt(environmentPointer, environment, StackPointer); + // Step 01: Load the environment + emit(Call(environmentRef.name, Ccc(), environmentType, ConstantGlobal("objectEnvironment"), List(objectReference))) - val headerPointer = LocalReference(stackPointerType, freshName("headerPointer")); - emit(Call(headerPointer.name, Ccc(), stackPointerType, stackAllocate, List(newStack, ConstantInt(24)))); + // Step 02: Load the reference count + emit(GetElementPtr(referenceCountPtr.name, headerType, LocalReference(objectType, "object"), List(0, 0))) + emit(Load(referenceCount.name, referenceCountType, referenceCountPtr, Object)) - val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); - emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, headerPointer, List(0, 0))); - val sharerPointer = LocalReference(PointerType(), freshName("sharer_pointer")); - emit(GetElementPtr(sharerPointer.name, frameHeaderType, headerPointer, List(0, 1))); - val eraserPointer = LocalReference(PointerType(), freshName("eraser_pointer")); - emit(GetElementPtr(eraserPointer.name, frameHeaderType, headerPointer, List(0, 2))); + // Step 03: Create the branches for dealing with the given reference count + createReleasePushOntoFreeListBasicBlock(pushOntoFreeListLabel, environment, freeInBody, objectReference, environmentRef) + createReleaseShareChildrenAndEraseBasicBlock(shareChildrenAndEraseLabel, environment, freeInBody, objectReference, environmentRef) - emit(Store(returnAddressPointer, ConstantGlobal(returnAddressName), StackPointer)); - emit(Store(sharerPointer, sharer, StackPointer)); - emit(Store(eraserPointer, eraser, StackPointer)); + // Step 04: Check the reference count + Switch(referenceCount, shareChildrenAndEraseLabel, List((0, pushOntoFreeListLabel))) } - def popEnvironmentFrom(stack: Operand, environment: machine.Environment)(using ModuleContext, FunctionContext, BlockContext): Unit = { - if (environment.isEmpty) { - () - } else { - val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - val size = ConstantInt(environmentSize(environment)); - emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); - loadEnvironmentAt(stackPointer, environment, StackPointer) + /** + * In this branch of the release function, we know that we can safely erase the object. + * It might be that an object consists of several slots. + * Each slot is pushed onto the free list. + */ + def createReleasePushOntoFreeListBasicBlock(releaseLabel: String, environment: machine.Environment, freeInBody: Set[machine.Variable], `object`: Operand, environmentRef: LocalReference)(using ModuleContext, FunctionContext, BlockContext): Unit = { + implicit val BC = BlockContext() + emit(Call("_", Ccc(), VoidType(), ConstantGlobal("pushOntoFreeList"), List(`object`))) + var currentEnvironmentPtr: Operand = environmentRef + val slottedEnvironment = splitEnvironment(environment) + + slottedEnvironment.zipWithIndex.foreach { case (slotEnv, index) => + val isLastSlot = index == slottedEnvironment.length - 1 + val slotEnvTypes = if (isLastSlot) environmentType(slotEnv) else StructureType(slotEnv.map { case machine.Variable(_, t) => transform(t) } :+ positiveType) + + slotEnv.zipWithIndex.foreach { case (variable @ machine.Variable(varName, tpe), i) => + tpe match { + case machine.Positive() | machine.Negative() | machine.Type.Stack() => + val variableType = transform(tpe) + val fieldPtr = LocalReference(PointerType(), freshName(varName + "_pointer")) + val loadedVar = LocalReference(variableType, freshName(varName)) + emit(GetElementPtr(fieldPtr.name, slotEnvTypes, currentEnvironmentPtr, List(0, i))) + emit(Load(loadedVar.name, variableType, fieldPtr, Object)) + if (!freeInBody.contains(variable)) { + emit(Call("_", Ccc(), VoidType(), getErasingFunctionForType(variableType), List(loadedVar))) + } + case _ => + } + } + + if (!isLastSlot) { + val linkIndexOfSlot = slotEnv.length + val linkPtr = LocalReference(PointerType(), freshName("link_pointer")) + val linkVal = LocalReference(positiveType, freshName("link")) + val nextObj = LocalReference(objectType, freshName("next_object")) + val nextEnv = LocalReference(environmentType, freshName("environment")) + emit(GetElementPtr(linkPtr.name, slotEnvTypes, currentEnvironmentPtr, List(0, linkIndexOfSlot))) + emit(Load(linkVal.name, positiveType, linkPtr, Object)) + emit(ExtractValue(nextObj.name, linkVal, 1)) + emit(Call("_", Ccc(), VoidType(), ConstantGlobal("pushOntoFreeList"), List(nextObj))) + emit(Call(nextEnv.name, Ccc(), environmentType, objectEnvironment, List(nextObj))) + currentEnvironmentPtr = nextEnv + } } + + emit(BasicBlock(releaseLabel, BC.instructions, RetVoid())) } - def environmentType(environment: machine.Environment): Type = - StructureType(environment.map { - case machine.Variable(_, tpe) => transform(tpe) - }) + def createReleaseShareChildrenAndEraseBasicBlock(label: String, environment: machine.Environment, freeInBody: Set[machine.Variable], `object`: Operand, environmentRef: LocalReference)(using ModuleContext, FunctionContext, BlockContext): Unit = { + implicit val BC = BlockContext() + loadObjectEnvironmentAt(environmentRef, environment) + shareValues(environment, freeInBody) + emit(Call("_", Ccc(), VoidType(), eraseObject, List(`object`))) + emit(BasicBlock(label, BC.instructions, RetVoid())) + } + + /** + * Loads the right erasing function for the given type + */ + def getErasingFunctionForType(tpe: Type) = tpe match { + case NamedType("Pos") => erasePositive + case NamedType("Neg") => eraseNegative + case NamedType("Stack") => eraseResumption + case _ => ??? + } def storeEnvironmentAt(pointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { val `type` = environmentType(environment) @@ -652,11 +831,11 @@ object Transformer { } def loadEnvironmentAt(pointer: Operand, environment: machine.Environment, alias: AliasInfo)(using ModuleContext, FunctionContext, BlockContext): Unit = { - val `type` = environmentType(environment) + val typ = environmentType(environment) environment.zipWithIndex.foreach { case (machine.Variable(name, tpe), i) => - val field = LocalReference(PointerType(), freshName(name + "_pointer")); - emit(GetElementPtr(field.name, `type`, pointer, List(0, i))); + val field = LocalReference(PointerType(), freshName(name + "_pointer")) + emit(GetElementPtr(field.name, typ, pointer, List(0, i))) emit(Load(name, transform(tpe), field, alias)) } } @@ -667,15 +846,15 @@ object Transformer { values match { case Nil => () case value :: values => - if values.contains(value) then { - shareValue(value); - loop(values) - } else if freeInBody.contains(value) then { - shareValue(value); - loop(values) - } else { - loop(values) - } + if values.contains(value) then { + shareValue(value); + loop(values) + } else if freeInBody.contains(value) then { + shareValue(value); + loop(values) + } else { + loop(values) + } } }; loop(values) @@ -702,19 +881,87 @@ object Transformer { }.map(emit) } - def popReturnAddressFrom(stack: Operand, returnAddressName: String)(using ModuleContext, FunctionContext, BlockContext): Unit = { + def getEraser(environment: machine.Environment, kind: EraserKind)(using C: ModuleContext): Operand = { + val types = environment.map{ _.tpe }; + val freshEnvironment = environment.map{ + case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) + }; - val stackPointer = LocalReference(stackPointerType, freshName("stackPointer")); - // TODO properly find size - val size = ConstantInt(24); - emit(Call(stackPointer.name, Ccc(), stackPointer.tpe, stackDeallocate, List(stack, size))); + C.erasers.getOrElseUpdate((types, kind), { + kind match { + case ObjectEraser => + // Called by @eraseObject. Stores the ChildrenEraser into the + // slot's eraser field, then pushes the slot onto the todo list for later reuse. + val childrenEraser = getEraser(freshEnvironment, ChildrenEraser) + val eraser = ConstantGlobal(freshName("eraser")) + val eraserFieldPtr = LocalReference(PointerType(), freshName("eraserPointer")) + defineFunction(eraser.name, List(Parameter(objectType, "object"))) { + emit(Comment(s"object eraser, ${freshEnvironment.length} free variables")) + emit(GetElementPtr(eraserFieldPtr.name, slotType, LocalReference(objectType, "object"), List(0, 1))) + emit(Store(eraserFieldPtr, childrenEraser, Object)) + emit(Call("", Ccc(), VoidType(), ConstantGlobal("pushOntoTodoList"), List(LocalReference(objectType, "object")))) + RetVoid() + } + eraser + case ChildrenEraser => + // Called by @acquire when a slot is popped from the todo list. + // Loads the object's environment and erases each heap-typed child (Pos, Neg, Stack). + val eraser = ConstantGlobal(freshName("eraser")) + defineFunction(eraser.name, List(Parameter(objectType, "object"))) { + emit(Comment(s"children eraser, ${freshEnvironment.length} free variables")) + val environmentRef = LocalReference(environmentType, freshName("environment")) + emit(Call(environmentRef.name, Ccc(), environmentType, ConstantGlobal("objectEnvironment"), List(LocalReference(objectType, "object")))) + loadObjectEnvironmentAt(environmentRef, freshEnvironment) + eraseValues(freshEnvironment, Set()) + RetVoid() + } + eraser + case StackEraser | StackFrameEraser => + val eraser = ConstantGlobal(freshName("eraser")); + defineFunction(eraser.name, List(Parameter(stackPointerType, "stackPointer"))) { + emit(Comment(s"${kind} eraser, ${freshEnvironment.length} free variables")) - val returnAddressPointer = LocalReference(PointerType(), freshName("returnAddress_pointer")); - emit(GetElementPtr(returnAddressPointer.name, frameHeaderType, stackPointer, List(0, 0))); + val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); + emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); - emit(Load(returnAddressName, returnAddressType, returnAddressPointer, StackPointer)); + loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer) + + eraseValues(freshEnvironment, Set()); + val next = if (kind == StackEraser) freeStack else eraseFrames // TODO: improve this (in RTS?) + emit(Call("_", Ccc(), VoidType(), next, List(nextStackPointer))); + RetVoid() + }; + eraser + } + }) } + def getSharer(environment: machine.Environment, kind: SharerKind)(using C: ModuleContext): Operand = { + val types = environment.map{ _.tpe }; + val freshEnvironment = environment.map{ + case machine.Variable(name, tpe) => machine.Variable(freshName(name), tpe) + }; + + C.sharers.getOrElseUpdate((types, kind), { + val sharer = ConstantGlobal(freshName("sharer")); + defineFunction(sharer.name, List(Parameter(stackPointerType, "stackPointer"))) { + emit(Comment(s"${kind} sharer, ${freshEnvironment.length} free variables")) + + val nextStackPointer = LocalReference(stackPointerType, freshName("stackPointer")); + emit(GetElementPtr(nextStackPointer.name, environmentType(freshEnvironment), LocalReference(stackPointerType, "stackPointer"), List(-1))); + loadEnvironmentAt(nextStackPointer, freshEnvironment, StackPointer) + + shareValues(freshEnvironment, Set.from(freshEnvironment)); + + if (kind == StackFrameSharer) // TODO: improve this (in RTS?) + emit(Call("_", Ccc(), VoidType(), shareFrames, List(nextStackPointer))); + RetVoid() + } + sharer + }) + } + + val initializeMemory = ConstantGlobal("initializeMemory"); val newObject = ConstantGlobal("newObject"); val objectEnvironment = ConstantGlobal("objectEnvironment"); @@ -792,4 +1039,5 @@ object Transformer { case None => acc += c } }.toString() + } diff --git a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala index bad907f93..5a76f83f0 100644 --- a/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/generator/llvm/Tree.scala @@ -102,6 +102,7 @@ export Operand.* enum EraserKind { case ObjectEraser + case ChildrenEraser case StackEraser case StackFrameEraser } diff --git a/libraries/common/array.effekt b/libraries/common/array.effekt index a2c345c1f..758e7a717 100644 --- a/libraries/common/array.effekt +++ b/libraries/common/array.effekt @@ -19,10 +19,15 @@ extern type Array[T] */ extern llvm """ -define void @array_erase_fields(ptr %array_pointer) { +define void @array_erase(%Object %object) { entry: - %data_pointer = getelementptr inbounds i64, ptr %array_pointer, i64 1 - %size = load i64, ptr %array_pointer, align 8 + ; header of arrays looks like that: rc (0), eraser (1), size (2), data (3) + %size_pointer = getelementptr inbounds i64, ptr %object, i64 2 + %size = load i64, ptr %size_pointer, align 8 + + ; data starts after 3 entries in our object + %data_pointer = getelementptr inbounds ptr, ptr %object, i64 3 + %size_eq_0 = icmp eq i64 %size, 0 br i1 %size_eq_0, label %exit, label %loop loop: @@ -31,11 +36,13 @@ loop: %element_pointer = getelementptr inbounds %Pos, ptr %data_pointer, i64 %loop_phi %element = load %Pos, ptr %element_pointer - call void @erasePositive(%Pos %element) + call void @erasePositive(%Pos %element) ; we erase each child of the array + %inc = add nuw i64 %loop_phi, 1 %cmp = icmp ult i64 %inc, %size br i1 %cmp, label %loop, label %exit exit: + call void @free(%Object %object) ; we delete the array ret void } """ @@ -52,7 +59,7 @@ extern def unsafeAllocate[T](size: Int) at global: Array[T] = %eraser_pointer = getelementptr ptr, ptr %calloc, i64 1 %size_pointer = getelementptr ptr, ptr %calloc, i64 2 store i64 0, ptr %calloc - store ptr @array_erase_fields, ptr %eraser_pointer + store ptr @array_erase, ptr %eraser_pointer store i64 ${size}, ptr %size_pointer %ret_pos = insertvalue %Pos { i64 0, ptr poison }, ptr %calloc, 1 ret %Pos %ret_pos diff --git a/libraries/common/bytearray.effekt b/libraries/common/bytearray.effekt index 3bcceaf37..d7c606ec8 100644 --- a/libraries/common/bytearray.effekt +++ b/libraries/common/bytearray.effekt @@ -31,7 +31,7 @@ extern def allocate(size: Int) at global: ByteArray = %object_alloc = tail call noalias ptr @calloc(i64 %object_size, i64 1) store i64 0, ptr %object_alloc, align 8 %object_data_ptr = getelementptr inbounds i8, ptr %object_alloc, i64 8 - store ptr @bytearray_erase_noop, ptr %object_data_ptr, align 8 + store ptr @bytearray_erase, ptr %object_data_ptr, align 8 %ret_object1 = insertvalue %Pos poison, i64 ${size}, 0 %ret_object2 = insertvalue %Pos %ret_object1, ptr %object_alloc, 1 ret %Pos %ret_object2 @@ -180,7 +180,8 @@ extern js """ """ extern llvm """ -define void @bytearray_erase_noop(ptr readnone %0) { +define void @bytearray_erase(ptr readnone %0) { + tail call void @free(ptr %0) ret void } """ diff --git a/libraries/common/ref.effekt b/libraries/common/ref.effekt index 5d6de8e09..f24e01bbb 100644 --- a/libraries/common/ref.effekt +++ b/libraries/common/ref.effekt @@ -10,9 +10,12 @@ extern js """ """ extern llvm """ -define void @c_ref_erase_field(ptr %0) { - %field = load %Pos, ptr %0, align 8 - tail call void @erasePositive(%Pos %field) +define void @c_ref_erase(%Object %object) { + %headerSize = ptrtoint ptr getelementptr (%Header, ptr null, i64 1) to i64 + %environment = getelementptr i8, ptr %object, i64 %headerSize + %field = load %Pos, ptr %environment, align 8 + call void @free(%Object %object) + call void @erasePositive(%Pos %field) ret void } """ @@ -41,9 +44,9 @@ extern def unsafeAllocate[T]() at global: Ref[T] = %fieldData_pointer = getelementptr ptr, ptr %ref, i64 3 store i64 0, ptr %ref, align 8 - store ptr @c_ref_erase_field, ptr %refEraser, align 8 + store ptr @c_ref_erase, ptr %refEraser, align 8 store i64 0, ptr %fieldTag, align 8 - store ptr null, ptr %fieldData_pointer, align 8 + store ptr null, ptr %fieldData_pointer, align 8 %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1 ret %Pos %refWrap @@ -62,7 +65,7 @@ extern def ref[T](init: T) at global: Ref[T] = %refField = getelementptr ptr, ptr %ref, i64 2 store i64 0, ptr %ref, align 8 - store ptr @c_ref_erase_field, ptr %refEraser, align 8 + store ptr @c_ref_erase, ptr %refEraser, align 8 store %Pos ${init}, ptr %refField, align 8 %refWrap = insertvalue %Pos { i64 0, ptr poison }, ptr %ref, 1 diff --git a/libraries/llvm/bytearray.c b/libraries/llvm/bytearray.c index 4fef7999b..15ab96486 100644 --- a/libraries/llvm/bytearray.c +++ b/libraries/llvm/bytearray.c @@ -2,8 +2,7 @@ #define EFFEKT_BYTEARRAY_C #include // For memcopy -#include -#include +#include /** We represent bytearrays like positive types. * @@ -18,12 +17,15 @@ */ -void c_bytearray_erase_noop(void *envPtr) { (void)envPtr; } +void c_bytearray_erase(void* object) { + free(object); +} struct Pos c_bytearray_new(const Int size) { - void *objPtr = malloc(sizeof(struct Header) + size); + int object_size = sizeof(struct Header) + size; + void *objPtr = malloc(object_size); struct Header *headerPtr = objPtr; - *headerPtr = (struct Header) { .rc = 0, .eraser = c_bytearray_erase_noop, }; + *headerPtr = (struct Header) { .rc = 0, .eraser = c_bytearray_erase, }; return (struct Pos) { .tag = size, .obj = objPtr, diff --git a/libraries/llvm/initialize-arena.c b/libraries/llvm/initialize-arena.c new file mode 100644 index 000000000..df635bdce --- /dev/null +++ b/libraries/llvm/initialize-arena.c @@ -0,0 +1,14 @@ +#include + +// Initialize a 4 GiB arena via mmap. +// Does not work in LLVM properly, therefore it is done in C. +void* initializeArena(void) { + return mmap( + NULL, // no specific start adress + 4294967296ULL, // 4 GiB + PROT_READ | PROT_WRITE, // Can read and write + MAP_PRIVATE | MAP_ANONYMOUS, // isolated, clean arena + -1, // No file descriptor + 0 // Offset + ); +} \ No newline at end of file diff --git a/libraries/llvm/io.c b/libraries/llvm/io.c index c33984117..44b4f9e51 100644 --- a/libraries/llvm/io.c +++ b/libraries/llvm/io.c @@ -589,11 +589,11 @@ typedef struct { Stack stack; } Signal; -void c_signal_erase(void *envPtr) { - // envPtr points to a Signal _after_ the eraser, so let's adjust it to point to the beginning. - Signal *signal = (Signal*) (envPtr - offsetof(Signal, state)); +void c_signal_erase(void *object) { + Signal *signal = (Signal*) object; erasePositive(signal->value); if (signal->stack != NULL) { eraseStack(signal->stack); } + free(signal); } struct Pos c_signal_make() { diff --git a/libraries/llvm/main.c b/libraries/llvm/main.c index 0ea587ab3..4d01be3c5 100644 --- a/libraries/llvm/main.c +++ b/libraries/llvm/main.c @@ -13,6 +13,7 @@ #include "bytearray.c" #include "io.c" #include "panic.c" +#include "initialize-arena.c" extern void effektMain(); diff --git a/libraries/llvm/rts.ll b/libraries/llvm/rts.ll index a2bc718c5..9c671edf8 100644 --- a/libraries/llvm/rts.ll +++ b/libraries/llvm/rts.ll @@ -93,7 +93,6 @@ %String = type %Pos ; Foreign imports - declare ptr @malloc(i64) declare void @free(ptr) declare ptr @realloc(ptr, i64) @@ -118,6 +117,111 @@ declare void @print(i64) declare void @exit(i64) declare void @llvm.assume(i1) +declare ptr @initializeArena() + +; typedef struct Slot +; { +; struct Slot* next; +; void (*eraser)(void *object); +; } Slot; +%Slot = type { ptr, %Eraser } + +; list of immediately available slots +@freeList = private unnamed_addr global ptr null +; list of slots where we still need to erase the children +@todoList = private unnamed_addr global ptr null +; space of unused consecutive slots +@bumpList = private unnamed_addr global ptr null + +; Initializes the memory for our effekt-objects that are created by newObject and deleted by eraseObject. +define private void @initializeMemory() nounwind { +entry: + ; we need this because flags for mmap differ between platforms + %startAddress = call noalias ptr @initializeArena() + store ptr %startAddress, ptr @bumpList + ret void +} + + +define private ptr @acquire() nounwind { +entry: + %freeHead = load ptr, ptr @freeList + %isFreeEmpty = icmp eq ptr %freeHead, null + br i1 %isFreeEmpty, label %checkTodo, label %freeReuse + +; 1. Fast Path: Reuse block from freeList +freeReuse: + %freeNextPtr = getelementptr %Slot, ptr %freeHead, i32 0, i32 0 + %freeNext = load ptr, ptr %freeNextPtr + store ptr %freeNext, ptr @freeList + + ret ptr %freeHead + +checkTodo: + %todoHead = load ptr, ptr @todoList + %isTodoEmpty = icmp eq ptr %todoHead, null + br i1 %isTodoEmpty, label %bumpAlloc, label %todoReuse + +; 2. Slot Path: Reuse block from To-Do-List. Here, we have to extra call the eraser to ensure that we can reuse it. +todoReuse: + %todoNextPtr = getelementptr inbounds %Slot, ptr %todoHead, i32 0, i32 0 + %todoNext = load ptr, ptr %todoNextPtr + store ptr %todoNext, ptr @todoList + + ; Call eraser + ; reusedSlot->eraser(reusedSlot); + %eraserPtr = getelementptr inbounds %Slot, ptr %todoHead, i32 0, i32 1 + %eraser = load %Eraser, ptr %eraserPtr, !alias.scope !14, !noalias !24 + call void %eraser(ptr %todoHead) + + ret ptr %todoHead + +; 3. Fallback - Bump Path: We have to allocate a new block. +bumpAlloc: + ; Load nextUnusedBlock + %fresh = load ptr, ptr @bumpList + + ; Move bump pointer forward by object size + %nextBump = getelementptr i8, ptr %fresh, i8 64 + + store ptr %nextBump, ptr @bumpList + + ret ptr %fresh +} + +; Pushes a slot on the top of the free-List. +define private void @pushOntoFreeList(ptr %slot) nounwind { +entry: + ; oldHead = freeList + %oldHead = load ptr, ptr @freeList + + ; ptr->next = oldHead + %nextPtr = getelementptr %Slot, ptr %slot, i32 0, i32 0 + store ptr %oldHead, ptr %nextPtr + + ; freeList = ptr + store ptr %slot, ptr @freeList + + ret void +} + +; Pushes a slot on the top of the To-Do-List. +define private void @pushOntoTodoList(ptr %slot) nounwind { +entry: + ; oldHead = todoList + %oldHead = load ptr, ptr @todoList + + ; ptr->next = oldHead + %nextPtr = getelementptr %Slot, ptr %slot, i32 0, i32 0 + store ptr %oldHead, ptr %nextPtr + + ; todoList = ptr + store ptr %slot, ptr @todoList + + ret void +} + + ; Prompts @@ -134,12 +238,8 @@ define private %Prompt @freshPrompt() { ret %Prompt %prompt } -; Garbage collection - define private %Object @newObject(%Eraser %eraser, i64 %environmentSize) alwaysinline { - %headerSize = ptrtoint ptr getelementptr (%Header, ptr null, i64 1) to i64 - %size = add i64 %environmentSize, %headerSize - %object = call ptr @malloc(i64 %size) + %object = call ptr @acquire() %objectReferenceCount = getelementptr %Header, ptr %object, i64 0, i32 0 %objectEraser = getelementptr %Header, ptr %object, i64 0, i32 1 store %ReferenceCount 0, ptr %objectReferenceCount, !alias.scope !14, !noalias !24 @@ -197,9 +297,7 @@ define private void @eraseObject(%Object %object) alwaysinline { free: %objectEraser = getelementptr %Header, ptr %object, i64 0, i32 1 %eraser = load %Eraser, ptr %objectEraser, !alias.scope !14, !noalias !24 - %environment = call %Environment @objectEnvironment(%Object %object) - call void %eraser(%Environment %environment) - call void @free(%Object %object) + call void %eraser(%Object %object) br label %done done: