College Online
0%

Verificacao de Tipos

Modulo 4 · Aula 3 ~20 min de leitura Nivel: Intermediario

Video da aula estara disponivel em breve

Sistemas de Tipos

Um sistema de tipos é um conjunto de regras que atribui tipos a construções do programa e verifica que operações são compatíveis com esses tipos. Segundo Aho, Sethi e Ullman (Dragon Book, Cap. 6), o type checker percorre a AST, atribui tipos a cada expressão usando regras formais, e verifica compatibilidade.

Definição formal: um sistema de tipos é um sistema dedutivo que associa a cada expressão e um tipo T através de regras de tipagem (typing rules). Uma regra tem a forma:

Notação
Regras de tipagem (notação de inferência):

    premissa₁   premissa₂
   ─────────────────────── (nome da regra)
         conclusão

Exemplo — regra para adição:

    Γ ⊢ e₁ : int    Γ ⊢ e₂ : int
   ─────────────────────────────── (T-Add-Int)
         Γ ⊢ e₁ + e₂ : int

    Γ ⊢ e₁ : float    Γ ⊢ e₂ : float
   ──────────────────────────────────── (T-Add-Float)
          Γ ⊢ e₁ + e₂ : float

    Γ ⊢ e₁ : int    Γ ⊢ e₂ : float
   ─────────────────────────────────── (T-Add-Promote)
         Γ ⊢ e₁ + e₂ : float

Onde Γ (gamma) é o ambiente de tipos (tabela de símbolos)
e "⊢" significa "no ambiente Γ, a expressão e tem tipo T".
Diagrama
Classificação de Sistemas de Tipos:

Static Typing (C, Java, Rust)        Dynamic Typing (Python, Ruby, JS)
+----------------------------+       +----------------------------+
| Tipos verificados em        |       | Tipos verificados em        |
| COMPILE-TIME                |       | RUNTIME                     |
| Erros antes de executar     |       | Erros durante execução      |
| Performance: sem checks     |       | Performance: checks em      |
|   em runtime                |       |   cada operação              |
| Exemplos: int x = "hello";  |       | Exemplos: x = "hello"       |
|   → ERRO de compilação      |       |   x + 1 → TypeError runtime  |
+----------------------------+       +----------------------------+

Strong Typing (Python, Rust)         Weak Typing (C, JavaScript)
- Não permite conversões implícitas   - Conversões implícitas
  perigosas                            "5" + 3 → "53" (JS)
- TypeError em operações inválidas     int* cast para void* (C)

Gradual Typing (TypeScript, Python + mypy)
- Combina static e dynamic: partes do programa
  são tipadas estaticamente, partes não.
- Tipo 'Any' serve como escape hatch.

Coerção e Promoção de Tipos

Quando operandos de uma operação têm tipos diferentes, o compilador pode realizar coerções (conversões implícitas). A hierarquia de promoção define quais conversões são seguras:

Diagrama
Hierarquia de promoção em C (widening — sem perda de dados):

  char → short → int → long → float → double
                 ↑
                unsigned int → unsigned long

Exemplos de coerção implícita:
  int a = 5;
  float b = 2.5;
  float c = a + b;     // a é promovido a float: 5.0 + 2.5 = 7.5

  char ch = 'A';
  int x = ch + 1;      // ch promovido a int: 65 + 1 = 66

Narrowing (conversão com perda) — requer cast explícito:
  double d = 3.14159;
  int i = (int)d;       // trunca: i = 3 (perda de .14159)
  float f = (float)d;   // perde precisão

Regra do Dragon Book (Cap. 6.5):
  O resultado de uma operação aritmética entre tipos numéricos
  é o tipo "mais largo" (widest) entre os operandos.

Type Checking: Implementação

O type checker é implementado como um Visitor que percorre a AST bottom-up, calculando o tipo de cada nó a partir dos tipos dos seus filhos:

Python
from dataclasses import dataclass
from typing import List, Optional

# Hierarquia de promoção numérica
TYPE_HIERARCHY = {'char': 0, 'int': 1, 'float': 2, 'double': 3}

def wider_type(t1, t2):
    """Retorna o tipo mais largo entre dois tipos numéricos."""
    if t1 not in TYPE_HIERARCHY or t2 not in TYPE_HIERARCHY:
        return None  # tipos incompatíveis
    if TYPE_HIERARCHY[t1] >= TYPE_HIERARCHY[t2]:
        return t1
    return t2

class TypeChecker(ASTVisitor):
    """Verificador de tipos para expressões — estilo Dragon Book."""

    def __init__(self, symbol_table):
        self.symbols = symbol_table
        self.errors = []

    def visit_NumberLit(self, node):
        if isinstance(node.value, int):
            return 'int'
        return 'float'

    def visit_StringLit(self, node):
        return 'string'

    def visit_BoolLit(self, node):
        return 'bool'

    def visit_Identifier(self, node):
        info = self.symbols.lookup(node.name)
        return info['type']

    def visit_BinOp(self, node):
        left_type = self.visit(node.left)
        right_type = self.visit(node.right)

        # Regras de tipo para operações aritméticas
        if node.op in ('+', '-', '*', '/'):
            # Concatenação de strings
            if node.op == '+' and left_type == 'string' and right_type == 'string':
                return 'string'
            # Promoção de tipo numérico
            result = wider_type(left_type, right_type)
            if result is None:
                self.errors.append(
                    f"Operação '{node.op}' inválida entre {left_type} e {right_type}"
                )
                return 'error'
            return result

        # Divisão inteira vs. real
        if node.op == '//':
            return 'int'

        # Comparações retornam bool
        if node.op in ('==', '!=', '<', '>', '<=', '>='):
            if not self.types_comparable(left_type, right_type):
                self.errors.append(
                    f"Não é possível comparar {left_type} com {right_type}"
                )
            return 'bool'

        # Operadores lógicos
        if node.op in ('and', 'or'):
            if left_type != 'bool' or right_type != 'bool':
                self.errors.append(
                    f"Operador '{node.op}' requer operandos bool, recebi {left_type} e {right_type}"
                )
            return 'bool'

        return 'error'

    def visit_Assignment(self, node):
        value_type = self.visit(node.value)
        target_info = self.symbols.lookup(node.target.name)
        target_type = target_info['type']

        if target_type != value_type and value_type != 'error':
            if not self.is_compatible(target_type, value_type):
                self.errors.append(
                    f"Não é possível atribuir {value_type} a {target_type}"
                )
        return value_type

    def visit_FuncCall(self, node):
        """Verifica tipos dos argumentos de uma chamada de função."""
        func_info = self.symbols.lookup(node.name)
        if func_info['category'] != 'func':
            self.errors.append(f"'{node.name}' não é uma função")
            return 'error'

        expected_params = func_info.get('params', [])
        if len(node.args) != len(expected_params):
            self.errors.append(
                f"'{node.name}' espera {len(expected_params)} argumentos, "
                f"recebeu {len(node.args)}"
            )
        else:
            for i, (arg, param) in enumerate(zip(node.args, expected_params)):
                arg_type = self.visit(arg)
                if not self.is_compatible(param['type'], arg_type):
                    self.errors.append(
                        f"Argumento {i+1} de '{node.name}': esperado "
                        f"{param['type']}, recebeu {arg_type}"
                    )

        return func_info.get('return_type', 'void')

    def is_compatible(self, target, source):
        """Verifica se source pode ser atribuído a target."""
        if target == source:
            return True
        # Widening: int → float → double
        w = wider_type(target, source)
        return w == target if w else False

    def types_comparable(self, t1, t2):
        """Verifica se dois tipos podem ser comparados."""
        numeric = {'char', 'int', 'float', 'double'}
        if t1 in numeric and t2 in numeric:
            return True
        return t1 == t2

Verificação de Tipos em C

Para reforçar o conceito em nível de graduação, eis uma implementação parcial de type checking em C, mostrando como compiladores reais lidam com a propagação de tipos em expressões:

C
/* Verificação de tipos simplificada em C */
typedef enum {
    TYPE_INT, TYPE_FLOAT, TYPE_CHAR,
    TYPE_STRING, TYPE_BOOL, TYPE_VOID, TYPE_ERROR
} Type;

const char *type_names[] = {
    "int", "float", "char",
    "string", "bool", "void", "error"
};

/* Tabela de promoção: type_promote[t1][t2] = resultado */
static Type type_promote[4][4] = {
    /*            INT         FLOAT       CHAR        STRING  */
    /* INT    */ {TYPE_INT,   TYPE_FLOAT, TYPE_INT,   TYPE_ERROR},
    /* FLOAT  */ {TYPE_FLOAT, TYPE_FLOAT, TYPE_FLOAT, TYPE_ERROR},
    /* CHAR   */ {TYPE_INT,   TYPE_FLOAT, TYPE_CHAR,  TYPE_ERROR},
    /* STRING */ {TYPE_ERROR, TYPE_ERROR, TYPE_ERROR, TYPE_STRING},
};

Type check_binop(Type left, Type right, char op) {
    if (left >= 4 || right >= 4)
        return TYPE_ERROR;

    Type result = type_promote[left][right];

    if (result == TYPE_ERROR) {
        fprintf(stderr,
            "Erro de tipo: não é possível aplicar '%c' a %s e %s\n",
            op, type_names[left], type_names[right]);
    }

    /* Operadores de comparação sempre retornam bool */
    if (op == '<' || op == '>' || op == '=')
        return (result != TYPE_ERROR) ? TYPE_BOOL : TYPE_ERROR;

    return result;
}

Inferência de Tipos

Type inference permite ao compilador deduzir tipos sem anotações explícitas. O algoritmo clássico é Hindley-Milner (usado em Haskell, OCaml, Rust parcialmente), que funciona em duas fases: (1) gerar restrições de tipo percorrendo a AST, e (2) unificar as restrições para encontrar a atribuição mais geral de tipos.

Python
# Inferência de tipos por restrições (simplificada)
# Fase 1: gerar restrições percorrendo a AST
# Fase 2: unificar restrições para resolver variáveis de tipo

class TypeVar:
    """Variável de tipo — tipo ainda não resolvido."""
    _counter = 0

    def __init__(self):
        TypeVar._counter += 1
        self.name = f"T{TypeVar._counter}"
        self.resolved = None

    def __repr__(self):
        if self.resolved:
            return str(self.resolved)
        return self.name

class TypeInferencer:
    """Inferência de tipos com unificação."""

    def __init__(self):
        self.constraints = []  # lista de (tipo1, tipo2) que devem ser iguais
        self.env = {}          # nome → tipo

    def infer(self, node):
        """Gera restrições e retorna o tipo inferido."""
        if isinstance(node, NumberLit):
            return 'int' if isinstance(node.value, int) else 'float'

        elif isinstance(node, StringLit):
            return 'string'

        elif isinstance(node, Identifier):
            if node.name in self.env:
                return self.env[node.name]
            # Criar variável de tipo para identificador desconhecido
            tv = TypeVar()
            self.env[node.name] = tv
            return tv

        elif isinstance(node, BinOp):
            lt = self.infer(node.left)
            rt = self.infer(node.right)
            result = TypeVar()
            # Restrição: operandos devem ser compatíveis
            self.constraints.append((lt, rt))
            self.constraints.append((result, wider_type(
                str(lt), str(rt)) or result))
            return result

        elif isinstance(node, Assignment):
            val_type = self.infer(node.value)
            self.env[node.target.name] = val_type
            return val_type

    def unify(self, t1, t2):
        """Unifica dois tipos — resolve variáveis de tipo."""
        if isinstance(t1, TypeVar) and t1.resolved:
            return self.unify(t1.resolved, t2)
        if isinstance(t2, TypeVar) and t2.resolved:
            return self.unify(t1, t2.resolved)
        if isinstance(t1, TypeVar):
            t1.resolved = t2
            return
        if isinstance(t2, TypeVar):
            t2.resolved = t1
            return
        if t1 != t2:
            raise TypeError(f"Não unifica: {t1} ≠ {t2}")

# Exemplo: inferir tipos sem anotações
# let x = 42         → infere x: int
# let y = x + 3.14   → infere y: float (promoção)
# let z = "hello"    → infere z: string

Equivalência e Compatibilidade de Tipos

Dois conceitos distintos que frequentemente confundem alunos são equivalência de tipos e compatibilidade de tipos:

Diagrama
Equivalência de Tipos — dois tipos são o MESMO tipo?

  1. Equivalência Estrutural: dois tipos são iguais se têm
     a mesma estrutura interna.
     struct A { int x; float y; }
     struct B { int x; float y; }
     → A ≡ B em equivalência estrutural (mesmos campos)

  2. Equivalência por Nome: dois tipos são iguais somente
     se têm o mesmo nome.
     → A ≢ B em equivalência por nome (nomes diferentes)

  C usa: equivalência por nome para structs
  OCaml usa: equivalência estrutural
  TypeScript usa: equivalência estrutural (duck typing)

Compatibilidade de Tipos — um tipo pode ser usado onde
outro é esperado?

  float f = 3;    // int é COMPATÍVEL com float (widening)
  int i = 3.14;   // float NÃO é compatível com int (narrowing)

  Relação: se A ≡ B, então A é compatível com B.
  Mas A compatível com B NÃO implica A ≡ B.

No harness.os

O conceito de verificação de tipos se aplica diretamente a qualquer sistema que aceite dados estruturados. O harness.os precisa validar schemas de knowledge chunks, configurações de projetos e parâmetros de tools MCP — isso é análogo a type checking em compiladores. Cada entrada tem campos obrigatórios com tipos esperados, e a validação rejeita dados malformados antes que eles causem erros em runtime.

A analogia vai além: assim como um compilador tem regras de promoção de tipo (int → float), o harness.os tem regras de conversão entre concerns e domains. Um chunk de conhecimento pode ser "promovido" de concern causal para governance quando passa a conter regras normativas, assim como um int é promovido a float quando participa de uma operação com float.

Python
# Schema validator para knowledge chunks do harness.os
# Análogo a type checking em compiladores

KNOWLEDGE_SCHEMA = {
    'title':   {'type': 'string', 'required': True},
    'domain':  {'type': 'string', 'required': True,
               'enum': ['build', 'product', 'operations', 'domain']},
    'concern': {'type': 'string', 'required': True,
               'enum': ['relational', 'governance', 'causal',
                       'metacognitive', 'security']},
    'content': {'type': 'string', 'required': True},
    'tokens':  {'type': 'int', 'required': False},
}

def validate_knowledge(chunk, schema=KNOWLEDGE_SCHEMA):
    """Type-check um knowledge chunk contra o schema."""
    errors = []
    for field, rules in schema.items():
        if rules['required'] and field not in chunk:
            errors.append(f"Campo obrigatório ausente: '{field}'")
            continue
        if field in chunk:
            value = chunk[field]
            expected = rules['type']
            actual = type(value).__name__
            if expected == 'string' and not isinstance(value, str):
                errors.append(f"'{field}': esperado string, achei {actual}")
            if 'enum' in rules and value not in rules['enum']:
                errors.append(f"'{field}': valor '{value}' não é válido. "
                              f"Opções: {rules['enum']}")
    return errors

# Testar
chunk_ok = {'title': 'Commit Hygiene', 'domain': 'build',
            'concern': 'governance', 'content': '...'}
print(validate_knowledge(chunk_ok))  # []

chunk_bad = {'title': 'Bad Chunk', 'domain': 'invalid'}
print(validate_knowledge(chunk_bad))
# ["'domain': valor 'invalid' não é válido...",
#  "Campo obrigatório ausente: 'concern'", ...]

Homework

  1. Type checker completo: Implemente um TypeChecker que percorra a AST da Aula 4.1 e verifique: operações aritméticas (com promoção int→float), concatenação de strings, comparações, e atribuições. O checker deve coletar todos os erros (não parar no primeiro) e reportar a linha de cada erro.
  2. Tabela de promoção em C: Implemente a tabela de promoção de tipos type_promote[4][4] em C, cobrindo char, int, float e double. Escreva testes que verifiquem as 16 combinações possíveis e imprima o tipo resultante de cada operação binária.
  3. Inferência com unificação: Estenda o TypeInferencer desta aula para inferir tipos de funções. Dada let f = fun(x) { x + 1 }, o sistema deve inferir f: int → int. Implemente a unificação e teste com pelo menos 3 funções com tipos diferentes.
  4. Schema validator com relações: Estenda o validate_knowledge para verificar relações entre campos: se domain é "build", então concern deve estar em ['governance', 'causal', 'metacognitive'] (não pode ser "security" em build). Documente pelo menos 3 regras de relação entre campos.

Resumo

Verifique seu entendimento

Quando o compilador encontra a expressão 3 + 2.5, qual operação o type checker realiza?

  • Rejeita a expressão porque os tipos são diferentes
  • Promoção de tipo: converte o int 3 para float 3.0 e o resultado é float
  • Converte 2.5 para int 2 e o resultado é int
  • Deixa para resolver em runtime

Verifique seu entendimento

Qual a diferença entre equivalência estrutural e equivalência por nome de tipos?

  • Equivalência estrutural compara nomes; equivalência por nome compara campos
  • Equivalência estrutural compara a estrutura interna (campos e tipos); equivalência por nome exige o mesmo identificador de tipo
  • São sinônimos — não há diferença
  • Equivalência estrutural só existe em linguagens dinâmicas

Verifique seu entendimento

O algoritmo Hindley-Milner funciona em duas fases. Quais são elas?

  • Análise léxica e análise sintática
  • Promoção de tipos e narrowing
  • Geração de restrições de tipo e unificação das restrições
  • Static checking e dynamic checking