Functions & Effects
A Sema function signature is def name(x: T) -> R !{effects}:. The !{…} at the
end is the effect row — the set of capabilities the function is allowed to
use. This is the feature that lets Sema prove the deterministic core is
deterministic, and that makes authority conspicuous rather than ambient.
The shape of a function
Section titled “The shape of a function”def candidate_score(bank: BankLine, entry: LedgerEntry) -> f32 !{model.embed}: return (bank.memo ~= entry.memo).score
def total(prices: list[Money]) -> Money !{}: # `!{}` = provably no I/O, no model mut sum = prices[0] for p in prices[1:]: sum = sum.combine(p) return sumParameters are typed; the return type follows ->; the effect row follows the
return type. Keyword arguments, defaults, *args, and **kwargs all work as in
Python:
def total(*nums) -> int !{}: # *args -> tuple of surplus positionals mut acc = 0 for n in nums: acc = acc + n return acc
def configured(**opts) -> Config !{}: # **kwargs -> dict of surplus keywords return Config.from_options(opts)Effects are rows on the type
Section titled “Effects are rows on the type”Sema types effects in rows, Koka-style. Each capability names a namespaced operation. The full, canonical vocabulary (shared across the whole language) is:
model.invoke model.embed model.loadfs.read fs.write net.connect net.listenproc.spawn code.gen code.exec code.patchdb.read db.write db.schemaclock random ffi.call memory.query memory.retainenv.read config.reload config.watch observe.record observe.exportevent.emit event.subscribepolicy.change package.install ui.render human.approveA function typed def f(x: int) -> int !{} provably performs no model calls
and no I/O — the deterministic core is a type-enforced sublanguage, not a
convention. This is the single most load-bearing property of Sema: everything in
the !{} sublanguage is safe to fuse, replay, cache, and reason about.
Effect instances are parameterized — net.connect("api.internal:443"),
fs.read("data/**") — and policies match on instances. See
/governance/policy/.
An omitted row is inferred — never a wildcard
Section titled “An omitted row is inferred — never a wildcard”Writing no !{…} does not grant ambient authority. It asks the compiler to
infer the minimal row from the body (fail-closed): a function that touches
nothing infers !{}. Authority is always conspicuous, never the silent default.
Two rules make this enforceable rather than aspirational:
assure silver+ requires an explicit row on every declared function (sema checkerrors otherwise). Inference stays anassure bronzeergonomic; the published surface — the caller contract, the verification cache key — must state the row so a latercode.execshows up as a signature diff, not a silent change. (Exempt because their row is derived elsewhere:simulate/bymodel-backed defs,portedports, andprovidefactories.)!{*}is the explicit all-effects top (⊤) — a loud, greppable escape hatch for spikes and REPL work, not the meaning of silence.sema checkwarns on it atbronzeand errors atsilver+, and the runtime refuses to admit a!{*}row under any policy that bounds capability.
def summarize(text: str) -> str: # no row written → inferred from the body return text.strip().slice(0, 200) # touches nothing → inferred !{}
def summarize(text: str) -> str !{}: # better: state it, so drift is a signature diff return text.strip().slice(0, 200)Calling an op is checked; declaring a capability is open
Section titled “Calling an op is checked; declaring a capability is open”There are two distinct rules, and the distinction is deliberate:
- An effect row may name any capability. Rows are extensible:
!{fs.raed}parses fine — you can declare a capability the runtime has never heard of. - But calling an operation is resolved like any builtin. A call to an
unrecognized op on a known namespace —
fs.raed("x"),json.pares(...)— raisesNameErrorat the call site, rather than silently journaling an effect and returningNone. Typos are caught, not swallowed.
Each effect namespace (fs, net, code, proc, observe, memory, event,
env, config, package, ui) and each fixed-op library namespace (json,
csv, http, sql, monitors) has a recognized callable surface and rejects
unknown ops. Intentionally dynamic namespaces stay open by design: log (by
level), and tools/mcp/skills/stream (by name).
Statement position is guarded too. The permissive parser accepts an unknown
word …: as an inert directive (the declarative-config escape), so a typo
(esnure false) or a misplaced clause (allow: inside a def) would parse and
do nothing. sema check warns on any directive in a def/simulate body
that no runtime handler recognizes — so these silent no-ops surface at check time.
This is Sema’s no-silent-no-ops guarantee in action.
Effect rows compose through higher-order code
Section titled “Effect rows compose through higher-order code”Because the effect row is part of the function type, a higher-order function cannot smuggle effects its own row does not admit. A parameter of function type carries its callee’s row:
# This function's own row must admit whatever `render` can do — you can't hide it.def render_all(items: list[Renderer], render: (Renderer) -> str !{}) -> str !{}: return "\n".join([render(it) for it in items])Lambdas
Section titled “Lambdas”Lambdas are first-class typed closures — same effect-row-in-type discipline as named functions. Two spellings, identical meaning:
inc = lambda x: x + 1scaled = lambda x, k: x * kdouble = x => x * 2 # `=>` formLambdas are expression-position only. The effect row of a closure is inferred and
travels in its type, so passing a closure that performs code.exec into a slot
that admits only !{} is a type error.
Usage and spend are ambient, not threaded
Section titled “Usage and spend are ambient, not threaded”Model calls cost tokens and money. Rather than thread a (result, usage) tuple
through every call, Sema accumulates usage ambiently in a scope:
with meter as u: answer = write_report(facts) # returns the value onlylog.info("run", tokens=u.total_tokens, cost=u.cost, calls=u.total_calls)
with budget(calls=200, tokens=1_000_000) as b: research = deep_search(query) # BudgetExceeded if it overspendswith meter as u: accumulates u.total_calls, u.prompt_tokens,
u.completion_tokens, u.total_tokens, and u.cost (priced from [pricing] per_token). with budget(tokens=N, calls=M) as b: is a meter with a hard cap: a
call that pushes spend past the cap raises BudgetExceeded rather than silently
overspending. Meters and budgets nest; each call attributes to all enclosing
frames. See /governance/budget/.
The guarantee lattice
Section titled “The guarantee lattice”Effects tie into Sema’s gradual-guarantee lattice — the status every obligation (type, contract, semantic predicate, policy) carries:
proved > checked > statistical(α) > best_effort > uncheckedproved = discharged statically; checked = a sound runtime check with blame;
statistical(α) = a calibrated conformal bound; best_effort = evaluated but
unbounded; unchecked = a visible hole. The compiler inserts checks at region
boundaries with blame labels naming the generative call at fault. A
statistical(α) obligation requires an active monitor on its input stream —
without one it decays to best_effort at the type level. See
/neurosymbolic/verification/ and
/governance/monitor/.
Failure modes and how they surface
Section titled “Failure modes and how they surface”- Unknown op on a known namespace (
fs.raed(...)) →NameErrorat the call site (not a silent no-op). - Misplaced/typo’d directive in a body →
sema checkwarning. !{*}undersilver+ →sema checkerror; refused at runtime under any bounding policy.- Missing explicit row under
silver+ →sema checkerror. - A closure escaping its declared effect row through a higher-order call → type error.
See also
Section titled “See also”- Effects (Governance) — how policies grant and confine capabilities.
- Effects Catalog — the full reference of every operation.
- Policy —
allow:/forbid:andwith policy(...)scopes. - Budget — hard spend caps with
BudgetExceeded.