Tacos 1.0.4
dotnet add package Tacos --version 1.0.4
NuGet\Install-Package Tacos -Version 1.0.4
<PackageReference Include="Tacos" Version="1.0.4" />
<PackageVersion Include="Tacos" Version="1.0.4" />
<PackageReference Include="Tacos" />
paket add Tacos --version 1.0.4
#r "nuget: Tacos, 1.0.4"
#:package Tacos@1.0.4
#addin nuget:?package=Tacos&version=1.0.4
#tool nuget:?package=Tacos&version=1.0.4
Tacos
An on-demand, incremental computation framework for .NET, heavily inspired by salsa.
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 | Versions 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. |
-
net10.0
- Tacos.SourceGenerator (>= 1.0.4)
-
net11.0
- Tacos.SourceGenerator (>= 1.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.