Processes and Command

Sec processes are independently scheduled execution entities in distinct process isolation domains. On hosted systems this normally means an operating-system process. Other targets may provide a different native mechanism, but only when it preserves the isolation, transfer, lifecycle, completion and cleanup semantics defined by Sec.

Sec has two process-creation families. A Sec callable is started in a new process with spawn process. An external executable is described and started through Command.

let worker := try spawn process Work(message)

let command := new Command("git", ["status"])
try command.Start()
Core rule: A process is not a task or a thread with a different spelling. spawn process requires a real process-isolation boundary. A target that cannot provide that boundary must reject the operation at compile time.

Purpose

Process execution solves a different problem from ordinary in-process concurrency. Tasks and threads may coordinate around one process address space. Processes instead introduce an isolation boundary and therefore require explicit transfer semantics for startup values, completion values and ongoing communication.

This chapter defines the user-facing model for:

  • Process[T] and typed Sec-callable subprocesses;
  • Command and external executable processes;
  • process identity, status and target-native metadata;
  • startup transfer and completion transfer;
  • observation, join, detach and hard termination;
  • standard-I/O inheritance and anonymous process pipes;
  • lifecycle ownership and native process reaping;
  • target capability requirements.

General ongoing inter-process messaging, shared-memory APIs and general handle transfer belong to Inter-Process Communication.

Task, Thread and Process

Execution kindMeaning
Task[T]Logical concurrent execution managed by a Sec runtime or scheduler.
Thread[T]Physical execution sharing the owning process address space.
Process[T]Independently scheduled execution in a distinct process isolation domain.

A backend must preserve the execution kind chosen by source code. It may not lower spawn process to a task, a worker job, an ordinary thread or an RTOS task that lacks the required isolation boundary.

Process Isolation

Ordinary Sec objects are process-local. Parent and child do not acquire ordinary aliases into one another's object graphs merely because one created the other.

parent process                     child process
ordinary Sec object A    !=        ordinary Sec object B

Values that cross the boundary use a canonical process-transfer representation. A backend may internally use copy-on-write or a fork-like mechanism, but that does not change the source semantics into shared ordinary Sec storage.

References do not cross

fn Inspect(data: ref Data) void {
    // ...
}

let worker := try spawn process Inspect(ref data)  // invalid

Neither ref T nor ref mut T becomes process-valid merely because the parent keeps the referenced object alive. Address-space validity is a separate requirement from lifetime validity.

RawPtr does not create cross-process validity

Copying the numeric representation of a RawPtr[T] to another process does not make it a valid pointer to corresponding child storage. unsafe does not waive this rule. Shared memory and native capability transfer require explicit IPC or process-transfer abstractions.

Two Creation Families

Sec-callable process

let worker := try spawn process Work(message)

If Work returns T, the raw spawn expression has type:

Result[Process[T], ProcessSpawnError]

After try, worker has type Process[T]. Process spawn is eager in Sec 0.1: a successfully returned handle represents an established process execution, not a deferred launch description.

External executable

let command := new Command("git", ["status", "--short"])
try command.Start()

new Command(...) creates only a launch description. The external process is created by Start().

Rule: Command is not an alias for Process[T]. The canonical family is Task[T], Thread[T], Process[T], and Command.

Core Process Types

ProcessID

type ProcessID uint

ProcessID is Sec's stable process-execution identity for the current program execution. It is copyable and equality-comparable. It is not necessarily the operating system's PID and grants no lifecycle control capability.

ExitStatus

type ExitStatus struct {
    Code: uint32
}

impl ExitStatus {
    property Success: bool {
        get {
            return Code == 0
        }
    }
}

ExitStatus is the normal result of an external Command. A non-zero exit code is still normal process completion.

ProcessTermination

enum ProcessTerminationKind {
    Requested
    External
    Fault
    Unknown
}

type ProcessTermination struct {
    Kind: ProcessTerminationKind
}

Portable Sec does not force POSIX signals, Windows status values and target-specific fault metadata into one universal integer field.

ProcessStatus

enum ProcessStatus {
    Created
    Running
    Completed
    CompletionFailed
    Panicked
    Terminated
}
StatusMeaning
CreatedA Command launch object exists, but no child process has been established.
RunningExecution exists and no terminal outcome has yet been observed.
CompletedNormal execution completed and the normal result has been established owner-side.
CompletionFailedExecution returned normally, but the declared result could not be transferred/materialized owner-side.
PanickedA Sec-callable subprocess ended through Sec panic and canonical panic information was established.
TerminatedThe process ended abnormally through termination, crash, fault or equivalent process-level event.

Created is visible for Command. A successfully returned Process[T] from spawn process is never source-visible in Created state.

If a callable returns Result[T, E], returning Err(error) is still normal completion. The process result value is that Result[T, E].

Process[T]

@noCopy
type Process[T]

Process[T] is compiler-known, generic in the callable's normal return type, and move-only regardless of whether T is copyable. It is not user-constructible.

Its required public surface is:

property ID: ProcessID
property Name: string
property Status: ProcessStatus
property Value: T
property CompletionError: ProcessCompletionError
property Panic: PanicInfo
property Termination: ProcessTermination
property Platform: ProcessPlatform
fn Observe() ProcessObserver
fn Terminate() Result[void, ProcessTerminationError]

The process value owns lifecycle responsibility, one join capability until resolved, result ownership, terminal metadata and the native reaping obligations that have not yet been transferred or released.

spawn process

fn Calculate() int {
    return 42
}

let worker := try spawn process Calculate()

The resulting type is:

Process[int]

Nested Result is preserved

fn Load() Result[Document, IOError] {
    // ...
}

let worker := try spawn process Load()

The type is:

Process[Result[Document, IOError]]

An Err(IOError...) returned by Load is not a process-creation failure, completion-transfer failure, panic or abnormal termination. It is the normal typed return value of the callable.

Callable materialization

A process entry may be a named function, a concrete generic specialization, an instance method, or a callable/lambda whose complete legal target set and captured environment can be materialized for the child process.

Ordinary borrowed captures into parent storage are invalid. Runtime-selected opaque foreign function addresses are not automatically valid Sec process callables.

Startup Transfer

Process startup is a boundary-specific transfer. Every explicit argument, receiver and capture expression is evaluated in the parent before startup commit.

A value must satisfy the canonical process-transferability rules. A value may be process-transferable through serialization, a dedicated transfer representation, shared-memory capability, native handle duplication/inheritance or another explicit adapter. Process-transferable does not mean bitwise-copyable.

Copyable value

let count: int := 10
let worker := try spawn process Work(count)

The parent remains able to use count.

Consumed named value

let data := LoadData()
let worker := try spawn process Work(<-data)

When an existing named binding is consumed across the process boundary, the explicit move marker documents that ownership may become unavailable when creation commits.

Fresh temporary

let worker := try spawn process Work(CreateData())

A fresh temporary does not need a synthetic move marker.

Transactional Moves During Spawn

Process startup is fallible, so ownership transfer must not be irreversibly committed before process creation succeeds.

match spawn process Work(<-data) {
    Ok(worker) => {
        // data is unavailable here
    }

    Err(error) => {
        // data is still owned here
    }
}

The transfer model has preparation, commit and rollback semantics. A failed spawn must not discard a source value that never successfully crossed the process boundary, and it must never leave a hidden orphan process running after returning Err.

Typed Completion

A child returning T and the parent obtaining T are separate events. The normal value must cross the process boundary and become a valid owner-side Sec value before the process is classified as Completed.

child returns T
    ↓
completion transfer
    ↓
owner-side materialization of T
    ↓
ProcessStatus.Completed

If the child returns normally but completion transport or materialization fails, the status is CompletionFailed and CompletionError is the terminal payload. The return type itself must therefore be process-transferable.

One terminal typed completion value belongs to process lifecycle. General streaming, repeated messages and ongoing bidirectional communication belong to IPC.

Command

@noCopy
type Command

Command represents an external executable launch description. It is move-only for its entire lifetime, including while still in Created state.

impl Command {
    init(executable: string, arguments: string[]) {
        // Compiler/core implementation.
    }
}
let command := new Command("git", ["status"])

Construction is infallible at the process API level. It does not search for the executable, prove permissions, create pipes or launch a child. It creates configuration state with Status == ProcessStatus.Created.

Command Public API

The required portable public surface is:

property Executable: ref string
property Arguments: ref string[]
property WorkingDirectory: Option[ref string]
property InheritsEnvironment: bool
property StdinMode: CommandInputMode
property StdoutMode: CommandOutputMode
property StderrMode: CommandOutputMode
property Status: ProcessStatus
property ID: Option[ProcessID]
property Name: string
property Value: ExitStatus
property CompletionError: ProcessCompletionError
property Termination: ProcessTermination
property Platform: Option[ProcessPlatform]

fn SetWorkingDirectory(path: string) Result[void, CommandConfigurationError]
fn ClearWorkingDirectory() Result[void, CommandConfigurationError]
fn SetEnvironment(name: string, value: string) Result[void, CommandConfigurationError]
fn RemoveEnvironment(name: string) Result[void, CommandConfigurationError]
fn ClearEnvironment() Result[void, CommandConfigurationError]
fn InheritEnvironment() Result[void, CommandConfigurationError]
fn SetStdin(mode: CommandInputMode) Result[void, CommandConfigurationError]
fn SetStdout(mode: CommandOutputMode) Result[void, CommandConfigurationError]
fn SetStderr(mode: CommandOutputMode) Result[void, CommandConfigurationError]
fn TakeStdinPipe() Option[PipeWriter]
fn TakeStdoutPipe() Option[PipeReader]
fn TakeStderrPipe() Option[PipeReader]
fn Validate() Result[void, CommandValidationError]
fn Start() Result[void, CommandStartError]
fn Observe() Option[ProcessObserver]
fn Terminate() Result[void, ProcessTerminationError]

All properties are read-only from ordinary source code.

Working Directory

By default the child inherits the parent's current working directory as observed at Start(). Command construction does not snapshot it.

property WorkingDirectory: Option[ref string]

None means inherited working directory. Some(path) means an explicitly configured child directory.

try command.SetWorkingDirectory(path)

The setter performs an eager current-environment check for representability, existence, directory identity and permission where the target can determine them. This does not reserve the directory. Validate() and Start() check the relevant conditions again.

Configuration methods are valid only in Created. A statically provable post-start configuration attempt should be diagnosed at compile time; otherwise the operation returns CommandConfigurationError.InvalidState.

Environment

The default is to inherit the effective parent environment at Start(). The command may then override, remove or clear entries explicitly.

try command.SetEnvironment("MODE", "batch")
try command.RemoveEnvironment("DEBUG")
try command.ClearEnvironment()
try command.InheritEnvironment()

Environment configuration is part of the launch description, not mutable global environment state in the parent process.

Validate and Start

Validate

try command.Validate()

Validate() is optional preflight. It may perform executable lookup, filesystem checks, permission checks and target representation checks, but it does not reserve process slots, pipes, memory or other creation resources.

A successful validation therefore does not guarantee that a later Start() succeeds. External filesystem state, permissions and resource availability can change.

Start

try command.Start()

Start() is the authoritative creation boundary. It returns Result[void, CommandStartError] and does not return the command again merely for method chaining.

Successful start establishes the actual child process, its identity, configured standard I/O and lifecycle ownership. Failed creation must roll back partially established launch resources.

No Implicit Shell

let command := new Command("git status && echo done", [])

This does not run a shell. It requests an executable literally identified by that string. Command does not silently provide shell quoting, expansion, globbing, redirection, pipelines or command substitution.

If a shell is intentionally required, the program must name the shell executable and supply its arguments explicitly.

Bare executable names use the selected target's canonical executable search mechanism. An explicit path is resolved as a path, with relative paths interpreted relative to the effective child working directory at the operation that resolves them.

Standard I/O

enum CommandInputMode {
    Inherit
    Closed
    Pipe
}

enum CommandOutputMode {
    Inherit
    Discard
    Pipe
}

The three streams are configured independently:

try command.SetStdin(CommandInputMode.Pipe)
try command.SetStdout(CommandOutputMode.Pipe)
try command.SetStderr(CommandOutputMode.Inherit)

These methods modify configuration only. Native pipes are created during Start(), and partial setup must be rolled back on start failure.

stdin Pipe:
parent PipeWriter  -> child stdin

stdout Pipe:
child stdout -> parent PipeReader

stderr Pipe:
child stderr -> parent PipeReader

Pipe Ownership

Pipe endpoints are extracted through dedicated methods instead of ordinary aggregate partial move:

let stdinPipe := command.TakeStdinPipe()
let stdoutPipe := command.TakeStdoutPipe()
let stderrPipe := command.TakeStderrPipe()

Each method returns Some(endpoint) only when the command successfully started with the matching Pipe mode and the command still owns that endpoint. Otherwise it returns None.

Extracting a pipe transfers that endpoint's ownership while leaving the outer Command complete and usable for join, Terminate() and Observe().

Important: join does not implicitly drain stdout or stderr. If a child can block on a full pipe, the parent must arrange appropriate pipe reading.

ProcessObserver

type ProcessObserver

ProcessObserver is compiler-known, non-owning and copyable.

property ID: ProcessID
property Name: string
property Status: ProcessStatus
property Platform: ProcessPlatform
fn Wait() ProcessStatus

An observer can inspect identity, status and platform metadata and may wait for a terminal status. It cannot obtain Value, panic information, termination authority, join capability or detach capability.

Process[T].Observe() is infallible because the process already exists. Command.Observe() returns Option[ProcessObserver] because a command in Created has no child process yet.

Wait() is repeatable and non-owning. Waiting through an observer does not resolve the owning handle's join/detach obligation.

join

Raw process handles are not awaitable in Sec 0.1.

await worker    // invalid

Process lifecycle completion uses:

join worker
join command

Join waits until the complete Sec terminal process outcome is known, including native completion/termination observation, typed completion transfer or terminal metadata, and required native reaping.

Successful join consumes the one join capability but does not consume the whole owning value. The same binding remains valid for terminal inspection afterward:

join worker

let id := worker.ID
let status := worker.Status

A second join is invalid.

Status polling does not replace join

if worker.Status == ProcessStatus.Completed {
    // worker.Value is still unavailable before join
}

Status is observational. Join is the ownership/lifecycle operation that establishes terminal synchronization and resolves the join-owned native lifecycle state.

Terminal Payloads

After successful join, payload availability follows the terminal status:

StatusAvailable payload
CompletedValue
CompletionFailedCompletionError
PanickedPanic on Process[T]
TerminatedTermination

Command does not expose portable Sec panic information because an external executable is not part of the typed Sec-callable panic transport model.

Move-only result extraction

If T is move-only, reading Process[T].Value after join transfers result ownership out of the internal result slot. The outer Process[T] remains valid for terminal metadata inspection, but the result slot cannot be extracted a second time.

External non-zero exit

join command

// Example normal external exit code 1:
command.Status == ProcessStatus.Completed
command.Value.Code == 1
command.Value.Success == false

A non-zero normal exit code is not abnormal process termination.

Synchronization

Successful process-start commit publishes fully materialized startup values to the child. Child user code must not observe half-published startup state.

Successful join establishes lifecycle synchronization for completion transfer, terminal metadata and native lifecycle resolution. It also proves that the child can perform no future execution.

Join does not make arbitrary child addresses visible in the parent. Process-local memory remains process-local. Shared-memory publication and IPC ordering are defined by the corresponding IPC abstractions.

Detach and a successful termination request do not establish completion synchronization. Join remains the terminal synchronization operation.

Hard Termination

try worker.Terminate()

Terminate() requests hard process termination. The portable operation is not classified as unsafe merely because it is destructive: the child exists behind a process isolation boundary rather than inside the parent's ordinary object graph.

Hard termination does not guarantee child cleanup. The child may not execute:

  • defer blocks;
  • deterministic destruction;
  • buffer flushes;
  • application shutdown handlers;
  • transaction rollback;
  • normal return or completion transfer.

A successful termination request means the platform accepted the request; it does not prove the child is already terminal and reaped. Canonical destructive shutdown is:

try worker.Terminate()
join worker

Normal completion and hard termination may race. Exactly one terminal outcome becomes authoritative.

Detach

A Process[void] may be detached as:

detach worker

A process with a non-void normal result requires explicit discard:

detach worker discard

Because Command has normal result type ExitStatus:

detach command discard

Detach:

  • consumes the owning process value;
  • relinquishes join capability and normal result ownership;
  • relinquishes owner terminal-payload ownership;
  • transfers required reaping/native cleanup responsibility to the runtime or target;
  • allows the child to continue running;
  • establishes no completion synchronization edge.
Rule: Detach is not termination, and a conforming detach implementation must not leak unreaped native process state.

Lifecycle Ownership

A successfully started Command or successful Process[T] owns unresolved lifecycle responsibility until it is joined, detached, moved to another owner or otherwise resolved through a canonical operation.

An unresolved owner must not silently leave scope. Sec does not fix forgotten process handles by implicitly joining, terminating or detaching them during destruction.

Normal parent completion therefore requires every reachable owned child process to have an explicitly resolved lifecycle path.

Cancellation

Cancellation of a task that owns or waits on a process does not implicitly terminate, detach or discard that process.

Cancellation paths must still resolve process lifecycle ownership explicitly. Depending on application policy, that may mean termination followed by join, detach, or ownership transfer.

A cancellable task may stop waiting through a ProcessObserver according to normal task wait rules without changing ownership of the child process itself.

Blocking and ISR Rules

The following operations may wait or perform native/blocking work:

join Process[T]
join Command
ProcessObserver.Wait()
spawn process ...
Command.Validate()
Command.Start()
Command.SetWorkingDirectory(...)
Process[T].Terminate()
Command.Terminate()

Their actual effect classification follows the concurrency blocking/effect model and selected target lowering. From task context, a backend may suspend rather than occupy a physical executor worker when process completion can be registered asynchronously.

These process lifecycle operations are not ISR-safe in Sec 0.1. Final call-path validation is owned by the interrupt rules.

Target Capabilities

The active CompilationPlan distinguishes at least:

  • ProcessExecution — canonical spawn process;
  • ExternalProcessExecution — external Command.Start();
  • ProcessDetach;
  • ProcessTermination;
  • ProcessStandardIOInheritance;
  • ProcessPipes.

These are compiler facts, not runtime bool properties. A statically impossible process operation is a compile-time diagnostic. Runtime Result errors describe failures that remain possible on a target that actually supports the capability.

Bare-metal or schedulerless targets do not gain process support merely because they can run concurrent code. Likewise, an RTOS task is not automatically a Sec process. The target must provide the required isolation and lifecycle model.

Relationship to IPC

Process creation includes startup transfer and one terminal completion value. It does not turn Process[T] into a general message channel.

The IPC chapter owns:

  • general process messaging;
  • shared memory;
  • process-aware synchronization;
  • general handle and capability transfer;
  • PipeReader and PipeWriter semantics.
Rule: Ordinary in-process Channel[T] is not process IPC.

Diagnostics

Useful process diagnostics should explain the violated boundary or lifecycle rule.

Examples include:

process spawn consumes "data"; write <-data to transfer ownership

ordinary reference ref Data cannot cross a process boundary

process lifecycle owner "worker" leaves scope unresolved
help: join, detach, or transfer ownership before leaving the scope

process execution is not supported by the selected target

Command configuration is no longer valid after Start()

The compiler should distinguish statically unsupported target capabilities from runtime creation failures instead of hiding both behind a generic native error.

Specification Status

The canonical process specification is Normative, document revision 2.0, for Sec 0.1. It defines the process type family, lifecycle, external command API, startup/completion transfer and target capability model.

Compiler and runtime implementation coverage may lag the normative surface. The project implementation-status ledger remains authoritative for which parts are currently accepted, analyzed and lowered by the toolchain.

Best Practices

  • Use a process when you require a real isolation boundary, not merely concurrency.
  • Use spawn process for a Sec callable and Command for an external executable.
  • Do not treat ordinary references or raw pointer values as cross-process shared memory.
  • Use explicit <- when an existing named move-only value is consumed during process startup.
  • Remember that spawn ownership transfer commits only when process creation commits.
  • Use join for terminal lifecycle synchronization; status polling is not a substitute.
  • Do not use await on Process[T] or Command in Sec 0.1.
  • Handle Completed, CompletionFailed, Panicked and Terminated as different outcomes.
  • Do not classify a non-zero external exit code as abnormal termination.
  • Resolve every owning process lifecycle explicitly before leaving scope.
  • Take required pipe endpoints before detach.
  • Drain pipes concurrently when a child may otherwise block on pipe backpressure.
  • Use Validate() as optional preflight, not as a guarantee that Start() cannot fail.
  • Do not expect Command to run a shell implicitly.
  • Use IPC abstractions for ongoing inter-process communication.

Summary

  • A Sec process is independently scheduled execution in a distinct isolation domain.
  • Tasks, threads and processes are different execution kinds and may not be silently substituted for one another.
  • spawn process Callable(...) creates a typed Sec subprocess and returns Result[Process[T], ProcessSpawnError].
  • Command describes and launches an external executable.
  • Process[T] and Command are move-only lifecycle owners.
  • Ordinary Sec references and raw pointer numbers do not become process-valid across the isolation boundary.
  • Process-transferability is boundary-specific and may require explicit adapters.
  • Existing named values consumed during startup use explicit <-.
  • Process startup moves are transactional: failure preserves source ownership until commit.
  • Normal callable completion carries one typed value back to the owner.
  • A failure to transfer/materialize that value is CompletionFailed, not ordinary callable failure.
  • Command construction does not start a process; Start() does.
  • Command never invokes a shell implicitly.
  • Standard streams are independently configured as inherit/closed-or-discard/pipe.
  • Pipe endpoints are extracted through Take*Pipe() without partially moving the command owner.
  • ProcessObserver is non-owning and cannot resolve lifecycle ownership.
  • Raw process handles are joined with join, not awaited.
  • Join consumes join capability, establishes terminal synchronization and performs required reaping.
  • Status polling does not unlock terminal result payloads.
  • Hard termination requests process death but does not guarantee child cleanup and does not replace join.
  • Detach transfers reaping responsibility and permits continued independent execution where the target supports it.
  • Unresolved owned process lifecycles must not silently disappear at scope exit.
  • Task cancellation does not implicitly cancel a child process.
  • The active CompilationPlan determines which process capabilities exist.
  • General ongoing communication belongs to IPC, not Process[T].