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 { @@ -98,6 +98,7 @@ tasks.withType { tasks.withType { if (jdkEnablePreview.toBoolean()) jvmArgs("--enable-preview") + useJUnitPlatform() } java { @@ -115,6 +116,7 @@ tasks.withType { 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; + +/** + *

Memory stack

+ * 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. + *

+ * It extends {@link SegmentAllocator}, which allows allocating from the given data. + *

+ * 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. + *

+ * Memory stack is not thread-safe; + * consider using the {@linkplain #ofLocal() local stacks} to manage with threads. + *

Push and pop

+ * It remembers the current offset when {@link #push()} is called, + * then it resets to the previous offset when {@link #pop()} is called. + *

+ * It extends {@link AutoCloseable} to allow using try-with-resources statement to call {@code pop} automatically. + *

+ * The push and pop operations must be symmetric. + *

+ * 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 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. + *

+ * 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 STACK_SIZE = new Entry<>(64L * 1024); + /** + * The default {@linkplain MemoryStack#frameCount() frame count} for a memory stack. + * Default value: 8 + */ + public static final Entry FRAME_COUNT = new Entry<>(8); + + private StackConfigurations() { + } + + /** + * A configuration entry + * + * @param the type of the value + */ + public static final class Entry { + 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)); + } +}