S9 (language)

From Wiki**3

Contents

THIS DESCRIPTION CORRESPONDS TO VERSION 200905031425

OFFICIAL VERSION: https://fenix.ist.utl.pt/disciplinas/com-2/2008-2009/2-semestre/projecto

Introdução

A linguagem S9 é uma linguagem imperativa e fortemente tipificada.

Tipos de dados

A linguagem define cinco tipos de dados manipuláveis, alguns dos quais estruturados. Ver também Gramática.

  • Números inteiros - Designados por int, ocupam 4 bytes em complemento para dois, alinhados a 32 bits.
  • Números reais - Designados por real, ocupam 8 bytes em vírgula flutuante (norma IEEE 754).
  • Cadeias de caracteres - Designadas por string, são terminadas pelo carácter com o valor 0 ASCII (NULL). As variáveis e literais do tipo string podem apenas ser utilizados em atribuições, em impressões e como argumentos/retornos de funções.
  • Ponteiros - Designados pela palavra ptr e pelo tipo apontado, representam endereços de outros objectos, ocupando 4 bytes, alinhados a 32 bits. Podem ser objecto de operações aritméticas (deslocamentos) e permitem aceder ao valor apontado.
  • Estruturas - Designados por struct e pelos elementos constituintes, representam tipos estruturados, i.e., organizações de objectos de outros tipos. A dimensão de uma estrutura (em bytes) depende da sua constituição.

Verificação de tipos

As operações em S9 dependem dos tipos de dados a que são aplicadas. Os tipos suportados por cada operador e a operação a realizar são indicados na definição das expressões (ver Expressões).

Manipulação de nomes

As entidades nomeadas (ver também Identificadores) em S9 correspondem a tipos estruturados, constantes, variáveis e funções. Nos pontos que se seguem, usa-se o termo entidade para as designar indiscriminadamente, usando-se listas explícitas quando a descrição for válida apenas para um subconjunto.

Espaços de nomes

O espaço de nomes global é único, pelo que um nome utilizado para designar uma entidade num dado contexto não pode ser utilizado para designar outras (ainda que de natureza diferente).

Validade das variáveis

As entidades globais (declaradas fora de qualquer função), existem do início ao fim da execução do programa. As contantes e variáveis locais a uma função existem apenas durante a sua execução, e os argumentos formais são válidos enquanto a função está activa.

Visibilidade dos identificadores

Os identificadores estão visíveis desde a sua declaração até ao fim do seu alcance: ficheiro (globais) ou à função (locais). A reutilização de um identificador num contexto inferior encobre possíveis declarações em contextos superiores. Em particular, a redeclaração de um identificador numa função encobre o global até ao fim da função. Não é possível definir funções dentro de funções. É possível importar símbolos globais dentro de funções e seus contextos inferiores (visibilidade restringida), embora não seja possível defini-los (ver Estruturas globais).

Convenções Lexicais

Existem seis grupos de elementos lexicais (tokens). Em cada caso, é considerada a maior sequência de caracteres que constitua um elemento lexical válido.

Caracteres brancos

São considerados caracteres brancos aqueles que, embora servindo para separar os elementos lexicais, não representam nenhum elemento lexical. São considerados caracteres brancos: espaço ASCII SP (0x20), mudança de linha ASCII LF (0x0A, ou \n), recuo do carreto ASCII CR (0x0D, \r) e tabulação horizontal ASCII HT (0x09, ou \t).

Comentários

Os comentários funcionam como separadores de elementos lexicais. Existem dois tipos de comentários.

  • Explicativos - Começam com a sequência // (desde que a sequência não faça parte de uma cadeia de caracteres), e acabam no fim da linha.
  • Operacionais - Começam com /* e terminam com */ (desde que não façam parte de cadeias de caracteres). Podem estar aninhados.

Identificadores

Também designados por nomes, são iniciados por uma letra (maiúscula ou minúscula) ou por _ (carácter sublinhado). O primeiro carácter é seguido por 0 (zero) ou mais letras, dígitos ou _. O número de caracteres que constituem um identificador é ilimitado e dois nomes designam identificadores distintos se houver alteração de maiúscula para minúscula, ou vice-versa, de pelo menos um carácter.

Palavras chave

As palavras indicadas de seguida estão reservados em S9 (palavras chave), não podendo ser utilizadas como identificadores. Estas palavras têm de ser escritas exactamente como indicado:

 const void int real ptr struct string public use sizeof
 next stop return if elif else while for

Literais

São notações para valores constantes de alguns tipos da linguagem (não confundir com constantes, i.e., identificadores que designam elementos cujo valor não pode sofrer alterações durante a execução do programa).

Inteiros

Um literal inteiro é um número não negativo (uma constante inteira pode, contudo, ser negativa: números negativos são construídos pela aplicação do operador menos unário (-) a um literal positivo).

Um literal inteiro em decimal é constituído por uma sequência de 1 (um) ou mais dígitos decimais (dígitos de 0 a 9) em que o primeiro dígito não é um 0 (zero), excepto no caso do número 0 (zero). Neste caso, é composto apenas pelo dígito único 0 (zero) (em qualquer base de numeração).

Um literal inteiro em octal começa sempre pelo dígito 0 (zero), sendo seguido de um ou mais dígitos de 0 a 7. Um literal inteiro em hexadecimal começa sempre pela sequência 0x, sendo seguido de um ou mais digitos de 0 a 9, de a a f ou de A a F. As letras de a a f, ou de A a F, representam os valores de 10 a 15 respectivamente. Um literal inteiro em binário começa sempre pela sequência 0b, sendo seguido de um ou mais digitos 0 (zero) ou 1 (um).

Se não for possível representar o literal inteiro na máquina, devido a um overflow, deverá ser gerado um erro lexical.

Reais

Os literais reais são expressos em notação científica (tal como em C) ou em notação de engenharia, onde a parte exponencial não existe e o ponto decimal é substituído pelas letras

  • y (yocto/iocto = 10-24 = 1E-24),
  • z (zecto = 10-21 = 1E-21),
  • a (atto/ato = 10-18 = 1E-18),
  • f (femto/fento = 10-15 = 1E-15),
  • p (pico = 10-12 = 1E-12),
  • n (nano = 10-9 = 1E-9),
  • u (micro = 10-6 = 1E-6),
  • m (milli/mili = 10-3 = 1E-3),
  • k (kilo/quilo = 103 = 1E+3),
  • M (mega = 106 = 1E+6),
  • G (giga = 109 = 1E+9),
  • T (tera = 1012 = 1E+12),
  • P (peta = 1015 = 1E+15),
  • E (exa = 1018 = 1E+18),
  • Z (zetta/zeta = 1021 = 1E+21),
  • Y (yotta/yota = 1024 = 1E+24).

Quando a notação for ambígua, é preferida a interpretação segundo a notação de engenharia.

Cadeias de caracteres

São consituídas por dois ou mais caracteres iniciadores seguidos, sem separadores, mas constituindo elementos lexicais distintos, bem como uma só cadeia de texto (opcionalmente, poderão existir caracteres brancos entre os elementos lexicais que compõem a cadeia de caracteres).

Os caracteres iniciadores podem ser valores inteiros, caracteres individuais ou cadeias de texto. Os valores inteiros em qualquer base, que representam um só carácter. Os caracteres individuais começam e terminam com o carácter plica (') podendo conter um só carácter ou uma sequência especial iniciada por (\). Uma sequência especial pode ser representada pelos caracteres ASCII LF, CR e HT (\n, \r e \t, respectivamente), ou ainda a plica (\') ou a barra (\\). Qualquer outro carácter pode ser representado por 1 ou 2 digitos hexadecimais precedidos do carácter \, por exemplo \0a ou apenas \A se o carácter seguinte não representar um digito hexadecimal.

Uma cadeia de texto, começa e termina com o carácter aspa ("), pode conter qualquer número de caracteres, podendo estes ser valores ASCII (excepto o 0 ou NULL). Dentro das cadeias de texto, os caracteres utilizados para iniciar ou terminar comentários têm o seu valor normal ASCII não iniciando ou terminando qualquer comentário. As mesmas sequências especiais dos caracteres individuais podem ser utilizados nas cadeia de texto, excepto a sequência \' que é substituída pela sequência \". O carácter aspa (") pode ser utilizado numa cadeia de texto, desde que precedido de \.

Estruturas

Os literais de estruturas são constituídos por sequências de outros literais separados por vírgulas (,). Estes literais são iniciados com { e terminados com }.

Ponteiros

O único literal admissível para ponteiros é 0 (zero), indicando o ponteiro nulo.

Operadores de expressões

São considerados operadores os seguintes elementos lexicais:

-  +  #  *  /  %  ^  =  <  >  ==  >=  <=  !=  ||  &&  !  [  ] . @ ? sizeof

Delimitadores

Os elementos lexicais seguintes são considerados delimitadores/terminadores:

; ,

Gramática

Gramática

A gramática da linguagem S9 pode ser resumida pelas regras abaixo. Considerou-se que os elementos em tipo fixo são literais, que os parênteses curvos agrupam elementos, que elementos alternativos são separados por uma barra vertical, que elementos opcionais estão entre parênteses rectos, que os elementos que se repetem zero ou mais vezes estão entre « e ». A barra vertical e os parênteses são elementos lexicais da linguagem quando representados em tipo fixo.

S9grammar.png

Elementos lexicais

Foram omitidos da gramática, por já terem sido definidos acima, os seguintes elementos: identificador (Identificadores), literal-inteiro (Inteiros), literal-real (Reais), carácter (Cadeias de caracteres) e cadeia (Cadeias de caracteres).

Expressões

Foi omitida da gramática a regra corresponte à definição de expressões: serão tratadas em Expressões.

Left value

Os elementos de uma expressão que podem ser utilizados como left-values encontram-se individualmente identificados em Expressões. Todos os left-values representam posições de memória passíveis de ser modificadas.

Constantes

A linguagem define identificadores constantes: precedem a declaração pela palavra reservada const, impedindo que o identificador declarado possa ser utilizado em operações que modifiquem o seu valor. Caso um identificador designe uma constante inteira não pública (ver Estruturas globais) o seu valor deverá ser directamente substituído no código, não ocupando espaço.

Ficheiros

Os ficheiros descritos em S9 são uniformes, sendo o principal aquele que contiver a função principal, designada por s9.

Tipos estruturados

Uma estrutura permite agrupar um conjunto de valores de vários tipos.

Declaração

As estruturas em S9 são sempre designadas por identificadores constantes seguidas de um corpo com as definições dos campos que as compõem. Não existem estruturas com corpos vazios. Alternativamente, uma estrutura pode ser definida a partir de um modelo: definido por um literal exemplo ou por um intervalo entre dois inteiros (neste caso, a estrutura é constituída por tantos campos quantos os inteiros no intervalo indicado, sendo acedidos os seus campos pela posição que ocupam).

A declaração de uma estrutura sem corpo é utilizada para efectuar declarações antecipadas (utilizadas para pré-declarar estruturas que sejam usadas antes de serem definidas). Caso a declaração tenha corpo, define-se uma nova estrutura.

Estruturas globais

Uma estrutura pode ser utilizada fora do módulo onde foi definida, desde que não seja necessário conhecer a sua definição. A palavra reservada public designa uma estrutura globalmente visível no módulo actual e acessível a partir de outros módulos. A declaração de uma estrutura globalmente visível faz-se como nas declarações antecipadas (ver Declaração).

Inicialização por omissão

A inicialização por omissão aplica-se a cada campo da estrutura e é utilizada quando a inicialização ou operador de atribuição não contêm toda a informação necessária para preencher todos os campos. Quando se aplicam operadores sobre valores incompletos, os valores omitidos devem correspondem aos últimos campos da estrutura.

A inicialização de cada campo é como descrita para as variáveis e constantes (Declaração de variáveis e constantes). Quando se reserva memória para uma estrutura, os valores por omissão devem ser respeitados.

Declaração de variáveis e constantes

Cada declaração permite declarar uma única variável ou constante. Uma declaração inclui os componentes descritos nos pontos seguintes.

Constante

Designada pela palavra reservada const, que torna o identificador constante, ou seja, cujo valor representado não pode ser modificado.

Símbolos globais

Existem dois qualificadores opcionais, que gerem a utilização de identificadores globais:

  • Símbolos públicos - Designados pela palavra reservada public. Os identificadores são globalmente visíveis no módulo actual e acessíveis a partir de outros módulos.
  • Pré-declarações - Designadas pela palavra reservada use, para o caso de constantes, variáveis ou funções, que podem estar declaradas mais à frente no ficheiro ou num outro ficheiro (possivelmente noutra linguagem).

Apenas é possível aplicar use a declarações. public pode também ser aplicada a definições.

Tipos de dados

Os tipos são como descrito em Tipos de dados.

Identificador

O identificador passa a nomear a entidade declarada.

Inicialização

A existir, inicia-se com o operador = seguido de expressão do tipo declarado: inteiro (uma expressão inteira), real (uma expressão real), ponteiro (uma expressão do tipo ponteiro), estrutura (uma expressão correspodente ao tipo da estrutura).

As cadeia de caracteres são (possivelmente) inicializadas com uma lista não nula de valores sem separadores (opcionalmente, poderão existir caracteres brancos entre os elementos lexicais que compõem a cadeia de caracteres). Os valores podem ser inteiros (ver Cadeias de caracteres), caracteres individuais ou cadeias de texto. Estes valores são sempre constantes, independentemente de o identificador que as designa ser constante ou não.

Estruturas com apenas um campo podem ser iniciadas a partir de elementos atómicos do tipo correspondente. Neste caso, os caracteres delimitadores do literal da estrutura são opcionais. Inicializadores de estruturas com valores por omissão devem obedecer às restrições descritas em Inicialização por omissão.

Notar que declarações de constantes não iniciadas só são possíveis se corresponderem a identificadores pré-declarados, pertencentes a outros módulos.

Funções

Uma função permite agrupar um conjunto de instruções num corpo, que é executado com base num conjunto de parâmetros (os argumentos formais) quando é invocada a partir de uma expressão. O programa tem início na função com nome s9.

Declaração

As funções em S9 são sempre designadas por identificadores constantes precedidos do tipo de dados devolvido pela função.

As funções que recebam argumentos devem indicá-los no cabeçalho. Funções sem argumentos definem um cabeçalho vazio.

Uma declaração de uma função sem corpo é utilizada para tipificar um identificador exterior ou para efectuar declarações antecipadas (utilizadas para pré-declarar funções que sejam usadas antes de ser definidas, por exemplo, entre duas funções mutuamente recursivas). Caso a declaração tenha corpo, define-se uma nova função.

Invocação

A função só pode ser invocada através de um identificador que refira uma função previamente declarada ou definida.

Caso existam argumentos, na invocação da função, o seu identificador é seguido de uma lista de expressões delimitadas por parênteses curvos. A lista de expressões é uma sequência, possivelmente vazia, de expressões separadas por vírgulas. As expressões são avaliadas da direita para a esquerda antes da invocação da função (convenção Cdecl) e o valor resultante passado por cópia (passagem de argumentos por valor).

O número e tipo de parâmetros actuais deve ser igual ao número e tipo dos parâmetros formais da função invocada. Caso existam parâmetros opcionais, iniciados na declaração da função, os seus valores são utilizados na falta de parâmetros actuais e apenas se forem os últimos. A ordem dos parâmetros actuais deverá ser a mesma dos argumentos formais da função a ser invocada. Os parâmetros actuais devem ser colocados na pilha de dados pela ordem inversa da sua declaração (o primeiro no topo da pilha) e o endereço de retorno no topo da pilha.

A função chamadora coloca os argumentos na pilha e é também responsável pela sua remoção, após o retorno da função chamada (convenção Cdecl). Ainda de acordo com esta convenção, a passagem de retorno que não é possível colocar num registo é feita por referência passada como o primeiro argumento (i.e., o chamador reserva o espaço necessário e envia a localização à função invocada).

Corpo

O valor devolvido por uma função, definido através do \textsl{left-value} especial que tem o mesmo nome da função, deve ser sempre do tipo declarado.

Se existir um valor declarado por omissão para o retorno da função (indicado pela notação -> seguindo a assinatura da função), então deve ser utilizado se não for especificado outro. A especificação do valor de retorno por omissão é obrigatoriamente um literal do tipo indicado. É um erro especificar um valor de retorno se o tipo de retorno for void.

Uma instrução return causa a interrupção imediata da função.

Instruções

Excepto quando indicado, as instruções são executadas em sequência, sendo os seus efeitos traduzidos, em geral, pela alteração do valor de variáveis.

Instrução condicional

Se a expressão que segue a palavra chave if for diferente de 0 (zero) então o bloco seguinte é executado. Caso existam cadeias com elif consecutivas, as suas expressões serão sucessivamente testadas, caso todas as condições anteriores sejam nulas (iguais a 0). Assim que uma dessas condições seja diferente de 0 (zero), o corpo correspondente é executado e a instrução termina. Caso exista uma palavra chave else, o seu corpo só é executado se nenhum dos anteriores corpos da instrução o tenha sido.

Instruções de iteração

A instrução for é semelhante à da linguagem C. O comportamento é controlado por três grupos de expressões, podendo o primeiro grupo corresponde a declarações de variáveis locais ao ciclo.

Existe um ciclo while análogo ao da linguagem C. A variante while-else permite que o corpo que segue a palavra reservada else seja executado quando a expressão de controlo do ciclo é inicialmente falsa (i.e., se o ciclo for executado, não é executada parte alternativa, mesmo que a condição fique falsa).

Intrução de retorno

Indicada pela palavra reservada return, a existir deverá ser a última instrução do bloco em que se insere. Ver Corpo.

Instrução de continuação

Iniciada pela palavra reservada next, a existir deverá ser a última instrução do bloco em que se insere. Esta instrução reinicia o ciclo mais interior em que a instrução se encontrar, tal como a instrução continue em C. Quando seguida de um número inteiro n, reinicia o n-ésimo ciclo a contar do actual (n=1). Esta instrução só pode existir dentro de um ciclo.

Instrução de terminação

Iniciada pela palavra reservada stop, a existir deverá ser a última instrução do bloco em que se insere. Esta instrução termina a execução do ciclo interior em que a instrução se encontrar, tal como a instrução break em C. Quando seguida de um número inteiro n, termina o n-ésimo ciclo a contar do actual (n=1). Esta instrução só pode existir dentro de um ciclo.

Expressão como instrução

Qualquer expressão pode ser utilizada como instrução, mesmo que não produza qualquer efeito secundário. Caso a expressão seja terminada por ! o seu valor deve ser impresso. O valor deve ser impresso em decimal para os valores do tipo int e em ASCII e ISO-LATIN-15 para valores do tipo string.

Expressões

Uma expressão é uma representação algébrica de uma quantidade. Assim, todas as expressões devolvem um valor. As expressões em S9 utilizam operadores algébricos comuns: soma, subtracção, multiplicação e divisão inteira e resto da divisão, além de outros operadores.

As expressões são sempre avaliadas da esquerda para a direita, independentemente da associatividade do operador. A precedência dos operadores é a mesma para operadores na mesma secção, sendo as secções seguintes de menor prioridade que as anteriores. O valor resultante da aplicação da expressão bem como a sua associatividade são indicados para cada operador.

A tabela seguinte que resume os operadores em S9, por grupos de precedência decrescente:

S9precs.png

Os operadores que têm o mesmo nome que em C têm também o mesmo significado que em C.

Expressões primárias

Identificadores

Um identificador é uma expressão desde que tenha sido devidamente declarado. Um identificador pode denotar uma variável ou uma constante.

Um identificador é o caso mais simples de um left value, i.e., uma entidade que pode ser utilizada no lado esquerdo (left value) de uma atribuição. O nome de uma função constitui um identificador especial dentro da função actual, designando o valor de retorno.

Expressões literais

Os literais podem ser valores constantes inteiros não negativos, reais, tal como definidos nas convenções lexicais, ou cadeias de caracteres. Ver Literais.

Parênteses curvos

Uma expressão entre parênteses curvos tem o valor da expressão sem os parênteses e permite alterar a prioridade dos operadores. Uma expressão entre parênteses não pode ser utilizada como left value (ver em indexação).

Indexação

Uma expressão indexação devolve o valor contido numa posição indicada por um ponteiro. Consiste de uma expressão ponteiro seguida do índice entre parênteses rectos. Se a expressão ponteiro for um left value, então a expressão indexação poderá também ser um left value.

Exemplos:

  • p[0] (acesso à posição apontada por p);
  • a@p[0] (acesso à posição apontada pelo campo p da estrutura a).

Invocação

Uma função só pode ser invocada através de um identificador que corresponda a uma função previamente declarada ou definida.

Leitura

A operação de leitura de um valor inteiro ou real pode ser efectuado pela expressão @, que devolve o valor lido, de acordo com o tipo esperado (inteiro ou real). Caso se use como argumento do operador de impressão (!), deve ser lido um inteiro.

Acesso a um campo

O acesso a campos de estruturas faz-se através do operador . e pela indicação do nome do campo (tal como em C) ou através da indicação da posição do campo na estrutura (como se de um vector se tratasse), através do operador @, sendo a primeira posição designada pelo valor 0 (zero).

Exemplos (sendo a uma variável com o campo x):

  • a.x (acesso ao campo x de a);
  • a@0 (acesso ao 1º campo de a).

Expressões unárias

Identidade

A expressão identidade (+) devolve o valor do seu argumento inteiro ou real.

Simétrico

A expressão valor simétrico (-) devolve o simétrico do seu argumento inteiro ou real.

Reserva de memória

A expressão reserva de memória (#) devolve o ponteiro que aponta para a zona de memória na pilha da função actual contendo espaço suficiente para o número de objectos indicados pelo seu argumento inteiro. O tipo de retorno é idêntico ao do left-value utilizado e o espaço deve ser calculado em função do tipo apontado.

Expressão de potência

A operação é apenas aplicável a valores inteiros, devolvendo a multiplicação do primeiro argumento por ele próprio tantas vezes quantas o valor do segundo argumento.

Expressões aditivas

As operações são apenas aplicáveis a valores inteiros e reais, devolvendo o resultado da respectiva operação algébrica.

Estas operações podem realizar-se sobre ponteiros, tendo o significado das operações correspondentes em C/C++:

(i) deslocamentos, i.e., um dos operandos deve ser do tipo ponteiro e o outro do tipo inteiro;
(ii) diferenças de ponteiros, i.e., apenas quando se aplica o operador - a dois ponteiros do mesmo tipo (o resultado é o número de objectos do tipo apontado entre eles).

Expressões multiplicativas

As operações são apenas aplicáveis a valores inteiros e reais, devolvendo o resultado da respectiva operação algébrica.

Expressões de grandeza

As operações são aplicáveis a valores inteiros ou reais devolvendo o valor inteiro 0 (zero) caso seja falsa e um valor diferente de zero caso contrário.

Expressões de igualdade

As operações são aplicáveis a valores inteiros e reais, tal como no caso anterior.

Expressões de negação lógica

A operação é aplicável a valores inteiros, devolvendo o valor inteiro 0 (zero) caso o argumento seja diferente de 0 (zero) e um valor diferente de zero caso contrário.

Expressões de junção lógica

A operação é aplicável a valores inteiros, devolvendo um valor diferente de zero caso ambos os argumentos sejam diferente de 0 (zero) e um valor diferente de zero caso contrário. Caso o primeiro argumento seja 0 (zero) o segundo argumento não deve ser avaliado.

Expressões de alternativa lógica

A operação é aplicável a valores inteiros, devolvendo o valor inteiro 0 (zero) caso ambos os argumentos sejam iguais a 0 (zero) e um valor diferente de zero caso contrário. Caso o primeiro argumento seja um valor diferente de zero o segundo argumento não deve ser avaliado.

Expressões de atribuição

O valor da expressão do lado direito do operador é guardado na posição indicada pelo left-value do lado esquerdo do operador de atribuição. Só podem atribuídos valores a variáveis do mesmo tipo.

Expressão "sizeof"

Indica o número de bytes realmente ocupados pela representação em memória do objecto ou do tipo indicados como argumento. Retorna um valor inteiro.

Exemplos:

  • sizeof(1+2) indica a dimensão do objecto resultante da avaliação (um inteiro);
  • sizeof(int) e sizeof(A) são expressões que indicam o tamanho dos tipos indicados (assume-se, neste exemplo, que A é o identificador de um tipo estruturado).

Expressão de indicação de posição

Expressa pelo operador sufixo ?, indica o endereço do objecto (endereçável) indicado como argumento. Retorna um valor do tipo do ponteiro apropriado para o objecto em causa.

Exemplos:

  • a? (indica o endereço de a);
  • a@1? (indica a posição do segundo campo de a).

Exemplos

Os exemplos apresentados não são exaustivos, pelo que não ilustram todos os aspectos da linguagem.