v0.22.0—Bounded nullability checks and practical .NET migration guidance.See what's new

Documentation

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:

Calor
§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

Calor
§E{code1,code2,...}
§E{}                      // No effects (pure function)

Place the effect declaration after the output type:

Calor
§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.

Calor
§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

Calor
§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:

Calor
§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:

Calor
§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

CodeEffectDescriptionC# Examples
cwConsole writeOutput to consoleConsole.WriteLine()
crConsole readInput from consoleConsole.ReadLine()
fs:rFilesystem readRead from filesystemFile.ReadAllText()
fs:wFilesystem writeWrite to filesystemFile.WriteAllText()
fs:rwFilesystem read/writeRead and write filesystemFile.Copy()
net:rNetwork readHTTP GET, etc.HttpClient.GetStringAsync()
net:wNetwork writeHTTP POST, etc.HttpClient.PostAsync()
net:rwNetwork read/writeHTTP operationsHttpClient.SendAsync()
db:rDatabase readDatabase queriesSELECT queries
db:wDatabase writeDatabase mutationsINSERT/UPDATE/DELETE
db:rwDatabase read/writeDatabase operationsORM calls

Examples

Pure Function (No Effects)

Calor
§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:

Calor
§F{f001:Add:pub}
  §I{i32:a}
  §I{i32:b}
  §O{i32}
  §E{}                    // Explicitly no effects
  §R (+ a b)

Console Output

Calor
§F{f001:Greet:pub}
  §I{str:name}
  §O{void}
  §E{cw}                  // Console write
  §P name

File Operations

Calor
§F{f001:CopyFile:pub}
  §I{str:source}
  §I{str:dest}
  §O{bool}
  §E{fs:rw}               // Filesystem read and write
  // ...

Network Call

Calor
§F{f001:FetchData:pub}
  §I{str:url}
  §O{str!str}
  §E{net:rw}              // Network operations
  // ...

Database with Logging

Calor
§F{f001:CreateUser:pub}
  §I{User:user}
  §O{i32}
  §E{db:rw,cw}            // Database and console (for logging)
  // ...

Multiple Effects

Calor
§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

Calor
// 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

Calor
§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":

Plain Text
// Agent searches for §E{..db..}

2. Refactoring Safety

"This function should be pure, but it has effects":

Calor
§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

Calor
// 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

CodeLevelWhat it means
Calor0410ErrorA function does something its §E{…} line does not list. The message often shows the chain of calls that got there.
Calor0411WarningThe compiler cannot look up what an outside call does. Add the method to a .calor-effects.json manifest. --strict-effects makes it an error.
Calor0417WarningA 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.
Calor0419WarningThe 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.
Calor0420ErrorAn override declares more effects than the method it overrides. Declaring fewer is fine.
Calor0421ErrorAn interface implementation declares more effects than the interface method. Declaring fewer is fine.
Calor0424ErrorA callback's effects do not fit where it is being bound, passed, or returned.
Calor0425WarningThe compiler cannot decide whether a callback fits, because one side carries no §E{…}. --strict-effects makes it an error.
Calor0418ErrorYou invoked something that is provably not a function — an i32, a string, an array — so there is no effect list to charge.
Calor0406ErrorEffect 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:

xml
<PropertyGroup>
  <CalorPermissiveEffects>true</CalorPermissiveEffects>
</PropertyGroup>

What it waives:

  • Calor0411 and Calor0425 — 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.
  • Calor0420 and Calor0421, an override or interface implementation that declares more effects than the member it inherits from. Declaring fewer is always allowed.
  • Calor0418 and Calor0406.
  • 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