Programming Language

Nocter

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

/development/std/io/file.nct

file.nct

//! File ownership and Darwin descriptor operations.

include ./index.nct
include ./core.nct

use std/mem.alloc_pages
use std/mem.free_pages
use std/internal/os.{OSError, OSErrorKind, SyscallResult, darwin_create_mode}
use std/internal/os.{darwin_errno_interrupted, darwin_open_create_append}
use std/internal/os.{darwin_open_create_truncate, darwin_open_read_only}
use std/internal/os.{darwin_sys_close, darwin_sys_open, darwin_sys_read, darwin_sys_write}
use std/internal/os.{errno, error_from_errno, syscall1, syscall3}
use std/path.validate as validate_path
use std/ptr.addr
use std/ptr.copy_str_to_ptr
use std/ptr.from_addr
use std/ptr.store_u8_to_ptr
use std/string.String
use std/string.bytes as string_bytes
use std/vec.Vec

#target: "arm64-darwin"
const stdout_fd: usize = 1

#target: "arm64-darwin"
const stderr_fd: usize = 2

#target: "arm64-darwin"
func open_read_fd(path: &str): usize! {
    return open_fd(path, darwin_open_read_only)?
}

#target: "arm64-darwin"
func create_fd(path: &str): usize! {
    return open_fd(path, darwin_open_create_truncate)?
}

#target: "arm64-darwin"
func append_fd(path: &str): usize! {
    return open_fd(path, darwin_open_create_append)?
}

#target: "arm64-darwin"
func open_fd(path: &str, flags: usize): usize! {
    validate_path(path)?
    let allocation_size: usize = path.len() + 1
    let address: usize = alloc_pages(allocation_size)?
    copy_str_to_ptr(from_addr(address), 0, path)
    store_u8_to_ptr(from_addr(address), allocation_size - 1, 0)

    var result: SyscallResult = syscall3(
        darwin_sys_open,
        address,
        flags,
        darwin_create_mode,
    )
    while result.errno == darwin_errno_interrupted {
        result = syscall3(darwin_sys_open, address, flags, darwin_create_mode)
    }
    free_pages(address, allocation_size)
    if result.errno != 0 {
        return from_os_error(error_from_errno(errno(result.errno)))
    }
    return result.value
}

#target: "arm64-darwin"
func read_fd(fd: usize, buffer: &+[u8]): usize! {
    var result: SyscallResult = syscall3(
        darwin_sys_read,
        fd,
        addr(buffer.ptr()),
        buffer.len(),
    )
    while result.errno == darwin_errno_interrupted {
        result = syscall3(darwin_sys_read, fd, addr(buffer.ptr()), buffer.len())
    }
    if result.errno != 0 {
        return from_os_error(error_from_errno(errno(result.errno)))
    }
    if result.value > buffer.len() {
        return invalid_read_count()
    }
    return result.value
}

#target: "arm64-darwin"
func write_fd(fd: usize, bytes: &[u8]): void! {
    var offset: usize = 0
    while offset < bytes.len() {
        let remaining: usize = bytes.len() - offset
        var result: SyscallResult = syscall3(
            darwin_sys_write,
            fd,
            addr(bytes.ptr()) + offset,
            remaining,
        )
        while result.errno == darwin_errno_interrupted {
            result = syscall3(
                darwin_sys_write,
                fd,
                addr(bytes.ptr()) + offset,
                remaining,
            )
        }
        if result.errno != 0 {
            return from_os_error(error_from_errno(errno(result.errno)))
        }
        if result.value == 0 {
            return write_zero()
        }
        if result.value > remaining {
            return invalid_write_count()
        }
        offset = offset + result.value
    }
    return
}

#target: "arm64-darwin"
func write_text_fd(fd: usize, text: &str): void! {
    write_fd(fd, string_bytes(text))?
    return
}

#target: "arm64-darwin"
func close_fd(fd: usize): void {
    // A failed close is deliberately not retried: Darwin may have consumed the
    // descriptor even when reporting an interruption, and retrying could close
    // an unrelated descriptor that another operation has reused.
    let _ = syscall1(darwin_sys_close, fd)
    return
}

struct File {
    fd: usize
    close_on_drop: bool
}

func from_os_error(os_error: OSError): error {
    if os_error.kind is OSErrorKind.interrupted {
        return error.new("std.io.interrupted", "operation interrupted")
    }
    if os_error.kind is OSErrorKind.would_block {
        return error.new("std.io.would_block", "operation would block")
    }
    if os_error.kind is OSErrorKind.not_found {
        return error.new("std.io.not_found", "file not found")
    }
    if os_error.kind is OSErrorKind.permission_denied {
        return error.new("std.io.permission_denied", "permission denied")
    }
    if os_error.kind is OSErrorKind.already_exists {
        return error.new("std.io.already_exists", "file already exists")
    }
    if os_error.kind is OSErrorKind.invalid_input {
        return error.new("std.io.invalid_input", "invalid I/O input")
    }
    if os_error.kind is OSErrorKind.broken_pipe {
        return error.new("std.io.broken_pipe", "broken pipe")
    }
    if os_error.kind is OSErrorKind.timed_out {
        return error.new("std.io.timed_out", "operation timed out")
    }
    if os_error.kind is OSErrorKind.unsupported {
        return error.new("std.io.unsupported", "unsupported I/O operation")
    }
    return error.new("std.io.os_error", "OS operation failed")
}

func stdout(): File {
    return File { fd: stdout_fd, close_on_drop: false }
}

func stderr(): File {
    return File { fd: stderr_fd, close_on_drop: false }
}

construct File {
    default func open(path: &str): Self! {
        let fd = open_read_fd(path)?
        return File { fd: fd, close_on_drop: true }
    }

    func create(path: &str): Self! {
        let fd = create_fd(path)?
        return File { fd: fd, close_on_drop: true }
    }

    func append(path: &str): Self! {
        let fd = append_fd(path)?
        return File { fd: fd, close_on_drop: true }
    }
}

func read(file: &+File, buffer: &+[u8]): usize! {
    return read_fd(file.fd, buffer)?
}

func write(file: &+File, bytes: &[u8]): void! {
    write_fd(file.fd, bytes)?
    return
}

func write_text(file: &+File, text: &str): void! {
    write_text_fd(file.fd, text)?
    return
}

func close(file: &+File): void {
    if file.close_on_drop {
        close_fd(file.fd)
        file.close_on_drop = false
    }
    return
}

instance File {
    method &+self.close(): void {
        close(self)
        return
    }
}

drop File(&+self) {
    close(self)
    return
}

conform Reader for File {
    method &+self.read(buffer: &+[u8]): usize! { return read(self, buffer)? }
}

conform Writer for File {
    method &+self.write(bytes: &[u8]): void! {
        write(self, bytes)?
        return
    }

    method &+self.flush(): void! { return }
}

func print(text: &str): void! {
    write_text_fd(1, text)?
    return
}

func unsupported(): error {
    return error.new("std.io.unsupported", "unsupported I/O operation")
}

func write_zero(): error {
    return error.new("std.io.write_zero", "writer made no progress")
}

func invalid_write_count(): error {
    return error.new(
        "std.io.invalid_write_count",
        "writer reported more bytes than the supplied input contains",
    )
}