Programming Language

Nocter

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

/development/std/process/subprocess_darwin.nct

subprocess_darwin.nct

//! Raw Darwin subprocess transitions.
//!
//! This source is the sole owner of fork result classification, launch-channel setup and
//! protocol reads, and terminal wait-status decoding. Public Command policy is layered above
//! these typed outcomes.

see ./darwin.nct

use /internal/os/darwin.SyscallPairResult
use /internal/os/darwin.SyscallResult
use /internal/os/darwin
use /ptr

#target: "arm64-darwin"
enum ForkAttempt {
    parent(pid: usize)
    child
    failed(errno: i32)
}

#target: "arm64-darwin"
enum LaunchPipeAttempt {
    ready(read_fd: usize, write_fd: usize)
    failed(errno: i32)
}

#target: "arm64-darwin"
enum WaitAttempt {
    exited(code: i32)
    signaled(signal: i32)
    interrupted
    failed(errno: i32)
    invalid(status: i32)
}

#target: "arm64-darwin"
enum WaitOutcome {
    exited(code: i32)
    signaled(signal: i32)
    failed(errno: i32)
    invalid(status: i32)
}

#target: "arm64-darwin"
enum ExecReportAttempt {
    executed
    rejected(errno: i32)
    channel_failed(errno: i32)
    malformed
}

#target: "arm64-darwin"
const EXEC_REPORT_BYTES: usize = 4

#target: "arm64-darwin"
noalloc func classify_fork_attempt(result: SyscallPairResult): ForkAttempt {
    if result.errno != 0 { return ForkAttempt.failed(result.errno) }
    if result.second != 0 { return ForkAttempt.child }
    return ForkAttempt.parent(result.first)
}

#target: "arm64-darwin"
noalloc func create_launch_pipe(): LaunchPipeAttempt {
    let result = darwin.syscall_pair0(darwin.SYS_PIPE)
    if result.errno != 0 { return LaunchPipeAttempt.failed(result.errno) }

    var flags = darwin.syscall3(
        darwin.SYS_FCNTL,
        result.second,
        darwin.F_SETFD,
        darwin.FD_CLOEXEC,
    )
    while flags.errno == darwin.ERRNO_INTERRUPTED {
        flags = darwin.syscall3(
            darwin.SYS_FCNTL,
            result.second,
            darwin.F_SETFD,
            darwin.FD_CLOEXEC,
        )
    }
    if flags.errno != 0 {
        let _ = darwin.syscall1(darwin.SYS_CLOSE, result.first)
        let _ = darwin.syscall1(darwin.SYS_CLOSE, result.second)
        return LaunchPipeAttempt.failed(flags.errno)
    }
    return LaunchPipeAttempt.ready(result.first, result.second)
}

#target: "arm64-darwin"
noalloc func fork_process(): ForkAttempt {
    return classify_fork_attempt(darwin.syscall_pair0(darwin.SYS_FORK))
}

#target: "arm64-darwin"
noalloc func write_exec_rejection_and_exit(fd: usize, errno: i32): never {
    let address = ptr.addr(ptr.from_ref(&errno))
    var offset: usize = 0
    while offset < EXEC_REPORT_BYTES {
        let result = darwin.syscall3(
            darwin.SYS_WRITE,
            fd,
            address + offset,
            EXEC_REPORT_BYTES - offset,
        )
        if result.errno == darwin.ERRNO_INTERRUPTED {
        } else if result.errno != 0 || result.value == 0
            || result.value > EXEC_REPORT_BYTES - offset {
            exit_raw(127)
        } else {
            offset += result.value
        }
    }
    let _ = darwin.syscall1(darwin.SYS_CLOSE, fd)
    exit_raw(127)
}

#target: "arm64-darwin"
noalloc func exec_child(
    path_address: usize,
    argv_address: usize,
    environment_address: usize,
    read_fd: usize,
    report_fd: usize,
): never {
    let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
    let result = darwin.syscall3(
        darwin.SYS_EXECVE,
        path_address,
        argv_address,
        environment_address,
    )
    write_exec_rejection_and_exit(report_fd, result.errno)
}

#target: "arm64-darwin"
noalloc func read_exec_report(fd: usize): ExecReportAttempt {
    var payload: i32 = 0
    let address = ptr.addr(ptr.from_ref_mut(&+payload))
    var offset: usize = 0
    while offset < EXEC_REPORT_BYTES {
        let result = darwin.syscall3(
            darwin.SYS_READ,
            fd,
            address + offset,
            EXEC_REPORT_BYTES - offset,
        )
        if result.errno == darwin.ERRNO_INTERRUPTED {
        } else if result.errno != 0 {
            return ExecReportAttempt.channel_failed(result.errno)
        } else if result.value == 0 {
            if offset == 0 { return ExecReportAttempt.executed }
            return ExecReportAttempt.malformed
        } else if result.value > EXEC_REPORT_BYTES - offset {
            return ExecReportAttempt.malformed
        } else {
            offset += result.value
        }
    }
    return ExecReportAttempt.rejected(payload)
}

#target: "arm64-darwin"
noalloc func classify_wait_attempt(result: SyscallResult, status: i32): WaitAttempt {
    if result.errno == darwin.ERRNO_INTERRUPTED { return WaitAttempt.interrupted }
    if result.errno != 0 { return WaitAttempt.failed(result.errno) }

    let terminal = status % 128
    if terminal == 0 { return WaitAttempt.exited((status / 256) % 256) }
    if terminal == 127 { return WaitAttempt.invalid(status) }
    return WaitAttempt.signaled(terminal)
}

#target: "arm64-darwin"
noalloc func wait_for_child(pid: usize): WaitOutcome {
    var status: i32 = 0
    loop {
        let result = darwin.syscall6(
            darwin.SYS_WAIT4,
            pid,
            ptr.addr(ptr.from_ref_mut(&+status)),
            0,
            0,
            0,
            0,
        )
        match classify_wait_attempt(result, status) {
            WaitAttempt.interrupted {}
            WaitAttempt.exited(code) { return WaitOutcome.exited(code) }
            WaitAttempt.signaled(signal) { return WaitOutcome.signaled(signal) }
            WaitAttempt.failed(errno) { return WaitOutcome.failed(errno) }
            WaitAttempt.invalid(raw) { return WaitOutcome.invalid(raw) }
        }
    }
}

test raw_subprocess_results_have_one_typed_interpretation {
    match classify_fork_attempt(SyscallPairResult { first: 41, second: 0, errno: 0 }) {
        ForkAttempt.parent(pid) { if pid != 41 { return error.new("std.process.test", "parent pid changed") } }
        ForkAttempt.child { return error.new("std.process.test", "parent result became child") }
        ForkAttempt.failed(_) { return error.new("std.process.test", "parent result became failure") }
    }

    match classify_fork_attempt(SyscallPairResult { first: 41, second: 1, errno: 0 }) {
        ForkAttempt.child {}
        ForkAttempt.parent(_) { return error.new("std.process.test", "child result became parent") }
        ForkAttempt.failed(_) { return error.new("std.process.test", "child result became failure") }
    }

    match classify_fork_attempt(SyscallPairResult { first: 9, second: 8, errno: 12 }) {
        ForkAttempt.failed(errno) { if errno != 12 { return error.new("std.process.test", "fork errno changed") } }
        ForkAttempt.parent(_) { return error.new("std.process.test", "failure result became parent") }
        ForkAttempt.child { return error.new("std.process.test", "failure result became child") }
    }

    match classify_wait_attempt(SyscallResult { value: 41, errno: 0 }, 1792) {
        WaitAttempt.exited(code) { if code != 7 { return error.new("std.process.test", "exit code changed") } }
        WaitAttempt.signaled(_) { return error.new("std.process.test", "exit became signal") }
        WaitAttempt.interrupted { return error.new("std.process.test", "exit became interruption") }
        WaitAttempt.failed(_) { return error.new("std.process.test", "exit became failure") }
        WaitAttempt.invalid(_) { return error.new("std.process.test", "exit became invalid") }
    }

    match classify_wait_attempt(SyscallResult { value: 41, errno: 0 }, 9) {
        WaitAttempt.signaled(signal) { if signal != 9 { return error.new("std.process.test", "signal changed") } }
        WaitAttempt.exited(_) { return error.new("std.process.test", "signal became exit") }
        WaitAttempt.interrupted { return error.new("std.process.test", "signal became interruption") }
        WaitAttempt.failed(_) { return error.new("std.process.test", "signal became failure") }
        WaitAttempt.invalid(_) { return error.new("std.process.test", "signal became invalid") }
    }

    match classify_wait_attempt(
        SyscallResult { value: 0, errno: darwin.ERRNO_INTERRUPTED },
        0,
    ) {
        WaitAttempt.interrupted {}
        WaitAttempt.exited(_) { return error.new("std.process.test", "interruption became exit") }
        WaitAttempt.signaled(_) { return error.new("std.process.test", "interruption became signal") }
        WaitAttempt.failed(_) { return error.new("std.process.test", "interruption became failure") }
        WaitAttempt.invalid(_) { return error.new("std.process.test", "interruption became invalid") }
    }

    match classify_wait_attempt(SyscallResult { value: 0, errno: 10 }, 0) {
        WaitAttempt.failed(errno) { if errno != 10 { return error.new("std.process.test", "wait errno changed") } }
        WaitAttempt.exited(_) { return error.new("std.process.test", "wait failure became exit") }
        WaitAttempt.signaled(_) { return error.new("std.process.test", "wait failure became signal") }
        WaitAttempt.interrupted { return error.new("std.process.test", "wait failure became interruption") }
        WaitAttempt.invalid(_) { return error.new("std.process.test", "wait failure became invalid") }
    }

    match classify_wait_attempt(SyscallResult { value: 41, errno: 0 }, 127) {
        WaitAttempt.invalid(status) { if status != 127 { return error.new("std.process.test", "invalid wait status changed") } }
        WaitAttempt.exited(_) { return error.new("std.process.test", "invalid wait became exit") }
        WaitAttempt.signaled(_) { return error.new("std.process.test", "invalid wait became signal") }
        WaitAttempt.interrupted { return error.new("std.process.test", "invalid wait became interruption") }
        WaitAttempt.failed(_) { return error.new("std.process.test", "invalid wait became failure") }
    }

    if env_vector_raw() == 0 {
        return error.new("std.process.test", "environment vector is absent")
    }

    match create_launch_pipe() {
        LaunchPipeAttempt.ready(read_fd, write_fd) {
            let write_close = darwin.syscall1(darwin.SYS_CLOSE, write_fd)
            if write_close.errno != 0 {
                let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
                return error.new("std.process.test", "launch pipe write close failed")
            }
            match read_exec_report(read_fd) {
                ExecReportAttempt.executed {}
                ExecReportAttempt.rejected(_) {
                    let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
                    return error.new("std.process.test", "closed report channel became rejection")
                }
                ExecReportAttempt.channel_failed(_) {
                    let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
                    return error.new("std.process.test", "closed report channel became failure")
                }
                ExecReportAttempt.malformed {
                    let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
                    return error.new("std.process.test", "closed report channel became malformed")
                }
            }
            let read_close = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
            if read_close.errno != 0 {
                return error.new("std.process.test", "launch pipe read close failed")
            }
        }
        LaunchPipeAttempt.failed(_) {
            return error.new("std.process.test", "launch pipe creation failed")
        }
    }

    match create_launch_pipe() {
        LaunchPipeAttempt.ready(read_fd, write_fd) {
            let expected_errno: i32 = 13
            let write_result = darwin.syscall3(
                darwin.SYS_WRITE,
                write_fd,
                ptr.addr(ptr.from_ref(&expected_errno)),
                EXEC_REPORT_BYTES,
            )
            let write_close = darwin.syscall1(darwin.SYS_CLOSE, write_fd)
            if write_result.errno != 0 || write_result.value != EXEC_REPORT_BYTES || write_close.errno != 0 {
                let _ = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
                return error.new("std.process.test", "exec rejection report write failed")
            }
            let report = read_exec_report(read_fd)
            let read_close = darwin.syscall1(darwin.SYS_CLOSE, read_fd)
            if read_close.errno != 0 {
                return error.new("std.process.test", "exec rejection report close failed")
            }
            match move report {
                ExecReportAttempt.rejected(errno) {
                    if errno != expected_errno {
                        return error.new("std.process.test", "exec rejection errno changed")
                    }
                }
                ExecReportAttempt.executed {
                    return error.new("std.process.test", "exec rejection became success")
                }
                ExecReportAttempt.channel_failed(_) {
                    return error.new("std.process.test", "exec rejection became channel failure")
                }
                ExecReportAttempt.malformed {
                    return error.new("std.process.test", "exec rejection became malformed")
                }
            }
        }
        LaunchPipeAttempt.failed(_) {
            return error.new("std.process.test", "rejection report pipe creation failed")
        }
    }
    return
}