Avaliação em tempo de compilação🌐 Esta página em: English · Português 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.
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:
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)
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]
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
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
O avaliador reconhece 5 padrões comuns de implementação:
Padrão:
public static Type Method(string arg1, int arg2)
=> new($"{arg1}/{arg2}");
Exemplo:
TW.WithOpacity("bg-white", 80) // → "bg-white/80"
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"
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"
Padrão:
public static Type Method(string value)
Exemplo:
TW.Create("flex") // → "flex"
Padrão:
public static Type Method(string prefix, string value)
=> new(prefix + ":" + value);
Exemplo:
TW.Prefix("dark", "bg-zinc-900") // → "dark:bg-zinc-900"
├── TryEvaluate(expression) ──────────► Ponto de entrada principal
│ ├── 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"
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(...))
│ └─ TW.WithOpacity(TW.Bg.White, 80)
│ ├─ Avalia TW.Bg.White → "bg-white"
│ └─ Padrão: string interpolada
│ └─ Resultado: "bg-white/80"
3. TrySymbolicCompilation(TW.Dark(...))
│ └─ 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"
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}");
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));
public static MyClass Create(string value)
// Padrão 5: expressão binária
public static MyClass Concat(string a, string b)
Exemplo do mundo real: AtomicClass
// Código C# do componente
var cardClasses = ClassBuilder.Create()
.Add(TW.P(4), TW.Rounded.Lg)
.Dark(TW.WithOpacity(TW.Bg.Zinc900, 95))
// JavaScript gerado (tudo em tempo de compilação!)
let cardClasses = ClassBuilder.create()
.add("p-4", "rounded-lg")
// Expressão aninhada complexa
TW.WithOpacity(TW.Bg.Blue600, 50)
"dark:hover:bg-blue-600/50"
// 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"
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
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
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)
Type = PatternType.Conditional,
Parameters = [.. methodSymbol.Parameters]
Depois implemente o executor:
private string? ExecuteConditionalPattern(MethodPattern pattern, List<string> args)
Lógica de avaliação própria
Para assemblies externos, sobreponha via reflexão:
private string? TryInvokeMethodViaReflection(
IMethodSymbol methodSymbol,
List<ITypeSymbol?>? argTypes = null)
// Lógica própria para métodos externos
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")
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");
evaluator.ClearCache(); // Para testes ou quando o semantic model muda
As falhas de avaliação em tempo de compilação são logadas como avisos:
warning: Could not evaluate compile-time expression at SourceFile([123..456))
Falling back to runtime code. Expression: TW.Complex(...)
•
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
•
Usar operações não determinísticas (Random, DateTime.Now)
•
Criar cadeias LINQ complexas
•
Modificar variáveis estáticas
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
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.