RFC 0001 — Effect / capability system¶
Status: Draft. Targets a future cobra4 release. Discussion: #TBD
Motivation¶
Today a cobra4 function's signature tells you what arguments it takes and what it returns. It does not tell you what it can do — read a file, hit the network, write to a database, mutate global state, sleep for an hour. For a language whose pitch is "deploy this to a Lambda / serverless container", that's a missing dimension.
If the type system tracks effects, three things become possible:
- Sandboxed execution — host an untrusted
.c4script and grant it[http]only. The runtime refuses to load anything that callsread("./local")orsecret(...). - Cost-aware deploy — adapter sees
with [http, db]and provisions the matching IAM policy / VPC config. No more "deploy succeeded but the lambda 403s on prod because IAM is wrong". - Compiler optimization — a
with [](pure) function is memoizable, parallelizable without locks, etc.
Proposed surface¶
fn pure_double(x) -> int with [] = x * 2
fn fetch_user(id: str) -> User with [http]
= http.get("https://api.example.com/users/{id}")
fn store(u: User) with [db, log]
{ db.insert(u); log("stored", id=u.id) }
fn pipeline(id) with [http, db, log]
{
u = fetch_user(id)
store(u)
}
The with [...] clause is part of the function type. A call site
is checked against the caller's effect set:
fn handler(req) with [http, db, log] {
pipeline(req.id) # OK — caller's effects ⊇ callee's
}
fn pure_helper(x) with [] {
pipeline(x) # ERROR — pure context cannot use [http, db, log]
}
Built-in effects¶
| Effect | Granted by |
|---|---|
http |
fetch, cobra4.stdlib.http |
fs |
read("./..."), save("./..."), cobra4.stdlib.fs |
db |
The sql plugin's sql_run, any registered DB handler |
log |
log(...) calls |
secret |
secret(...) calls |
ssh |
run(host=...) against a remote |
time |
time.sleep, every, on event from queue(...) |
deploy |
The deploy builder family |
Inference¶
Most user functions don't need an explicit with — the checker
infers from the body (the union of all callees' effects). Annotation
becomes interesting at module boundaries (public API) and for
sandboxing.
Inference vs annotation conflict¶
If the user writes fn f() with [log] but the body calls
http.get(...) (which has [http]), the checker reports a
E001: missing effect error and lists which call introduced
which effect. Not a warning — annotation is a contract.
Sandboxing¶
A new top-level sandbox keyword:
Inside the block, only http and log are granted. The cobra4
runtime injects a guard around every effectful builtin that checks
the active effect mask. Violation raises EffectViolation at runtime
(belt and braces — the static check should have caught it, but
dynamic plugin code might evade).
Implementation sketch¶
- Grammar: extend
fn_declwith optional"with" "[" effect_list? "]"afterreturn_type?. - AST: add
effects: list[str]toFnDecl. - Type checker: extend
_FnSigto carry effects. New pass: for each fn body, compute the union of called fns' effect sets. Report mismatch. - Runtime:
with_effects(*names) {...}context manager (Python) maintained as a thread-local. Sandbox wraps a Pythonexecin this manager. Built-in helpers check the mask and raise on violation. - Codegen: emit an explicit decorator on each fn that records declared effects (for runtime sandboxing). No-op when sandboxing is disabled.
Open questions¶
- Effect polymorphism (
fn map(xs, fn) with effects_of(fn)): needed for higher-order functions to be useful. May need a tiny effect-variable language. - Existing functions with no annotation: do they default to
with [*](any effect)? Or are they audit-required? - The
each ... in parallelcase: does the parallel scope inherit caller effects? Probably yes, no special rule.
Estimated effort¶
~3-4 days for a usable v1 (declared annotations only, no inference, no sandboxing). Sandboxing + inference doubles that.