Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 172 additions & 92 deletions spec/arena_spec.rb
Original file line number Diff line number Diff line change
@@ -1,139 +1,219 @@
require 'rspec'
require_relative '../src/arena'
require_relative '../src/value'
require_relative '../src/types' # Needed for FluxObject

# A helper class to track poisoning status without complex logic
class TestObject < FluxObject
def initialize(register: true)
super(register: register)
@poisoned = false
end
end

RSpec.describe Arena do
before(:each) do
Arena.reset!
end

describe "Registration & Lookup" do
it "registers an object and finds it by masked address" do
obj = TestObject.new
context "Nested Stack Frames" do
it "destroys all objects on a rewind with no promotion" do
# 1. Start Mark
start_mark = Arena.current.mark
expect(start_mark).to eq(0)

# 2. Allocate Real Objects
str1 = FluxString.new("First", register: true)
str2 = FluxString.new("Second", register: true)

# Calculate expected address using the mask
expected_addr = obj.flux_id & Arena::ID_MASK
expect(str1.address).to eq(0)
expect(str2.address).to eq(1)
expect(Arena.current.mark).to eq(2)

found = Arena.current.find_object_by_address(expected_addr)
expect(found).to eq(obj)
# 3. Rewind (End of Scope)
Arena.current.rewind(start_mark)

expect(Arena.current.mark).to eq(0)
end

it "does not find objects that were never registered" do
obj = TestObject.new(register: false)
addr = obj.flux_id & Arena::ID_MASK
it "overwrites 'dead' objects when new ones are allocated" do
# 1. Allocate a "Ghost"
ghost_str = FluxString.new("I will die", register: true)
expect(ghost_str.address).to eq(0)

# 2. Rewind (Ghost is now in the dead zone)
Arena.current.rewind(0)

# 3. Allocate a New Object
new_str = FluxString.new("I am new", register: true)

# 4. Verify Overwrite (Address Reuse)
expect(new_str.address).to eq(0)

found = Arena.current.find_object_by_address(addr)
expect(found).to be_nil
# Verify the slot in memory points to the new object
expect(Arena.current.memory[0]).to eq(new_str)
expect(Arena.current.memory[0].to_s).to eq("I am new")
end

it "promotes an object correctly across stack frames" do
frame_start_mark = Arena.current.mark # 0

# 1. Allocate "Local" Object in Frame
local_list = FluxArray.new(10, nil, type: :int64)
local_list[0] = Value.box_number(100)
local_list[1] = Value.box_number(220)
local_list[2] = Value.box_number(333)

expect(local_list.address).to eq(0)
expect(Arena.current.mark).to eq(1) # [local_list]

# 2. PROMOTE
survivor = Arena.current.promote(local_list, frame_start_mark)
expect(survivor).to be_a(FluxArray)
expect(survivor.object_id).to_not eq(local_list.object_id)

# 3. Verify Address Reuse
# Step A: Rewind moved cursor to 0.
# Step B: Registering the survivor moved cursor back to 1.
# Therefore, the survivor effectively "stole" the slot of the dead object.
expect(Arena.current.mark).to eq(1)
expect(survivor.address).to eq(0)

# 6. Verify Data Integrity
# Check that the deep copy actually worked.
expect(survivor.read_at(0)).to eq(100)
expect(survivor.read_at(1)).to eq(220)
expect(survivor.read_at(2)).to eq(333)
end
end

describe "Static Registration (Constants)" do
it "registers static objects that persist across resets" do
# 1. Register a static object
static_obj = TestObject.new(register: :static)
static_addr = static_obj.flux_id & Arena::ID_MASK
it "promotes a Nexted object (2D array) correctly across stack frames" do
frame_start_mark = Arena.current.mark # 0

# 2. Reset the Arena (Simulate new VM run)
Arena.reset!
# INNER LIST: A packed NanBox array [10.5, 20]
# Allocated at Address 0
inner_list = FluxArray.new(5, nil, type: :nanbox)
inner_list[0] = Value.box_number(10.5)
inner_list[1] = Value.box_number(20)

expect(inner_list.address).to eq(0)

# OUTER LIST: A standard Object array containing a pointer to inner_list
# Allocated at Address 1
# Note: Outer list MUST be :obj type to store Heap Pointers
outer_list = FluxArray.new(1, nil, type: :obj)
outer_list[0] = Value.box_obj(inner_list)

expect(outer_list.address).to eq(1)
expect(Arena.current.mark).to eq(2) # [inner, outer]

# 3. It should still be found
found = Arena.current.find_object_by_address(static_addr)
expect(found).to eq(static_obj)
expect(found.is_alive).to be true
# PROMOTE
survivor = Arena.current.promote(outer_list, frame_start_mark)
expect(survivor).to be_a(FluxArray)

# Verify Address Reuse
expect(survivor.object_id).to_not eq(outer_list.object_id)
expect(Arena.current.mark).to eq(2)
expect(survivor.address).to eq(0)

# Verify Child
boxed_child = survivor[0]
survivor_inner = Value.as_obj(boxed_child)
expect(survivor_inner.address).to eq(1)
expect(survivor_inner.object_id).to_not eq(inner_list.object_id)


# Verify Data Integrity - Check that the deep copy actually worked.
expect(survivor_inner.read_at(0)).to eq(10.5)
expect(survivor_inner.read_at(1)).to eq(20)
end
end

describe "Stack Lifecycle (Mark & Rewind)" do
it "poisons objects allocated after the mark" do
# 1. Base level object
root_obj = TestObject.new

# 2. Mark the stack (Start of function)
it "promotes a FluxString correctly" do
mark = Arena.current.mark

# 3. Allocate 'local' objects
local_obj = TestObject.new

# 4. Rewind (End of function)
Arena.current.rewind(mark)
# 1. Allocate
local_str = FluxString.new("Hello World", register: true)
expect(local_str.address).to eq(0)

# Root should be alive
expect(root_obj.is_alive?).to be(true)
expect(Arena.current.find_object_by_address(root_obj.flux_id & Arena::ID_MASK)).to eq(root_obj)
# 2. Promote
survivor = Arena.current.promote(local_str, mark)

# Local should be dead and removed from registry
expect(local_obj.is_poisoned?).to be(true)
expect(Arena.current.find_object_by_address(local_obj.flux_id & Arena::ID_MASK)).to be_nil
# 3. Verify
expect(survivor.to_s).to eq("Hello World")
expect(survivor.address).to eq(0)
expect(survivor.object_id).to_not eq(local_str.object_id)
end

it "handles nested stack frames correctly" do
mark1 = Arena.current.mark
obj1 = TestObject.new
it "discards objects that were NOT promoted" do
start_mark = Arena.current.mark

# 1. Allocate Survivor (String)
survivor_source = FluxString.new("Survivor", register: true)

# 2. Allocate Victim (Array)
victim = FluxArray.new(10, nil, type: :int64)

mark2 = Arena.current.mark
obj2 = TestObject.new
expect(Arena.current.mark).to eq(2) # [String, Array]

# Rewind inner frame
Arena.current.rewind(mark2)
expect(obj2.is_poisoned?).to be(true)
expect(obj1.is_alive?).to be(true)
# 3. Promote only the String
final_obj = Arena.current.promote(survivor_source, start_mark)

# Rewind outer frame
Arena.current.rewind(mark1)
expect(obj1.is_poisoned?).to be(true)
# 4. Assertions
# Cursor should be back at 1 (Only final_obj exists)
expect(Arena.current.mark).to eq(1)

# Slot 0 should hold the survivor
expect(Arena.current.memory[0]).to eq(final_obj)
expect(final_obj.to_s).to eq("Survivor")

# Slot 1 is now free/garbage.
# (If we allocate now, it should take slot 1)
next_obj = FluxString.new("Next", register: true)
expect(next_obj.address).to eq(1)
end
end

describe "Promotion (RVO)" do
it "protects promoted objects from being poisoned during rewind" do
mark = Arena.current.mark
describe "Static Registration" do
it "registers static strings with High Bit set" do
Arena.reset!

# Allocate object in this scope
result_obj = TestObject.new
garbage_obj = TestObject.new
# 1. Register Static String (e.g. Constant Pool)
static_str = FluxString.new("Global Constant", register: :static)

# Promote the result
Arena.current.promote(result_obj)
# Check address format (High bit 47 set)
expected_mask = (1 << 47)
expect(static_str.address & expected_mask).to eq(expected_mask)

# Rewind
Arena.current.rewind(mark)
# 2. Verify Lookup
found = Arena.current[static_str.address]
expect(found).to eq(static_str)
expect(found.to_s).to eq("Global Constant")
end

it "persists static objects across Arena.reset!" do
static_str = FluxString.new("Persistent", register: :static)
addr = static_str.address

# Result should survive
expect(result_obj.is_alive?).to be(true)
expect(result_obj.is_alive?).to be(true)
# Registry lookup should still work
expect(Arena.current.find_object_by_address(result_obj.flux_id & Arena::ID_MASK)).to eq(result_obj)
# Wipe Heap
Arena.reset!
expect(Arena.current.mark).to eq(0)

# Garbage should be dead
expect(garbage_obj.is_poisoned?).to be(true)
# Static lookup still works
found = Arena.current[addr]
expect(found).to eq(static_str)
end
end

describe "Masking Logic" do
it "handles object_ids larger than the mask correctly" do
# This is harder to mock without stubbing object_id,
# but we can verify the mask constant exists and is applied.
describe "Views (MemorySlice)" do
it "Run-time Errors if attempted to promote" do
frame_mark = Arena.current.mark

obj = TestObject.new
raw_id = obj.flux_id
masked_id = raw_id & Arena::ID_MASK
# 1. Create a String on the Heap
str_obj = FluxString.new("Hello World", register: true)

# Verify internal storage uses masked key
internal_map = Arena.current.instance_variable_get(:@live_objects)
expect(internal_map.key?(masked_id)).to be true
# 2. Create a View (Slice) pointing to it
# Slices are usually registers (untracked), so we don't register this one
# (or we do, doesn't matter for this test logic).
slice = MemorySlice.new(str_obj, 0, 5)

# If the ID was small enough, masked == raw
# If large, masked != raw. Both should be consistent.
found = Arena.current.find_object_by_address(masked_id)
expect(found).to eq(obj)
# 3. Promote the Slice
# This forces the Arena to look at slice.container (str_obj)
# and Deep Copy it too.
expect {
Arena.current.promote(slice, frame_mark)
}.to raise_error(RuntimeError, /Dangling Pointer/)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/memory_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
it "raises Segfault/MemoryError when unboxing a poisoned (dead) object" do
# 1. Create object
mark = Arena.current.mark
obj = FluxString.new("Temporary")
obj = FluxString.new("Temporary", register: true)
boxed = Value.box_obj(obj)

# 2. Kill it (Rewind)
Expand Down
Loading