A compile-time macro annotation for Scala 3 and Scala Native that strictly enforces Zero Heap Allocation within annotated methods.
In systems programming, low-latency applications, and embedded target environments (e.g., Scala Native), heap allocations introduce garbage collection overhead and non-deterministic latency spikes.
@zeroAlloc acts as a compile-time barrier. It inspects the Abstract Syntax Tree (AST) of annotated methods and fails the compilation if any code path allocates memory on the heap.
import zeroalloc.*
import scala.annotation.experimental
@experimental
@zeroAlloc
def processSignal(input: Int): Int = {
// ✅ OK: Primitive stack operations
val result = input * 2
// ❌ Compile Error: 'new' triggers heap allocation!
val person = new Person()
result
}- Compile-Time Validation: Catch heap allocations during compilation—zero runtime overhead.
- Heap Instantiation Checks: Blocks direct
newobject creation. - Case Class & Companion Method Guards: Detects implicit heap allocations via factory methods (e.g.,
User(...)). - String Context Protection: Rejects heap-allocating string interpolations (e.g.,
s"Value: $x"). - Escape Hatch (
unsafe): Allows controlled local heap allocations when explicitly wrapped inzeroalloc.unsafe { ... }. - Scala Native Friendly: Fully compatible with Scala Native stack/primitive abstractions.
Add the dependency to your build.sbt:
libraryDependencies += "br.com.mobilemind.zeroalloc" %%% "zeroalloc" % "0.1.0"
// Required for Scala 3 MacroAnnotations
scalacOptions += "-experimental"
Primitive calculations, passing value parameters, and calls to other @zeroAlloc methods pass validation.
import zeroalloc.*
import scala.annotation.experimental
@experimental
@zeroAlloc
def add(a: Int, b: Int): Int = a + b
@experimental
@zeroAlloc
def compute(): Int = {
val x = 10
val y = 20
add(x, y) // ✅ Allowed: Calling another @zeroAlloc method
}
Static string literals are backed by the constant pool and do not allocate at runtime, whereas dynamic interpolations create StringBuilder or StringContext instances.
@experimental
@zeroAlloc
def stringHandling(): Unit = {
val staticStr = "Hello, World!" // ✅ Allowed: String Pool constant
val x = 42
val dynamicStr = s"Value: $x" // ❌ Compile Error: String interpolation allocates on the Heap!
}
Creating instances of classes or calling companion factory methods allocates memory on the heap.
class Person
case class User(name: String)
@experimental
@zeroAlloc
def createEntities(): Unit = {
val p = new Person() // ❌ Compile Error: Allocation detected with 'new Person'
val u = User("Alice") // ❌ Compile Error: Unsafe call to companion method 'apply'
}
When interacting with legacy APIs or performing one-off setup tasks where heap allocations are acceptable, wrap the allocating code inside zeroalloc.unsafe.
@experimental
@zeroAlloc
def processWithUnsafeEscape(): Int = {
val count = 100
// Temporarily bypass zero-allocation checks
zeroalloc.unsafe {
println(s"Debug logging count: $count") // Heap allocation isolated here
val tempPerson = new Person()
}
count * 2 // Back under strict zero-allocation enforcement
}
During macro expansion (quotes.reflect), the @zeroAlloc annotation traverses the method's Definition:
InlinedNodes: Checks if an inlined block originates fromzeroalloc.unsafe. If so, subtree traversal is skipped.NewTerm Inspection: Aborts compilation if aNew(tpt)AST node is encountered outside of anunsafeblock.ApplyMethod Checks: Inspects target symbols. Rejects methods originating fromscala.StringContextor functions not annotated with@zeroAlloc.
To run the test suite (powered by MUnit):
sbt "tests/test"
To run macro compilation assertions individually:
sbt "tests/testOnly zeroalloc.MacroCompileSuite"
This project is licensed under the Apache 2.0 License.