Demand, async, and defer
Koskari bytecode runs on an async-native evaluator: suspension and resumption are normal, and many Python calls that return coroutines or awaitables are auto-awaited at call boundaries. How you host that evaluator (with or without an asyncio event loop) is a separate choice.
Ambient async execution
Python treats asynchronous execution as opt-in: functions are marked async,
and calling an async function returns a coroutine that must be awaited.
Koskari treats asynchronous execution as the default. Every function can suspend and resume; the runtime tries to produce a concrete value at each step and only suspends when it cannot continue. A stackless evaluator (frames on the heap, not the C stack) makes that model practical for tail calls, continuations, generators, and async work in one pipeline.
Three consequences:
Every Koskari function is capable of suspension.
Most code reads synchronously – explicit
awaitis uncommon.Suspension propagates through the evaluator rather than through coroutine objects at every call site.
Shorthand:
Python often defaults to returning awaitables; Koskari defaults to returning values when it can.
Compared with Python
async def fetch_user():
return await db.get_user()
result = fetch_user()
print(result) # <coroutine object ...>
The caller must await (or schedule) the coroutine. Async tends to spread
through callers – sometimes called async contagion.
In Koskari:
def fetch-user []:
(db.get-user)
var user (fetch-user)
(print user)
If db.get-user finishes immediately, user is a normal value and print
runs. If it must wait on Python, the evaluator suspends the current context and
resumes when the result is ready – without requiring async on fetch-user
or an explicit await at the call site.
Conceptually, (fetch-user) at a use site behaves like Python’s
await fetch_user() when suspension is needed, except the await is implicit
and managed by the runtime.
Behaviour |
Python |
Koskari |
|---|---|---|
Functions async by default |
No |
Yes (capable of suspend) |
Explicit |
Required for async defs |
Not required |
Explicit |
Usually yes |
Usually no |
Call returns coroutine object by default |
Yes (for async def) |
No – value or suspend |
Runtime tries immediate value resolution |
No |
Yes |
Suspension handled by evaluator |
No (caller awaits) |
Yes |
Intentionally hold work for later |
Tasks, coroutines |
|
Python dunder / slot may suspend |
No |
No – see demand |
Demand (sync-forbidden) execution path |
N/A |
Yes |
Auto-await at call boundaries
Coroutines, async generators, and async context managers (with / dyn-with)
integrate with the same evaluator. Ordinary calls into Python that return
awaitables are typically driven to completion at the boundary, so the stack
holds the result rather than a bare coroutine. See
compile/await and the
VM opcode reference for opcode-level detail.
Two kinds of values are excluded and arrive as plain values: a
defer handle, and any asyncio future (anything passing
asyncio.isfuture, including Task). Futures are handles to work owned by
the event loop; hold them, cancel them, or resolve them with an explicit
(await fut) when the value is wanted.
Suspension in sequential code
def fetch-user [id]:
(db.fetch-user id)
var user (fetch-user id)
(print user)
If db.fetch-user cannot complete immediately, the evaluator suspends, other
work may run on the host loop, the result arrives, execution resumes, and
print runs. The source stays sequential even though suspension occurred.
defer
Sometimes a program must not resolve a value immediately. A defer is a
single-shot await handle (Python builtin defer, also exposed in bootstrap).
The wrapped call runs when the defer is constructed; the VM does not
auto-await the defer value at the next use site.
var task (defer fetch-user)
;; ... pass task around ...
(await task)
That mirrors holding a coroutine in Python, and like a coroutine the
handle is consumed by its first await – awaiting it again raises
RuntimeError. When you need a handle that can be awaited more than
once, cancelled, or given callbacks, schedule it as a Task with
create-task. Defer is explicit – immediate resolution remains the
default for ordinary calls.
Use explicit (await expr) when expr is a defer
or any other awaitable you must suspend on at that program point. See
koskari.lib for the C/Python type surface.
Bare (await) with no argument performs a cooperative suspend on the
async-hosted path; on the demand path it triggers KoskariSyncViolation
(see on-demand? below).
Demand evaluation
Demand evaluation runs Koskari to completion without hosting an asyncio
loop for that run. Each top-level form is driven synchronously from the host
(for example demand_call on a compiled expression).
Typical uses:
Default module import path (
exec_module_demandon the meta path finder)Loading bootstrap without an event loop (demand builtins path via
koskari.bootstrap)load_module_demand– same read/compile loop as async load, but each expression is executed on the demand path
On the demand path, suspension that requires waiting on a Python awaitable
raises KoskariSyncViolation. Koskari code meant to run under demand must
not rely on await at points where the host cannot resume.
Why demand exists
Many Python integration points must not suspend: numeric operators, attribute access, sequence protocols, iterator hooks, and most CPython slot implementations expect a synchronous result or an exception – not a suspended computation.
x + y # cannot yield to an event loop mid-operation
When Python invokes a Koskari dunder (for example __add__), the runtime routes
that work through demand execution: produce a value now, or raise; never
suspend. That preserves predictable behaviour for obj + other and similar
calls while still allowing suspension inside ordinary Koskari-to-Koskari
evaluation.
Demand establishes a boundary:
Inside Koskari evaluation (async-hosted), suspension is allowed.
At synchronous Python-facing boundaries, suspension is forbidden.
Async-hosted evaluation
Async-hosted evaluation loads or runs code under an asyncio-driven host: each
compiled top-level expression is awaited. The CLI and
load_module_async use this path when an event loop
is available.
Same bytecode and namespaces as demand; different host and allowed suspensions.
Aspect |
Demand path |
Async-hosted path |
|---|---|---|
Host loop |
None required for the Koskari run |
asyncio (or compatible) |
Top-level module run |
|
|
|
Forbidden where sync completion is required |
Allowed when the awaitable can run |
Python dunder / slot entry |
Demand only |
Demand only |
Importer details: koskari.importer,
koskari.module.
Choosing a construct
Koskari resolves ordinary calls to values automatically, so most code needs none of these. Reach for one only when you want to override that default – run without a loop, hold a result instead of resolving it, or hand Koskari async work to a raw asyncio API.
Decision table
Your situation |
Use |
Kind |
|---|---|---|
The code must run to completion now, with no event loop – a module imported without a host, or a body reached through a Python dunder |
|
function wrapper |
You want to hold an async result and await it later, not resolve it at the next use site |
|
builtin type |
You must hand a Koskari function to a Python asyncio API that requires a coroutine function (a task factory, framework hook) |
|
builtin |
You need to schedule or gather held async work on the asyncio loop |
|
asyncio bridge |
The first two shape how your own code runs; the last two are bridges to
raw asyncio. If you are not crossing into an asyncio API by hand, you almost
certainly want demand/defer, not the coroutine bridges.
Auto-await caveat. Koskari auto-awaits an awaitable at the call boundary, so calling a coroutine function from Koskari resolves it to a value. To hand a real coroutine to asyncio, pass the function (
coroutine-fn) or adefer, never a call result.
demand – run without a loop
Runs on the demand path: produce a value now, or raise – never suspend. A body
that tries to await a Python awaitable here raises KoskariSyncViolation.
@demand
def checksum [a b]:
(+ a b)
(checksum 2 3) ; -> 5, computed with no event loop hosting the run
The decorator lines read as (decorate [demand] (def checksum [a b] ...)),
which binds checksum to (demand (function checksum [a b] ...)). See
compile/decorate.
defer – hold work instead of resolving it
A defer holds an async result instead of resolving it at the next use
site, and an explicit (await ...) consumes it. defer above
has the rules.
coroutine-fn – a genuine coroutine function
Some asyncio entry points require a real coroutine function – one that
asyncio.iscoroutinefunction accepts. coroutine-fn wraps a function so it
qualifies; you hand the function to the API, and when Python calls it, it
gets a coroutine.
(import asyncio)
(import-from asyncio sleep)
@coroutine-fn
def work [x]:
(await (sleep 0.1))
x
(asyncio.iscoroutinefunction work) ; -> True
Pass work itself to the asyncio API. Do not call (work 1) from Koskari
first – that auto-awaits to the value (see the caveat above).
@coroutine-fn above def work binds work to
(coroutine-fn (function work [x] ...)). See
compile/decorate.
Scheduling and gathering – create-task, defer + gather
To run held async work on the loop, schedule a defer (or coroutine) with
create-task. It returns the real asyncio.Task – a handle. Tasks
(like all asyncio futures) are excluded from implicit auto-await, so the
handle can be stored, cancelled, or given done-callbacks, and is resolved
by explicit await:
var t (create-task (defer fetch-user))
;; ... other work runs meanwhile ...
;; (t.cancel) would stop it; (await t) resolves it
(await t)
Keep a reference to the task while it runs – the event loop holds only a
weak one. Passing a defer to create-task consumes it (a defer is
single-shot); the Task owns the work from then on.
For concurrency, hold each call as a defer and await them together – defers
are awaitable, so asyncio.gather accepts them directly:
(import asyncio)
var a (defer (function fetch [] (fetch-user 1)))
var b (defer (function fetch [] (fetch-user 2)))
(await (asyncio.gather a b)) ; both run concurrently
A defer satisfies asyncio.iscoroutine (it carries the full coroutine
protocol), so the strict surfaces – asyncio.create_task, asyncio.run,
run_coroutine_threadsafe – accept it directly. The lower-level
as_coroutine helper (in koskari.asyncio, reached by import) remains
for the rare non-coroutine awaitable that needs to satisfy those checks.
See also
compile/await: explicit await and bare suspend
Hosting Koskari from Python: the load paths
koskari.asyncio: bridging Koskari and asyncio tasks