eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
Docspt-BR
Avaliação em tempo de compilação
Edit this page
3 min read
🌐 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:
1
2
3
4
5
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
[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]:
1
2
3
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:
1
2
3
4
5
6
7
// 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:
1
2
3
4
// 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:
1
2
private readonly Dictionary<string, string> _cache = [];
private readonly Dictionary<string, ITypeSymbol> _cacheTypes = [];
5. Proteção contra recursão
Detecta dependências circulares:
1
2
3
4
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:
1
2
public static Type Method(string arg1, int arg2)
=> new($"{arg1}/{arg2}");
Exemplo:
1
2
TW.WithOpacity("bg-white", 80) // → "bg-white/80"
TW.Px(4) // → "px-4"
2. String.Join
Padrão:
1
2
public static Type Method(params string[] classes)
=> new(string.Join(" ", classes));
Exemplo:
1
TW.Multi("flex", "items-center", "gap-4") // → "flex items-center gap-4"
3. String.Format
Padrão:
1
2
public static Type Method(string prefix, string value)
=> new(string.Format("{0}:{1}", prefix, value));
Exemplo:
1
TW.Format("hover", "bg-blue-500") // → "hover:bg-blue-500"
4. Repasse de parâmetro
Padrão:
1
2
public static Type Method(string value)
=> new(value);
Exemplo:
1
TW.Create("flex") // → "flex"
5. Expressão binária
Padrão:
1
2
public static Type Method(string prefix, string value)
=> new(prefix + ":" + value);
Exemplo:
1
TW.Prefix("dark", "bg-zinc-900") // → "dark:bg-zinc-900"
Arquitetura
Diagrama de classes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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
1
2
3
4
5
6
7
8
9
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:
1
2
3
// 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:
1
2
3
// 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():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:
1
2
3
4
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:
1
2
3
4
5
6
7
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:
1
2
public static MyClass Random()
=> new(Guid.NewGuid().ToString()); // ❌ Não determinístico
Estado externo:
1
2
3
private static int counter = 0;
public static MyClass Counter()
=> new($"item-{counter++}"); // ❌ Estado mutável
LINQ complexo:
1
2
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:
1
2
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:
1
2
3
4
5
// 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:
1
2
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
1
2
3
4
5
6
7
8
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
1
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:
1
2
3
4
5
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.