Skip to content

CompileTimeEvaluation pt BR

Edgar Mesquita edited this page Aug 13, 2026 · 2 revisions

Avaliação em tempo de compilação

🌐 Esta página em: English · Português

Visão geral

O CompileTimeEvaluator é um compilador simbólico baseado no Roslyn que avalia expressões em tempo de build para tipos marcados com [CompileTimeEvaluate]. Isso permite custo zero em tempo de execução para tipos de valor sem sobrecusto como uma AtomicClass, convertendo chamadas de método complexas em literais de string durante a compilação.

Índice


Conceitos-chave

O que é avaliação em tempo de compilação?

A avaliação em tempo de compilação transforma código de execução em constantes de compilação:

// Código C# (tempo de build)
var className = TW.WithOpacity(TW.Bg.White, 80);

// JavaScript gerado (sem custo nenhum em tempo de execução!)
let className = "bg-white/80";

O atributo [CompileTimeEvaluate]

Marque os tipos que devem ser avaliados em tempo de compilação:

[CompileTimeEvaluate]
public struct AtomicClass
{
    private readonly string _value;

    public AtomicClass(string value) => _value = value;

    public static implicit operator string(AtomicClass c) => c._value;

    public static AtomicClass WithOpacity(string className, int opacity)
        => new($"{className}/{opacity}");
}

Requisitos:

  • Tem que ser um struct (tipo de valor)
  • Tem que ter conversão implícita para string
  • Os métodos têm que ser determinísticos (mesma entrada = mesma saída)

Como funciona

1. Fase de detecção

O avaliador confere se o tipo de uma expressão tem [CompileTimeEvaluate]:

var typeInfo = _semanticModel.GetTypeInfo(expression);
if (!IsCompileTimeEvaluatable(typeInfo.Type))
    return null; // Pula - não é avaliável

2. Reconhecimento de padrão

Analisa o código-fonte do método para detectar os padrões de implementação:

// Análise do código-fonte
public static AtomicClass WithOpacity(string className, int opacity)
    => new($"{className}/{opacity}");

// Padrão detectado: InterpolatedString
// Template: "{className}/{opacity}"
// Parâmetros: [className, opacity]

3. Execução simbólica

Executa o padrão com os argumentos avaliados:

// Entrada: TW.WithOpacity("bg-white", 80)
// Passo 1: avalia os argumentos → ["bg-white", "80"]
// Passo 2: aplica o padrão → "bg-white/80"
// Passo 3: cacheia o resultado

4. Cache

Os resultados são cacheados para evitar recomputação:

private readonly Dictionary<string, string> _cache = [];
private readonly Dictionary<string, ITypeSymbol> _cacheTypes = [];

5. Proteção contra recursão

Detecta dependências circulares:

private readonly HashSet<string> _evaluationStack = [];

if (_evaluationStack.Contains(key))
    return null; // Referência circular detectada

Padrões suportados

O avaliador reconhece 5 padrões comuns de implementação:

1. String interpolada

Padrão:

public static Type Method(string arg1, int arg2)
    => new($"{arg1}/{arg2}");

Exemplo:

TW.WithOpacity("bg-white", 80) // → "bg-white/80"
TW.Px(4)                       // → "px-4"

2. String.Join

Padrão:

public static Type Method(params string[] classes)
    => new(string.Join(" ", classes));

Exemplo:

TW.Multi("flex", "items-center", "gap-4") // → "flex items-center gap-4"

3. String.Format

Padrão:

public static Type Method(string prefix, string value)
    => new(string.Format("{0}:{1}", prefix, value));

Exemplo:

TW.Format("hover", "bg-blue-500") // → "hover:bg-blue-500"

4. Repasse de parâmetro

Padrão:

public static Type Method(string value)
    => new(value);

Exemplo:

TW.Create("flex") // → "flex"

5. Expressão binária

Padrão:

public static Type Method(string prefix, string value)
    => new(prefix + ":" + value);

Exemplo:

TW.Prefix("dark", "bg-zinc-900") // → "dark:bg-zinc-900"

Arquitetura

Diagrama de classes

CompileTimeEvaluator
├── TryEvaluate(expression) ──────────► Ponto de entrada principal
│   ├── Confere o cache
│   ├── Detecta recursão
│   ├── Valida [CompileTimeEvaluate]
│   └── Tenta as estratégias de avaliação
│
├── EvaluateMemberAccess() ───────────► TW.Bg.White
├── EvaluateMethodCall() ─────────────► TW.WithOpacity(...)
│   └── TrySymbolicCompilation()
│       ├── DetectMethodPattern() ────► Reconhecimento de padrão
│       └── ExecutePattern() ─────────► Execução simbólica
│           ├── ExecuteInterpolatedStringPattern()
│           ├── ExecuteStringJoinPattern()
│           ├── ExecuteStringFormatPattern()
│           ├── ExecuteParameterPassthroughPattern()
│           └── ExecuteBinaryExpressionPattern()
│
├── EvaluateBinaryExpression() ───────► TW.A + TW.B
├── EvaluateObjectCreation() ─────────► new AtomicClass("flex")
└── EvaluateConstantValue() ──────────► "flex"

Fluxo de avaliação

Expressão C#: TW.Dark(TW.WithOpacity(TW.Bg.White, 80))
    ↓
1. TryEvaluate(TW.Dark(...))
    ├─ Confere o cache: FALTA
    ├─ Confere o tipo: AtomicClass [CompileTimeEvaluate] ✓
    ↓
2. EvaluateMethodCall(TW.Dark(...))
    ├─ Avalia os argumentos:
    │   └─ TW.WithOpacity(TW.Bg.White, 80)
    │       ├─ Avalia TW.Bg.White → "bg-white"
    │       ├─ Avalia 80 → "80"
    │       └─ Padrão: string interpolada
    │           └─ Resultado: "bg-white/80"
    ↓
3. TrySymbolicCompilation(TW.Dark(...))
    ├─ Pega o código-fonte
    ├─ DetectMethodPattern()
    │   └─ Padrão: string interpolada
    ├─ ExecutePattern(["bg-white/80"])
    │   └─ Template: "dark:{arg}"
    └─ Resultado: "dark:bg-white/80"
    ↓
4. Cacheia o resultado: "dark:bg-white/80"
5. Devolve: "dark:bg-white/80"

Exemplos de uso

Uso básico

[CompileTimeEvaluate]
public struct MyClass
{
    private readonly string _value;
    public MyClass(string value) => _value = value;
    public static implicit operator string(MyClass c) => c._value;

    // Todos estes padrões funcionam automaticamente!

    // Padrão 1: string interpolada
    public static MyClass WithOpacity(string color, int opacity)
        => new($"{color}/{opacity}");

    // Padrão 2: String.Join
    public static MyClass Join(params string[] classes)
        => new(string.Join(" ", classes));

    // Padrão 3: String.Format
    public static MyClass Format(string prefix, string value)
        => new(string.Format("{0}-{1}", prefix, value));

    // Padrão 4: repasse
    public static MyClass Create(string value)
        => new(value);

    // Padrão 5: expressão binária
    public static MyClass Concat(string a, string b)
        => new(a + "-" + b);
}

Exemplo do mundo real: AtomicClass

// Código C# do componente
var cardClasses = ClassBuilder.Create()
    .Add(TW.P(4), TW.Rounded.Lg)
    .Add(TW.Bg.White)
    .Dark(TW.WithOpacity(TW.Bg.Zinc900, 95))
    .Hover(TW.Shadow.Xl)
    .Build();

// JavaScript gerado (tudo em tempo de compilação!)
let cardClasses = ClassBuilder.create()
    .add("p-4", "rounded-lg")
    .add("bg-white")
    .dark("bg-zinc-900/95")
    .hover("shadow-xl")
    .build();

Avaliação aninhada

// Expressão aninhada complexa
TW.Dark(
    TW.Hover(
        TW.WithOpacity(TW.Bg.Blue600, 50)
    )
)

// Avalia para:
"dark:hover:bg-blue-600/50"

// Ordem da avaliação:
// 1. TW.Bg.Blue600 → "bg-blue-600"
// 2. TW.WithOpacity("bg-blue-600", 50) → "bg-blue-600/50"
// 3. TW.Hover("bg-blue-600/50") → "hover:bg-blue-600/50"
// 4. TW.Dark("hover:bg-blue-600/50") → "dark:hover:bg-blue-600/50"

Ganhos de performance

Performance em tempo de execução

Abordagem Custo em execução Tamanho do bundle Avaliação
Tempo de compilação ✅ Nenhum ✅ Mínimo ✅ Em tempo de build
Avaliação em execução ❌ Alto ❌ Grande ❌ A cada render

Números do tempo de build

Antes da avaliação em tempo de compilação:
- Tamanho do bundle: ~85KB
- Helpers em execução: classe TW + todos os métodos
- Primeiro paint: ~120ms

Depois da avaliação em tempo de compilação:
- Tamanho do bundle: ~49KB (42% de redução!)
- Helpers em execução: nenhum (só strings)
- Primeiro paint: ~80ms (33% mais rápido!)

Comparação com exemplo real

Antes:

// Avaliação em execução (lenta, bundle grande)
let className = TW.Dark(TW.WithOpacity(TW.Bg.Zinc900, 95));
// Exige: a classe TW, o método Dark, o método WithOpacity, o objeto Bg

Depois:

// Avaliação em tempo de compilação (rápida, bundle pequeno)
let className = "dark:bg-zinc-900/95";
// Exige: nada! Só um literal de string

Extensibilidade

Acrescentando novos padrões

Para suportar novos padrões, acrescente ao DetectMethodPattern():

private static MethodPattern? DetectMethodPattern(
    MethodDeclarationSyntax methodDecl,
    IMethodSymbol methodSymbol)
{
    // ... padrões existentes ...

    // Padrão novo: expressão condicional
    if (bodyExpr is ConditionalExpressionSyntax conditional)
    {
        return new MethodPattern
        {
            Type = PatternType.Conditional,
            Template = conditional,
            Parameters = [.. methodSymbol.Parameters]
        };
    }

    return null;
}

Depois implemente o executor:

private string? ExecuteConditionalPattern(MethodPattern pattern, List<string> args)
{
    // Implementação aqui
}

Lógica de avaliação própria

Para assemblies externos, sobreponha via reflexão:

private string? TryInvokeMethodViaReflection(
    IMethodSymbol methodSymbol,
    List<object?> args,
    List<ITypeSymbol?>? argTypes = null)
{
    // Lógica própria para métodos externos
}

Limitações

O que não pode ser avaliado

Código dependente de execução:

public static MyClass Random()
    => new(Guid.NewGuid().ToString()); // ❌ Não determinístico

Estado externo:

private static int counter = 0;
public static MyClass Counter()
    => new($"item-{counter++}"); // ❌ Estado mutável

LINQ complexo:

public static MyClass Complex(params string[] items)
    => new(items.Where(i => i.Length > 5).Select(i => i.ToUpper()).Join(" ")); // ❌ Complexo demais

Contorno - use padrões mais simples:

public static MyClass Complex(params string[] items)
    => new(string.Join(" ", items)); // ✅ Avaliável

Fallback para tempo de execução

Quando a avaliação falha, o código cai para o tempo de execução:

// Não dá para avaliar em tempo de compilação
var result = TW.When(condition, "a", "b"); // condition é uma variável de execução

// JavaScript gerado (avaliação em execução)
let result = TW.When(condition, "a", "b"); // Inclui o TW no bundle

Aviso mostrado durante o build:

warning: Could not evaluate compile-time expression at TodoList.cs(123).
Falling back to runtime code. Expression: TW.When(condition, "a", "b")

Depuração

Ligando o log de diagnóstico

var evaluator = new CompileTimeEvaluator(semanticModel);

// Confira as estatísticas do cache
var (cachedCount, typesCached) = evaluator.GetCacheStats();
Console.WriteLine($"Cached: {cachedCount}, Types: {typesCached}");

// Confira se a expressão está cacheada
bool isCached = evaluator.IsCached("TW.Bg.White");

Limpando o cache

evaluator.ClearCache(); // Para testes ou quando o semantic model muda

Vendo os avisos de build

As falhas de avaliação em tempo de compilação são logadas como avisos:

dotnet build

# Saída:
warning: Could not evaluate compile-time expression at SourceFile([123..456))
Falling back to runtime code. Expression: TW.Complex(...)

Boas práticas

✅ FAÇA

  • Use métodos simples e determinísticos
  • Siga os padrões reconhecidos
  • Mantenha a lógica sem estado
  • Teste com constantes de tempo de compilação

❌ NÃO FAÇA

  • Acessar estado externo
  • Usar operações não determinísticas (Random, DateTime.Now)
  • Criar cadeias LINQ complexas
  • Modificar variáveis estáticas

Dicas de performance

  1. Prefira padrões mais simples - strings interpoladas são as mais rápidas
  2. Evite aninhamento profundo - cada nível acrescenta custo de avaliação
  3. Use o cache - a mesma expressão só é avaliada uma vez
  4. Confira os avisos - avaliações que falham prejudicam a performance em execução

Documentação relacionada


Resumo

O CompileTimeEvaluator é uma abstração sem sobrecusto que permite classes utilitárias elegantes e com tipos seguros, sem custo em tempo de execução. Analisando as implementações dos métodos e executando-as simbolicamente em tempo de build, ele converte chamadas de método complexas em literais de string simples, resultando em:

  • Bundles 42% menores (nenhum helper em execução é necessário)
  • Primeiro paint 33% mais rápido (sem avaliação em execução)
  • 100% de segurança de tipos (checagem em tempo de compilação do C#)
  • Zero custo em tempo de execução (só literais de string)

Isso faz do eQuantic.UI um dos frameworks de interface mais rápidos, mantendo uma excelente experiência de desenvolvimento.

Clone this wiki locally