Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/development/std/mem.nct

mem.nct

//! Nocter standard library memory API.
//!
//! This file is the initial public surface for `std/mem`. The compiler must not
//! special-case the names Allocator, Layout, RawBuffer, alloc, free, or
//! page_allocator. Region-specific behavior is tracked through allocator
//! provenance from the `region ... using ...` language construct.
//!
//! The public functions below are ordinary standard-library APIs. Allocation is
//! not part of the compiler primitive set; target-gated std internals
//! provide the OS boundary behind this common API.

use std/error.Error
use std/os.{syscall2, syscall6}
use std/ptr.addr
use std/ptr.copy_ptr_to_ptr
use std/ptr.from_addr
use std/ptr.slice_from_raw_parts
use std/ptr.slice_from_raw_parts_mut
use std/ptr.slice_from_raw_parts_value
use std/ptr.store_value_to_ptr

pub copy struct Layout {
    pub(nocter) size: usize
    pub(nocter) align: usize
}

pub struct RawBuffer {
    pub(nocter) ptr: *u8
    pub(nocter) len: usize
    pub(nocter) align: usize
    pub(nocter) allocator_state: usize
    pub(nocter) allocator_kind: usize
}

pub struct Allocator {
    state: usize
    kind: usize
}

pub struct TryAllocator {
    state: usize
    kind: usize
}

pub(nocter) primitive current_allocator_state(): usize
pub(nocter) primitive current_allocator_kind(): usize
pub(nocter) primitive allocation_abort_raw(): never

pub(nocter) func current_allocator(): Allocator {
    return Allocator {
        state: current_allocator_state(),
        kind: current_allocator_kind(),
    }
}

pub func page_allocator(): Allocator {
    return Allocator { state: 0, kind: 0 }
}

pub func page_try_allocator(): TryAllocator {
    return TryAllocator { state: 0, kind: 0 }
}

pub(nocter) func empty_page_buffer(align: usize): RawBuffer {
    // Kind 2 is an unbound empty sentinel. It owns no storage and therefore
    // carries no allocator origin. The first growth operation binds it to the
    // allocator selected at that operation.
    return RawBuffer {
        ptr: from_addr(1),
        len: 0,
        align: align,
        allocator_state: 0,
        allocator_kind: 2,
    }
}

construct Layout {
    pub default func new(size: usize, align: usize): Self! {
        if !valid_alignment(align) {
            return invalid_argument()
        }
        return Layout { size: size, align: align }
    }
}

pub func layout(size: usize, align: usize): Layout! {
    return Layout.new(size, align)?
}

pub func layout_size(value: &Layout): usize {
    return value.size
}

pub func layout_align(value: &Layout): usize {
    return value.align
}

#target: "arm64-darwin"
pub(nocter) func alloc_pages(size: usize): usize! {
    let result = syscall6(0x020000c5, 0, size, 3, 0x1002, 0xffffffffffffffff, 0)
    if result.errno != 0 {
        return Error.new("std.mem.out_of_memory", "allocation failed")
    }
    return result.value
}

#target: "arm64-darwin"
pub(nocter) func free_pages(address: usize, size: usize): void {
    let result = syscall2(0x02000049, address, size)
    return
}

pub(nocter) func region_alloc_pages(
    state: usize,
    size: usize,
    align: usize,
): usize! {
    // Every mapping begins with a release-list header. The payload is aligned
    // within the mapping, so region release can walk mappings without storing
    // metadata beside or inside user-visible bytes.
    let header_size: usize = 16
    if size > 18446744073709551615 - header_size - align {
        return invalid_argument()
    }
    let mapping_size = size + header_size + align
    let mapping = alloc_pages(mapping_size)?
    let payload_start = mapping + header_size
    let remainder = payload_start % align
    var padding: usize = 0
    if remainder != 0 {
        padding = align - remainder
    }

    let state_pointer: *usize = from_addr(state)
    let state_words: &[usize] = slice_from_raw_parts_value(state_pointer, 1)
    let previous: usize = state_words[0]
    let header: *usize = from_addr(mapping)
    store_value_to_ptr(header, 0, previous)
    store_value_to_ptr(header, 8, mapping_size)
    store_value_to_ptr(state_pointer, 0, mapping)
    return payload_start + padding
}

pub func try_alloc(
    allocator: &+TryAllocator,
    size: usize,
    align: usize,
): RawBuffer! from allocator {
    let requested = Layout.new(size, align)?
    return try_alloc_layout(allocator, requested)?
}

pub func try_alloc_layout(
    allocator: &+TryAllocator,
    requested: Layout,
): RawBuffer! from allocator {
    if requested.size == 0 {
        return RawBuffer {
            ptr: from_addr(1),
            len: 0,
            align: requested.align,
            allocator_state: allocator.state,
            allocator_kind: allocator.kind,
        }
    }

    var address: usize = 0
    if allocator.kind == 0 {
        address = alloc_pages(requested.size)?
    } else if allocator.kind == 1 {
        address = region_alloc_pages(allocator.state, requested.size, requested.align)?
    } else {
        return invalid_argument()
    }
    return RawBuffer {
        ptr: from_addr(address),
        len: requested.size,
        align: requested.align,
        allocator_state: allocator.state,
        allocator_kind: allocator.kind,
    }
}

pub func alloc(
    allocator: &+Allocator,
    size: usize,
    align: usize,
): RawBuffer from allocator {
    var fallible = TryAllocator { state: allocator.state, kind: allocator.kind }
    return try_alloc(&+fallible, size, align) catch allocation_error {
        return allocation_abort_raw()
    }
}

pub func alloc_layout(
    allocator: &+Allocator,
    requested: Layout,
): RawBuffer from allocator {
    var fallible = TryAllocator { state: allocator.state, kind: allocator.kind }
    return try_alloc_layout(&+fallible, requested) catch allocation_error {
        return allocation_abort_raw()
    }
}

pub func free(allocator: &+Allocator, buffer: RawBuffer): void {
    // Consuming `buffer` transfers its one deallocation obligation into this
    // call. RawBuffer.drop performs the provenance-aware release on return.
    return
}

pub func try_free(allocator: &+TryAllocator, buffer: RawBuffer): void {
    // `RawBuffer.drop` owns the backend-aware release. The capability argument
    // documents and validates the caller's selected recoverable backend.
    return
}

pub func try_grow(
    allocator: &+TryAllocator,
    buffer: &+RawBuffer,
    new_size: usize,
): void! {
    if new_size < buffer.len {
        return invalid_argument()
    }
    if new_size == buffer.len {
        return
    }
    if buffer.allocator_kind != 2 {
        if allocator.state != buffer.allocator_state {
            return invalid_argument()
        }
        if allocator.kind != buffer.allocator_kind {
            return invalid_argument()
        }
    }

    var replacement = try_alloc(allocator, new_size, buffer.align)?
    if buffer.len != 0 {
        copy_ptr_to_ptr(replacement.ptr, buffer.ptr, buffer.len)
    }

    var previous = RawBuffer {
        ptr: buffer.ptr,
        len: buffer.len,
        align: buffer.align,
        allocator_state: buffer.allocator_state,
        allocator_kind: buffer.allocator_kind,
    }

    buffer.ptr = replacement.ptr
    buffer.len = replacement.len
    buffer.align = replacement.align
    buffer.allocator_state = replacement.allocator_state
    buffer.allocator_kind = replacement.allocator_kind

    replacement.ptr = from_addr(1)
    replacement.len = 0
    return
}

pub(nocter) func try_grow_owned(buffer: &+RawBuffer, new_size: usize): void! {
    var allocator = TryAllocator {
        state: buffer.allocator_state,
        kind: buffer.allocator_kind,
    }
    if buffer.allocator_kind == 2 {
        allocator.state = current_allocator_state()
        allocator.kind = current_allocator_kind()
    }
    try_grow(&+allocator, buffer, new_size)?
    return
}

pub func grow(allocator: &+Allocator, buffer: &+RawBuffer, new_size: usize): void {
    var fallible = TryAllocator { state: allocator.state, kind: allocator.kind }
    try_grow(&+fallible, buffer, new_size) catch allocation_error {
        return allocation_abort_raw()
    }
    return
}

pub(nocter) func grow_owned(buffer: &+RawBuffer, new_size: usize): void {
    try_grow_owned(buffer, new_size) catch allocation_error {
        return allocation_abort_raw()
    }
    return
}

pub func bytes(buffer: &RawBuffer): &[u8] from buffer {
    return slice_from_raw_parts(buffer.ptr, buffer.len)
}

pub func bytes_mut(buffer: &+RawBuffer): &+[u8] from buffer {
    return slice_from_raw_parts_mut(buffer.ptr, buffer.len)
}

pub func prefix(buffer: &RawBuffer, len: usize): &[u8]! from buffer {
    if len > buffer.len {
        return invalid_argument()
    }
    return slice_from_raw_parts(buffer.ptr, len)
}

pub func prefix_mut(buffer: &+RawBuffer, len: usize): &+[u8]! from buffer {
    if len > buffer.len {
        return invalid_argument()
    }
    return slice_from_raw_parts_mut(buffer.ptr, len)
}

impl Allocator {
    pub method &+self.alloc(size: usize, align: usize): RawBuffer from self {
        return alloc(self, size, align)
    }

    pub method &+self.free(buffer: RawBuffer): void {
        free(self, move buffer)
        return
    }

    pub method &+self.grow(buffer: &+RawBuffer, new_size: usize): void {
        grow(self, buffer, new_size)
        return
    }
}

impl TryAllocator {
    pub method &+self.try_alloc(size: usize, align: usize): RawBuffer! from self {
        return try_alloc(self, size, align)?
    }

    pub method &+self.try_grow(buffer: &+RawBuffer, new_size: usize): void! {
        try_grow(self, buffer, new_size)?
        return
    }


    pub method &+self.free(buffer: RawBuffer): void {
        try_free(self, move buffer)
        return
    }
}

impl RawBuffer {
    pub method &self.bytes(): &[u8] from self {
        return bytes(self)
    }

    pub method &+self.bytes_mut(): &+[u8] from self {
        return bytes_mut(self)
    }

    pub method &self.prefix(len: usize): &[u8]! from self {
        return prefix(self, len)?
    }

    pub method &+self.prefix_mut(len: usize): &+[u8]! from self {
        return prefix_mut(self, len)?
    }

    drop &+self {
        if self.len != 0 {
            if self.allocator_kind == 0 {
                free_pages(addr(self.ptr), self.len)
            } else if self.allocator_kind != 1 {
                return allocation_abort_raw()
            }
            self.ptr = from_addr(1)
            self.len = 0
        }
        return
    }
}

impl Layout {
    pub method &self.size(): usize {
        return layout_size(self)
    }

    pub method &self.align(): usize {
        return layout_align(self)
    }
}

func valid_alignment(align: usize): bool {
    if align == 0 {
        return false
    }
    if align > 16384 {
        return false
    }

    var remaining = align
    while remaining > 1 {
        if remaining % 2 != 0 {
            return false
        }
        remaining = remaining / 2
    }
    return true
}

pub func out_of_memory(): error {
    return Error.new("std.mem.out_of_memory", "allocation failed")
}

pub func invalid_argument(): error {
    return Error.new("std.mem.invalid_argument", "invalid allocation request")
}

pub(nocter) func abort_allocation(allocation_error: error): never {
    // The compiler-owned boundary exits directly without allocating or
    // unwinding Nocter scopes.
    allocation_abort_raw()
}