🚨 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 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. |