Effects
Effects declare the side effects a function may have. This is unique to Calor - traditional languages leave side effects implicit.
Why Declare Effects?
In traditional languages, you must read the entire implementation to know if a function:
- Writes to console
- Reads files
- Makes network calls
- Modifies a database
Calor requires explicit effect declarations:
§F{f001:SaveUser:pub}
§I{User:user}
§O{bool}
§E{db:rw,net:rw} // Declares: database and network operations
// ...
Now an agent knows immediately what side effects to expect.
Effect Syntax
§E{code1,code2,...}
§E{} // No effects (pure function)
Place the effect declaration after the output type:
§F{id:name:vis}
§I{...}
§O{...}
§E{effects} // Here
§Q ...
§S ...
// body
Effect Rows for Callbacks
When a function accepts another function — a callback, handler, predicate, or transform — an effect row says what that callback is allowed to do.
The line matters. A §E{...} on its own line is the containing
function's effect declaration. A §E{...} on the same line as a function
type belongs to that callback, binding, field, property, or return value.
§M{m001:Callbacks}
§F{f001:Apply:pub} (Func<i32,i32>:transform §E{cw}, i32:value) -> i32
§E{cw}
§R §C{transform} §A value §/C
The §E{cw} beside transform belongs to the callback type. The §E{cw} on
its own line belongs to Apply. Calling transform charges its cw effect to
Apply, so both rows are required here.
Rows are part of the type. Func<i32,i32> §E{} and
Func<i32,i32> §E{cw} have the same inputs and output, but different effect
permissions.
Where a row can appear
§M{m001:Rows}
§F{f001:Use:pub} (Func<i32,i32>:callback §E{cw}, i32:value) -> Func<i32,i32> §E{cw}
§E{cw}
§B{saved:Func<i32,i32>} §E{cw} callback
§B{local} §LAM{lam1:x:i32} §E{cw}
§P x
§R x
§/LAM{lam1}
§R callback
§DEL{d001:Handler}
§I{i32:value}
§O{void}
§E{cw}
§CL{c001:Callbacks:pub}
§FLD{Action<i32>:onChange:pri} §E{cw}
§PROP{p001:Current:Action<i32>:pub:get,set} §E{cw}
Rows are supported on:
- function and method declarations,
- lambdas and delegates,
- parameters in tag or inline signature form,
- return types in tag or arrow form,
- bindings, fields, and properties.
A type carries one row. Put multiple permissions in that row
(§E{cw,fs:w}), rather than writing multiple §E tags.
Effect placeholders
A higher-order helper often needs to have exactly the effects of the callback
passed to it. Declare an effect placeholder with <eff name> and use it in the
callback and function rows:
§M{m001:HigherOrder}
§F{f001:Twice:pub}<eff e> (Func<i32,i32>:callback §E{e}, i32:value) -> i32
§E{e}
§B{first:i32} §C{callback} §A value §/C
§R §C{callback} §A first §/C
Twice is pure when callback is pure and carries cw when the callback may
write to the console. The compiler resolves e at each call site.
Effect placeholders can be declared on functions and methods, including
interface methods. They can be used in that declaration's own row and its
parameter rows. A placeholder cannot use the name of a real effect code such
as cw.
What the compiler checks
The compiler checks a row whenever a function value moves:
- binding or reassigning it,
- passing it as an argument,
- returning it,
- overriding a method,
- implementing an interface member.
A callback whose effects exceed the destination row is Calor0424, always an
error. If one side has no row and the compiler cannot decide, it reports
Calor0425; strict mode promotes that warning and permissive mode waives it.
Lambda rows are promises too. If a lambda body does more than its row allows, the compiler reports the mismatch. If the lambda has no written row, the compiler infers one from the body when it can.
Common row mistakes
Calor0405 means a row is attached to something that cannot carry one, is on
the wrong line, or duplicates another row:
§FLD{i32:count:pri}
§E{cw} // Wrong: count is not a function value, and this is a separate line.
Calor0404 means an effect placeholder is declared or used somewhere it is
not allowed, or is named after a real effect code.
Current limitations
Effect rows do not yet fully verify callbacks reached through reflection or
dynamic, event-handler +=, or delegates returned by .NET libraries.
Unresolved cases fail closed: the caller is charged unknown, and an
under-declared caller fails the build. These boundaries are never silently
treated as verified. Permissive effects explicitly waive that fail-closed
behavior for unknown calls. Async rows remain limited; consult the diagnostics
and the project index before relying on a higher-order
boundary.
Effect Codes
| Code | Effect | Description | C# Examples |
|---|---|---|---|
cw | Console write | Output to console | Console.WriteLine() |
cr | Console read | Input from console | Console.ReadLine() |
fs:r | Filesystem read | Read from filesystem | File.ReadAllText() |
fs:w | Filesystem write | Write to filesystem | File.WriteAllText() |
fs:rw | Filesystem read/write | Read and write filesystem | File.Copy() |
net:r | Network read | HTTP GET, etc. | HttpClient.GetStringAsync() |
net:w | Network write | HTTP POST, etc. | HttpClient.PostAsync() |
net:rw | Network read/write | HTTP operations | HttpClient.SendAsync() |
db:r | Database read | Database queries | SELECT queries |
db:w | Database write | Database mutations | INSERT/UPDATE/DELETE |
db:rw | Database read/write | Database operations | ORM calls |
Examples
Pure Function (No Effects)
§F{f001:Add:pub}
§I{i32:a}
§I{i32:b}
§O{i32}
// No §E declares this function pure; an effectful body would be an error.
§R (+ a b)
Or explicitly:
§F{f001:Add:pub}
§I{i32:a}
§I{i32:b}
§O{i32}
§E{} // Explicitly no effects
§R (+ a b)
Console Output
§F{f001:Greet:pub}
§I{str:name}
§O{void}
§E{cw} // Console write
§P name
File Operations
§F{f001:CopyFile:pub}
§I{str:source}
§I{str:dest}
§O{bool}
§E{fs:rw} // Filesystem read and write
// ...
Network Call
§F{f001:FetchData:pub}
§I{str:url}
§O{str!str}
§E{net:rw} // Network operations
// ...
Database with Logging
§F{f001:CreateUser:pub}
§I{User:user}
§O{i32}
§E{db:rw,cw} // Database and console (for logging)
// ...
Multiple Effects
§F{f001:ProcessOrder:pub}
§I{Order:order}
§O{bool}
§E{db:rw,net:rw,fs:w,cw} // Database, network, filesystem write, console write
// ...
Effect Patterns
Read-Only vs Read-Write
// Read-only file operation
§F{f001:LoadConfig:pub}
§I{str:path}
§O{Config}
§E{fs:r} // Only filesystem read
// ...
// Read-write file operation
§F{f002:UpdateConfig:pub}
§I{str:path}
§I{Config:config}
§O{void}
§E{fs:rw} // Filesystem read and write
// ...
Interactive Console
§F{f001:Prompt:pub}
§I{str:question}
§O{str}
§E{cw,cr} // Console write and read
§P question
§R §C{Console.ReadLine} §/C
Benefits for Agents
1. Filtering by Effect
"Find all functions that access the database":
// Agent searches for §E{..db..}
2. Refactoring Safety
"This function should be pure, but it has effects":
§F{f001:Calculate:pub}
§O{i32}
§E{cw} // Wait, why is Calculate logging?
3. Testing Strategy
- Functions with no effects: Unit test directly
- Functions with
cw/cr: Mock console - Functions with
fs:r/fs:w/fs:rw: Mock filesystem - Functions with
net:r/net:w/net:rw: Mock HTTP - Functions with
db:r/db:w/db:rw: Mock database
4. Composition Analysis
// If f1 calls f2, f1's effects must include f2's effects
§F{f001:ProcessAndSave:pub}
§E{db:rw,cw} // Must include f002's effects
§C{f002:Process} ... §/C
§F{f002:Process:pri}
§E{cw} // Has console write effect
// ...
Effect Checking
Effect enforcement is on by default for compile, build, and watch workflows. Two rules drive everything: a function must declare every effect its body has, and a caller must declare everything its callees may do. A call the compiler cannot look up is never quietly treated as harmless.
Collection mutators such as Add, Remove, Clear, Insert, Sort, and
CopyTo are not on the known-pure list.
What the Compiler Reports
| Code | Level | What it means |
|---|---|---|
Calor0410 | Error | A function does something its §E{…} line does not list. The message often shows the chain of calls that got there. |
Calor0411 | Warning | The compiler cannot look up what an outside call does. Add the method to a .calor-effects.json manifest. --strict-effects makes it an error. |
Calor0417 | Warning | A public function has no §E{…} at all, so callers in other files have nothing to check against. Raised by the cross-module pass, so it only appears when you compile more than one file together. |
Calor0419 | Warning | The effects are assumed rather than checked — the body holds raw C# interop, or something the compiler does not recognise, or a call to a function whose own effects are assumed. --strict-effects makes it an error. |
Calor0420 | Error | An override declares more effects than the method it overrides. Declaring fewer is fine. |
Calor0421 | Error | An interface implementation declares more effects than the interface method. Declaring fewer is fine. |
Calor0424 | Error | A callback's effects do not fit where it is being bound, passed, or returned. |
Calor0425 | Warning | The compiler cannot decide whether a callback fits, because one side carries no §E{…}. --strict-effects makes it an error. |
Calor0418 | Error | You invoked something that is provably not a function — an i32, a string, an array — so there is no effect list to charge. |
Calor0406 | Error | Effect checking stopped before it finished. New in 0.16; see below. |
Calling a function value — a callback parameter, a lambda you bound to a name,
a callback field — is not an error. Since 0.15 the compiler reads the
callback's own §E{…} and charges it to whoever calls it. Calor0418 is left
for the one case where the thing being invoked is not a function at all.
When Effect Checking Gives Up: Calor0406
New in 0.16. Effect checking runs two loops, and each has a safety limit so that a tangle of functions calling each other cannot make the compiler spin forever:
- Working out the effects of a group of functions that all call each other. The limit is 100 rounds. From 100 functions upward the limit is instead one round per function in the group, plus one more round to confirm nothing changed — so a big but ordinary group never trips it.
- Carrying the real value of an effect placeholder up through a chain of callers. The limit is 10,000 steps.
If either loop reaches its limit while the answer is still changing, the
compiler reports Calor0406, naming which loop stopped, the limit, and the
functions involved. It is always an error and no flag waives it: a check the
compiler did not finish must not be handed back as a clean build.
Before 0.16 the first loop reported Calor0600 — a code that belongs to a
different family — and the second stopped without saying anything at all.
Turning the Checks Down
--no-enforce-effects switches effect checking off. Use it only as a
deliberate migration waiver.
--permissive-effects is the gentler step. It keeps checking on but assumes a
call the compiler cannot look up does nothing. In a project file:
<PropertyGroup>
<CalorPermissiveEffects>true</CalorPermissiveEffects>
</PropertyGroup>
What it waives:
Calor0411andCalor0425— the two "I cannot tell what this does" messages — are not reported at all.Calor0410, "this function does something it never said it would", is reported as a warning instead of an error. That holds both inside a single file and across files.
What it never waives — these stay errors under every flag:
Calor0424, a callback whose effects do not fit where it is going.Calor0420andCalor0421, an override or interface implementation that declares more effects than the member it inherits from. Declaring fewer is always allowed.Calor0418andCalor0406.- Three narrower forms of
Calor0410: the one about a lambda's own§E{…}line, and two about effect variables.
Waiving "we cannot tell" is honest; waiving "we know this is wrong" is not.
Because unknown calls are counted as doing nothing, permissive mode gives up the effect guarantee. Review packets disclose that waiver on their first line.
Next
- Effect manifests and package import - External call coverage
- calor query - Ask what a function declares, what it really does, and what a change would break
- Adoption Playbook - Applying effects in an existing solution