Concurrency
Task syntax, containers, views, and the first-pass thread-aware execution model in Walrus.
Walrus now has a thread-aware concurrency surface built around five core pieces:
@taskspawnawaitcontainerview<T>
These features are meant to move the language toward a data-oriented, thread-safe, and eventually truly threaded model.
Important First-Pass Status
The current implementation is a first pass.
That means:
- the language model is thread-aware
- the data model is being shaped for thread-safe usage
- the task syntax is already in the language
- but the runtime does not yet run tasks in parallel on worker threads
Today, spawn still executes through a synchronous first-pass path under the hood and await unwraps the stored result. The syntax, typing rules, and data wrappers are already in place so the scheduler and storage model can evolve later without breaking script code.
So the accurate description is:
- Walrus is now designed for threaded and thread-safe programming
- Walrus is not yet running a real parallel scheduler in this first pass
Design Goal
Walrus is moving toward a model where the language stays object-like on the surface, but the runtime becomes increasingly data-oriented underneath.
The intended shape is:
- containers define structured data that is safe to reason about in concurrent systems
- views provide lightweight access to batches of container data
- tasks represent schedulable work units
- spawn starts work
- await joins the result
This gives Walrus a concurrency model that is meant to feel simple in scripts while still fitting engine-style execution.
@task
Use @task on a function when that function is intended to be scheduled as asynchronous work.
@task
fn buildStuff() -> int:
return 5What @task means
- the function is marked as schedulable work
- it is expected to be started with
spawn - it may return a value
- its result type becomes the payload type of a task handle
What is not allowed
Task functions are not meant to be called like ordinary functions:
@task
fn buildStuff() -> int:
return 5
fn main() -> int:
let value = buildStuff() # invalid in the current model
return valueThis reports:
task functions must be started with spawnspawn
spawn starts a task function and returns a task handle.
@task
fn buildStuff() -> int:
return 5
fn main() -> int:
let task = spawn buildStuff()
let result = await task
return resultResult type
If a task returns T, then:
spawn someTask(...)producestask_handle<T>awaiton that handle producesT
First-pass runtime behavior
In this first pass, spawn is already the correct language form, but the runtime path is still minimal:
- the task is executed immediately through the current runtime path
- the result is wrapped into a task handle
- no worker-thread pool is active yet
This is deliberate. The language surface is being stabilized before the full scheduler is added.
await
await resolves the result of a task handle.
let task = spawn buildStuff()
let result = await taskType rule
await only accepts task handles.
If you try to await a normal value:
let value = 4
let result = await valueyou get:
await requires a task handlecontainer
container is the first-pass structured-data declaration for Walrus’ data-oriented direction.
container velocity:
float speed = 5
vector3 directionWhy container exists
Walrus already has classes and objects, but container is meant for a different role:
- structured runtime data
- predictable field layout direction
- future ECS/storage lowering
- future concurrency-aware access patterns
In other words, a container is not just “another name for class” conceptually, even though the first-pass implementation reuses the class/type pipeline internally.
First-pass behavior
Right now, containers:
- define named fields
- allow optional defaults
- can be constructed and accessed like other structured runtime values
- can be used with
view<T>
That means this works:
container velocity:
int speed = 5
int direction_x = 0
fn main() -> int:
let v = velocity { speed = 2, direction_x = 10 }
return v.direction_xview<T>
view<T> is a lightweight access wrapper over container data.
In this first pass, view<T> is backed by:
- an array of
T - a start offset
- a count
It is designed to feel like a non-owning slice-style window over a batch of structured values.
Restriction in this first pass
view<T> currently only accepts container types:
fn compute(values: view<velocity>) -> view<velocity>This is valid if velocity is a container.
This is invalid:
fn bad(values: view<int>) -> int:
return values.len()because the current rule is:
view<T> currently only supports container typesCreating a view
Use viewOf<T>(array) to create a writable view:
let world = [
velocity { speed = 2, direction_x = 10 },
velocity { speed = 4, direction_x = 20 }
]
let all = viewOf<velocity>(world)Read-only views
Use readonly() when the view should not permit mutable iteration:
let ro = all.readonly()View APIs
Current first-pass helpers:
| Method | Description |
|---|---|
len() | Number of items in the view |
is_empty() | Returns true if no items are present |
first() | Returns the first item, or nil in empty cases |
slice(start, count) | Produces a sub-view without copying |
readonly() | Produces a read-only view wrapper |
last() | Returns the last item, or nil when empty |
take(count) | Returns the first count items as a view |
skip(count) | Returns a view advanced by count items |
contains_index(index) | Bounds check helper for view-local indices |
Example
container velocity:
int speed = 0
int direction_x = 0
fn main() -> int:
let world = [
velocity { speed = 2, direction_x = 10 },
velocity { speed = 4, direction_x = 20 },
velocity { speed = 6, direction_x = 30 }
]
let all = viewOf<velocity>(world)
let middle = all.slice(1, 1)
if middle.len() == 1:
return middle.first().direction_x
return 0Iterating Views
Views are meant to support cache-friendly batched iteration.
Read-only iteration
for v => vel:
print(v.speed)Mutable iteration
for mut v => vel:
v.direction_x += v.speedReadonly safety
Mutable iteration is rejected on read-only views:
let ro = viewOf<velocity>(world).readonly()
for mut v => ro:
v.direction_x += v.speedThis reports:
mutable iteration is not allowed on readonly viewsThreaded and Thread-Safe Meaning
When we say Walrus is becoming threaded and thread-safe, this is the intended meaning of the model:
- task functions describe work that can be scheduled independently
- containers represent structured data meant for disciplined concurrent access
- views express batch access over container data instead of arbitrary pointer chasing
- readonly vs writable access is becoming explicit in the language surface
That is the right foundation for:
- worker-thread scheduling
- chunked processing
- ECS-like storage backends
- task graph execution
- engine job systems
What is already true
- the syntax and type system now recognize the concurrency-oriented model
- task handles and view wrappers already exist in the runtime core
- the language pushes code toward data batches rather than ad hoc shared mutation
What is not yet true
- tasks are not yet dispatched across real worker threads
- the runtime does not yet have a production scheduler
- there is not yet a full parallel storage backend for containers
- there is not yet advanced alias analysis or borrow enforcement across task boundaries
So the safest summary is:
Walrus now has a thread-aware, thread-safe-by-design language surface, while real parallel execution is still being built underneath it.
Full Example
container velocity:
int speed = 5
int direction_x = 0
@task
fn compute(chunk: view<velocity>) -> view<velocity>:
for mut v => chunk:
v.direction_x += v.speed
return chunk
@task
fn buildStuff() -> int:
return 5
fn main() -> int:
let world = [
velocity { speed = 2, direction_x = 10 },
velocity { speed = 4, direction_x = 20 },
velocity { speed = 6, direction_x = 30 }
]
let work = viewOf<velocity>(world).slice(0, 2)
let computeTask = spawn compute(work)
let setupTask = spawn buildStuff()
let result = await computeTask
let setupValue = await setupTask
print(result.first().direction_x)
print(setupValue)
return 0Deferred Work
The following pieces are intentionally deferred after this first pass:
- real multi-threaded scheduler execution
- task stealing or worker pools
- cancellation
- futures/promises beyond the basic task handle
- callback-style
for_each(...) - native kernel/task integration
- lower-level storage policies for containers
- chunk-based parallel iteration APIs