Gramática da Linguagem

A especificação formal LALR(1) do Synesis — comentada regra por regra

Para quem é este documento?

Devs que precisam entender, estender ou depurar a sintaxe do Synesis. A gramática abaixo é a especificação canônica — o compilador a executa literalmente. Se o compilador aceita ou rejeita algo, a resposta está aqui.

Para entender o fluxo completo de compilação (não apenas a gramática), veja Por Dentro do Compilador.

1 Como ler este documento

A gramática usa o formato Lark EBNF, que funciona assim:

Sintaxe Significado
regra: A B C regra é composta por A, depois B, depois C, nessa ordem
A \| B A ou B (alternativa)
A* Zero ou mais repetições de A
A+ Uma ou mais repetições de A
A? A é opcional (zero ou uma vez)
"texto" Literal exato
/regex/ Padrão regex
/regex/i Regex case-insensitive
TOKEN.N Token com prioridade N (maior = preferido em caso de ambiguidade)
_INDENT / _DEDENT Tokens de indentação gerados automaticamente

Regras em minúsculas (project_block) são regras gramaticais — produzem nós na árvore sintática. Regras em MAIÚSCULAS (IDENTIFIER) são tokens — unidades atômicas reconhecidas pelo lexer.

2 Ponto de partida

Toda compilação começa aqui. A regra start aceita uma sequência de blocos separados por linhas em branco:

start: (block | NEWLINE)* block (block | NEWLINE)*

block: project_block
     | source_block
     | item_block
     | ontology_block
     | template_header
     | field_spec_block
     | field_def_block

Um arquivo Synesis é uma lista de blocos. Quais blocos são válidos depende do tipo de arquivo:

  • .synp contém um project_block
  • .syn contém source_block e item_block
  • .syno contém ontology_block
  • .synt contém template_header, field_spec_block e field_def_block

A gramática não impõe essa regra — ela aceita qualquer combinação. É o validador semântico (estágio posterior) que verifica se os blocos fazem sentido no contexto do tipo de arquivo.

Por que (block | NEWLINE)*?

As linhas em branco (NEWLINE) entre blocos precisam ser aceitas explicitamente. Sem isso, o parser exigiria que os blocos fossem colados uns nos outros.

3 Keywords

Todas as palavras-chave são case-insensitivePROJECT, project e Project são equivalentes:

KW_PROJECT.5: /project/i
KW_SOURCE.5: /source/i
KW_ITEM.5: /item/i
KW_ONTOLOGY.5: /ontology/i
KW_TEMPLATE.5: /template/i
KW_FIELD.5: /field/i
KW_END.5: /end/i
KW_INCLUDE.5: /include/i
KW_BIBLIOGRAPHY.5: /bibliography/i
KW_ANNOTATIONS.5: /annotations/i
KW_FIELDS.5: /fields/i
KW_REQUIRED.5: /required/i
KW_OPTIONAL.5: /optional/i
KW_FORBIDDEN.5: /forbidden/i
KW_BUNDLE.5: /bundle/i
KW_TYPE.5: /type/i
KW_SCOPE.5: /scope/i
KW_FORMAT.5: /format/i
KW_DESCRIPTION.5: /description/i
KW_ARITY.5: /arity/i
KW_VALUES.5: /values/i
KW_RELATIONS.5: /relations/i
KW_METADATA.5: /metadata/i
KW_QUOTATION.5: /quotation/i
KW_MEMO.5: /memo/i
KW_CODE.5: /code/i
KW_CHAIN.5: /chain/i
KW_TEXT.5: /text/i
KW_DATE.5: /date/i
KW_SCALE.5: /scale/i
KW_ENUMERATED.5: /enumerated/i
KW_ORDERED.5: /ordered/i
KW_TOPIC.5: /topic/i
KW_GUIDELINES.5: /guidelines/i

A prioridade .5 garante que keywords tenham preferência sobre IDENTIFIER e FIELD_NAME (prioridade .1) quando há ambiguidade. Isso é crítico — sem essa prioridade, SOURCE seria reconhecido como um identificador genérico em vez de uma keyword.

4 Bloco PROJECT

O bloco PROJECT é o ponto de entrada de todo projeto Synesis:

project_block: KW_PROJECT IDENTIFIER NEWLINE project_body KW_END KW_PROJECT

project_body: _INDENT project_items _DEDENT
            | project_items

project_items: (include_stmt | metadata | description | NEWLINE)+

Aceita um nome (IDENTIFIER), seguido de declarações internas: includes, metadata e description.

4.1 Declarações INCLUDE e TEMPLATE

include_stmt: KW_TEMPLATE STRING NEWLINE
            | KW_INCLUDE include_type STRING NEWLINE
include_type: KW_BIBLIOGRAPHY | KW_ANNOTATIONS | KW_ONTOLOGY

O TEMPLATE é declarado diretamente; os demais arquivos usam INCLUDE tipo "caminho".

Exemplo de uso:

PROJECT demo
    TEMPLATE "template.synt"
    INCLUDE BIBLIOGRAPHY "references.bib"
    INCLUDE ANNOTATIONS "annotations.syn"
    INCLUDE ONTOLOGY "ontology.syno"
END PROJECT

4.2 Metadata e Description

metadata: KW_METADATA NEWLINE _INDENT (metadata_line | NEWLINE)+ _DEDENT
          KW_END KW_METADATA NEWLINE
metadata_line: TEXT_LINE NEWLINE

description: KW_DESCRIPTION NEWLINE description_lines
             KW_END KW_DESCRIPTION NEWLINE?
description_lines: (TEXT_LINE NEWLINE | NEWLINE)+

Metadata e description são blocos de texto livre, delimitados por END METADATA e END DESCRIPTION.

5 Blocos de anotação

Os três blocos de anotação compartilham a mesma estrutura interna — apenas a keyword de abertura/fechamento muda:

source_block: KW_SOURCE BIBREF NEWLINE
              (_INDENT (field_entry | NEWLINE)* _DEDENT)?
              KW_END KW_SOURCE

item_block: KW_ITEM BIBREF NEWLINE
            (_INDENT (field_entry | NEWLINE)* _DEDENT)?
            KW_END KW_ITEM

ontology_block: KW_ONTOLOGY concept_name NEWLINE
                (_INDENT (field_entry | NEWLINE)* _DEDENT)?
                KW_END KW_ONTOLOGY

concept_name: CONCEPT_NAME

Aspectos importantes:

  • SOURCE e ITEM recebem um BIBREF (ex: @smith2024).
  • ONTOLOGY recebe um concept_name — que é mais flexível (pode conter espaços; veja a seção de Tokens).
  • O corpo (field_entry | NEWLINE)* é opcional — blocos vazios são sintaticamente válidos (mas o validador semântico pode rejeitá-los, como E023: EmptyItemBlock).
  • A indentação (_INDENT/_DEDENT) delimita o corpo do bloco.

6 Campos e valores

Os campos (field_entry) aparecem dentro dos blocos de anotação. São o coração dos dados:

field_entry: field_key ":" value NEWLINE continuation_block?
           | field_key ":" NEWLINE _INDENT text_block _DEDENT NEWLINE?

Duas formas: valor na mesma linha (com continuação opcional), ou valor multilinha indentado.

6.1 Chaves de campo

field_key: FIELD_NAME
         | KW_TOPIC | KW_DESCRIPTION | KW_CODE | KW_CHAIN
         | KW_TEXT | KW_MEMO | KW_QUOTATION | KW_DATE
         | KW_SCALE | KW_ENUMERATED | KW_ORDERED

O field_key aceita tanto nomes definidos pelo usuário (FIELD_NAME) quanto keywords que podem ser usadas como nomes de campo. Isso permite campos como code:, chain: e description: — que são simultaneamente keywords e nomes de campo.

6.2 Tipos de valor

value: STRING
     | NUMBER TEXT_LINE
     | NUMBER
     | chain_expr
     | code_list
     | TEXT_LINE

O parser tenta cada alternativa na ordem acima. A prioridade dos tokens (.2, .3, .4) resolve ambiguidades:

  • NUMBER.4 tem prioridade máxima — 42 é reconhecido como número, não como texto
  • CHAIN_ELEMENT.2 e CODE_ELEMENT.2 têm prioridade intermediária
  • TEXT_LINE.3 captura todo o resto

6.3 Listas de código e expressões de cadeia

code_list: CODE_ELEMENT ("," CODE_ELEMENT)*

chain_expr: CHAIN_ELEMENT ("->" CHAIN_ELEMENT)+

Códigos são separados por vírgula; elementos de cadeia são separados por ->.

Exemplos:

code: Social_Cohesion, Collective_Action

chain: Trust -> Cooperation -> Collective_Action

6.4 Texto multilinha

continuation_block: _INDENT text_block _DEDENT
text_block: TEXT_LINE (NEWLINE TEXT_LINE)* NEWLINE?

Quando o valor de um campo não cabe numa linha, o texto continua nas linhas seguintes com indentação:

note: Participant describes spontaneous collective action as a
    primary resilience mechanism, bypassing formal institutions.
    Suggests strong bonding social capital.

O _INDENT e _DEDENT são gerados pelo SynesisIndenter — funciona como a indentação significativa do Python.

7 Bloco TEMPLATE

O template define a estrutura que todos os blocos de anotação devem seguir:

7.1 Cabeçalho

template_header: KW_TEMPLATE IDENTIFIER template_meta*
template_meta: IDENTIFIER ":" value

7.2 Especificação de campos por escopo

field_spec_block: scope_type KW_FIELDS NEWLINE _INDENT field_list _DEDENT
                  KW_END scope_type KW_FIELDS
scope_type: KW_SOURCE | KW_ITEM | KW_ONTOLOGY

field_list: (requirement_clause | NEWLINE)*
requirement_clause: KW_REQUIRED bundle_modifier? field_names
                  | KW_OPTIONAL field_names
                  | KW_FORBIDDEN field_names

bundle_modifier: KW_BUNDLE
field_names: field_key ("," field_key)*

Define quais campos são obrigatórios, opcionais ou proibidos para cada escopo. O modificador BUNDLE indica que os campos devem aparecer sempre juntos.

Exemplo:

ITEM FIELDS
    REQUIRED citation, note, code
END ITEM FIELDS

7.3 Definição de campo individual

field_def_block: KW_FIELD field_key KW_TYPE type_spec
                 (field_props | NEWLINE)*
                 (_INDENT (field_props | NEWLINE)* _DEDENT)?
                 KW_END KW_FIELD

type_spec: simple_type | KW_CHAIN | KW_ENUMERATED | KW_ORDERED | KW_TOPIC
simple_type: KW_QUOTATION | KW_MEMO | KW_CODE | KW_TEXT | KW_DATE | KW_SCALE

Cada campo tem um nome, um tipo, e propriedades opcionais:

field_props: KW_SCOPE scope_type
           | KW_FORMAT format_spec
           | KW_DESCRIPTION TEXT_LINE
           | KW_ARITY COMPARATOR NUMBER
           | KW_VALUES value_list KW_END KW_VALUES
           | KW_RELATIONS relation_list KW_END KW_RELATIONS
           | guidelines_block

Cada tipo de campo usa propriedades diferentes:

Tipo Propriedades típicas
QUOTATION, MEMO, TEXT SCOPE, DESCRIPTION
CODE SCOPE, DESCRIPTION
CHAIN SCOPE, ARITY, RELATIONS, DESCRIPTION
SCALE SCOPE, FORMAT ([min..max]), DESCRIPTION
ORDERED SCOPE, VALUES (com [N] prefixo), DESCRIPTION
ENUMERATED SCOPE, VALUES, DESCRIPTION
TOPIC SCOPE, DESCRIPTION
DATE SCOPE, DESCRIPTION

7.4 VALUES e RELATIONS

value_list: (value_entry | NEWLINE)+
          | NEWLINE _INDENT (value_entry | NEWLINE)+ _DEDENT
          | _INDENT (value_entry | NEWLINE)+ _DEDENT
value_entry: index_prefix? IDENTIFIER ":" TEXT_LINE
index_prefix: "[" NUMBER "]"

relation_list: (relation_entry | NEWLINE)+
             | NEWLINE _INDENT (relation_entry | NEWLINE)+ _DEDENT
             | _INDENT (relation_entry | NEWLINE)+ _DEDENT
relation_entry: IDENTIFIER ":" TEXT_LINE

VALUES aceita um prefixo de índice opcional ([1], [2], etc.) para campos ORDERED.

7.5 FORMAT e ARITY

format_spec: IDENTIFIER | scale_format
scale_format: "[" NUMBER ".." NUMBER "]"

FORMAT define o intervalo para campos SCALE: [0..10], [1..5], etc.

KW_ARITY COMPARATOR NUMBER

ARITY define quantos elementos um CHAIN deve ter: >=2, =3, <5, etc.

7.6 Bloco GUIDELINES

guidelines_block: KW_GUIDELINES NEWLINE guidelines_lines
                  KW_END KW_GUIDELINES
guidelines_lines: (_INDENT | _DEDENT | guideline_line | NEWLINE)*
guideline_line: guideline_token+ NEWLINE

O GUIDELINES é um bloco de texto livre dentro de uma definição de FIELD. Aceita praticamente qualquer token — incluindo keywords, identificadores, strings, números e operadores — porque seu conteúdo é documentação em prosa, não código estruturado.

guideline_token: TEXT_LINE | IDENTIFIER | FIELD_NAME | BIBREF | STRING
               | NUMBER | COMPARATOR
               | KW_PROJECT | KW_SOURCE | KW_ITEM | KW_ONTOLOGY
               | KW_TEMPLATE | KW_FIELD | KW_INCLUDE | KW_BIBLIOGRAPHY
               | KW_ANNOTATIONS | KW_FIELDS | KW_REQUIRED | KW_OPTIONAL
               | KW_FORBIDDEN | KW_BUNDLE | KW_TYPE | KW_SCOPE | KW_FORMAT
               | KW_DESCRIPTION | KW_ARITY | KW_VALUES | KW_RELATIONS
               | KW_METADATA | KW_QUOTATION | KW_MEMO | KW_CODE | KW_CHAIN
               | KW_TEXT | KW_DATE | KW_SCALE | KW_ENUMERATED | KW_ORDERED
               | KW_TOPIC | KW_GUIDELINES
               | COLON | "," | "->"

COLON: ":"

8 Tokens

Os tokens são as unidades atômicas reconhecidas pelo lexer, antes de qualquer regra gramatical ser aplicada.

8.1 Identificadores e referências

BIBREF: "@" /[a-zA-Z][a-zA-Z0-9_-]*/

IDENTIFIER.1: /[\p{L}_][\p{L}\p{N}_\-]*/u

FIELD_NAME.1: /(?![eE][nN][dD]\b)[\p{L}_][\p{L}\p{N}_\-]*/u

CONCEPT_NAME.0: /[^\s#\n\r][^#\n\r]*/u
Token Aceita Exemplo
BIBREF @ + letra + alfanumérico/underline/hífen @smith2024, @WHO-2023
IDENTIFIER Letra/underline + alfanumérico/underline/hífen demo, Social_Cohesion
FIELD_NAME Como IDENTIFIER, mas exclui END code, citation (nunca end)
CONCEPT_NAME Qualquer texto até # ou fim de linha Social Cohesion, Trust-Based Cooperation
FIELD_NAME vs END

O FIELD_NAME usa um lookahead negativo (?![eE][nN][dD]\b) para evitar que a keyword END seja capturada como nome de campo. Sem isso, END ITEM seria parseado como um campo chamado END com valor ITEM.

CONCEPT_NAME e espaços

CONCEPT_NAME tem prioridade .0 (a mais baixa) e aceita espaços. Isso permite conceitos como Social Cohesion na ontologia. Os outros tokens (IDENTIFIER, FIELD_NAME) não aceitam espaços, garantindo que a gramática não seja ambígua nos demais contextos.

8.2 Elementos de código e cadeia

CHAIN_ELEMENT.2: /[\p{L}_](?:(?!->)[\p{L}\p{N}_\- ])*/u

CODE_ELEMENT.2: /[\p{L}_](?:(?!,)[\p{L}\p{N}_\- ])*/u

Ambos aceitam espaços internos, mas param antes do seu delimitador:

  • CHAIN_ELEMENT para antes de -> (operador de cadeia)
  • CODE_ELEMENT para antes de , (separador de lista)

Isso permite códigos com espaços como Social Cohesion em listas: Social Cohesion, Collective Action.

8.3 Valores e texto

STRING: /"[^"]*"/
NUMBER.4: /[0-9]+(\.[0-9]+)?/
COMPARATOR: ">=" | "<=" | "=" | ">" | "<"

TEXT_LINE.3: /[^\s\n\r:][^\n\r]*/
Token Aceita Prioridade
STRING Texto entre aspas duplas
NUMBER Inteiro ou decimal .4 (alta)
COMPARATOR Operadores de comparação
TEXT_LINE Qualquer texto que não começa com espaço/: .3

TEXT_LINE é o token “coringa” — captura tudo que não foi reconhecido por tokens mais específicos. A restrição [^\s\n\r:] no primeiro caractere evita conflito com o : dos campos e com indentação.

8.4 Tokens especiais

COMMENT: "#" /[^\n]*/

NEWLINE: /(\r?\n[ \t]*)+/
%declare _INDENT _DEDENT

%ignore /[ \t]+/
%ignore COMMENT
  • Comentários (#) são ignorados pelo parser — podem aparecer em qualquer lugar.
  • NEWLINE captura quebras de linha junto com espaços/tabs iniciais da linha seguinte — isso alimenta o SynesisIndenter que gera os tokens _INDENT/_DEDENT.
  • Espaços e tabs são ignorados (fora do contexto de NEWLINE), tornando a indentação de conteúdo flexível.

9 Prioridades — como ambiguidades são resolvidas

A tabela abaixo resume as prioridades e por que cada uma é necessária:

Token Prioridade Por que
Keywords (KW_*) .5 Sempre prevalecem sobre identificadores
NUMBER .4 42 é número, não texto
TEXT_LINE .3 Captura texto genérico, mas cede a tokens mais específicos
CHAIN_ELEMENT, CODE_ELEMENT .2 Prevalecem sobre IDENTIFIER em contexto de valor
IDENTIFIER, FIELD_NAME .1 Prioridade base para nomes
CONCEPT_NAME .0 Menor prioridade — só usado quando nada mais encaixa

Se um trecho de texto casa com múltiplos tokens, o de maior prioridade vence. Isso é o que permite à gramática LALR(1) funcionar sem ambiguidade.

10 Indentação significativa

O Synesis usa indentação significativa (como Python). O SynesisIndenter — um componente do lexer — processa as linhas e gera tokens _INDENT e _DEDENT:

  1. Tabs são convertidos em 4 espaços
  2. Se uma linha tem mais indentação que a anterior → gera _INDENT
  3. Se tem menos → gera _DEDENT
  4. Se tem a mesma → nada

Os tokens _INDENT/_DEDENT são declarados com %declare e geridos pelo Lark, não pela gramática em si. Eles aparecem nas regras como delimitadores de bloco:

project_body: _INDENT project_items _DEDENT
source_block: KW_SOURCE BIBREF NEWLINE (_INDENT ... _DEDENT)? KW_END KW_SOURCE
Indentação inconsistente

Se um arquivo mistura tabs e espaços de forma inconsistente, o SynesisIndenter pode gerar tokens _INDENT/_DEDENT inesperados, resultando em erros de sintaxe difíceis de diagnosticar. A normalização de tabs para 4 espaços ameniza, mas não elimina completamente esse risco.