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()
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;Commandand 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 kind | Meaning |
|---|---|
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().
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
}
| Status | Meaning |
|---|---|
Created | A Command launch object exists, but no child process has been established. |
Running | Execution exists and no terminal outcome has yet been observed. |
Completed | Normal execution completed and the normal result has been established owner-side. |
CompletionFailed | Execution returned normally, but the declared result could not be transferred/materialized owner-side. |
Panicked | A Sec-callable subprocess ended through Sec panic and canonical panic information was established. |
Terminated | The 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().
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:
| Status | Available payload |
|---|---|
Completed | Value |
CompletionFailed | CompletionError |
Panicked | Panic on Process[T] |
Terminated | Termination |
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:
deferblocks;- 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.
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— canonicalspawn process;ExternalProcessExecution— externalCommand.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;
PipeReaderandPipeWritersemantics.
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 processfor a Sec callable andCommandfor 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
joinfor terminal lifecycle synchronization; status polling is not a substitute. - Do not use
awaitonProcess[T]orCommandin Sec 0.1. - Handle
Completed,CompletionFailed,PanickedandTerminatedas 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 thatStart()cannot fail. - Do not expect
Commandto 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 returnsResult[Process[T], ProcessSpawnError].Commanddescribes and launches an external executable.Process[T]andCommandare 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. Commandconstruction does not start a process;Start()does.Commandnever 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. ProcessObserveris 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].