Documentation/Chapters

Coroutines and concurrent tasks

On this page

A coroutine is an execution that can suspend and later resume with its local state intact. BASIL’s task runtime uses stackful coroutines: an ordinary helper can suspend deep inside a call chain without adding ASYNC/AWAIT syntax to every function. A task adds an owned running operation with completion, errors and cancellation that a parent can observe.

Concurrency means several operations can make progress over overlapping time. Parallelism means executing work simultaneously on multiple CPU cores. BASIL supports both within its placement rules; starting a task does not promise that every library operation will execute in parallel.

Start and join

worker = SPAWN Square(12)
IF LET answer = Join(worker) THEN
    PRINT answer
ELSE
    PRINT "Cancelled"
END IF
FUNCTION Square(value AS INTEGER) AS INTEGER = value * value

SPAWN returns a noncopyable typed handle. Join waits through task cleanup. A FUNCTION task returns SOME(result) on success and NONE on cancellation. A SUB task returns TRUE/FALSE instead. Operational errors propagate through checked calls rather than becoming cancellation or an empty optional.

Copyable arguments are independent snapshots taken in written argument order. Named arguments and constant defaults work as in ordinary calls. Generic entries infer types from supplied values. No implicit capture of parent locals occurs. Borrowed/BYREF task arguments remain unsupported; channel endpoints have a specific checked scope relationship instead.

Understand scheduling

first = SPAWN Compute(10)
second = SPAWN Compute(20)
IF LET value = Join(first) THEN
    PRINT value
END IF
IF LET value = Join(second) THEN
    PRINT value
END IF
FUNCTION Compute(value AS INTEGER) AS INTEGER
    Yield()
    RETURN value * 2
END FUNCTION

The printed order is determined here by the parent’s joins, not by which task finishes first. Ordinary user function entries and loop backedges contain budgeted scheduling safe points, so CPU loops do not normally need manual Yield. Sleep(milliseconds AS INTEGER) parks for a positive interval and yields for nonpositive values. Time units are explicit; Sleep takes integer milliseconds, whereas SELECT AFTER takes a typed Duration.

Verified independent CPU tasks can run on a bounded worker pool. Channel, I/O, resource, foreign and other unverified work stays on the coordinator under the current rules. Native file/DNS/database worker pools are separate. There is no OS thread per task. Blocking native calls and long library transactions can delay other work and cancellation; no hard latency or unrestricted multicore guarantee is implied.

Request cancellation and observe it

worker = SPAWN WaitForever()
Cancel(worker)
IF NOT Join(worker) THEN PRINT "Stopped"
SUB WaitForever()
    WHILE TRUE
        Sleep(1000)
    WEND
END SUB

Cancel requests stopping; it does not wait. Join confirms the task and dependent cleanup have finished. Cancellation bypasses typed CATCH, and cleanup hooks can defer its acknowledgement while releasing resources. An existing terminal error is retained: Cancel cannot overwrite it. Observe a task’s outcome even after a timeout or a COMPLETED notification.

Normal statement-SPAWN children are joined at their invocation’s scope exit; failure/cancellation cancels and joins them before dependent locals are destroyed. Retained handle cleanup supplies cancellation/join fallback and error observation. Do not rely on cleanup to finish a protocol that can wait forever: explicitly close producers or cancel outstanding work.

WaitTasks/CancelTasks act on the current invocation’s compatibility statement-child scope, not every task in the process. Prefer explicit handles when retaining results or operational errors. Statement SPAWN requires infallible entries; fallible entries need a handle and checked supervision.

Tasks can own resources

file = OpenFile("/dev/null")
worker = SPAWN FileSize(TAKE file)
IF LET size = Join(worker) THEN
    PRINT size
END IF
FUNCTION FileSize(input AS File) AS INTEGER FAILS IoError
    RETURN GetFileInfo(input).Size
END FUNCTION

The task owns the argument storage, while its ordinary input parameter borrows that storage. A named resource needs TAKE; a fresh resource result can be adopted. Cancellation before entry still releases task-owned arguments.

Tasks can return resources too. A successful Join transfers that result once; another successful extraction reports TaskResultTaken. Copyable results may be joined repeatedly as independent values. A successful resource optional NONE returns SOME(NONE), distinct from cancellation. Unclaimed resource results are released automatically. Repeated failed joins retain the original concrete error.

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.