Data-Race & Alters Safety

A systematic guide to how Vela guarantees race-free concurrency, prevents unexpected mutation cascades, and enforces rigorous compile-time alters contracts.

Race Elimination Alters Invariants Static Analysis

🚨 The Threat: Why Concurrency & Mutation Cause Bugs

In systems programming, data races and unexpected mutation cascades represent the most catastrophic class of software defects. They cause memory corruption, intermittent deadlocks, and silent state divergence.

A data race occurs whenever two or more concurrent execution paths access the same memory location simultaneously, at least one access is a write, and there is no synchronization.

An alters hazard occurs when a piece of code unexpectedly mutates state that an active caller was relying upon, or when cascading event triggers cause hidden side effects or infinite recursive loops.

Vela's Design Philosophy: "Safety through static structure, not defensive runtime locks." Vela eliminates data races and uncontrolled mutations at compile time through four cooperating architectural layers.

🛡️ Vela's 4-Layer Defense Architecture

Layer 1: Linear Ownership Transfer (own T)

Traditional channel implementations (like Go's) allow sending pointers across channels without transferring ownership, leaving both sender and receiver capable of mutating the same data concurrently.

In Vela, cross-task communication over a Chan[T] requires transferring ownership:

fixed p = Packet { id: 101, payload: data }
dispatch(q, own p)
; 'p' is now invalidated! Any subsequent read or write to 'p' is a compile-time error.

Because the sender permanently relinquishes access, the carried object is only ever accessed by one thread at a time. Data races are mathematically impossible.

Layer 2: Actor Isolation & The Share-Nothing Model

An actor completely encapsulates its state. Fields cannot be accessed or changed from the outside:

fixed worker = spawn Worker(0)
; worker.processed = 5  -- COMPILE ERROR: actor state is private

Every actor executes on its own thread and processes incoming messages sequentially. Inside an actor handler, mutations occur without any mutexes or locks because concurrency is eliminated within the actor's boundary.

Layer 3: The Alters Contract & Transitive Indirect Alters

To prevent "action-at-a-distance" bugs where an event hook silently alters unrelated variables, Vela requires every event that modifies state to declare an explicit contract:

alters(.shield, .alive) event absorb on .hp
    when .shield > 0
        .shield -= 5
    fi
fi

The Indirect Alters Rule: If event absorb alters .shield, and mutating .shield triggers another event check_death that alters .alive, then absorb MUST also declare .alive! If omitted, the compiler halts with a detailed diagnostic tracing the exact trigger path.

Layer 4: Terminating Guarded Cycle Policy

If Event A modifies X, which triggers Event B that modifies Y, which triggers Event A again, uncontrolled execution would quickly cause a stack overflow.

Vela builds the full directed trigger graph at compile time. Unconditional cycles are rejected immediately. A cyclic event dependency is permitted only if every event participating in the cycle is strictly guarded by a terminating when condition.

⚠️ Known Concurrency & Mutation Hazards

Below is a comprehensive catalog of potential hazards when working with concurrency and events in Vela, along with how the language protects against them.

Hazard 1 Cross-Task Pointer Escapes

The Danger: If a task allocates an object in a local region and sends a raw pointer (^T@r) or reference (&T) over a channel to another thread, both threads could access the memory concurrently, or the region could terminate before the receiver finishes reading it.

Vela's Protection: The type system restricts channel payloads to own T or value copies. Raw pointers and regional references cannot cross thread boundaries unless the entire region's ownership is transferred via own region.

Hazard 2 Dynamic Event Attachment Aliasing

The Danger: Unlike global shared events, non-shared shape events can be dormant on some instances and active on others via instance.listen(...). If code assumes an event listener is always active on every instance, state updates may silently be missed on unattached instances.

Vela's Protection: Vela provides the auto keyword for shape events that must run automatically across all instances. For non-auto events, the compiler verifies that event names passed to .listen() exist on the target shape and generates a deterministic 32-bit bitmask filter.

Hazard 3 Slice Reallocation Under Mutation

The Danger: When an alters method called on an item of a slice causes the slice to grow (allocating a larger backing buffer), the item moves to a new location. If the method writes to .field afterwards, it could write to the abandoned memory.

Vela's Protection: The "place given once" rule prevents aliasing pointers to what holds a slice, and compiler static safety checks warn when an altering method on a slice item takes handles capable of growing that same slice.

Hazard 4 Synchronous Hook Re-Entrancy

The Danger: Synchronous event hooks execute immediately on the caller's thread during field assignment. If an event makes a network call or blocks, it freezes the mutating task and may observe intermediate, partially-updated states.

Vela's Recommendation: For long-running, I/O-heavy, or complex tasks, event handlers should avoid deep work directly. Instead, forward a message into an Actor's channel (e.g. actor.dispatch(Msg(...))) to decouple the trigger from asynchronous execution.

Hazard 5 Shared Field Mutations Across Tasks

The Danger: shared flex fields on shapes represent global state accessible across tasks without passing an instance. Uncontrolled writes could cause race conditions.

Vela's Protection: Shared field mutations automatically trigger global events matching the field. Developers must declare alters contracts on tasks modifying shared fields, making state changes auditable.

✅ Developer Best-Practices Checklist

Pattern Recommended Approach Anti-Pattern to Avoid
Concurrent State Use actor with private fields and message handlers. Global shared mutable variables under locks.
Data Handoff Transfer ownership explicitly using dispatch(chan, own data). Passing raw shared pointers across tasks.
Reactive Hooks Use event on .field with explicit alters(...). Hidden callbacks buried inside deep helper tasks.
Event Cycles Strictly guard recursive triggers with terminating when conditions. Unconditional triggers that loop endlessly.
Heavy Workloads Bridge events to actors: event sends message to actor mailbox. Blocking I/O operations directly inside synchronous event hooks.