• Nenhum resultado encontrado

Compiladores-Final-Complemento

N/A
N/A
Protected

Academic year: 2021

Share "Compiladores-Final-Complemento"

Copied!
39
0
0

Texto

(1)

       

 

 

 

PCS2508 

Linguagens e Compiladores 

­ 

Entrega final complementada 

 

 

 

 

 

 

 

 

Fábio Rachid Baptista ­ No. USP 7209871 

 

 

 

(2)

Índice 

 

Introdução……….3 

Especificações técnicas………..4 

Autômato de reconhecimento léxico………....5 

Funcionamento do programa………8 

Linguagem………..10 

Autômato de reconhecimento sintático………..12 

Analisador semântico………....17 

Simulação………32 

Conclusão………39 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(3)

Introdução 

 

Esta entrega constitui no detalhamento e formalização da linguagem que será utilizada  no compilador de acordo com as especificações do enunciado.    

 

 

 

 

 

 

 

 

 

 

 

(4)

Especificações técnicas 

  O analisador léxico, bem como as outras partes do compilador, serão desenvolvidos na  linguagem Python. O ambiente utilizado será o Linux Ubuntu.                                                                     

(5)

Autômato de reconhecimento léxico 

 

Abaixo, a representação diagramática do autômato para reconhecimento léxico.  Os círculos representam os estados; círculos com contorno mais forte representam  estados finais.  Os retângulos próximos aos círculos representam as classes associadas aos tokens  recebidos pelo autômato.  Para classificar os tokens em uma classe, foram seguidas as instruções presentes no  enunciado.  Abaixo, exemplos do funcionamento do autômato. 

 

 

(6)

 

Exemplo    ­ Cadeia de entrada: a    Entra­se com uma letra ‘a’, vamos para o estado inicial q0 para q1, que é de aceitação.  Como resultado, teremos que esse token pertencerá à classe id. Como saída dessa  entrada, teremos o par (id, 1), ou seja, a classe a que pertence o token e seu índice  (segundo o enunciado, deve ser associado um índice ao token pertence à classe id).     

(7)

­ Cadeia de entrada: a = b    Nesse caso, teremos 3 tokens: ‘a’, ‘=’ e ‘b’.   O primeiro token a ser processado é ‘a’. Depois de processado, o par (‘id’, 1),  correspondente a ‘a’, é guardado numa lista. O token seguinte é ‘=’, pertencente à  classe ‘igualdade’. O par (igualdade, =) também é armazenado. Finalmente,   ‘b’ é classificado como ‘id’ e o par (id, 2) é armazenado.

 

 

 

 

 

(8)

Funcionamento do programa 

 

­ Estruturas de dados utilizadas 

 

> Tuplas    Na linguagem Python, uma tupla constitui um conjunto de valores armazenados e  imutáveis. Para o analisador léxico, foram utilizadas de dimensão 2, utilizadas da  seguinte maneira:    (classe, valor)    Desse modo, depois do processamento do token no autômato, há o armazenamento,  na tupla, da classe do token e de seu valor. Cada tupla é inserida numa lista (explicado  adiante).  Por exemplo, o token ‘=’, depois de seu processamento, é armazenado numa tupla  (igualdade, =).    > Lista    No programa, foi utilizada uma lista para armazenar os pares (classe, valor), para que  esses pares fossem mostrados no final do programa, como mostrado a seguir:    Lista = {(classe x, valor a), (classe x, valor b), (classe y, valor c), … , (classe z, valor t)}     Essa lista é utilizada para, no final do programa, as tuplas com as classes e valores  sejam impressas na tela e armazenadas num arquivo de saída.    O programa, na linguagem Python, foi feito de forma a permitir modificações mais  facilmente, modularizando o programa.   

> Algoritmo da execução do programa 

  O programa recebe, como entrada, uma cadeia de caracteres que constituem os  tokens. O arquivo de entrada é lido linha a linha, onde cada linha é armazenada em  uma string que será percorrida, caracter a caracter.  O programa entra em um loop que percorre cada caracter da linha armazenada na 

(9)

O caracter em análise é verificado em cada um dos casos especificados no autômato  acima: se é letra, é identificado como ‘id’ e a ele é atribuído um valor; se é número, é  identificado como ‘int’; se é o símbolo ‘+’, é identificado como ‘soma’ e assim em diante.  O estado do autômato é atualizado de acordo com o símbolo consumido.  Depois de um token ser processado (entenda­se depois que o autômato chega a um  estado final), ele é armazenado numa tupla de acordo com sua classe no formato  citado acima. Depois de consumido, o processo se inicia novamente, analisando o  token seguinte, voltando o autômato ao seu estado inicial.  Depois que todos os tokens da entrada forem consumidos, o programa deve imprimir  na tela as tuplas resultantes e deve passar o resultado para um arquivo.   

> Estruturamento do analisador léxico 

 

Para tornar a implementação mais flexível e mais fácil de ser modificada, o analisador  léxico, com seus métodos e atributos, foi implementado em uma classe separada da  main. Portanto, para a execução do analisador léxico, devemos usar três arquivos:    ­ analisadorLexico.py  ­ main.py  ­ entrada.py    No arquivo analisadorLexico.py, temos a função de transição do autômato, uma lista  que armazena as tuplas (classe, valor) e uma lista com os símbolos dos identificadores.  O arquivo main.py executa a função de transição e mostra os tokens obtidos  associados às suas classes.  No arquivo entrada.py, tem­se o arquivo de texto a ser lido e processado no analisador  léxico.                       

(10)

Linguagem 

 

Como especificado no enunciado, deve­se definir uma linguagem que será utilizada  pelo compilador, que será denominada convenientemente de KCF. Segue, abaixo, a  linguagem definida, na notação de Wirth:      program = block "."

block =["const" ident "=" number {"," ident "=" number}";"]

["var" ident {"," ident}";"]

{"procedure" ident ";" block ";"} statement

statement =[ ident ":=" expression |"call" ident |"?" ident |"!" expression

|"begin" statement {";" statement }"end"

|"if" condition "then" statement |"while" condition "do" statement ]

condition ="odd" expression |

expression ("="|"#"|"<"|"<="|">"|">=") expression

expression =["+"|"-"] term {("+"|"-") term}.

term = factor {("*"|"/") factor}

factor = ident | number |"(" expression ")"

     

­ Exemplo 

 

Um exemplo do código que utiliza a linguagem KCF:   

(11)

VAR x, squ; PROCEDURE square; BEGIN squ:= x * x END; BEGIN x :=1; WHILE x <=10 DO BEGIN CALL square; ! squ; x := x +1 END END.                                               

(12)

 

Automato de reconhecimento sintático 

 

O analisador sintático tem a função de verificar se as regras, definidas na seção        anterior, são seguidas. Dessa forma, por exemplo, no caso da linguagem KCF, ao se        escrever END espera­se que, em seguida, venha um ponto­e­vírgula. 

Para realizar tal tarefa, o analisador sintático é composto por um autômato de pilha        estruturado e por um analisador léxico. 

O autômato principal seria a raiz da linguagem. No caso, seria a máquina definida pelo        não terminal Program. Os demais não­terminais formam a gama de submáquinas que        podem ser chamadas para tratar ocorrências em específico. Para obter tais autômatos        a partir das expressões obtidas da notação de wirth, é possível utilizar o método visto        em sala ou até mesmo seguindo a lógica e montar de forma livre já o minimizando.  A interação entre o analisador sintático e o léxico se dá através dos tokens, onde o        analisador sintático irá solicitar um token ao analisador léxico e, então, obtido o token,        irá transitar em sua máquina. Além disso, o analisador sintático deve ter acesso à        tabela de símbolos contida no analisado léxico. 

Para estruturar tal organização foi novamente utilizado o paradigma orientado a objetos        de tal modo que a troca de mensagens entre os objetos realizará as funções        desejadas. 

O autômato de reconhecimento sintático está representado nos diagramas abaixo.   

(13)
(14)
(15)

 

 

(16)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(17)

Analisador semântico 

 

Como descrito no tópico anterior, o analisador sintático não é capaz de determinar se  uma dada extensão faz sentido lógico, ele somente determina se a sentença segue a  regra da linguagem. Assim, é fundamental o uso de outro analisador que verifique, por  exemplo, se uma atribuição está correta (não pode permitir que uma variável int guarde  um char, por exemplo). Além disso, ele será a ferramenta que irá ser utilizar para gerar  o código compilado (em assembly).   O analisador semântico será chamado através do analisador sintático e possuirá  diversas funções que estão associadas a transições do autômato do analisador  sintático.   O código gerado, detalhado a seguir, foi dividida em quatro partes: uma parte básica,  que ocorre sempre que o compilador é chamado (não é influenciada pela sintaxe do  programa), a parte referente ao autômato de reconhecimento sintático program, a  parte referente ao autômato de reconhecimento sintático statement e a parte referente  ao autômato de reconhecimento sintático expression.   

> Básico 

  Antes de qualquer processamento das subrotinas semânticas associadas ao analisador  léxico, é impresso um código padrão que ajusta o ponteiro para a pilha, chama a  subrotina main e termina a execução quando main retorna. Também são geradas as  subrotinas de Push e Pop juntamente com as constantes por elas utilizadas.  Ao fim do processamento das subrotinas semânticas, também é gerado código: uma  label indicando que o espaço de memória a seguir é a pilha de execução e o  “commando” “#”, que indica ao montador que o código de montagem chegou ao fim.      Código padrão de início 

init LV stack_area ; adjusts the stack pointer 

MM stack_pointer ; adjusts the stack pointer 

SC sub_main ; calls main 

HM init ; halts the machine when main returns 

   

(18)

Código padrão de final 

stack_area K /0000 ; stack fromthis address and on

# init

 

Subrotina Push 

push_tmp K /0000 ; temporary

Push JP /0000 ;return address

MM push_tmp ; stores the value in a temporary

LD MM_code ; loads the "move to memory" code

+ stack_pointer ; composes it with the top of

stack address

MM push_write ; writes the instruction to

push_write

LD push_tmp ; loads the value to be pushed

push_write K /0000 ; writes the value on the stack

LD stack_pointer ; loads the stack pointer

+ two ; increments it

MM stack_pointer ; saves

LD push_tmp ; returns the pushed value

RS Push ; returns

 

Subrotina Pop 

Pop JP /0000 ;return address

LD stack_pointer ; loads the stack pointer

- two ; decrements it

MM stack_pointer ; saves

LD LD_code ; loads the "load code"

+ stack_pointer ; composes it with the top of

stack address

MM pop_read ; writes the instruction to pop_read

pop_read K /0000 ; reads the value from the stack

(19)

> Program 

  O funcionamento de program consiste basicamente em gerar labels de início da  subrotinas (onde é guardado o endereço de retorno desta), gerar um endereço de  memória que age como variável temporária para a resolução de expressões e, para  cada parâmetro de uma subrotina, gerar endereços de memória e desempilhá­los para  estes endereços (os parâmetros são passados pela pilha), de forma a facilitar  consideravelmente a realização de operações com esses parâmetros.    É o menor autômato e o que possui rotinas semânticas mais simples.    Exemplo:  PROCEDURE soma; BEGIN (...) END;    Gera: 

sub_soma JP /0000 ;return address 

SC Pop ; gets the parameter from the stack

SC Pop ; gets the parameter from the stack

(...)

RS sub_soma ; returns

(20)

> Statement 

  As rotinas semânticas de command variam muito entre cada “ramo” do autômato e,  portanto, serão descritas separadamente a seguir:    VAR id; : é gerado um endereço de memória da forma var_ + <nome da subrotina  atual> +   <id>    Exemplo:  VAR x;    Gera:  (...)  var_x K /0000 ;var       x := expression; : são rodadas as rotinas de expression, que, ao final, deixam o  resultado a expressão no acumulador (detalhado mais adiante). Em seguida é gerado  um comando move para a variável id (var_ + <nome da subrotina atual> +   <id>).    Exemplo:  VAR a; BEGIN a := 2 END;   Gera:  (...)  LV /0002

MM var_main_a ; assings the expression to the

(21)

    PROCEDURE id;  : cada expressão é avaliada por expression (resultado no  acumulador) e empilhada (Push). A chamada de função (sub + <id>) é, então, gerada    Exemplo:  VAR a, b, c; (…) PROCEDURE func; Gera:  (…) ;

SC Push ; pushes the routine parameter to

the stack

(…) ; b

SC Push ; pushes the routine parameter to

the stack

(…) ; c

SC Push ; pushes the routine parameter to

the stack

SC sub_func ; calls the routine

    IF condition THEN statement: a expressão é avaliada (resultado no acumulador) e é  gerado um jump if zero pra sair do if, se ela for avaliada para falso. Antes do label de  saída do if, é gerado um load de 1 (que é usado para avaliar o else).    Exemplo:  (...) IF a =1 THEN BEGIN CALL print; END;

(22)

  Gera: 

(…) ; 1 

JZ out_if_1 ;if the "if" condition fails, jumps

out of the "if"

(…) ;print(1)

LV one ; loads "true"

out_if_1 + zero ;thisisout of the "if"(NOP)

  IF ODD condition THEN statement: se o if não foi executado, ou seja, a expressão do  if foi avaliada para 0, este zero continua no acumulador; se o if foi executado, o “LV  one” colocou 1 no acumulador. Assim, o else é executado apenas se houver um 0 no  acumulador.    Exemplo:    IF ODD a :=2 THEN BEGIN CALL print; END;    Gera: 

JZ else_2 ; if we didn't pass in the "if",

pass in the "else" 

JP out_else_2 ; but if we did, don't go throught

the "else"

else_2 + zero ;thisiswhere the "else" begins

(NOP)

(…) ;print(2);

out_else_2 + zero ;thisisout of the "else"(NOP)

 

WHILE expression DO statement: um token de inicio é gerado, a expressão é 

(23)

for avaliada para falso. Antes do label de saída do while, é gerado um jump para o  token de início (para testar a expressão novamente).    Exemplo:  WHILE 1 DO BEGIN CALL print; END;   Gera: 

while_1 + zero ;while starts here (NOP)

(...) ; 1

JZ out_while_1 ;if the expression evaluates to 0,

go out

(…) ;print(1);

JP while_1 ; goes to the start

out_while_1 + zero ;while ends here (NOP) 

 

> Expression 

  As subrotinas semânticas de expression são as que geram o código responsável pela  resolução de expressões lógico­aritméticas (não foi feita distinção clara entre as duas:  valores diferentes de zero foram tratados como verdadeiros e o valor zero foi tratado  como falso).  Sua operação consiste em empilhar todos os operandos e fazer com que os  operadores calculem o resultado sobre os termos do topo da pilha. Ou seja, no caso de  uma adição, por exemplo, são desempilhados os dois operandos do topo da pilha e o  resultado é empilhado. Ao fim de uma expressão, seu resultado dever ser  desempilhado para o acumulador.  Esse modo de operação exige que a ordem de realização das operações seja similar à  da resolução de expressões pósfixas. Como a cadeia de entrada contém expressões  infixas, muito do código se assemelha ao algoritmo de conversão entre as duas formas  de apresentar uma expressão.   

(24)

> Observação importante    Ao final do reconhecimento de expression, nada garante sobre um id não processado  ou operadores na pilha. Por isso, toda chamada de uma submáquina expression deve  verificar se esse id existe (e, se existir, ele deve ser tratado como uma variável de expr,  e portanto colocado na pilha) e, no caso da pilha não estar vazia, remover um a um os  operadores da pilha, gerando o código referente a eles.    > Código referente aos diferentes operadores    O código a ser gerado referente aos diferentes operadores deve pegar os parâmetros  da pilha e colocar o resultado na pilha. Será detalhado apenas o código do +, já que  todos são análogos (podem ser obtidos apenas trocando a última linha por uma linha  que realize operação desejada ou, no caso de operações mais complexas, por um  conjunto de linhas que realize essa operação). 

SC Pop ; pops one parameter 

MM <tmp> ;and moves it to a temporary

SC Pop ; pops the other parameter

+ <tmp> ;and sums them both

SC Push ;finally, put the result back to the stack

> Programa exemplo 

 

O programa a seguir calcula os 10 primeiros números da sequência de Fibonacci:    VAR a, b, i, tmp; BEGIN a := 1 b := 1 i := 0 WHILE i <10 DO BEGIN

(25)

a := a + b b := tmp i := i + 1 END; END.    Código gerado   

init LV stack_area ; adjusts the stack pointer

MM stack_pointer; adjusts the stack pointer

SC sub_main ; calls main

HM init ; halts the machine when main

returns

stack_pointer K /0000 ; points to the stack

LD_code K /8000 ; load code

MM_code K /9000 ; move to memory code

one_hundred K =100 ;100 ten K =10 ;10 two K =2 ; 2 one K =1 ; 1 zero K =0 ; 0 ascii_zero K /0030 ;'0'

ascii_lf K /000A ; line feed

not K /0000 ; inverts the accumulator logically

JZ not_zero ;if it's false, jumps to not_zero

LD zero ; it was true, loads 0

RS not ; returns

not_zero LD one ; it was false, loads 1

(26)

logic K /0000 ; adequates true to 1

JZ logic_zero ;if it's false, jumps to not_zero

LD one ; it was true, loads 1

RS logic ; returns

logic_zero LD zero ; it was false, loads 0

RS logic ; returns

push_tmp K /0000 ; temporary

Push JP /0000 ;return address

MM push_tmp ; stores the value in a temporary

LD MM_code ; loads the move to memory code

+ stack_pointer; composes it with the top of stack

address

MM push_write ; writes the instruction to

push_write

LD push_tmp ; loads the value to be pushed

push_write K /0000 ; writes the value on the stack

LD stack_pointer; loads the stack pointer

+ two ; increments it

MM stack_pointer; saves

LD push_tmp ; returns the pushed value

RS Push ; returns

Pop JP /0000 ;return address

LD stack_pointer; loads the stack pointer

- two ; decrements it

MM stack_pointer; saves

(27)

+ stack_pointer; composes it with the top of stack address

MM pop_read ; writes the instruction to pop_read 

 

pop_read K /0000 ; reads the value from the stack

RS Pop ; returns

Print_var K /0000 ; stores the print parameter

Print_hundreds K /0000 ; hundreds of the parameter Print_tens K /0000 ; tens of the parameter

Print_units K /0000 ; units of the parameter

sub_print JP /0000 ;return address

SC Pop ; gets the parameter from the stack

MM Print_var ; makes a copy

/ one_hundred ;/100

* one_hundred ;*100

MM Print_hundreds ;= hundreds

LD Print_var ; loads the original value

- Print_hundreds ;- hundreds (= tens + units)

/ ten ;/10

* ten ;*10

MM Print_tens ;= tens

LD Print_var ; loads the original value

- Print_hundreds ;- hundreds - Print_tens ;- tens MM Print_units ;= units LD Print_hundreds ; hundreds / one_hundred ;/100 + ascii_zero ;+'0'

PD /0100 ; writes on the screen

(28)

/ ten ;/10 

+ ascii_zero ;+'0'

PD /0100 ; writes on the screen

LD Print_units ; units

+ ascii_zero ;+'0'

PD /0100 ; writes on the screen

LD ascii_lf ; line feed

PD /0100 ; writes on the screen

LD Print_var ; returns the parameter value

RS sub_print ; returns

sub_main JP /0000 ; reservado p/end de retorno

LV =1 ; loads the value

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_a ; assings the expression to the

variable

LV =1 ; loads the value

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_b ; assings the expression to the

variable

LV =0 ; loads the value

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

(29)

for_1 + zero ;for starts here (NOP)

LD var_main_i ; loads the var

SC Push ;and pushes it to the stack

LV =10 ; loads the value

SC Push ;and pushes it to the stack 

SC Pop ; pops one parameter

MM tmp_main_expr;and moves it to a temporary

SC Pop ; pops the other parameter

- tmp_main_expr;and subtracts them both

JN isless_2 ; jumps to isless if it's really

less

LD zero ; loads falseif it's not less

JP operator_end_2 ; goes to end

isless_2 LD one ; loads trueif it's really less

operator_end_2 SC Push ;finally, put the result

back to the stack

SC Pop ;final result of the expression ->

accumulator

JZ out_for_1 ;if expression evaluates to 0, go

out of the for

JP for_command_1; does the for commands

for_post_1 + zero ;for post (3rd term) starts here

(NOP)

LD var_main_i ; loads the var

SC Push ;and pushes it to the stack

LV =1 ; loads the value

SC Push ;and pushes it to the stack

SC Pop ; pops one parameter

(30)

SC Pop ; pops the other parameter

+ tmp_main_expr;and sums them both

operator_end_3 SC Push ;finally, put the result

back to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_i ; assings the expression to the

variable

JP for_1 ; does the for again

for_command_1 + zero ;for command starts here

(NOP)

LD var_main_b ; loads the var

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

SC Push ; pushes the routine parameter to

the stack

SC sub_print ; calls the routine

LD var_main_a ; loads the var

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_tmp; assings the expression to the

variable

LD var_main_a ; loads the var

SC Push ;and pushes it to the stack

LD var_main_b ; loads the var

SC Push ;and pushes it to the stack

SC Pop ; pops one parameter

(31)

+ tmp_main_expr;and sums them both

operator_end_4 SC Push ;finally, put the result

back to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_a ; assings the expression to the

variable

LD var_main_tmp; loads the var 

SC Push ;and pushes it to the stack

SC Pop ;final result of the expression ->

accumulator

MM var_main_b ; assings the expression to the

variable

JP for_post_1 ; does the for post

out_for_1 + zero ;for ends here (NOP)

RS sub_main ; termino da subrotina

tmp_main_expr K /0000 ;var

var_main_a K /0000 ;var

var_main_b K /0000 ;var

var_main_i K /0000 ;var

var_main_tmpK /0000 ;var

stack_area K /0000 ; stack fromthis address and on

# init ;

     

(32)

Simulações    Abaixo, seguem as simulações do compilador, envolvendo todas as partes (léxica,  sintática e semântica), através do código    ­ Cadeia de entrada:   PROCEDURE soma; BEGIN END;   

(33)

­ Cadeia de entrada:  VAR a; BEGIN a := 2 END;     

(34)

­ Cadeia de entrada:  IF a =1 THEN BEGIN CALL print; END;  (abaixo)             

(35)

   

(36)

­ Cadeia de entrada:  WHILE 1 DO BEGIN CALL print; END;     

(37)

­ Cadeia de entrada:  VAR a, b, i, tmp; BEGIN a := 1 b := 1 i := 0 WHILE i <10 DO BEGIN tmp := a a := a + b b := tmp i := i + 1 END; END.

(abaixo; utilizar zoom para ampliar)

                 

(38)
(39)

Conclusão   

A construção de um compilador foi interessante, pois se pôde integrar conhecimentos  de várias áreas da computação, além de conseguir entender o funcionamento, a fundo,  dos compiladores. 

Referências

Documentos relacionados

O objetivo deste trabalho é mostrar as características, importância e aplicações que os semicondutores têm para a eletrônica, as suas funções em equipamentos

Do lado de cá, os órfãos de Estaline (e de Trotsky – quem é que escreveu que Trotsky não se virou contra Estaline por ele matar pessoas mas por ele não matar as pessoas devidas?) e

9.2 A autodeclaração dos candidatos pretos e pardos, realizada no ato da inscrição deste Processo Seletivo, para candidatos aprovados dentro das vagas reservadas

Concretamente, uma sessão de Constelação Familiar consiste em um encontro que varia de poucas horas a alguns dias, durante o qual um grupo se reúne e, neste, os clientes, um por

Os antigos druidas acreditavam que em uma certa noite (31 de outubro), bruxas, fantasmas, espíritos, fadas, e duendes saiam para prejudicar as pessoas.. LUA CHEIA, GATOS

Diante do exposto, a presente pesquisa teve como objetivo identificar o imaginário coletivo de professores sobre o processo de inclusão escolar por meio do uso do Procedimento

 Clematis – sonhador pensando no futuro, falta de praticidade e concretização. Util para pessoas sonolentas, que nunca estão totalmente despertas, nem demonstram

E) O sistema social é uma interação dinâmica entre indivíduos (inseridos ou não em organizações formais) cujo comportamento é constrangido, não apenas por