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 * valueSPAWN 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 FUNCTIONThe 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 SUBCancel 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 FUNCTIONThe 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.