Skip to content

ND-500 ENTB (Enter Block) - Complete Implementation Guide

Table of Contents

Quick Reference

ENTB allocates a local data area from the heap using buddy allocation and enters a subroutine.

Instruction Format:

ENTB <log size/r/BY>
Hex: 0x0BD | Octal: 275

What it does: 1. Allocates block of size 2^logSize words from heap 2. Sets B register to allocated block address 3. Initializes stack frame (like ENTS) 4. Stores log size in B.LOG 5. Triggers STO trap if no memory available


1. Heap Structure and Memory Layout

Heap Variables (pointed to by TOS register)

The TOS register points to a structure containing heap management variables:

Offset  | Field        | Description
--------|--------------|------------------------------------------
+0      | MAXL         | Maximum log size of allocatable blocks
+1      | STAH         | Start address of heap (not used by CPU)
+2      | ENDH         | End address of heap (not used by CPU)
+3      | FLOG0        | Free list for 2^0 = 1 word blocks
+4      | FLOG1        | Free list for 2^1 = 2 word blocks
+5      | FLOG2        | Free list for 2^2 = 4 word blocks
+6      | FLOG3        | Free list for 2^3 = 8 word blocks
...     | ...          | ...
+3+n    | FLOG[n]      | Free list for 2^n word blocks
...     | ...          | ...
+3+MAXL | FLOG[MAXL]   | Free list for largest blocks

Important Notes: - STAH and ENDH are reserved for user trap handlers (CPU doesn't use them) - Each FLOG[n] is a pointer to the head of the free list for that size - Value of 0 means the list is empty

Free Block Format

Each free block in the heap has this structure:

Block Address → [NEXT]  // Address of next free block (0 = end of list)
                [...]   // Rest of block (unused)

Key Points: - Only the FIRST word contains the next pointer - Rest of the block is unused (available for allocation) - When allocated, entire block is given to program


2. ENTB Algorithm (Step by Step)

Step 1: Read Heap Variables

uint tosAddr = cpu.TOS;
ushort maxl = memory.ReadWord(tosAddr + 0);      // MAXL
ushort stah = memory.ReadWord(tosAddr + 1);      // STAH (optional)
ushort endh = memory.ReadWord(tosAddr + 2);      // ENDH (optional)

Step 2: Validate Log Size

byte logSize = GetLogSizeOperand(); // from instruction operand

// Check if requested size exceeds maximum
if (logSize > maxl)
{
    cpu.RaiseTrap(TrapType.StackOverflow); // STO trap
    return;
}

Step 3: Search Free Lists for Available Block

Try to find a block of the requested size or larger:

uint allocatedBlock = 0;
byte actualLogSize = logSize;

// Search from requested size up to MAXL
for (byte searchLog = logSize; searchLog <= maxl; searchLog++)
{
    uint flogAddr = tosAddr + 3 + searchLog;
    uint blockAddr = memory.ReadWord(flogAddr);

    if (blockAddr != 0)
    {
        // Found a block!
        allocatedBlock = blockAddr;
        actualLogSize = searchLog;

        // Unlink from free list
        uint nextBlock = memory.ReadWord(blockAddr); // Read NEXT pointer
        memory.WriteWord(flogAddr, (ushort)nextBlock); // Update list head
        break;
    }
}

// If no block found, raise STO trap
if (allocatedBlock == 0)
{
    cpu.RaiseTrap(TrapType.StackOverflow);
    return;
}

Step 4: Split Block if Necessary (Buddy System)

If we found a larger block than needed, split it:

// Split larger blocks down to requested size
while (actualLogSize > logSize)
{
    actualLogSize--; // Reduce by one power of 2

    uint blockSize = (uint)(1 << actualLogSize); // 2^actualLogSize
    uint buddyAddr = allocatedBlock + blockSize; // Second half

    // Add buddy to appropriate free list
    uint flogAddr = tosAddr + 3 + actualLogSize;
    uint currentHead = memory.ReadWord(flogAddr);

    memory.WriteWord(buddyAddr, (ushort)currentHead); // buddy.NEXT = old head
    memory.WriteWord(flogAddr, (ushort)buddyAddr);    // FLOG[n] = buddy
}

// Now allocatedBlock is exactly 2^logSize words

Step 5: Initialize Stack Frame (Like ENTS)

uint blockAddr = allocatedBlock;
uint oldB = cpu.B;

// B.PREVB (offset +0) - previous B register
memory.WriteWord(blockAddr + 0, (ushort)oldB);

// B.RETA (offset +1) - return address from L register
memory.WriteWord(blockAddr + 1, (ushort)cpu.L);

// B.SP (offset +2) - copy from old stack frame
if (oldB != 0)
{
    ushort oldSP = memory.ReadWord(oldB + 2);
    memory.WriteWord(blockAddr + 2, oldSP);
}
else
{
    memory.WriteWord(blockAddr + 2, 0);
}

// B.N (offset +10) - number of arguments (from descriptor if any)
// This would come from CALL instruction
ushort numArgs = 0; // Get from call context
memory.WriteWord(blockAddr + 10, numArgs);

// B.LOG (offset +11) - store LOG SIZE (not block size!)
memory.WriteWord(blockAddr + 11, logSize);

// B.ARG (offset +12 onwards) - copy argument addresses
// Copy from call instruction context

Step 6: Update CPU Registers

// Set B register to allocated block
cpu.B = blockAddr;

// Update L register (RETA was already copied above)
cpu.L = cpu.P; // Current instruction as return address
cpu.P = targetAddress; // Jump to subroutine (from CALL/operand)

3. RETB / RETBK - Return and Free Block

When returning from an ENTB subroutine, the block must be freed:

RETB Algorithm

public void ExecuteRETB(bool setK)
{
    uint blockAddr = cpu.B;

    // Read log size from B.LOG
    byte logSize = (byte)memory.ReadWord(blockAddr + 11);

    // Read return information
    uint prevB = memory.ReadWord(blockAddr + 0); // B.PREVB
    uint retAddr = memory.ReadWord(blockAddr + 1); // B.RETA

    // Free the block back to heap
    FreeBlock(blockAddr, logSize);

    // Restore registers
    cpu.B = prevB;
    cpu.P = retAddr;
    cpu.L = retAddr;

    // Set/clear K flag
    if (setK)
        cpu.STATUS.K = true;
    else
        cpu.STATUS.K = false;
}

Free Block Algorithm (FREEB)

public void FreeBlock(uint blockAddr, byte logSize)
{
    uint tosAddr = cpu.TOS;
    ushort maxl = memory.ReadWord(tosAddr + 0);

    // Validate log size
    if (logSize > maxl)
    {
        throw new InvalidOperationException($"Invalid log size {logSize}");
    }

    // Get current head of free list
    uint flogAddr = tosAddr + 3 + logSize;
    uint currentHead = memory.ReadWord(flogAddr);

    // Link block to head of free list
    memory.WriteWord(blockAddr, (ushort)currentHead); // block.NEXT = old head
    memory.WriteWord(flogAddr, (ushort)blockAddr);     // FLOG[n] = block

    // Note: Blocks are NOT combined (buddy merging)
    // That's done by user trap handler for STO trap
}

4. Complete C# Implementation

EntbInstruction.cs

using System;

namespace ND500.CPU.Instructions
{
    /// <summary>
    /// ENTB - Enter subroutine with buddy allocation
    /// Allocates local data area from heap using buddy system
    /// Opcode: 0x0BD (275 octal)
    /// </summary>
    public class EntbInstruction : IInstruction
    {
        private readonly ND500CPU _cpu;
        private readonly IMemory _memory;

        public EntbInstruction(ND500CPU cpu, IMemory memory)
        {
            _cpu = cpu;
            _memory = memory;
        }

        public void Execute(InstructionContext context)
        {
            // Get log size operand
            byte logSize = context.GetByteOperand();

            // Get target address for subroutine
            uint targetAddress = context.TargetAddress;

            // Allocate block from heap
            uint blockAddr = AllocateBlock(logSize);

            if (blockAddr == 0)
            {
                // No memory available - STO trap
                _cpu.RaiseTrap(TrapType.StackOverflow);
                return;
            }

            // Initialize stack frame
            InitializeStackFrame(blockAddr, logSize, targetAddress, context);

            // Update CPU registers
            _cpu.B = blockAddr;
            _cpu.P = targetAddress;
        }

        private uint AllocateBlock(byte logSize)
        {
            uint tosAddr = _cpu.TOS;

            // Read MAXL
            ushort maxl = _memory.ReadWord(tosAddr + 0);

            // Validate log size
            if (logSize > maxl)
            {
                return 0; // Will trigger STO trap
            }

            // Search for available block (requested size or larger)
            uint allocatedBlock = 0;
            byte actualLogSize = logSize;

            for (byte searchLog = logSize; searchLog <= maxl; searchLog++)
            {
                uint flogAddr = tosAddr + 3 + searchLog;
                uint blockAddr = _memory.ReadWord(flogAddr);

                if (blockAddr != 0)
                {
                    // Found a block - unlink from free list
                    uint nextBlock = _memory.ReadWord(blockAddr);
                    _memory.WriteWord(flogAddr, (ushort)nextBlock);

                    allocatedBlock = blockAddr;
                    actualLogSize = searchLog;
                    break;
                }
            }

            if (allocatedBlock == 0)
            {
                return 0; // No block found
            }

            // Split block if necessary (buddy system)
            while (actualLogSize > logSize)
            {
                actualLogSize--;

                uint blockSize = (uint)(1 << actualLogSize); // 2^actualLogSize
                uint buddyAddr = allocatedBlock + blockSize;

                // Add buddy to free list
                uint flogAddr = tosAddr + 3 + actualLogSize;
                uint currentHead = _memory.ReadWord(flogAddr);

                _memory.WriteWord(buddyAddr, (ushort)currentHead);
                _memory.WriteWord(flogAddr, (ushort)buddyAddr);
            }

            return allocatedBlock;
        }

        private void InitializeStackFrame(uint blockAddr, byte logSize,
                                          uint targetAddress, InstructionContext context)
        {
            uint oldB = _cpu.B;

            // B.PREVB (offset +0) - previous B register
            _memory.WriteWord(blockAddr + 0, (ushort)oldB);

            // B.RETA (offset +1) - return address
            _memory.WriteWord(blockAddr + 1, (ushort)_cpu.L);

            // B.SP (offset +2) - stack pointer from old frame
            if (oldB != 0)
            {
                ushort oldSP = _memory.ReadWord(oldB + 2);
                _memory.WriteWord(blockAddr + 2, oldSP);
            }
            else
            {
                _memory.WriteWord(blockAddr + 2, 0);
            }

            // B.N (offset +10) - number of arguments
            ushort numArgs = context.NumArguments;
            _memory.WriteWord(blockAddr + 10, numArgs);

            // B.LOG (offset +11) - CRITICAL: Store log size, not block size!
            _memory.WriteWord(blockAddr + 11, logSize);

            // B.ARG (offset +12 onwards) - copy argument pointers
            for (int i = 0; i < numArgs; i++)
            {
                uint argAddr = context.GetArgumentAddress(i);
                _memory.WriteWord(blockAddr + 12 + (uint)i, (ushort)argAddr);
            }
        }
    }
}

RetbInstruction.cs

using System;

namespace ND500.CPU.Instructions
{
    /// <summary>
    /// RETB - Return from buddy subroutine
    /// Opcode: 0xFE1C (177034 octal)
    ///
    /// RETBK - Return from buddy subroutine with K flag set
    /// Opcode: 0xFE1D (177035 octal)
    /// </summary>
    public class RetbInstruction : IInstruction
    {
        private readonly ND500CPU _cpu;
        private readonly IMemory _memory;
        private readonly bool _setK;

        public RetbInstruction(ND500CPU cpu, IMemory memory, bool setK)
        {
            _cpu = cpu;
            _memory = memory;
            _setK = setK;
        }

        public void Execute(InstructionContext context)
        {
            uint blockAddr = _cpu.B;

            if (blockAddr == 0)
            {
                _cpu.RaiseTrap(TrapType.StackUnderflow);
                return;
            }

            // Read log size from B.LOG (offset +11)
            byte logSize = (byte)_memory.ReadWord(blockAddr + 11);

            // Read return information
            uint prevB = _memory.ReadWord(blockAddr + 0); // B.PREVB
            uint retAddr = _memory.ReadWord(blockAddr + 1); // B.RETA

            // Free the block back to heap
            FreeBlock(blockAddr, logSize);

            // Restore registers
            _cpu.B = prevB;
            _cpu.P = retAddr;
            _cpu.L = retAddr;

            // Set/clear K flag
            _cpu.STATUS.K = _setK;
        }

        private void FreeBlock(uint blockAddr, byte logSize)
        {
            uint tosAddr = _cpu.TOS;

            // Read MAXL
            ushort maxl = _memory.ReadWord(tosAddr + 0);

            // Validate log size
            if (logSize > maxl)
            {
                throw new InvalidOperationException(
                    $"Invalid log size {logSize} in RETB (MAXL={maxl})");
            }

            // Get free list head for this size
            uint flogAddr = tosAddr + 3 + logSize;
            uint currentHead = _memory.ReadWord(flogAddr);

            // Link block to head of free list
            _memory.WriteWord(blockAddr, (ushort)currentHead); // block.NEXT = old head
            _memory.WriteWord(flogAddr, (ushort)blockAddr);     // FLOG[n] = block

            // Note: Blocks are NOT automatically combined
            // Buddy merging is done by user trap handler for STO trap
        }
    }
}

GetbInstruction.cs

using System;

namespace ND500.CPU.Instructions
{
    /// <summary>
    /// GETB - Get buddy element from heap
    /// Format: Wn GETB <log size/r/BY>
    /// Opcode: 0xFE4C+(n-1) (177114B+(n-1) octal)
    /// </summary>
    public class GetbInstruction : IInstruction
    {
        private readonly ND500CPU _cpu;
        private readonly IMemory _memory;
        private readonly byte _destRegister; // W1-W7

        public GetbInstruction(ND500CPU cpu, IMemory memory, byte destRegister)
        {
            _cpu = cpu;
            _memory = memory;
            _destRegister = destRegister;
        }

        public void Execute(InstructionContext context)
        {
            // Get log size operand
            byte logSize = context.GetByteOperand();

            // Allocate block (same algorithm as ENTB)
            uint blockAddr = AllocateBlock(logSize);

            if (blockAddr == 0)
            {
                _cpu.RaiseTrap(TrapType.StackOverflow);
                return;
            }

            // Store address in destination register
            _cpu.W[_destRegister] = (ushort)blockAddr;
        }

        private uint AllocateBlock(byte logSize)
        {
            uint tosAddr = _cpu.TOS;
            ushort maxl = _memory.ReadWord(tosAddr + 0);

            if (logSize > maxl)
                return 0;

            // Search for available block
            uint allocatedBlock = 0;
            byte actualLogSize = logSize;

            for (byte searchLog = logSize; searchLog <= maxl; searchLog++)
            {
                uint flogAddr = tosAddr + 3 + searchLog;
                uint blockAddr = _memory.ReadWord(flogAddr);

                if (blockAddr != 0)
                {
                    uint nextBlock = _memory.ReadWord(blockAddr);
                    _memory.WriteWord(flogAddr, (ushort)nextBlock);

                    allocatedBlock = blockAddr;
                    actualLogSize = searchLog;
                    break;
                }
            }

            if (allocatedBlock == 0)
                return 0;

            // Split block if necessary
            while (actualLogSize > logSize)
            {
                actualLogSize--;
                uint blockSize = (uint)(1 << actualLogSize);
                uint buddyAddr = allocatedBlock + blockSize;

                uint flogAddr = tosAddr + 3 + actualLogSize;
                uint currentHead = _memory.ReadWord(flogAddr);

                _memory.WriteWord(buddyAddr, (ushort)currentHead);
                _memory.WriteWord(flogAddr, (ushort)buddyAddr);
            }

            return allocatedBlock;
        }
    }
}

FreebInstruction.cs

using System;

namespace ND500.CPU.Instructions
{
    /// <summary>
    /// FREEB - Free buddy element back to heap
    /// Format: FREEB <log size/r/BY>,<element/s/W>
    /// Opcode: 0xFDB6 (176666 octal)
    /// </summary>
    public class FreebInstruction : IInstruction
    {
        private readonly ND500CPU _cpu;
        private readonly IMemory _memory;

        public FreebInstruction(ND500CPU cpu, IMemory memory)
        {
            _cpu = cpu;
            _memory = memory;
        }

        public void Execute(InstructionContext context)
        {
            // Get operands
            byte logSize = context.GetByteOperand();
            uint elementAddr = context.GetWordOperand();

            // Free the block
            FreeBlock(elementAddr, logSize);
        }

        private void FreeBlock(uint blockAddr, byte logSize)
        {
            uint tosAddr = _cpu.TOS;
            ushort maxl = _memory.ReadWord(tosAddr + 0);

            if (logSize > maxl)
            {
                throw new InvalidOperationException(
                    $"Invalid log size {logSize} in FREEB");
            }

            // Get free list head
            uint flogAddr = tosAddr + 3 + logSize;
            uint currentHead = _memory.ReadWord(flogAddr);

            // Link block to head of list
            _memory.WriteWord(blockAddr, (ushort)currentHead);
            _memory.WriteWord(flogAddr, (ushort)blockAddr);
        }
    }
}

5. Heap Initialization Example

Before using ENTB, the heap must be initialized by user code:

public void InitializeHeap(uint tosAddr, uint heapStart, uint heapEnd, byte maxLogSize)
{
    // Set MAXL
    _memory.WriteWord(tosAddr + 0, maxLogSize);

    // Set STAH and ENDH (for user trap handlers)
    _memory.WriteWord(tosAddr + 1, (ushort)heapStart);
    _memory.WriteWord(tosAddr + 2, (ushort)heapEnd);

    // Initialize all free lists to empty
    for (byte i = 0; i <= maxLogSize; i++)
    {
        _memory.WriteWord(tosAddr + 3 + i, 0);
    }

    // Add entire heap as one large block (if possible)
    uint heapSize = heapEnd - heapStart;
    byte actualLog = (byte)Math.Floor(Math.Log2(heapSize));

    if (actualLog <= maxLogSize)
    {
        // Add heap as single block to largest possible free list
        uint flogAddr = tosAddr + 3 + actualLog;
        _memory.WriteWord(heapStart, 0); // No next block
        _memory.WriteWord(flogAddr, (ushort)heapStart);
    }
}

6. Test Example

Test Case: Allocate 64-word block

[Test]
public void TestENTB_Allocate64WordBlock()
{
    // Setup heap: TOS at 0x1000, heap from 0x2000-0x3000
    uint tosAddr = 0x1000;
    uint heapStart = 0x2000;
    uint heapEnd = 0x3000;
    byte maxLogSize = 10; // Up to 2^10 = 1024 words

    InitializeHeap(tosAddr, heapStart, heapEnd, maxLogSize);
    _cpu.TOS = tosAddr;

    // Execute ENTB with log size 6 (2^6 = 64 words)
    var instruction = new EntbInstruction(_cpu, _memory);
    var context = new InstructionContext { LogSize = 6, TargetAddress = 0x5000 };

    instruction.Execute(context);

    // Verify B register points to allocated block
    Assert.AreEqual(heapStart, _cpu.B);

    // Verify B.LOG contains 6
    byte storedLog = (byte)_memory.ReadWord(_cpu.B + 11);
    Assert.AreEqual(6, storedLog);

    // Verify P was updated
    Assert.AreEqual(0x5000, _cpu.P);
}

[Test]
public void TestRETB_FreesBlock()
{
    // ... setup from previous test ...

    // Execute RETB
    var retInstr = new RetbInstruction(_cpu, _memory, false);
    retInstr.Execute(new InstructionContext());

    // Verify block was returned to free list
    uint flogAddr = tosAddr + 3 + 6; // FLOG6
    uint freedBlock = _memory.ReadWord(flogAddr);
    Assert.AreEqual(heapStart, freedBlock);
}

7. Key Implementation Notes

⚠️ Critical Points

  1. B.LOG stores LOG SIZE, not block size

    • Store the logSize parameter (e.g., 6)
    • NOT the calculated block size (e.g., 64)
  2. Block splitting creates buddies

    • If you find a 2^8 block but need 2^6:
    • Split 2^8 → two 2^7 blocks
    • Keep one 2^7, split again → two 2^6 blocks
    • Keep one 2^6 for allocation
    • Add unused 2^7 and 2^6 back to free lists
  3. Free lists are simple linked lists

    • First word of free block = next pointer
    • No size stored in block (it's implicit from which list it's in)
  4. STAH and ENDH are not used by CPU

    • Optional for user trap handlers
    • Can be left uninitialized
  5. Blocks are NOT automatically merged on free

    • FREEB just adds to list
    • Buddy merging done by STO trap handler (user code)
  6. Stack frame initialization identical to ENTS

    • Copy PREVB, RETA, SP, N, ARG fields
    • Only difference: add LOG field

8. Troubleshooting

Problem: STO trap on ENTB with empty heap

Cause: Free lists not initialized

Fix: Call InitializeHeap() before first ENTB

Problem: Block corruption after RETB

Cause: Wrong log size in B.LOG

Fix: Ensure ENTB stores logSize (not blockSize) in B.LOG

Problem: Memory leak - blocks not freed

Cause: Using RET instead of RETB

Fix: ENTB routines MUST use RETB/RETBK, not RET/RETK

Problem: Heap fragmentation

Cause: No buddy merging

Fix: Implement STO trap handler that merges adjacent buddies


Summary

ENTB Implementation Checklist:

✅ Read heap variables from TOS ✅ Validate log size <= MAXL ✅ Search free lists from requested size upward ✅ Unlink block from free list ✅ Split larger blocks (buddy system) ✅ Initialize stack frame (PREVB, RETA, SP, N, LOG, ARG) ✅ Set B register to allocated block ✅ RETB: Free block back to appropriate free list ✅ GETB: Same allocation, store address in register ✅ FREEB: Add block to free list head

You now have everything needed to implement ENTB! 🎉