From 2e0a07404e26b40594313bfe6c8a8f808166bede Mon Sep 17 00:00:00 2001
From: squid233 <60126026+squid233@users.noreply.github.com>
Date: Fri, 23 Aug 2024 16:04:01 +0800
Subject: [PATCH] 0.1.0

---
 .github/workflows/gradle.yml                  |   2 +-
 LICENSE                                       | 142 ++------------
 README.md                                     |  47 ++++-
 build.gradle.kts                              |   6 +-
 gradle.properties                             |  34 ++--
 .../overrun/memstack/DefaultMemoryStack.java  |  90 +++++++++
 .../github/overrun/memstack/MemoryStack.java  | 184 ++++++++++++++++++
 .../overrun/memstack/StackConfigurations.java |  52 +++++
 src/main/java/module-info.java                |   9 +
 src/main/java/org/example/Main.java           |  13 --
 .../memstack/test/MemoryStackTest.java        | 109 +++++++++++
 11 files changed, 530 insertions(+), 158 deletions(-)
 create mode 100644 src/main/java/io/github/overrun/memstack/DefaultMemoryStack.java
 create mode 100644 src/main/java/io/github/overrun/memstack/MemoryStack.java
 create mode 100644 src/main/java/io/github/overrun/memstack/StackConfigurations.java
 create mode 100644 src/main/java/module-info.java
 delete mode 100644 src/main/java/org/example/Main.java
 create mode 100644 src/test/java/io/github/overrun/memstack/test/MemoryStackTest.java

diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index fbc4c40..99b7700 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -19,7 +19,7 @@ jobs:
     strategy:
       matrix:
         java: [
-          21
+          22
         ]
         os: [ ubuntu-latest, windows-latest ]
     runs-on: ${{ matrix.os }}
diff --git a/LICENSE b/LICENSE
index 0e259d4..0532fc2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,121 +1,21 @@
-Creative Commons Legal Code
-
-CC0 1.0 Universal
-
-    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
-    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
-    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
-    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
-    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
-    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
-    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
-    HEREUNDER.
-
-Statement of Purpose
-
-The laws of most jurisdictions throughout the world automatically confer
-exclusive Copyright and Related Rights (defined below) upon the creator
-and subsequent owner(s) (each and all, an "owner") of an original work of
-authorship and/or a database (each, a "Work").
-
-Certain owners wish to permanently relinquish those rights to a Work for
-the purpose of contributing to a commons of creative, cultural and
-scientific works ("Commons") that the public can reliably and without fear
-of later claims of infringement build upon, modify, incorporate in other
-works, reuse and redistribute as freely as possible in any form whatsoever
-and for any purposes, including without limitation commercial purposes.
-These owners may contribute to the Commons to promote the ideal of a free
-culture and the further production of creative, cultural and scientific
-works, or to gain reputation or greater distribution for their Work in
-part through the use and efforts of others.
-
-For these and/or other purposes and motivations, and without any
-expectation of additional consideration or compensation, the person
-associating CC0 with a Work (the "Affirmer"), to the extent that he or she
-is an owner of Copyright and Related Rights in the Work, voluntarily
-elects to apply CC0 to the Work and publicly distribute the Work under its
-terms, with knowledge of his or her Copyright and Related Rights in the
-Work and the meaning and intended legal effect of CC0 on those rights.
-
-1. Copyright and Related Rights. A Work made available under CC0 may be
-protected by copyright and related or neighboring rights ("Copyright and
-Related Rights"). Copyright and Related Rights include, but are not
-limited to, the following:
-
-  i. the right to reproduce, adapt, distribute, perform, display,
-     communicate, and translate a Work;
- ii. moral rights retained by the original author(s) and/or performer(s);
-iii. publicity and privacy rights pertaining to a person's image or
-     likeness depicted in a Work;
- iv. rights protecting against unfair competition in regards to a Work,
-     subject to the limitations in paragraph 4(a), below;
-  v. rights protecting the extraction, dissemination, use and reuse of data
-     in a Work;
- vi. database rights (such as those arising under Directive 96/9/EC of the
-     European Parliament and of the Council of 11 March 1996 on the legal
-     protection of databases, and under any national implementation
-     thereof, including any amended or successor version of such
-     directive); and
-vii. other similar, equivalent or corresponding rights throughout the
-     world based on applicable law or treaty, and any national
-     implementations thereof.
-
-2. Waiver. To the greatest extent permitted by, but not in contravention
-of, applicable law, Affirmer hereby overtly, fully, permanently,
-irrevocably and unconditionally waives, abandons, and surrenders all of
-Affirmer's Copyright and Related Rights and associated claims and causes
-of action, whether now known or unknown (including existing as well as
-future claims and causes of action), in the Work (i) in all territories
-worldwide, (ii) for the maximum duration provided by applicable law or
-treaty (including future time extensions), (iii) in any current or future
-medium and for any number of copies, and (iv) for any purpose whatsoever,
-including without limitation commercial, advertising or promotional
-purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
-member of the public at large and to the detriment of Affirmer's heirs and
-successors, fully intending that such Waiver shall not be subject to
-revocation, rescission, cancellation, termination, or any other legal or
-equitable action to disrupt the quiet enjoyment of the Work by the public
-as contemplated by Affirmer's express Statement of Purpose.
-
-3. Public License Fallback. Should any part of the Waiver for any reason
-be judged legally invalid or ineffective under applicable law, then the
-Waiver shall be preserved to the maximum extent permitted taking into
-account Affirmer's express Statement of Purpose. In addition, to the
-extent the Waiver is so judged Affirmer hereby grants to each affected
-person a royalty-free, non transferable, non sublicensable, non exclusive,
-irrevocable and unconditional license to exercise Affirmer's Copyright and
-Related Rights in the Work (i) in all territories worldwide, (ii) for the
-maximum duration provided by applicable law or treaty (including future
-time extensions), (iii) in any current or future medium and for any number
-of copies, and (iv) for any purpose whatsoever, including without
-limitation commercial, advertising or promotional purposes (the
-"License"). The License shall be deemed effective as of the date CC0 was
-applied by Affirmer to the Work. Should any part of the License for any
-reason be judged legally invalid or ineffective under applicable law, such
-partial invalidity or ineffectiveness shall not invalidate the remainder
-of the License, and in such case Affirmer hereby affirms that he or she
-will not (i) exercise any of his or her remaining Copyright and Related
-Rights in the Work or (ii) assert any associated claims and causes of
-action with respect to the Work, in either case contrary to Affirmer's
-express Statement of Purpose.
-
-4. Limitations and Disclaimers.
-
- a. No trademark or patent rights held by Affirmer are waived, abandoned,
-    surrendered, licensed or otherwise affected by this document.
- b. Affirmer offers the Work as-is and makes no representations or
-    warranties of any kind concerning the Work, express, implied,
-    statutory or otherwise, including without limitation warranties of
-    title, merchantability, fitness for a particular purpose, non
-    infringement, or the absence of latent or other defects, accuracy, or
-    the present or absence of errors, whether or not discoverable, all to
-    the greatest extent permissible under applicable law.
- c. Affirmer disclaims responsibility for clearing rights of other persons
-    that may apply to the Work or any use thereof, including without
-    limitation any person's Copyright and Related Rights in the Work.
-    Further, Affirmer disclaims responsibility for obtaining any necessary
-    consents, permissions or other rights required for any use of the
-    Work.
- d. Affirmer understands and acknowledges that Creative Commons is not a
-    party to this document and has no duty or obligation with respect to
-    this CC0 or use of the Work.
+MIT License
+
+Copyright (c) 2024 Overrun Organization
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 22c9291..f115f14 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,46 @@
-# Example Project
+# Memory stack
 
-This is an example project.
+Memory stack for FFM API.
 
-You can change the options in gradle.properties and build.gradle.kts.
+## Overview
 
-You can use this template by clicking "Use this template" or download ZIP.
+```java
+void main() {
+    // push a frame of the memory stack stored with thread-local variable
+    try (var stack = MemoryStack.pushLocal()) {
+        // allocate using methods in SegmentAllocator
+        // you should initialize the allocated memory segment at once, either by fill((byte)0) or C functions
+        var segment = stack.allocate(ValueLayout.JAVA_INT);
+        // pass to C functions
+        storeToPointer(segment);
+        // access the memory segment
+        readData(segment.get(ValueLayout.JAVA_INT, 0L));
+    }
+    // the memory stack automatically pops with try-with-resources statement
+}
+```
+
+This is equivalent to C code:
+
+```c
+void storeToPointer(int* p) { *p = ...; }
+void readData(int i);
+
+int main() {
+    int i;
+    storeToPointer(&i);
+    readData(i);
+}
+```
+
+## Download
+
+Maven coordinate: `io.github.over-run:memstack:VERSION`
+
+Gradle:
+
+```groovy
+dependencies {
+    implementation("io.github.over-run:memstack:0.1.0")
+}
+```
diff --git a/build.gradle.kts b/build.gradle.kts
index 542c2dc..6566e2d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -33,7 +33,7 @@ val jdkEarlyAccessDoc: String? by rootProject
 val targetJavaVersion = jdkVersion.toInt()
 
 val projDevelopers = arrayOf(
-    Developer("example")
+    Developer("squid233")
 )
 
 data class Organization(
@@ -85,7 +85,7 @@ repositories {
 }
 
 dependencies {
-    // add your dependencies
+    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
 }
 
 tasks.withType<JavaCompile> {
@@ -98,6 +98,7 @@ tasks.withType<JavaCompile> {
 
 tasks.withType<Test> {
     if (jdkEnablePreview.toBoolean()) jvmArgs("--enable-preview")
+    useJUnitPlatform()
 }
 
 java {
@@ -115,6 +116,7 @@ tasks.withType<Javadoc> {
         encoding = "UTF-8"
         locale = "en_US"
         windowTitle = "$projName $projVersion Javadoc"
+        jFlags("-Duser.language=en")
         if (this is StandardJavadocDocletOptions) {
             charSet = "UTF-8"
             isAuthor = true
diff --git a/gradle.properties b/gradle.properties
index 49cf0d1..4dbf430 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,33 +1,33 @@
 # Gradle Options
 org.gradle.jvmargs=-Dfile.encoding=UTF-8
 
-hasPublication=false
-publicationSigning=false
+hasPublication=true
+publicationSigning=true
 
-hasJavadocJar=false
-hasSourcesJar=false
+hasJavadocJar=true
+hasSourcesJar=true
 
 # Project Information
-projGroupId=org.example
-projArtifactId=example
+projGroupId=io.github.over-run
+projArtifactId=memstack
 # The project name should only contain lowercase letters, numbers and hyphen.
-projName=project-template
-projVersion=0.1.0-SNAPSHOT
-projDesc=An example project.
+projName=memstack
+projVersion=0.1.0
+projDesc=Memory stack for FFM API
 # Uncomment them if you want to publish to maven repository.
-#projUrl=https://github.com/Over-Run/project-template
-#projLicenseUrl=https://raw.githubusercontent.com/Over-Run/project-template/main/LICENSE
-#projScmConnection=scm:git:https://github.com/Over-Run/project-template.git
-#projScmUrl=https://github.com/Over-Run/project-template.git
-projLicense=CC0-1.0
+projUrl=https://github.com/Over-Run/memstack
+projLicenseUrl=https://raw.githubusercontent.com/Over-Run/memstack/main/LICENSE
+projScmConnection=scm:git:https://github.com/Over-Run/memstack.git
+projScmUrl=https://github.com/Over-Run/memstack.git
+projLicense=MIT
 projLicenseFileName=LICENSE
 
 # Organization Information
-orgName=Example
-orgUrl=https://example.org/
+orgName=Overrun Organization
+orgUrl=https://over-run.github.io/
 
 # JDK Options
-jdkVersion=21
+jdkVersion=22
 jdkEnablePreview=false
 # javadoc link of JDK early access build
 # https://download.java.net/java/early_access/$jdkEarlyAccessDoc/docs/api/
diff --git a/src/main/java/io/github/overrun/memstack/DefaultMemoryStack.java b/src/main/java/io/github/overrun/memstack/DefaultMemoryStack.java
new file mode 100644
index 0000000..f68dd54
--- /dev/null
+++ b/src/main/java/io/github/overrun/memstack/DefaultMemoryStack.java
@@ -0,0 +1,90 @@
+package io.github.overrun.memstack;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * The default implementation of {@link MemoryStack}.
+ *
+ * @author squid233
+ * @since 0.1.0
+ */
+public class DefaultMemoryStack implements MemoryStack {
+    private final MemorySegment segment;
+    private final long[] frames;
+    private long offset = 0L;
+    private int frameIndex = 0;
+
+    /**
+     * Creates the default memory stack with the given segment and frame count.
+     *
+     * @param segment    the memory segment
+     * @param frameCount the frame count
+     */
+    public DefaultMemoryStack(MemorySegment segment, int frameCount) {
+        this.segment = segment;
+        this.frames = new long[frameCount];
+    }
+
+    private MemorySegment trySlice(long byteSize, long byteAlignment) {
+        long min = segment.address();
+        long start = ((min + offset + byteAlignment - 1) & -byteAlignment) - min;
+        MemorySegment slice = segment.asSlice(start, byteSize, byteAlignment);
+        offset = start + byteSize;
+        return slice;
+    }
+
+    @Override
+    public MemorySegment allocate(long byteSize, long byteAlignment) {
+        if (byteSize < 0) {
+            throw new IllegalArgumentException("The provided allocation size is negative: " + byteSize);
+        }
+        if (byteAlignment <= 0 || ((byteAlignment & (byteAlignment - 1)) != 0L)) {
+            throw new IllegalArgumentException("Invalid alignment constraint: " + byteAlignment);
+        }
+        return trySlice(byteSize, byteAlignment);
+    }
+
+    @Override
+    public MemoryStack push() {
+        if (frameIndex >= frames.length) {
+            throw new IndexOutOfBoundsException("stack frame overflow; max frame count: " + frames.length);
+        }
+        frames[frameIndex] = offset;
+        frameIndex++;
+        return this;
+    }
+
+    @Override
+    public void pop() {
+        if (frameIndex <= 0) {
+            throw new IndexOutOfBoundsException("stack frame underflow");
+        }
+        frameIndex--;
+        offset = frames[frameIndex];
+    }
+
+    @Override
+    public int frameCount() {
+        return frames.length;
+    }
+
+    @Override
+    public int frameIndex() {
+        return frameIndex;
+    }
+
+    @Override
+    public long stackPointer() {
+        return offset;
+    }
+
+    @Override
+    public void setPointer(long pointer) {
+        offset = pointer;
+    }
+
+    @Override
+    public MemorySegment segment() {
+        return segment;
+    }
+}
diff --git a/src/main/java/io/github/overrun/memstack/MemoryStack.java b/src/main/java/io/github/overrun/memstack/MemoryStack.java
new file mode 100644
index 0000000..3c4d1d7
--- /dev/null
+++ b/src/main/java/io/github/overrun/memstack/MemoryStack.java
@@ -0,0 +1,184 @@
+package io.github.overrun.memstack;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.SegmentAllocator;
+
+/**
+ * <h2>Memory stack</h2>
+ * Memory stack is backed with a {@linkplain MemorySegment memory segment}.
+ * Each allocation returns a slice of the segment starts at the current offset
+ * (modulo additional padding to satisfy alignment constraint),
+ * with the given size.
+ * <p>
+ * It extends {@link SegmentAllocator}, which allows allocating from the given data.
+ * <p>
+ * It does not extend {@link Arena} since the memory stack is not supposed to be a long-alive arena allocator.
+ * The stack itself does not bind to any segment scope;
+ * it just slices the backing segment.
+ * <p>
+ * Memory stack is not thread-safe;
+ * consider using the {@linkplain #ofLocal() local stacks} to manage with threads.
+ * <h3>Push and pop</h3>
+ * It remembers the current offset when {@link #push()} is called,
+ * then it resets to the previous offset when {@link #pop()} is called.
+ * <p>
+ * It extends {@link AutoCloseable} to allow using try-with-resources statement to call {@code pop} automatically.
+ * <p>
+ * The push and pop operations must be symmetric.
+ * <p>
+ * Using memory stack without push and pop operations
+ * has the same effect as {@linkplain SegmentAllocator#slicingAllocator(MemorySegment) slicing allocator}.
+ *
+ * @author squid233
+ * @since 0.1.0
+ */
+public interface MemoryStack extends SegmentAllocator, AutoCloseable {
+    /**
+     * Creates a default memory stack backed with the given memory segment and {@linkplain #frameCount() frame count}.
+     *
+     * @param segment    the memory segment to be sliced
+     * @param frameCount the frame count of the memory stack
+     * @return a new memory stack
+     * @throws IllegalArgumentException if {@code segment} is {@linkplain MemorySegment#isReadOnly() read-only}
+     *                                  or {@code frameCount <= 0}
+     */
+    static MemoryStack of(MemorySegment segment, int frameCount) {
+        assertWritable(segment);
+        checkSize(frameCount, "invalid frame count");
+        return new DefaultMemoryStack(segment, frameCount);
+    }
+
+    /**
+     * Creates a memory stack,
+     * backed with a memory segment allocated with an {@linkplain Arena#ofAuto() auto arena} and the given size,
+     * with the given {@linkplain #frameCount() frame count}.
+     *
+     * @param byteSize   the size of the memory segment
+     * @param frameCount the frame count of the memory stack
+     * @return a new memory stack
+     * @throws IllegalArgumentException if {@code segment} is {@linkplain MemorySegment#isReadOnly() read-only},
+     *                                  {@code byteSize <= 0} or {@code frameCount <= 0}
+     * @see #of(MemorySegment, int)
+     */
+    static MemoryStack of(long byteSize, int frameCount) {
+        checkSize(byteSize, "invalid stack size");
+        return of(Arena.ofAuto().allocate(byteSize), frameCount);
+    }
+
+    /**
+     * Creates a memory stack with the default size and {@linkplain #frameCount() frame count}.
+     *
+     * @return a new memory stack
+     * @see #of(long, int)
+     * @see StackConfigurations#STACK_SIZE
+     * @see StackConfigurations#FRAME_COUNT
+     */
+    static MemoryStack of() {
+        return of(StackConfigurations.STACK_SIZE.get(), StackConfigurations.FRAME_COUNT.get());
+    }
+
+    /**
+     * {@return the memory stack for the current thread}
+     *
+     * @see #of()
+     */
+    static MemoryStack ofLocal() {
+        class Holder {
+            static final ThreadLocal<MemoryStack> TLS = ThreadLocal.withInitial(MemoryStack::of);
+        }
+        return Holder.TLS.get();
+    }
+
+    /**
+     * Calls {@link #push()} of the {@linkplain #ofLocal() local memory stack}.
+     *
+     * @return the local memory stack
+     */
+    static MemoryStack pushLocal() {
+        return ofLocal().push();
+    }
+
+    /**
+     * Calls {@link #pop()} of the {@linkplain #ofLocal() local memory stack}.
+     */
+    static void popLocal() {
+        ofLocal().pop();
+    }
+
+    private static void assertWritable(MemorySegment segment) {
+        if (segment.isReadOnly()) {
+            throw new IllegalArgumentException("read-only segment");
+        }
+    }
+
+    private static void checkSize(long size, String message) {
+        if (size <= 0) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * The returned memory segment is a slice of the {@linkplain #segment() backing segment}
+     * and is not initialized with zero.
+     * <p>
+     * Use {@link MemorySegment#fill(byte) fill((byte)0)} to initialize with zero.
+     *
+     * @throws IndexOutOfBoundsException if there is not enough space to allocate
+     * @throws IllegalArgumentException  if {@code byteSize < 0}, {@code byteAlignment <= 0},
+     *                                   or if {@code byteAlignment} is not a power of 2
+     */
+    @Override
+    MemorySegment allocate(long byteSize, long byteAlignment);
+
+    /**
+     * Remembers the current offset and pushes a frame for next allocations.
+     *
+     * @return {@code this}
+     * @throws IndexOutOfBoundsException if there is not enough frames to push
+     */
+    MemoryStack push();
+
+    /**
+     * Pops to the previous frame and sets the current offset.
+     *
+     * @throws IndexOutOfBoundsException if there is not enough frames to pop
+     */
+    void pop();
+
+    /**
+     * Calls {@link #pop()}.
+     */
+    @Override
+    default void close() {
+        pop();
+    }
+
+    /**
+     * {@return the count of the offsets this stack can store}
+     */
+    int frameCount();
+
+    /**
+     * {@return the current frame index}
+     */
+    int frameIndex();
+
+    /**
+     * {@return the current offset of this stack}
+     */
+    long stackPointer();
+
+    /**
+     * Sets the offset of this stack.
+     *
+     * @param pointer the new offset
+     */
+    void setPointer(long pointer);
+
+    /**
+     * {@return the backing memory segment}
+     */
+    MemorySegment segment();
+}
diff --git a/src/main/java/io/github/overrun/memstack/StackConfigurations.java b/src/main/java/io/github/overrun/memstack/StackConfigurations.java
new file mode 100644
index 0000000..1426612
--- /dev/null
+++ b/src/main/java/io/github/overrun/memstack/StackConfigurations.java
@@ -0,0 +1,52 @@
+package io.github.overrun.memstack;
+
+/**
+ * The configurations of memory stack.
+ *
+ * @author squid233
+ * @since 0.1.0
+ */
+public final class StackConfigurations {
+    /**
+     * The default stack size in bytes.
+     * Default value: 65536 (64 KiB)
+     */
+    public static final Entry<Long> STACK_SIZE = new Entry<>(64L * 1024);
+    /**
+     * The default {@linkplain MemoryStack#frameCount() frame count} for a memory stack.
+     * Default value: 8
+     */
+    public static final Entry<Integer> FRAME_COUNT = new Entry<>(8);
+
+    private StackConfigurations() {
+    }
+
+    /**
+     * A configuration entry
+     *
+     * @param <T> the type of the value
+     */
+    public static final class Entry<T> {
+        private T value;
+
+        private Entry(T value) {
+            this.value = value;
+        }
+
+        /**
+         * {@return the value}
+         */
+        public T get() {
+            return value;
+        }
+
+        /**
+         * Sets the value.
+         *
+         * @param value the new value
+         */
+        public void set(T value) {
+            this.value = value;
+        }
+    }
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 0000000..29119be
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,9 @@
+/**
+ * Memory stack
+ *
+ * @author squid233
+ * @since 0.1.0
+ */
+module io.github.overrun.memstack {
+    exports io.github.overrun.memstack;
+}
diff --git a/src/main/java/org/example/Main.java b/src/main/java/org/example/Main.java
deleted file mode 100644
index c34d46a..0000000
--- a/src/main/java/org/example/Main.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.example;
-
-/**
- * An example
- *
- * @author You
- * @since 0.1.0
- */
-public class Main {
-    public static void main(String[] args) {
-        System.out.println("Hello world");
-    }
-}
diff --git a/src/test/java/io/github/overrun/memstack/test/MemoryStackTest.java b/src/test/java/io/github/overrun/memstack/test/MemoryStackTest.java
new file mode 100644
index 0000000..7496963
--- /dev/null
+++ b/src/test/java/io/github/overrun/memstack/test/MemoryStackTest.java
@@ -0,0 +1,109 @@
+package io.github.overrun.memstack.test;
+
+import io.github.overrun.memstack.MemoryStack;
+import io.github.overrun.memstack.StackConfigurations;
+import org.junit.jupiter.api.Test;
+
+import java.lang.foreign.ValueLayout;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+/**
+ * @author squid233
+ * @since 0.1.0
+ */
+public class MemoryStackTest {
+    @Test
+    void testPushAndPop() {
+        MemoryStack local = MemoryStack.ofLocal();
+
+        assertEquals(0L, local.stackPointer());
+
+        local.setPointer(4L);
+        assertEquals(4L, local.stackPointer());
+
+        try {
+            local.push();
+            local.setPointer(8L);
+            assertEquals(8L, local.stackPointer());
+        } finally {
+            local.pop();
+        }
+        assertEquals(4L, local.stackPointer());
+
+        // try-with-resources
+        try (var stack = local.push()) {
+            stack.setPointer(8L);
+            assertEquals(8L, stack.stackPointer());
+        }
+        assertEquals(4L, local.stackPointer());
+
+        // static methods
+        try {
+            MemoryStack stack = MemoryStack.pushLocal();
+            stack.setPointer(8L);
+            assertEquals(8L, stack.stackPointer());
+        } finally {
+            MemoryStack.popLocal();
+        }
+        assertEquals(4L, local.stackPointer());
+
+        try (MemoryStack stack = MemoryStack.pushLocal()) {
+            stack.setPointer(8L);
+            assertEquals(8L, stack.stackPointer());
+        }
+        assertEquals(4L, local.stackPointer());
+    }
+
+    @Test
+    void testAllocate() {
+        assertEquals(0L, MemoryStack.ofLocal().stackPointer());
+        try (MemoryStack stack = MemoryStack.pushLocal()) {
+            stack.allocate(ValueLayout.JAVA_INT);
+            assertEquals(4L, stack.stackPointer());
+
+            stack.allocate(ValueLayout.JAVA_INT);
+            assertEquals(8L, stack.stackPointer());
+
+            stack.allocate(ValueLayout.JAVA_BYTE);
+            assertEquals(9L, stack.stackPointer());
+
+            try (MemoryStack stack1 = MemoryStack.pushLocal()) {
+                stack1.allocate(ValueLayout.JAVA_INT);
+                assertEquals(16L, stack1.stackPointer());
+            }
+            assertEquals(9L, stack.stackPointer());
+
+            stack.allocate(ValueLayout.JAVA_INT);
+            assertEquals(16L, stack.stackPointer());
+
+            stack.allocate(ValueLayout.JAVA_LONG);
+            assertEquals(24L, stack.stackPointer());
+
+            stack.allocate(ValueLayout.JAVA_INT);
+            assertEquals(28L, stack.stackPointer());
+        }
+        assertEquals(0L, MemoryStack.ofLocal().stackPointer());
+    }
+
+    @Test
+    void testOverflow() {
+        MemoryStack stack = MemoryStack.of();
+        for (int i = 0; i < stack.frameCount(); i++) {
+            stack.push();
+        }
+        assertThrowsExactly(IndexOutOfBoundsException.class, stack::push);
+    }
+
+    @Test
+    void testUnderflow() {
+        assertThrowsExactly(IndexOutOfBoundsException.class, MemoryStack.of()::pop);
+    }
+
+    @Test
+    void testOutOfMemory() {
+        assertThrowsExactly(IndexOutOfBoundsException.class, () ->
+            MemoryStack.of().allocate(StackConfigurations.STACK_SIZE.get() + 1));
+    }
+}