Function Disciplines (pure, task, async)
This document specifies Silk’s intended “function discipline” system: how functions declare whether they are pure, asynchronous, or safe to run as parallel tasks.
Const functions (const fn) are specified separately in
const functions. The const modifier is orthogonal to the
discipline system described here (a const fn may also be declared pure).
, but Silk currently now implements
pure fn parsing and a strict purity checker. Concurrency disciplines (task /
async) are parsed and Task(T) / Promise(T) handles plus yield (task
values) and await (promise values) are implemented in the Supported forms
(await Task(T) is rejected). On the hosted linux/x86_64 target, the compiler
now ships a bring-up async runtime (single-threaded executor + stackful
coroutines in libsilk_rt) so await can suspend and resume without blocking
the OS thread. A compiler state-machine coroutine transform, structured
concurrency scope semantics, and task-safety (Send/Sync)-like rules remain
future work. See concurrency for the concurrency model and
implementation status.
Overview#
The language design distinguishes:
fn— normal function (may perform effects; blocking).pure fn— function with no observable side effects (referentially transparent).task fn— function safe to execute on a worker pool as a parallel task.async fn— function that may suspend atawaitpoints (returns an awaitable).async task fn— async function executed as a separate task (self-contained worker).
Intended Call Rules (Design)#
The checker is expected to enforce:
purecode may call onlypurecode (and cannot perform I/O or mutation outside local, non-escaping temporaries).taskcode may calltaskandpurecode, and must satisfy task-safety rules for captured/argument data.asynccode mayawaitother async operations; it may callpurecode and may offload blocking work via explicit adapters .
Crossing discipline boundaries is intended to be explicit and diagnostic-driven (for example suggesting the correct adapter/intrinsic).
Standard Intrinsics#
The standard library is expected to provide typed adapters to cross boundaries safely (names and exact signatures are design work):
- lifting sync work onto a task pool,
- presenting a task as an async operation,
- running blocking work from async without stalling the event loop,
- structured spawn/join primitives.
These APIs are not yet present in the in-tree std/ implementation.
Implementation Notes#
Today:
pure fnis parsed and checked (Supported forms):- a
pure fnmay call onlypurefunctions;extis treated as impure, - the checker also supports purity inference (“auto-pure”) for ordinary
fndeclarations andimplmethods: - when an unannotated function/method has an eligible signature and its
body satisfies the purity rules, it is treated as
purefor call checking, and may be called frompurecode, - functions/methods with
&Tparameters are not eligible for inference (explicitpure fnremains supported for&Tparameters in the current subset), purecannot be combined withtaskorasyncin the Supported forms,- a
pure fnmay not havemutparameters, - a
pure fnmay not declare mutable locals (varorlet mut) and may not perform mutation via assignment, - a
pure fnmay not allocate (new) in the Supported forms, - a
pure fnmay not have a typed-error contract (-> T | Error...) and may not containpanicstatements. task fn,async fn, andasync task fnare parsed and preserved in the AST.- Calls across disciplines are now reflected in expression types:
- calling a
task fnyieldsTask(T), - calling an
async fnyieldsPromise(T), - calling an
async task fnyieldsPromise(Task(T)), yieldsupports both statement and expression forms:- statement forms (only inside an enclosing
task fn/async task fn): yield v;sends a value to the task’s yield stream,yield * t;forwards all values fromtinto the task’s yield stream,- expression forms:
yield treceives the next yielded value fromt,yield * tdrains/collects remaining values fromtintoT[],awaitunwrapsPromise(T)and yieldsT(await Task(T)is rejected), andawait * psunwrapsPromise(T)[]intoT[].await <expr>andasync { ... }/task { ... }blocks are enforced as async-only constructs:awaitis only permitted insideasyncfunctions (includingasync task fn),async { ... }/task { ... }blocks are only permitted insideasyncfunctions.yield <expr>is enforced as a task-only construct:yieldexpression forms (yield t/yield * t) are permitted only insidetaskfunctions (task fn/async task fn) and insidetask { ... }/task loop { ... }blocks.yieldstatement forms (yield v;/yield * t;) require an enclosingtask fn/async task fn.- Lowering/codegen implements
taskexecution using OS threads onlinux/x86_64and implementsyield/yield *for task values plusawaitfor promises. - By default, each
task fncall is scheduled on the global task pool. attr(task=thread)forces a dedicated OS thread per call.- On hosted
linux/x86_64, the compiler ships a bring-up async runtime (src/silk_rt_async.c) soawaitis a true suspension point: - awaiting a pending
Promise(T)parks the current fiber and allows other runnable fibers to execute (it does not block the OS thread), - outside the executor owner thread (including when no executor is active),
awaitblocks the OS thread until the promise resolves. yield/yield *waits on task output use the same hosted fd-wait runtime path, so waiting for task values from executor-driven async code suspends the current coroutine instead of blocking the executor owner thread.- the long-term design remains a compiler coroutine transform plus a stable
std::runtime::event_loopAPI; see async runtime. async { ... }/task { ... }blocks remain lexical scopes, but scope exit is now runtime-backed for live handle cleanup:- live
Promise(T)bindings are awaited/destroyed, - live
Task(T)bindings are drained/destroyed (joining dedicated-thread tasks; pooled/default tasks skip the join), - and the same cleanup runs on overwrite and early scope exit.
- Function types are parsed in type positions (notably for
ext). - Function expressions are implemented as first-class function values:
fn (x: int) -> x + 1(expression body),fn (x: int) -> int { return x + 1; }(block body).fn (x: int) { ... }(block body, implicitvoidresult).- Function expressions may not declare
&Structparameters; only single-slot scalar&Tparameters (for example&int) are supported in the Supported forms. - Function expressions are eligible for purity inference (“auto-pure”):
- when the body satisfies the
purerules, the function value is treated aspurefor call checking (it may be called frompurecode), - otherwise the function value is impure and may not be called from
purecode. - Capturing closures are supported as a subset:
- a function expression may reference immutable locals/parameters from an enclosing scope,
- captures are by-value copies into a heap environment (scalar-only in the Supported forms),
- forming captures inside
purecode is rejected (capture environments allocate), - capturing closures are also eligible for purity inference (a closure
whose body satisfies the
purerules is callable frompurecode). - Function values (both non-capturing and capturing) are supported end-to-end: they may be passed, returned, stored, and called indirectly.
Source repository · Edit this page · View Markdown