Async Execution in Sigil
Overview
Sigil uses one async-capable runtime model and one explicit concurrency surface.
The rule is:
- ordinary Sigil expressions compose through one async-capable runtime model
- explicit concurrency is introduced only through named concurrent regions
That gives Sigil one function model without forcing users to write await, while keeping batching, rate limits, and stop behavior visible in the source.
Ordinary Evaluation
Sigil functions compile to JavaScript functions that return promise-shaped values. Ordinary expression structure is not a concurrency surface.
- sibling expressions are not an implicit fanout boundary
- list operators do not lower through broad
Promise.all - pure
mapandfilterremain canonical list transforms reduce ... from ...remains ordered reduction
So Sigil hides promise plumbing, but it does not treat ordinary expression structure as permission to widen work.
Named Concurrent Regions
Sigil uses one concurrency surface:
λisTransportFailure(err:String)=>Bool=err="NETWORK"
λmain()=>!Timer [ConcurrentOutcome[Int,String]]=concurrent urlAudit@5:{jitterMs:Some({max:25,min:1}),stopOn:isTransportFailure,windowMs:Some(1000)}{
spawnEach ["alpha","beta"] processUrl
}
λprocessUrl(url:String)=>!Timer Result[Int,String]={
l _=(§time.sleepMs(0):Unit);
Ok(#url)
}
Region rules:
- regions are named:
concurrent regionName@width{ ... } - width is required after
@ - width may be a literal, identifier, postfix chain, or a parenthesized expression
- optional policy attaches as
:{...} - policy fields are alphabetical when present:
- jitterMs - stopOn - windowMs
- region bodies are spawn-only:
- spawn expr - spawnEach list fn
Region Policy
The region surface is:
- width after
@:Int jitterMs:Option[{max:Int,min:Int}]stopOn: λ(E)=>BoolwindowMs:Option[Int]
Semantics:
- width is the maximum number of live child tasks
windowMsmeans no more thanwidthchild starts in anywindowMs
window
jitterMsadds a randomized start delay per childstopOnis evaluated on child failures and decides whether the region should
stop scheduling new work
Defaults:
- omitted
jitterMsbehaves likeNone() - omitted
stopOnnever stops early - omitted
windowMsbehaves likeNone()
Canonical code omits default-valued policy entirely, so the smallest region is:
λmain()=>!Timer [ConcurrentOutcome[Int,String]]=concurrent urlAudit@5{
spawnEach ["alpha","beta"] processUrl
}
λprocessUrl(url:String)=>!Timer Result[Int,String]={
l _=(§time.sleepMs(0):Unit);
Ok(#url)
}
Child Result Shape
Regions return one ordered list of outcomes:
t ConcurrentOutcome[T,E]=Aborted()|Failure(E)|Success(T)
Ordering is stable:
spawnpreserves spawn orderspawnEachpreserves input order
That means a concurrent batch has one deterministic result shape even when work resolves at different times.
Stop Behavior
Children inside a region return Result[T,E].
The region maps them into ConcurrentOutcome[T,E]:
Ok(value)becomesSuccess(value)Err(error)becomesFailure(error)- stopped or not-yet-finished work becomes
Aborted()
When stopOn(error) returns true:
- Sigil stops scheduling new work
- unfinished work becomes
Aborted()on a best-effort basis
This is intentionally not a claim of universal force-cancellation. The runtime stops new starts immediately, but already-started work may still settle if the underlying backend surface cannot be cooperatively cancelled.
Why Sigil Does It This Way
Real batch workflows need more than width:
- rate windows
- jitter
- best-effort collection
- selective stop on systemic failures
Those are properties of an execution region, not properties of map itself.
So Sigil has:
- one canonical list-processing surface for value transforms
- one canonical concurrent-region surface for explicit widening
What This Does Not Mean
Sigil does not promise:
- automatic CPU parallelism
- general cancellation of arbitrary started tasks
- implicit concurrency everywhere a collection is traversed
The language keeps one async-capable runtime model, and explicit widening belongs to named concurrent regions.