Como as Linguagens se Compartam? Parte II
Olá, mundo!
Tenho um pouco distante das postagens, eu sei. No entanto retorno após longo hiato, devidos aos trabalhos e estudos.
Como falamos no Post anterior, sobre como as linguagens se comportam, falaremos sobre compiladores.
Introdução
Compiladores são responsáveis pela tradução literal das instruções que fazemos nas linguagens de programação para a compreensão, e por fim, a execução pela máquina.
Em linguagens consagradas como C#, por exemplo, temos a arquitetura .NET nela temos a CLR (Common Language Runtime), um ambiente que executa o código, parecido como a Máquina Virtual Java assim o faz, otimizando a execução dos códigos escritos em C#, F#, VB.NET. Já a linguagem Java se beneficia do maduro e estável compilador que possui hotspot, ou seja, o partes de execução do código em que estejam em execução são otimizadas pelo JIT, performando em tempo de execução.

Em linguagens interpretadas, como Python, PHP, Ruby, Javascript – caso ainda tenha dúvidas sobre as diferenças entre linguagens interpretadas e compiladas, clique aqui – há melhorias substanciais utilizando o melhor dos mundos, ou seja, incorporando um compilador JIT, como foi feito a partir da versão 8 do PHP, assim como o Numba para Python.
Mas afinal, o que são compiladores?!
“If you don’t know how compilers work, then you don’t know how computers work” – Steve Yegge
Os compiladores exercem a função essencial da computação, se não fossem por eles não teríamos qualquer interatividade com a máquina, até mesmo não seria viável os interpretadores como Basic, pois não teríamos sequer uma abstração mais profunda com o Hardware, logo estamos falando sobre a fundamentação, a Pedra Angular da Computação.
Sequência de ações de um compilador
Para que possamos compreender melhor o funcionamento interno de um compilador vejamos o diagrama abaixo, na sequência, de maneira pormenorizada, será explanada cada etapa que o compilador executa.

Analisador Léxico
O Analisador Léxico tem a função de realizar o agrupamento de caracteres que estão dispostos em um código fonte, separando-as as palavras em Tokens, o que seriam equivalentes a uma sequência minimalista que possui significado para a linguagem, o qual o código foi escrito.
Exemplo de função em Golang:
package main
import "fmt"
func main (){
fmt.Println("Olá, Mundo!)
}
No código acima o Analisador Léxico separa cada palavra reservada em Tokens, na linha 1 ele entende, pela convenção adotada do Golang que package define nosso pacote, na sequência main.
Na linha 2, a palavra reservada import atua para importar bibliotecas da própria linguagem, como extensões criadas por outros membros da comunidade, neste caso, importamos a biblioteca fmt, que realiza a formatação de I/O.
Uma observação importante, linhas em branco, espaços, nesta etapa são ignorados, apenas as palavras reservadas e as definições atribuídas à uma variável, por exemplo, serão devidamente transformadas e analisadas.
Continuando na linha 5, teremos nossa func que irá definir nossa função, seguida por main(), a qual é o nome de nossa função. As chaves { } delimitam a estrutura de execução, atribuições de nossa função.
Na linha 7, temos a chamada de nossa biblioteca de formatação, seguido do objeto que faz a impressão da expressão “Olá, Mundo!” fmt.Println(“Olá, Mundo!”).
Analisador Sintático
Com os Tokens devidamente agrupados, a função do Analisador Sintático é aglutinar em expressões para verificá-las se elas são válidas pela linguagem, no nosso exemplo a linguagem Go. É como uma análise gramatical de uma oração, de nossa Língua Portuguesa. Nesta fase, utiliza-se a estrutura de dados da Ávore de Abstração de Sintaxe (AST – Abstract Syntax Tree), dispondo toda a extensão do código de maneira hierarquizada, condizente com as respectivas funções de comando exercidadas pelos Tokens no contexto de expressão literal.
Veja como ficará a Árvore de nosso código, transformada em JSON:
{
"Comments": [],
"Decls": [
{
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 26,
"Line": 3,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 14,
"Line": 3,
"Column": 1
}
},
"Lparen": 0,
"Rparen": 0,
"Specs": [
{
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 26,
"Line": 3,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 21,
"Line": 3,
"Column": 8
}
},
"Path": {
"Kind": "STRING",
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 26,
"Line": 3,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 21,
"Line": 3,
"Column": 8
}
},
"Value": "\"fmt\"",
"_type": "BasicLit"
},
"_type": "ImportSpec"
}
],
"Tok": "import",
"_type": "GenDecl"
},
{
"Body": {
"Lbrace": 40,
"List": [
{
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 69,
"Line": 6,
"Column": 29
},
"Start": {
"Filename": "main.go",
"Offset": 43,
"Line": 6,
"Column": 3
}
},
"X": {
"Args": [
{
"Kind": "STRING",
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 68,
"Line": 6,
"Column": 28
},
"Start": {
"Filename": "main.go",
"Offset": 55,
"Line": 6,
"Column": 15
}
},
"Value": "\"Olá, Mundo\"",
"_type": "BasicLit"
}
],
"Ellipsis": 0,
"Fun": {
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 54,
"Line": 6,
"Column": 14
},
"Start": {
"Filename": "main.go",
"Offset": 43,
"Line": 6,
"Column": 3
}
},
"Sel": {
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 54,
"Line": 6,
"Column": 14
},
"Start": {
"Filename": "main.go",
"Offset": 47,
"Line": 6,
"Column": 7
}
},
"Name": "Println",
"_type": "Ident"
},
"X": {
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 46,
"Line": 6,
"Column": 6
},
"Start": {
"Filename": "main.go",
"Offset": 43,
"Line": 6,
"Column": 3
}
},
"Name": "fmt",
"_type": "Ident"
},
"_type": "SelectorExpr"
},
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 69,
"Line": 6,
"Column": 29
},
"Start": {
"Filename": "main.go",
"Offset": 43,
"Line": 6,
"Column": 3
}
},
"Lparen": 55,
"Rparen": 69,
"_type": "CallExpr"
},
"_type": "ExprStmt"
}
],
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 71,
"Line": 7,
"Column": 2
},
"Start": {
"Filename": "main.go",
"Offset": 39,
"Line": 5,
"Column": 12
}
},
"Rbrace": 71,
"_type": "BlockStmt"
},
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 71,
"Line": 7,
"Column": 2
},
"Start": {
"Filename": "main.go",
"Offset": 28,
"Line": 5,
"Column": 1
}
},
"Name": {
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 37,
"Line": 5,
"Column": 10
},
"Start": {
"Filename": "main.go",
"Offset": 33,
"Line": 5,
"Column": 6
}
},
"Name": "main",
"_type": "Ident"
},
"Type": {
"Func": 29,
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 39,
"Line": 5,
"Column": 12
},
"Start": {
"Filename": "main.go",
"Offset": 28,
"Line": 5,
"Column": 1
}
},
"Params": {
"Closing": 39,
"List": [],
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 39,
"Line": 5,
"Column": 12
},
"Start": {
"Filename": "main.go",
"Offset": 37,
"Line": 5,
"Column": 10
}
},
"Opening": 38,
"_type": "FieldList"
},
"_type": "FuncType"
},
"_type": "FuncDecl"
}
],
"Imports": [
{
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 26,
"Line": 3,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 21,
"Line": 3,
"Column": 8
}
},
"Path": {
"Kind": "STRING",
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 26,
"Line": 3,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 21,
"Line": 3,
"Column": 8
}
},
"Value": "\"fmt\"",
"_type": "BasicLit"
},
"_type": "ImportSpec"
}
],
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 71,
"Line": 7,
"Column": 2
},
"Start": {
"Filename": "main.go",
"Offset": 0,
"Line": 1,
"Column": 1
}
},
"Name": {
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 12,
"Line": 1,
"Column": 13
},
"Start": {
"Filename": "main.go",
"Offset": 8,
"Line": 1,
"Column": 9
}
},
"Name": "main",
"_type": "Ident"
},
"Package": 1,
"Unresolved": [
{
"Loc": {
"End": {
"Filename": "main.go",
"Offset": 46,
"Line": 6,
"Column": 6
},
"Start": {
"Filename": "main.go",
"Offset": 43,
"Line": 6,
"Column": 3
}
},
"Name": "fmt",
"_type": "Ident"
}
],
"_type": "File"
}
Como podemos ver, a estrutra verifica a contextualização do código a cada detalhe, separando cada item, como por exemplo o tipo de dado de saída, que no nosso exemplo será STRING, a importação da biblioteca, tudo de uma maneira bem mais assertiva e pragmática do que a etapa anterior.
Analisador Semântico
Aqui ele verifica se as regras gramaticais estão de acordo, como por exemplo se a variável foi declarada, qual o tipo agregada para ela, e etc.
package main
import "fmt"
func somar(x int, y int) {
resultado := x + y
fmt.Println("A soma de X + Y é: ", resultado)
}
func main() {
somar(1, 1)
}
Neste exemplo, a função somar() recebe as variáveis x e y, declaradas como inteiro, a variável resultado tem o short assign com a funçao aritmética de soma entre as respectivas variáveis. Na sequência imprime a variável resultado.
Na função main() é feito callback da função somar atribuindo os valores 1 e 1, que resultará em dois.

É tarefa do Analisador de Semântica a inferência de tipo das variáveis, verificar se uma variável está em escopo, se ela é compatível com demais identificadores e operadores.
Gerador de Código Intermediário
Nesta etapa, o compilador transforma nossas instruções com um código mais compreensível para a linguagem de máquina.
main_main_pc0:
TEXT main.main(SB), NOFRAME|ABIInternal, $40-0
MOVL TLS, CX
PCDATA $0, $-2
MOVL (CX)(TLS*2), CX
PCDATA $0, $-1
CMPL SP, 8(CX)
PCDATA $0, $-2
JLS main_main_pc109
PCDATA $0, $-1
SUBL $40, SP
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·EaPwxsZ75yY1hHMVZLmk6g==(SB)
FUNCDATA $2, main.main.stkobj(SB)
MOVL $0, main..autotmp_8+32(SP)
MOVL $0, main..autotmp_8+36(SP)
LEAL type:string(SB), AX
MOVL AX, main..autotmp_8+32(SP)
LEAL main..stmp_0(SB), AX
MOVL AX, main..autotmp_8+36(SP)
MOVL os.Stdout(SB), AX
NOP
LEAL go:itab.*os.File,io.Writer(SB), CX
MOVL CX, (SP)
MOVL AX, 4(SP)
LEAL main..autotmp_8+32(SP), AX
MOVL AX, 8(SP)
MOVL $1, 12(SP)
MOVL $1, 16(SP)
PCDATA $1, $0
CALL fmt.Fprintln(SB)
ADDL $40, SP
RET
main_main_pc109:
NOP
PCDATA $1, $-1
PCDATA $0, $-2
CALL runtime.morestack_noctxt(SB)
PCDATA $0, $-1
JMP main_main_pc0
Acima vemos a otimização feita nesta etapa para que o código execute de maneira mais eficiente.
Otimização do Código
Nesta etapa, que antecede a final, ela otimiza o código para que seja executado com o melhor desempenho possível.
main_main_pc0:
TEXT main.main(SB), NOFRAME|ABIInternal, $92-0
MOVL TLS, CX
PCDATA $0, $-2
MOVL (CX)(TLS*2), CX
PCDATA $0, $-1
CMPL SP, 8(CX)
PCDATA $0, $-2
JLS main_main_pc268
PCDATA $0, $-1
SUBL $92, SP
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
FUNCDATA $1, gclocals·E5oFiNjeiS1rQQ8jVbtNeA==(SB)
FUNCDATA $2, main.main.stkobj(SB)
MOVL $0, main..autotmp_8+64(SP)
MOVL $0, main..autotmp_8+68(SP)
LEAL main..autotmp_8+64(SP), AX
MOVL AX, main..autotmp_7+72(SP)
TESTB AX, (AX)
LEAL type:string(SB), CX
MOVL CX, (AX)
MOVL runtime.writeBarrier(SB), CX
PCDATA $0, $-2
TESTL CX, CX
JEQ main_main_pc71
JMP main_main_pc73
main_main_pc71:
JMP main_main_pc85
main_main_pc73:
MOVL 4(AX), CX
CALL runtime.gcWriteBarrier1(SB)
MOVL CX, (DI)
JMP main_main_pc85
main_main_pc85:
LEAL main..stmp_0(SB), CX
MOVL CX, 4(AX)
PCDATA $0, $-1
TESTB AX, (AX)
JMP main_main_pc98
main_main_pc98:
MOVL AX, fmt.a+52(SP)
MOVL $1, fmt.a+56(SP)
MOVL $1, fmt.a+60(SP)
MOVL $0, fmt.n+32(SP)
MOVL $0, fmt.err+44(SP)
MOVL $0, fmt.err+48(SP)
NOP
MOVL $0, main..autotmp_6+76(SP)
MOVL $0, main..autotmp_6+80(SP)
MOVL fmt.a+52(SP), AX
MOVL fmt.a+56(SP), CX
MOVL fmt.a+60(SP), DX
MOVL os.Stdout(SB), BX
LEAL go:itab.*os.File,io.Writer(SB), BP
MOVL BP, (SP)
MOVL BX, 4(SP)
MOVL AX, 8(SP)
MOVL CX, 12(SP)
MOVL DX, 16(SP)
PCDATA $1, $0
CALL fmt.Fprintln(SB)
MOVL 24(SP), AX
MOVL 20(SP), CX
MOVL 28(SP), DX
MOVL CX, main..autotmp_5+36(SP)
MOVL AX, main..autotmp_6+76(SP)
MOVL DX, main..autotmp_6+80(SP)
MOVL main..autotmp_5+36(SP), CX
MOVL CX, main..autotmp_3+40(SP)
MOVL AX, main..autotmp_4+84(SP)
MOVL DX, main..autotmp_4+88(SP)
MOVL main..autotmp_3+40(SP), CX
MOVL CX, fmt.n+32(SP)
MOVL AX, fmt.err+44(SP)
MOVL DX, fmt.err+48(SP)
JMP main_main_pc264
main_main_pc264:
ADDL $92, SP
RET
main_main_pc268:
NOP
PCDATA $1, $-1
PCDATA $0, $-2
CALL runtime.morestack_noctxt(SB)
PCDATA $0, $-1
JMP main_main_pc0
E por fim é gerado o executável, podendo no caso da Golang ser opcional para cada S.O, aqui compilei para Linux, mas caso queira compilar para Windows ou Mac, só utilizar as flags de seus respectivos S.O, que irá perfazer toda a etapa que foi descrita acima.
O papel dos compiladores na informática é de suma importância, pois dependendo do cenário, exige-se que o código seja executado em tempo de execução, uma manipulação de hardware precisa, como foi até citado neste artigo anterior, sobre a Missão Apollo 11.
Como sempre deixo aqui as referências, e sempre lembro de que se aprofundem na pesquisa, aqui foi apenas uma superficialidade do funcionamento dos compiladores, no entanto há muito mais para se explorar.
Até a próxima! ; )
Referência
AHO, Alfred V.; SETHI, Ravi; ULLMAN, Jeffrey D. , Compiladores : princípios, técnicas e
ferramentas. Rio de Janeiro: Guanabara Koogan, 1995.
O cérebro é como um músculo. Quando pensamos bem, nos sentimos bem.