v0.5.0Roslyn 5.3.0 upgrade — convert C# 14 code to Calor, exhaustive match enforcement on sum types.See what's new

Cross-Assembly IL Analysis

Automatic effect discovery through referenced .NET assemblies

When your Calor code calls a method in a referenced assembly, and that method eventually calls DbContext.SaveChanges(), Calor can now trace through the IL of the referenced assembly and discover the db:w effect automatically — without you writing a manifest entry for every intermediate method.


The Problem

Effect manifests cover .NET framework types (DbCommand, HttpClient, ILogger), but they don't cover your code. If you have a data access layer:

C#
// In your MyApp.DataAccess.dll
public class UserRepository
{
    public void Save(User user)
    {
        using var conn = CreateConnection();
        conn.Open();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "INSERT INTO Users ...";
        cmd.ExecuteNonQuery(); // This is the actual db:w
    }
}

And your Calor code calls it:

Plain Text
§F{f001:CreateUser:pub}
  §I{str:name}
  §O{void}
  §E{db:w}
  §C{UserRepository.Save} §A name §/C
§/F{f001}

Without IL analysis, the compiler doesn't know that UserRepository.Save has db:w — it would flag it as an Unknown external call (Calor0411). You'd need to write a custom manifest for every intermediate method.

With IL analysis, the compiler reads the IL of UserRepository.Save, traces the call chain to DbCommand.ExecuteNonQuery, finds the db:w seed in the built-in manifest, and propagates it back. No custom manifest needed.

Enabling IL Analysis

Add one property to your project file:

xml
<PropertyGroup>
  <CalorEnableILAnalysis>true</CalorEnableILAnalysis>
</PropertyGroup>

That's it. The compiler will analyze all referenced assemblies during the next build.

What It Finds

IL analysis traces through concrete call chains — method A calls method B which calls method C which calls a manifest-covered method. It handles:

PatternHow it works
Direct callsRepo.Save()DbCommand.ExecuteNonQuery()db:w
Transitive chainsService.Process()Repo.Save()DbCommand.ExecuteNonQuery()db:w
Async methodsTraces through compiler-generated state machines (MoveNext())
Iterator methodsTraces through yield-based state machines
Delegate creationTask.Run(() => repo.Save()) traces through the lambda via ldftn
Virtual dispatchUnions effects from concrete implementations (up to 5)
Deep chainsFollows up to 50 hops (configurable)
Mutual recursionFixpoint iteration converges correctly

Example: What the compiler sees

For a typical 3-layer architecture:

Plain Text
UserService.CreateUser()     → db:rw, db:w   (propagated)
  UserRepository.Save()      → db:rw, db:w   (propagated)
    DbConnection.Open()      → db:rw         (from manifest)
    DbCommand.ExecuteNonQuery() → db:w       (from manifest)
  UserRepository.FindById()  → db:r, db:rw   (propagated)
    DbCommand.ExecuteReader() → db:r          (from manifest)
MathHelper.Add()             → pure           (no effects found)

What It Doesn't Find

IL analysis has known limitations. These patterns still require manifests:

PatternWhySolution
Interface-heavy calls (ILogger.Log, IConfiguration.GetSection)0% resolution rate — delegates and BCL internals block the traceUse built-in Tier B manifests
Delegate invocations (Func<T>.Invoke)Runtime-determined target, not visible in ILMark as Unknown or cover with manifests
Reflection (MethodInfo.Invoke, dynamic)Not visible as direct calls in ILCover with manifests
P/Invoke / native callsNo IL body to analyzeCover with manifests

The key insight: IL analysis works well for your code (repositories, services, handlers) where the call chain is concrete. For .NET framework types (ILogger, IConfiguration), the built-in manifests already cover them. The two approaches complement each other.

How It Works

The resolution pipeline, in order:

Plain Text
1. Specific method in manifest
2. Method name in manifest
3. Wildcard in manifest
4. Type default effects in manifest
5. Namespace defaults in manifest
6. IL analysis                       ← new
7. Unknown (Calor0411)

Manifests always win. IL analysis only runs for calls that no manifest covers. If IL analysis can't resolve a method either (e.g., the trace hits a depth limit or too many virtual implementations), it falls through to Unknown — it never reports a method as pure when it's unsure.

Soundness guarantee

IL analysis uses three-state resolution:

  • Resolved — complete call graph analyzed, effects are precise
  • Pure — complete call graph analyzed, no effects found
  • Incomplete — analysis cut short (depth limit, missing body, too many implementations)

Incomplete traces are never reported as pure. They fall through to Unknown, and you get the same Calor0411 diagnostic you would without IL analysis.

Performance

IL analysis is fast because it only runs at build time and caches results:

  • Assembly index construction: ~2ms for typical projects
  • Full analysis: ~3ms for 8 call sites across 2 assemblies
  • Scaling: analysis cost is proportional to the number of Unknown external calls, not total file count
  • Caching: the analyzer is constructed once per build and reused across all .calr files

When CalorEnableILAnalysis is false (the default), there is zero overhead.

Interaction with Incremental Builds

When IL analysis is enabled, changes to referenced assemblies (e.g., updating a NuGet package) will trigger recompilation of affected .calr files. This is handled by the incremental build cache's options hash — the EnableILAnalysis flag is included in the cache invalidation key.

Diagnostics

CodeSeverityMeaning
Calor0414Info (verbose only)Effect resolved via IL analysis — tells you which assembly the effect was traced through
Calor0415InfoIL analysis could not resolve a method — explains why (depth limit, missing body, etc.)
Calor0416WarningIL analysis budget exhausted — too many methods visited, remaining treated as Unknown

When to Enable It

Enable if:

  • Your project references custom DLLs (data access layers, service libraries) that call effectful .NET methods
  • You're getting many Calor0411 (Unknown external call) warnings for methods in your own assemblies
  • You want to reduce the number of custom manifest entries you need to write

Don't bother if:

  • All your external calls are to .NET framework types already covered by manifests
  • Your project is small enough that a few custom manifest entries are easy to maintain
  • Build time is critical and every millisecond matters (though the overhead is under 5ms)

Relationship to Manifests

IL analysis reduces the number of manifests you need to write — it doesn't replace them. Think of it as:

  • Manifests: curated, precise, reviewed. Best for framework types and ecosystem libraries.
  • IL analysis: automatic, best-effort. Best for your own code and concrete call chains.

The two work together. Manifests provide the "seeds" (e.g., DbCommand.ExecuteNonQuery → db:w), and IL analysis propagates those seeds backward through your code's call graph.