Tacos 1.0.4

dotnet add package Tacos --version 1.0.4
                    
NuGet\Install-Package Tacos -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tacos" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tacos" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Tacos" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tacos --version 1.0.4
                    
#r "nuget: Tacos, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tacos@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tacos&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Tacos&version=1.0.4
                    
Install as a Cake Tool

Tacos

An on-demand, incremental computation framework for .NET, heavily inspired by salsa.

NuGet Releases license

English| 日本語

Overview

Tacos is a framework for on-demand, incremental computation for .NET. Its concepts and API are heavily inspired by salsa, and it provides high-performance incremental computation features leveraging Source Generators.

Installation

Tacos requires .NET 10 or later. It can be installed via NuGet.

dotnet add Tacos

Usage

Input

All inputs in Tacos are represented by structs decorated with the [Input] attribute. Defining them as readonly record struct is recommended.

using Tacos;

// Struct representing a source file
[Input]
public readonly record struct SourceFileData(string Path, string Text);

Database

In Tacos, outputs for all inputs are cached and stored in a single in-memory database. This is defined as a partial class that inherits from TacosDatabase.

public sealed partial class ExampleDatabase : TacosDatabase { }

Register the previously defined Input type to it:

[TacosInput(typeof(SourceFileData))]
public sealed partial class ExampleDatabase : TacosDatabase { }

This allows the Source Generator to generate the corresponding methods.

var db = new ExampleDatabase();

var source = """
Console.WriteLine("Hello!");
Console.WriteLine("How are you?");
Console.WriteLine("Goodbye!");
"""

InputId<SourceFile> file = db.CreateSourceFile("Program.cs", source);
string text = db.GetSourceFileText(file);
db.SetSourceFileText(file, newText);

The Input struct is represented internally in the database as a typed integer ID (InputId<T>).

Tracked Methods

Next, define a Tracked method. This is a method defined within the database that is tracked by Tacos, and its result is automatically memoized.

Tracked methods are defined as follows:

public sealed partial class ExampleDatabase : TacosDatabase
{
    [Tracked]
    public partial int LineCount(InputId<SourceFileData> file);

    int LineCountCore(InputId<SourceFileData> file)
    {
        return GetSourceFileText(file).Split('\n').Length;
    }
}

Methods intended as an external API are marked with the [Tracked] attribute, and only their declaration is written as partial. For the input, specify InputId<T> with the previously defined Input struct as the type argument.

[Tracked]
public partial int LineCount(InputId<SourceFileData> file);

The actual implementation is written in a method named MethodName + Core.

int LineCountCore(InputId<SourceFileData> file)
{
    return GetSourceFileText(file).Split('\n').Length;
}

It is also possible to specify the implementation method name like this: [Tracked(Implementation = nameof(ComputeLineCount))].

It can be used as follows:

var db = new ExampleDatabase();

var source = """
Console.WriteLine("Hello!");
Console.WriteLine("How are you?");
Console.WriteLine("Goodbye!");
"""

var file = db.CreateSourceFile("Program.cs", source);

_ = db.LineCount(file); // Calculation is executed
_ = db.LineCount(file); // Memoized, no recalculation occurs

Updating Values

You can update internal values using the generated setter methods. When a value is changed, any Tracked methods that depend on that input are recalculated.

db.SetSourceFileText(file, newText);

If you want to publish multiple changes simultaneously, you can use the generated Update.

using (var update = db.BeginUpdate())
{
    update.SetSourceFileText(fileA, textA);
    update.SetSourceFileText(fileB, textB);
    update.Commit();
}

Snapshot

You can also create a Snapshot for read-only access.

using var snapshot = db.CreateSnapshot();

var tree = snapshot.Parse("Program.cs");
var diagnostics = snapshot.Check("Program.cs");
Console.WriteLine(snapshot.Revision);

This can also be passed to another thread.

var snapshot = db.CreateSnapshot();
var diagnostics = await Task.Run(() =>
{
    using (snapshot)
    {
        return snapshot.Check(file);
    }
});

When a new write is requested, queries running on the old Snapshot will be interrupted with an OperationCanceledException. In that case, Dispose() the old Snapshot and, if necessary, create a new one to retry the process.

Do not update inputs synchronously from a thread that holds a Snapshot. This may cause a deadlock as it waits for the Snapshot to complete.

Accumulator

In addition to query results, you can maintain information such as diagnostics as an Accumulator.

public sealed record Diagnostic(string Code, string Message);

[Tracked]
public partial SyntaxTree Parse(InputId<SourceFileData> file);

private SyntaxTree ParseCore(InputId<SourceFileData> file)
{
    var tree = Parser.Parse(GetSourceFileText(file));
    foreach (var error in tree.Errors)
    {
        Accumulate(new Diagnostic("PARSE001", error.Message));
    }

    return tree;
}

Since XxxWithAccumulations is generated for each [Tracked] method, you can use it to retrieve these values.

var result = db.ParseWithAccumulations("Program.cs");

SyntaxTree tree = result.Value;
IEnumerable<Diagnostic> diagnostics = result.GetAccumulated<Diagnostic>();

Interned

For types where the same values—such as identifiers or paths—appear frequently, you can use [Interned] to register them in an internal intern pool and manage them as IDs.

[Interned]
public readonly record struct Identifier(string Text);

[TacosIntern(typeof(Identifier))]
public sealed partial class ExampleDatabase : TacosDatabase;

This generates the following API:

 InternId<Identifier> first = db.InternIdentifier("System");
 InternId<Identifier> same = db.InternIdentifier("System");

Debug.Assert(first == same);
Debug.Assert(db.GetIdentifierText(first) == "System");

Tracked Entity

By marking a struct with [Tracked], you can assign stable IDs to objects like Symbols or syntax nodes, preserving them before and after modifications.

[Tracked]
public readonly record struct Symbol(
    [property: Identity] InternId<Identifier> Name,
    SyntaxNode Syntax,
    TypeSymbol Type
);

[TacosTracked(typeof(Symbol))]
public sealed partial class ExampleDatabase : TacosDatabase;

In response, the Source Generator produces getter methods for each property.

InternId<Identifier> name = db.GetSymbolName(id);
TypeSymbol type = db.GetSymbolType(id);

bool exists = db.TryResolveSymbol(id, out Symbol? wholeValue);

You can generate these within a query as follows:

[Tracked]
public partial TrackedId<SymbolData>[] ParseSymbols(InputId<SourceFile> file);

TrackedId<SymbolData>[] ParseSymbolsCore(InputId<SourceFile> file)
{
    return Parser.Parse(GetSourceFileText(file))
        .Select(symbol => CreateSymbol(
            InternIdentifier(symbol.Name),
            symbol.Syntax,
            TypeSymbol.Unknown))
        .ToArray();
}

Custom EqualityComparer

You can use [TacosKeyComparer] or [TacosComparer] to swap the IEqualityComparer<T> used for determining equality in keys or query results.

public sealed class IgnoreCaseComparer : IEqualityComparer<string>
{
    public bool Equals(string? x, string? y)
        => StringComparer.OrdinalIgnoreCase.Equals(x, y);

    public int GetHashCode(string value)
        => StringComparer.OrdinalIgnoreCase.GetHashCode(value);
}

[Input]
public readonly record struct ProjectData(
    string Name,
    [property: TacosComparer(typeof(IgnoreCaseComparer))] string Label
);

Query Cancellation

Once a write operation to an Input or Entity begins, any running calculations are automatically cancelled. To request cancellation explicitly, call RequestCancellation().

db.RequestCancellation();

Cancellation is notified as an OperationCanceledException.

Additionally, you can receive a CancellationToken within a query.

[Tracked]
public partial AnalysisResult Analyze(string path);

private AnalysisResult AnalyzeCore(string path, CancellationToken cancellationToken)
{
    var tree = Parse(path);
    foreach (var node in tree.Nodes)
    {
        cancellationToken.ThrowIfCancellationRequested();
        // Some processing
    }

    return Analyzer.Analyze(tree);
}

Circular References

By default, if a circular dependency occurs during calculation, a TacosCycleException is thrown.

ResolveType(A) -> ResolveType(B) -> ResolveType(A)

To handle recovery during a cycle, specify a CycleFallback.

[Tracked(CycleFallback = nameof(UnknownType))]
public partial TypeSymbol ResolveType(SymbolId symbol);

TypeSymbol UnknownType(SymbolId symbol)
{
    return TypeSymbol.Unknown;
}

You can also specify CycleMaxIterations to iterate until the values converge.

[Tracked(CycleFallback = nameof(UnknownType), CycleMaxIterations = 16)]
public partial TypeSymbol ResolveType(SymbolId symbol);

If the value does not converge within the limit, a TacosException will be thrown.

Cache Management

Several APIs are provided to explicitly manage the cache.

db.ClearCaches(); // Clears all caches
db.ClearQueryCache("Parse"); // Clears the cache for the specified query
int removed = db.TrimCaches(20); // Removes entries unused for longer than 20 revisions

// Gets cache statistics
TacosCacheStatistics stats = db.GetCacheStatistics();
Console.WriteLine(stats.TotalEntryCount);

Event Handling

It is possible to subscribe to database events using ITacosObserver.

public sealed class QueryObserver : ITacosObserver
{
    public void OnWillExecute(in TacosWillExecuteEventArgs args)
    {
        Console.WriteLine($"{args.Revision}: {args.QueryName} {args.ExecutionReason}");
    }

    public void OnValueChanged(in TacosValueChangedEventArgs args)
    {
        Console.WriteLine($"{args.Revision}: changed {args.QueryName}");
    }
}

IDisposable subscription = db.SubscribeEvents(new QueryObserver());

License

This library is released under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 151 7/11/2026
1.0.3 106 7/10/2026
1.0.2 110 7/10/2026
1.0.1 116 7/10/2026
1.0.0 118 7/10/2026