Compare commits

...

12 Commits

Author SHA1 Message Date
d58a96a4c4 Merge pull request 'dev/pbs-symbol-documentation-and-hover-markdown' (#13) from dev/pbs-symbol-documentation-and-hover-markdown into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 61.37% (17351/28272) * Branch Coverage: 52.31% (6722/12850) * Lines of Code: 28272 * Cyclomatic Complexity: 11325 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 601
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #13
2026-07-15 06:36:29 +00:00
725c28b3f5
implements PLN-0102
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 61.37% (17351/28272) * Branch Coverage: 52.31% (6722/12850) * Lines of Code: 28272 * Cyclomatic Complexity: 11325 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 601
Intrepid/Prometeu/Studio/pipeline/pr-master This commit looks good
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
2026-07-15 07:22:58 +01:00
d8db99eb81
implements PLN-0101 2026-07-15 07:20:42 +01:00
63215ac02f
implements PLN-0100 2026-07-15 07:18:19 +01:00
e7647165bc
implements PLN-0099 2026-07-15 07:12:03 +01:00
f32a25eb59
implements PLN-0098 2026-07-15 07:10:15 +01:00
d680504c31
implements PLN-0097 2026-07-15 07:08:11 +01:00
aeea8ca900
implements PLN-0096 2026-07-15 07:05:45 +01:00
05dd37a30a
implements PLN-0095 2026-07-15 07:00:43 +01:00
f700e400d5
implements PLN-0094 2026-07-15 06:59:16 +01:00
2b25364252
implements PLN-0093 2026-07-15 06:57:19 +01:00
917b0e1b9a
implements PLN-0092 2026-07-15 06:55:26 +01:00
46 changed files with 2381 additions and 97 deletions

View File

@ -1,7 +1,7 @@
{"type":"meta","next_id":{"DSC":39,"AGD":42,"DEC":39,"PLN":92,"LSN":55,"CLSN":1}} {"type":"meta","next_id":{"DSC":39,"AGD":42,"DEC":40,"PLN":103,"LSN":55,"CLSN":1}}
{"type":"discussion","id":"DSC-0038","status":"done","ticket":"studio-packer-rgba8888-asset-pipeline","title":"Studio and Packer RGBA8888 Asset Pipeline Alignment","created_at":"2026-05-23","updated_at":"2026-07-14","tags":["studio","packer","assets","glyph-bank","palette","rgba8888","runtime-alignment"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0038-studio-packer-rgba8888-asset-pipeline/LSN-0053-rgba8888-is-the-canonical-studio-packer-palette-contract.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14"}]} {"type":"discussion","id":"DSC-0038","status":"done","ticket":"studio-packer-rgba8888-asset-pipeline","title":"Studio and Packer RGBA8888 Asset Pipeline Alignment","created_at":"2026-05-23","updated_at":"2026-07-14","tags":["studio","packer","assets","glyph-bank","palette","rgba8888","runtime-alignment"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0038-studio-packer-rgba8888-asset-pipeline/LSN-0053-rgba8888-is-the-canonical-studio-packer-palette-contract.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14"}]}
{"type":"discussion","id":"DSC-0037","status":"done","ticket":"pbs-autocomplete-parameter-names","title":"PBS autocomplete parameter names for stdlib and method calls","created_at":"2026-05-08","updated_at":"2026-05-14","tags":["compiler-pbs","studio","lsp","autocomplete","signature-help","stdlib"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0037-pbs-autocomplete-parameter-names/LSN-0052-canonical-callable-parameter-names-through-pbs-editor-assistance.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]} {"type":"discussion","id":"DSC-0037","status":"done","ticket":"pbs-autocomplete-parameter-names","title":"PBS autocomplete parameter names for stdlib and method calls","created_at":"2026-05-08","updated_at":"2026-05-14","tags":["compiler-pbs","studio","lsp","autocomplete","signature-help","stdlib"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0037-pbs-autocomplete-parameter-names/LSN-0052-canonical-callable-parameter-names-through-pbs-editor-assistance.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]}
{"type":"discussion","id":"DSC-0036","status":"open","ticket":"pbs-symbol-documentation-and-hover-markdown","title":"Modelo de documentacao de simbolos em PBS e consumo markdown no hover","created_at":"2026-05-08","updated_at":"2026-05-08","tags":["compiler","compiler-pbs","studio","lsp","vscode","editor","hover","documentation","markdown"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-pbs-symbol-documentation-and-hover-markdown.md","status":"open","created_at":"2026-05-08","updated_at":"2026-05-08"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"pbs-symbol-documentation-and-hover-markdown","title":"Modelo de documentacao de simbolos em PBS e consumo markdown no hover","created_at":"2026-05-08","updated_at":"2026-07-15","tags":["compiler","compiler-pbs","studio","lsp","vscode","editor","hover","documentation","markdown"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-pbs-symbol-documentation-and-hover-markdown.md","status":"accepted","created_at":"2026-05-08","updated_at":"2026-07-15"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-pbs-symbol-documentation-with-doc-markdown-text-blocks.md","status":"accepted","created_at":"2026-07-15","updated_at":"2026-07-15","ref_agenda":"AGD-0039"}],"plans":[{"id":"PLN-0092","file":"PLN-0092-specify-pbs-doc-attribute-and-markdown-text-block-syntax.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0093","file":"PLN-0093-implement-pbs-lexer-support-for-documentation-text-blocks.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0094","file":"PLN-0094-extend-pbs-attribute-parser-and-ast-for-doc-markdown-payloads.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0095","file":"PLN-0095-implement-documentation-text-block-normalization.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0096","file":"PLN-0096-validate-doc-attribute-semantics-and-diagnostics.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0097","file":"PLN-0097-attach-doc-metadata-to-pbs-semantic-symbols.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0098","file":"PLN-0098-expose-doc-metadata-through-pbs-editorial-and-lsp-surfaces.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0099","file":"PLN-0099-render-doc-markdown-in-hover-composition.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0100","file":"PLN-0100-author-doc-metadata-for-stdlib-sdk-and-interface-declarations.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0101","file":"PLN-0101-protect-runtime-artifacts-from-doc-metadata-lowering.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]},{"id":"PLN-0102","file":"PLN-0102-add-end-to-end-doc-documentation-conformance-coverage.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0035","status":"done","ticket":"pbs-lsp-editor-assistance-wave-1","title":"Wave 1 de assistencia editorial via LSP para PBS no VS Code","created_at":"2026-05-08","updated_at":"2026-05-08","tags":["studio","lsp","vscode","compiler","compiler-pbs","editor","completion","hover","signature-help"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0051","file":"discussion/lessons/DSC-0035-pbs-lsp-editor-assistance-wave-1/LSN-0051-compiler-backed-editor-assistance-for-pbs.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]} {"type":"discussion","id":"DSC-0035","status":"done","ticket":"pbs-lsp-editor-assistance-wave-1","title":"Wave 1 de assistencia editorial via LSP para PBS no VS Code","created_at":"2026-05-08","updated_at":"2026-05-08","tags":["studio","lsp","vscode","compiler","compiler-pbs","editor","completion","hover","signature-help"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0051","file":"discussion/lessons/DSC-0035-pbs-lsp-editor-assistance-wave-1/LSN-0051-compiler-backed-editor-assistance-for-pbs.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]}
{"type":"discussion","id":"DSC-0034","status":"done","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-07","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]} {"type":"discussion","id":"DSC-0034","status":"done","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-07","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]}
{"type":"discussion","id":"DSC-0033","status":"done","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-08","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0050","file":"discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]} {"type":"discussion","id":"DSC-0033","status":"done","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-08","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0050","file":"discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]}

View File

@ -2,7 +2,7 @@
id: AGD-0039 id: AGD-0039
ticket: pbs-symbol-documentation-and-hover-markdown ticket: pbs-symbol-documentation-and-hover-markdown
title: Modelo de documentacao de simbolos em PBS e consumo markdown no hover title: Modelo de documentacao de simbolos em PBS e consumo markdown no hover
status: open status: accepted
created: 2026-05-08 created: 2026-05-08
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown] tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
--- ---
@ -20,9 +20,9 @@ Sem esse modelo, o hover pode até mostrar:
Mas continua sem uma superficie limpa para explicar intencao de uso, contrato semantico, detalhes de parametros ou observacoes de API. Mas continua sem uma superficie limpa para explicar intencao de uso, contrato semantico, detalhes de parametros ou observacoes de API.
Hoje a ideia proposta eh algo como: Hoje a direcao consolidada eh algo como:
- `Document(qualquer markdown aqui dentro...)` - `[Doc(markdown = """qualquer markdown aqui dentro...""")]`
como anotacao consumida diretamente pelo hover. como anotacao consumida diretamente pelo hover.
@ -49,19 +49,19 @@ O objetivo eh evitar que a primeira wave de assistencia editorial fique bloquead
## Open Questions ## Open Questions
- A autoria da documentacao deve acontecer por anotacao explicita como `Document(...)`, por comentario estruturado, ou por outro mecanismo? - A autoria da documentacao deve acontecer por anotacao explicita como `[Doc(...)]`, por comentario estruturado, ou por outro mecanismo?
- O payload authored deve aceitar markdown arbitrario como fonte canonica, ou deve haver alguma normalizacao/restricao? - O payload authored deve aceitar um subset Markdown como fonte canonica, ou deve haver outra normalizacao/restricao?
- Quais simbolos podem carregar documentacao na wave inicial: - Quais declaracoes nomeadas de API podem carregar documentacao na wave inicial:
funcoes, methods, services, hosts, builtin types, structs, enums, fields, constructors? funcoes, methods, classes/structs, services, hosts, builtin/stdlib declarations e interface declarations?
- A documentacao deve viver no AST/semantics como superficie compiler-backed explicita? - A documentacao deve viver no AST/semantics como superficie compiler-backed explicita?
- Como a documentacao authored em PBS se relaciona com documentacao de stdlib gerada ou importada de outras fontes? - Como a documentacao authored em PBS se relaciona com documentacao de stdlib gerada ou importada de outras fontes?
- O hover deve consumir markdown pronto sem transformacao, ou deve haver um pipeline intermediario de sanitizacao/normalizacao? - O hover deve consumir markdown pronto sem transformacao, ou deve haver um pipeline intermediario de sanitizacao/normalizacao?
- Como tratar multilinhas, escaping e ergonomia de escrita se a surface for `Document(...)`? - Como tratar multilinhas, escaping e ergonomia de escrita se a surface for `[Doc(...)]`?
- A agenda deve definir apenas o modelo de autoria, ou tambem o transporte LSP e a composicao final do hover? - A agenda deve definir apenas o modelo de autoria, ou tambem o transporte LSP e a composicao final do hover?
## Options ## Options
### Option A - Anotacao `Document(...)` com markdown como payload canonico ### Option A - Anotacao `[Doc(...)]` com Markdown subset como payload canonico
- **Approach:** introduzir uma anotacao explicita na linguagem PBS cujo conteudo ja eh markdown pronto para o hover. - **Approach:** introduzir uma anotacao explicita na linguagem PBS cujo conteudo ja eh markdown pronto para o hover.
- **Pro:** autoria direta, intencao clara, pipeline simples de consumo no LSP e no hover. - **Pro:** autoria direta, intencao clara, pipeline simples de consumo no LSP e no hover.
@ -94,14 +94,25 @@ Se a equipe realmente quer markdown authored pronto para consumo no hover, Optio
## Recommendation ## Recommendation
Discutir esta agenda com viés inicial para **Option A**, mas sem fechar ainda a sintaxe final. Discutir esta agenda com viés inicial para **Option A** e fechar a decision com um shape canonico unico:
```pbs
[Doc(markdown = """
Draws a sprite.
- `x`: screen x
- `y`: screen y
""")]
```
O consenso da discussao eh que `Doc` eh curto e pratico, e que o nome do argumento `markdown` ja carrega a semantica do payload. Um prefixo lexical como `md"""..."""` nao deve entrar na v1 porque adiciona uma nova familia de literais antes de haver necessidade concreta para multiplos payloads especializados.
A recomendacao preliminar eh separar o problema em duas perguntas: A recomendacao preliminar eh separar o problema em duas perguntas:
1. qual eh a surface canonical de autoria da documentacao em PBS? 1. qual eh a surface canonical de autoria da documentacao em PBS?
2. como essa documentacao vira um payload editorial resolvido para hover e futuros consumers? 2. como essa documentacao vira um payload editorial resolvido para hover e futuros consumers?
Mesmo que `Document(...)` seja a direcao vencedora, a decisao deve deixar claro: Mesmo que `[Doc(...)]` seja a direcao vencedora, a decision deve deixar claro:
- onde ela pode ser usada; - onde ela pode ser usada;
- qual formato textual aceita; - qual formato textual aceita;
@ -109,6 +120,45 @@ Mesmo que `Document(...)` seja a direcao vencedora, a decisao deve deixar claro:
- como chega ao LSP; - como chega ao LSP;
- e como o hover deve compor isso com assinatura e metadados do simbolo. - e como o hover deve compor isso com assinatura e metadados do simbolo.
## Proposed Resolution
Fechar a agenda com a seguinte direcao:
- PBS deve suportar documentacao explicita de simbolos por atributo `[Doc(markdown = """...""")]`.
- `Doc` deve ter um unico shape canonico na v1: argumento nomeado `markdown` com payload em triple-quoted text block.
- O atributo segue a regra normal de atributos PBS: associa-se a declaracao imediatamente seguinte.
- `[Doc]` deve ser valido para declaracoes nomeadas de API que introduzem simbolo semantico nomeado e que possam carregar atributos, incluindo funcoes, methods, classes/structs, services, hosts, builtin/stdlib declarations e interface declarations.
- Parametros nao devem receber `[Doc]` na v1 em nenhuma situacao; a documentacao de parametros deve viver no Markdown da declaracao que os possui.
- A intencao normativa eh nao limitar artificialmente declaracoes documentaveis por origem; se algum ponto da gramatica de declaracao ainda nao aceita atributos, o plano deve tratar isso como trabalho sintatico explicito.
- A ordem entre `[Doc]` e outros atributos deve ser irrelevante.
- Mais de um `[Doc]` no mesmo simbolo deve ser erro semantico, nao erro de parse.
- `[Doc]` sem `markdown`, com argumentos extras, com payload vazio/whitespace-only, ou aplicado fora da superficie documentavel deve produzir diagnostico claro.
- O payload `"""..."""` deve ser tratado inicialmente como text block documental, nao como string runtime geral de PBS.
- O text block deve aplicar normalizacao de indentacao equivalente ao modelo de text blocks de Java/Javadoc: remover linhas externas vazias incidentais, calcular indentacao comum das linhas nao vazias, remover essa indentacao comum, preservar quebras internas e preservar o conteudo Markdown.
- O compiler deve preservar o Markdown normalizado como metadata semantica do simbolo.
- O LSP e demais consumers devem consumir documentacao resolvida pelo compiler; nao devem reparsear source para descobrir docs.
- Um subset Markdown deve ser o protocolo de documentacao authored em PBS.
- O Markdown deve ser definido como payload editorial preservado, nao como garantia de renderizacao identica em todos os clientes. Renderizacao, sanitizacao e suporte a constructs especificos sao responsabilidades do consumer.
- Formatters devem preservar o conteudo interno do text block e nao devem reflowar Markdown authored.
- Rename/refactor nao precisa atualizar referencias textuais dentro do Markdown nesta fase do projeto.
- Specs e documentacao da stdlib devem ser escritas em ingles.
- Testes devem validar o payload Markdown normalizado no compiler/LSP; validacao visual no VSCode pode ser manual.
- Stdlib, SDK e interface modules devem usar o mesmo modelo `[Doc(markdown = """...""")]` para documentacao authored.
- Se no futuro houver documentacao importada ou gerada, documentacao authored no source deve ter precedencia, e conflitos devem ter regra deterministica ou diagnostico explicito.
## Maintenance Friction Points
Pontos que a decision e o plano devem proteger para evitar que uma feature simples de editor vire divida de linguagem:
- "Todas as declaracoes nomeadas de API documentaveis" precisa ser especificado como "toda declaracao de API que introduz simbolo semantico nomeado", evitando ambiguidade sobre parametros, fields, enum cases, tuple fields, expressoes e variaveis locais.
- A gramatica de atributos pode precisar ser expandida para declaracoes que hoje ainda nao aceitam atributos; isso deve ser tratado como parte real do trabalho, nao como excecao informal.
- A v1 nao deve aceitar aliases como `text`, argumento posicional, `html`, `format`, `Document`, `Documentation`, ou `md"""..."""`.
- `"""..."""` nao deve virar literal runtime geral sem uma decision propria.
- A normalizacao do text block deve ser especificada no texto normativo, nao apenas referenciada informalmente como "igual Java".
- Parametros nao recebem `[Doc]` na v1; fields, enum cases e tuple fields so devem receber `[Doc]` se a decision os classificar explicitamente como declaracoes nomeadas de API documentaveis.
- O formatter futuro eh um ponto sensivel: ele pode alinhar a declaracao, mas nao pode alterar o Markdown authored dentro do text block.
- A composicao visual do hover pode ficar para plano/implementacao, mas a ownership deve ficar clara: compiler resolve documentacao; LSP transporta; editor renderiza.
## Discussion ## Discussion
Esta agenda nao deve ser reduzida a "mostrar markdown no hover". Esta agenda nao deve ser reduzida a "mostrar markdown no hover".
@ -130,8 +180,8 @@ Por isso, mesmo sendo uma agenda separada, ela conversa diretamente com a futura
## Resolution ## Resolution
Ainda em aberto. Consenso de agenda formado em favor de `[Doc(markdown = """...""")]` como anotacao explicita, compiler-owned, com payload em subset Markdown e text block documental normalizado.
## Next Step ## Next Step
Se o enquadramento fizer sentido, o proximo passo eh discutir as open questions e fechar uma `decision` propria para o modelo de documentacao de simbolos em PBS. Aceitar a agenda e fechar uma `decision` propria para o modelo de documentacao de simbolos em PBS.

View File

@ -0,0 +1,137 @@
---
id: DEC-0039
ticket: pbs-symbol-documentation-and-hover-markdown
title: PBS symbol documentation with Doc markdown text blocks
status: accepted
created: 2026-07-15
ref_agenda: AGD-0039
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Context
PBS editor assistance needs hover content that can explain author intent, API contracts, parameter meaning, semantic constraints, and usage notes. Existing symbol surfaces can expose mechanical information such as kind, signature, origin, and type/shape, but PBS does not yet define a canonical authored documentation model.
This decision belongs to domain owner `compiler/pbs`.
Touched subdomains:
- `compiler/general`
- `studio/lsp`
- `tools/vscode-extension`
This decision resolves `AGD-0039`. It does not replace the existing wave-1 editor-assistance work; it defines the source-language and compiler-backed documentation model that hover, completion, signature help, stdlib documentation, and future editor consumers can reuse.
## Decision
PBS MUST support explicit authored documentation for named API declarations through a reserved `[Doc]` attribute.
The only canonical v1 shape is:
```pbs
[Doc(markdown = """
Draws a sprite.
- `x`: screen x
- `y`: screen y
""")]
```
`Doc` MUST be a reserved compiler-recognized attribute name. It MUST NOT be treated as an ordinary user-defined attribute with language-specific reinterpretation.
`Doc` MUST use a single required argument named `markdown`. The `markdown` argument MUST use a triple-quoted documentation text block as its payload. The v1 surface MUST NOT accept aliases or alternate forms such as `text`, positional arguments, `html`, `format`, `Document`, `Documentation`, or `md"""..."""`.
`[Doc]` MUST attach to the declaration immediately following it, following the normal PBS attribute association rule. Attribute order MUST be semantically irrelevant; `[Doc]` before or after other attributes on the same declaration MUST produce the same documentation result.
`[Doc]` MUST be valid only on named API declarations that introduce named semantic symbols and can carry attributes. The v1 documentable surface includes:
- functions;
- methods;
- classes/structs, when present in the PBS declaration surface;
- services;
- hosts;
- builtin and stdlib declarations;
- interface declarations.
Parameters MUST NOT receive `[Doc]` in v1 under any circumstance. Parameter documentation, when needed, MUST be authored inside the Markdown payload of the owning declaration.
Fields, enum cases, tuple fields, local variables, expressions, and other non-API declaration surfaces MUST NOT receive `[Doc]` unless a future decision explicitly classifies them as named API declarations documentable by `[Doc]`.
The `markdown` payload MUST be a PBS documentation text block, not a general runtime string literal. `"""..."""` MUST NOT become a general PBS runtime string literal as part of this decision.
The documentation text block MUST be normalized before semantic validation and before exposure to editor consumers. Normalization MUST:
- remove an incidental leading empty line after the opening delimiter;
- remove an incidental trailing empty line before the closing delimiter;
- compute the common indentation of non-empty content lines;
- remove that common indentation;
- preserve internal line breaks;
- preserve Markdown content after indentation normalization.
After normalization, an empty or whitespace-only documentation payload MUST be rejected.
The authored documentation format for PBS v1 MUST be a supported Markdown subset. PBS defines Markdown as the documentation protocol, not as a guarantee that every client renders every Markdown construct identically.
The compiler MUST preserve normalized Markdown documentation as semantic metadata of the documented symbol. LSP and editor consumers MUST consume compiler-resolved documentation metadata; they MUST NOT reparse PBS source to discover documentation.
`[Doc]` MUST be compile/editor metadata. It MUST NOT be lowered into runtime or cartridge artifacts unless a future runtime or packaging decision explicitly requires that.
Stdlib, SDK, and interface modules MUST use the same `[Doc(markdown = """...""")]` model for authored documentation. If imported or generated documentation exists in the future, authored source documentation MUST take precedence. Conflicts MUST be deterministic or produce explicit diagnostics.
## Rationale
An explicit attribute keeps documentation attached to language-owned semantic symbols instead of relying on proximity between comments and declarations. This avoids a separate comment-binding model and gives the compiler one canonical source of truth for editor assistance.
The short attribute name `Doc` is practical at the authoring site and remains clear when paired with the required `markdown` argument. The argument name carries the payload format, so a prefixed literal such as `md"""..."""` would add lexer and grammar surface without enough v1 benefit.
Restricting the v1 shape to `[Doc(markdown = """...""")]` protects maintainability. It avoids aliases, ambiguous forms, and compatibility pressure around multiple documentation payload formats.
Restricting triple-quoted text blocks to documentation avoids accidentally introducing general multiline runtime strings. Runtime strings raise separate questions about type behavior, lowering, constant pools, equality, escapes, and artifact representation; those questions do not belong to this documentation decision.
Compiler ownership keeps hover, completion, signature help, stdlib docs, and future editor consumers aligned. The LSP layer should transport resolved documentation, not infer language semantics from raw source text.
## Implications
- PBS syntax and parsing MUST recognize triple-quoted documentation text blocks in `[Doc(markdown = ...)]`.
- PBS semantic validation MUST enforce the canonical `Doc` shape, allowed declaration sites, duplicate detection, empty-payload rejection, and invalid-argument diagnostics.
- A declaration MUST NOT have more than one `[Doc]`; duplicates MUST be semantic errors rather than parse errors.
- Formatter implementations MUST preserve the internal Markdown text of documentation blocks and MUST NOT reflow authored Markdown.
- Rename/refactor tools are not required in this phase to update textual references inside Markdown documentation.
- Specs and stdlib documentation authored for repository-owned surfaces MUST be written in English.
- Tests SHOULD validate normalized Markdown payloads at compiler and LSP boundaries. VSCode visual validation may remain manual for this wave.
- Hover composition may be implemented later, but the ownership boundary is fixed: compiler resolves documentation, LSP transports it, and the editor renders it.
## Propagation Targets
- specs:
- `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md`
- `docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md`
- `docs/specs/compiler-languages/pbs/11. AST Specification.md`
- `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md`
- `docs/specs/compiler-languages/pbs/5. Manifest, Stdlib, and SDK Resolution Specification.md`
- LSP/editor-facing specs if/when a dedicated Studio/LSP spec surface exists.
- plans:
- Create an implementation plan for syntax, AST, semantics, documentation metadata, LSP exposure, tests, and stdlib authoring.
- code:
- PBS lexer/parser/AST.
- PBS semantic validation and symbol/editorial surfaces.
- LSP hover/editorial transport.
- Stdlib/interface PBS sources.
- tests:
- Lexer/parser coverage for documentation text blocks.
- Semantic diagnostics for invalid `Doc` usage.
- Normalization tests for indentation and empty payloads.
- Compiler/editorial surface tests proving documentation is attached to resolved symbols.
- LSP hover or symbol-documentation payload tests.
- docs:
- PBS language specs.
- Any lessons generated after implementation and housekeeping.
## References
- Agenda: AGD-0039
- Source discussion: DSC-0036
## Revision Log
- 2026-07-15: Initial decision drafted from accepted `AGD-0039`.

View File

@ -0,0 +1,63 @@
---
id: PLN-0092
ticket: pbs-symbol-documentation-and-hover-markdown
title: Specify PBS Doc attribute and markdown text block syntax
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Update the PBS normative specs so `DEC-0039` is reflected before code changes begin. This plan is the specification gate for the `[Doc(markdown = """...""")]` feature.
## Objective
Define the syntax, static semantics, AST shape, diagnostics, and stdlib documentation policy for compiler-owned PBS symbol documentation.
## Dependencies
- Requires accepted decision `DEC-0039`.
- Must be completed before parser, semantic, LSP, and stdlib implementation plans are accepted as complete.
## Scope
- Add `[Doc(markdown = """...""")]` to the PBS attribute surface.
- Define triple-quoted documentation text blocks as documentation-only payloads, not runtime strings.
- Define documentable named API declarations and explicitly exclude parameters.
- Define normalization, duplicate handling, invalid shape diagnostics, and non-runtime lowering rules.
## Non-Goals
- Implement lexer, parser, semantics, or LSP code.
- Define general multiline runtime strings.
- Define VSCode rendering details beyond the compiler/LSP ownership boundary.
## Execution Method
1. Update `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md` with the canonical `[Doc(markdown = """...""")]` syntax, allowed declaration sites, attribute ordering rule, and text block delimiter rules.
2. Update `docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md` with reserved `Doc` validation, duplicate rejection, parameter exclusion, empty normalized payload rejection, and non-documentable target diagnostics.
3. Update `docs/specs/compiler-languages/pbs/11. AST Specification.md` with an AST representation for documentation attributes and documentation text block payloads.
4. Update `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md` with required diagnostic categories for unterminated text block, invalid `Doc` shape, duplicate `Doc`, empty payload, and invalid target.
5. Update `docs/specs/compiler-languages/pbs/5. Manifest, Stdlib, and SDK Resolution Specification.md` with the rule that stdlib, SDK, and interface modules use the same authored `Doc` surface.
## Acceptance Criteria
- Specs define all normative rules from `DEC-0039` without deferring syntax or semantics to implementation judgment.
- The specs explicitly state that `"""..."""` is documentation-only for this feature.
- The specs explicitly state that parameters never receive `[Doc]` in v1.
- The specs identify propagation boundaries: compiler resolves, LSP transports, editor renders.
## Tests
- Run any existing spec conformance tests that validate PBS spec references.
- No code behavior tests are required in this plan.
## Affected Artifacts
- `docs/specs/compiler-languages/pbs/3. Core Syntax Specification.md`
- `docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md`
- `docs/specs/compiler-languages/pbs/11. AST Specification.md`
- `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md`
- `docs/specs/compiler-languages/pbs/5. Manifest, Stdlib, and SDK Resolution Specification.md`

View File

@ -0,0 +1,62 @@
---
id: PLN-0093
ticket: pbs-symbol-documentation-and-hover-markdown
title: Implement PBS lexer support for documentation text blocks
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Add lexical support for triple-quoted documentation text blocks required by `[Doc(markdown = """...""")]`.
## Objective
Teach the PBS lexer to tokenize documentation text blocks without turning triple quotes into a general runtime string feature.
## Dependencies
- Depends on `PLN-0092` for the normative syntax.
## Scope
- Add a dedicated token kind for documentation text block payloads or an equivalent lexer representation.
- Recognize `"""` open and close delimiters.
- Preserve source text and spans for parser and diagnostics.
- Report unterminated documentation text blocks with useful span information.
## Non-Goals
- Parse or validate `[Doc]` attribute shapes.
- Normalize indentation.
- Lower documentation to runtime artifacts.
- Add general multiline string expressions.
## Execution Method
1. Update `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java` with a documentation text block token if a distinct token is the cleanest local pattern.
2. Update `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java` to recognize `"""..."""` and preserve its raw lexeme and span.
3. Ensure ordinary string scanning remains unchanged for `"..."`.
4. Add or update lexer diagnostics in `LexErrors.java` only if existing unterminated-string diagnostics cannot accurately represent unterminated documentation text blocks.
5. Add focused tests in `PbsLexerTest` for valid text blocks, multiline payloads, embedded single quotes/double quotes, and unterminated blocks.
## Acceptance Criteria
- The lexer can tokenize a multiline documentation text block.
- Unterminated documentation text blocks produce a deterministic diagnostic.
- Ordinary string literals keep current behavior.
- `"""..."""` is not accepted as an expression-level runtime string by this plan.
## Tests
- `./gradlew :prometeu-compiler:frontends:prometeu-frontend-pbs:test` or the repository-equivalent targeted compiler frontend test task.
- Focused `PbsLexerTest` cases for documentation text blocks.
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/LexErrors.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/lexer/PbsLexerTest.java`

View File

@ -0,0 +1,59 @@
---
id: PLN-0094
ticket: pbs-symbol-documentation-and-hover-markdown
title: Extend PBS attribute parser and AST for Doc markdown payloads
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Extend parsing and AST representation so `[Doc(markdown = """...""")]` can be represented structurally before semantic validation.
## Objective
Parse documentation text block attribute values and expose them in AST without accepting alternate `Doc` shapes as valid semantics.
## Dependencies
- Depends on `PLN-0092`.
- Depends on `PLN-0093` for lexer support.
## Scope
- Extend attribute values to carry documentation text block payloads.
- Preserve raw payload and spans needed for normalization and diagnostics.
- Keep parser recovery consistent with existing attribute parsing.
## Non-Goals
- Validate whether `Doc` is allowed on a declaration.
- Normalize the text block.
- Attach documentation to semantic symbols.
## Execution Method
1. Update `PbsAst.AttributeValue` variants in `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java` to include a documentation text block value or equivalent typed payload.
2. Update `PbsAttributeParser` so attribute arguments can parse `markdown = """..."""`.
3. Keep existing string, int, and bool attribute argument parsing unchanged.
4. Ensure parser diagnostics remain syntax-level: malformed attribute structure is parse error; invalid `Doc` shape is left to semantic validation where possible.
5. Add parser tests proving `Doc` attributes parse on top-level and nested declaration forms that already support attributes.
## Acceptance Criteria
- `[Doc(markdown = """...""")]` parses into a distinct AST value or unambiguous attribute value representation.
- Existing attributes such as `[Init]`, `[Frame]`, `[Host(...)]`, and `[InitAllowed]` continue to parse.
- Invalid generic attribute syntax still recovers at the same boundaries as before.
## Tests
- Targeted parser tests under `prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser`.
- Existing PBS parser test suite.
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsAttributeParser.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java`

View File

@ -0,0 +1,59 @@
---
id: PLN-0095
ticket: pbs-symbol-documentation-and-hover-markdown
title: Implement documentation text block normalization
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Implement the normalization algorithm required before semantic validation and editor exposure.
## Objective
Produce normalized Markdown text from a raw documentation text block using the rules locked by `DEC-0039`.
## Dependencies
- Depends on `PLN-0092`.
- Depends on `PLN-0094` for AST access to raw text block payloads.
## Scope
- Strip incidental leading and trailing empty lines.
- Compute and remove common indentation from non-empty lines.
- Preserve internal line breaks and Markdown content after normalization.
- Provide a single reusable normalization implementation for parser/semantics/editorial surfaces.
## Non-Goals
- Render Markdown.
- Sanitize Markdown for a specific editor.
- Reflow authored text.
## Execution Method
1. Add a small normalization helper in the PBS frontend package, colocated with parser or semantic support according to local patterns.
2. Feed the helper raw documentation text block content without delimiters.
3. Normalize line endings deterministically before indentation calculation if existing source handling requires it.
4. Validate empty/whitespace-only content after normalization.
5. Add unit tests for no indentation, common indentation, blank first/last lines, mixed blank internal lines, Markdown lists, code spans, and whitespace-only payloads.
## Acceptance Criteria
- Normalized output matches the spec for all required indentation cases.
- Whitespace-only content is detectable after normalization.
- The normalizer does not interpret Markdown and does not alter non-indentation content.
## Tests
- New focused unit tests for the normalizer.
- Parser/semantic tests that consume normalized output through `[Doc]`.
## Affected Artifacts
- New or existing PBS frontend helper under `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs`
- Corresponding tests under `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs`

View File

@ -0,0 +1,64 @@
---
id: PLN-0096
ticket: pbs-symbol-documentation-and-hover-markdown
title: Validate Doc attribute semantics and diagnostics
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Enforce the semantic contract of `[Doc]` after syntax can represent it.
## Objective
Reject invalid `Doc` usage and produce clear diagnostics for all invalid shapes and targets required by `DEC-0039`.
## Dependencies
- Depends on `PLN-0092`.
- Depends on `PLN-0094`.
- Depends on `PLN-0095`.
## Scope
- Reserve `Doc` as a compiler-recognized attribute.
- Accept only `Doc(markdown = """...""")`.
- Reject aliases, positional arguments, extra arguments, duplicate `Doc`, empty normalized payload, parameter targets, and non-documentable targets.
- Keep duplicates as semantic errors rather than parse errors.
## Non-Goals
- Attach documentation to final symbol metadata.
- Expose documentation through LSP.
- Author stdlib documentation.
## Execution Method
1. Locate existing declaration and reserved attribute validators, including `PbsDeclarationSemanticsValidator`, `PbsDeclarationRuleValidator`, and interface-module reserved attribute checks.
2. Add `Doc` validation in the semantic phase that already validates declaration attributes.
3. Define allowed declaration categories according to the spec: named API declarations that can carry attributes.
4. Add or extend `PbsSemanticsErrors` diagnostics for invalid `Doc` target, invalid shape, duplicate attribute, missing `markdown`, extra arguments, and empty payload.
5. Add tests covering valid declaration targets and every invalid case.
## Acceptance Criteria
- Every invalid shape named by `DEC-0039` produces a semantic diagnostic.
- Parameters never accept `[Doc]`.
- Duplicate `[Doc]` on one declaration is rejected semantically.
- Existing reserved attributes keep their current behavior.
## Tests
- `PbsSemanticsDeclarationsTest`
- `PbsInterfaceModuleSemanticsTest`
- `PbsDiagnosticsContractTest`
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationRuleValidator.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsSemanticsErrors.java`
- PBS semantics tests

View File

@ -0,0 +1,60 @@
---
id: PLN-0097
ticket: pbs-symbol-documentation-and-hover-markdown
title: Attach Doc metadata to PBS semantic symbols
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Once `[Doc]` is syntactically and semantically valid, attach normalized documentation to compiler-owned semantic symbol data.
## Objective
Make documentation available from resolved PBS symbols without requiring LSP or editor code to reparse source.
## Dependencies
- Depends on `PLN-0095`.
- Depends on `PLN-0096`.
## Scope
- Introduce an internal documentation metadata model with format and text.
- Attach normalized Markdown to documented named API symbols.
- Preserve the distinction between documentation metadata and runtime semantics.
## Non-Goals
- Render hover markdown.
- Write stdlib documentation content.
- Lower documentation into runtime artifacts.
## Execution Method
1. Add a compiler-side documentation metadata model, for example `format = markdown` plus normalized `text`.
2. Extend symbol or editorial-resolution structures so documented declarations expose this metadata.
3. Update symbol collection/binding paths for functions, methods, structs/classes, services, hosts, builtin/stdlib declarations, and interface declarations.
4. Ensure imported/stdlib declarations can carry the same metadata once parsed.
5. Add compiler tests proving documentation is attached to the intended declaration and not to parameters or unrelated symbols.
## Acceptance Criteria
- Resolved symbols can expose documentation as structured metadata, not an untyped hover-only string.
- Documentation metadata is absent for undocumented symbols.
- Documentation does not change type checking, name resolution, or runtime behavior.
## Tests
- Focused PBS semantic/editorial support tests.
- Existing name resolution and declaration tests to catch regressions.
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics`
- `PbsEditorialResolvedSymbol`
- `PbsEditorialSupportService`
- Symbol/declaration binding code touched by documented declaration categories

View File

@ -0,0 +1,60 @@
---
id: PLN-0098
ticket: pbs-symbol-documentation-and-hover-markdown
title: Expose Doc metadata through PBS editorial and LSP surfaces
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Transport compiler-resolved documentation through the existing PBS editorial and LSP message boundaries.
## Objective
Expose normalized Markdown documentation to LSP consumers without source reparsing.
## Dependencies
- Depends on `PLN-0097`.
## Scope
- Extend compiler editorial result models to include documentation metadata.
- Extend LSP baseline messages where required.
- Keep documentation as Markdown-oriented structured data at the boundary.
## Non-Goals
- Final hover layout composition.
- VSCode automated rendering tests.
- Runtime artifact changes.
## Execution Method
1. Extend PBS editorial models such as `PbsEditorialResolvedSymbol`, completion candidates, or hover source models with optional documentation metadata.
2. Extend LSP API records under `prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages` only where the baseline protocol needs to carry documentation.
3. Update bridge code under `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler` to map compiler documentation to LSP baseline messages.
4. Preserve backward-compatible behavior for symbols without documentation.
5. Add tests proving the LSP service receives compiler-resolved documentation.
## Acceptance Criteria
- LSP-facing code consumes compiler-resolved documentation metadata.
- No LSP code reparses PBS source to discover `[Doc]`.
- Existing completion, hover, and signature help behavior remains stable for undocumented code.
## Tests
- PBS editorial support tests.
- LSP bridge tests.
- Existing `prometeu-lsp` protocol mapping tests.
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java`
- `prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages`
- `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler`
- Related LSP tests

View File

@ -0,0 +1,60 @@
---
id: PLN-0099
ticket: pbs-symbol-documentation-and-hover-markdown
title: Render Doc markdown in hover composition
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Use transported documentation metadata in hover output while preserving the compiler/LSP/editor ownership boundary.
## Objective
Compose hover Markdown from signature/kind metadata and normalized `[Doc]` documentation.
## Dependencies
- Depends on `PLN-0098`.
## Scope
- Add documentation content to PBS hover composition.
- Keep Markdown rendering responsibility in the editor/client.
- Preserve hover behavior for undocumented symbols.
## Non-Goals
- Automate VSCode visual tests.
- Define a full documentation website generator.
- Change signature help parameter documentation behavior.
## Execution Method
1. Locate the PBS hover composition path in `PbsEditorialSupportService` and LSP bridge/mapping code.
2. Compose hover Markdown in deterministic order: signature/kind surface, documentation Markdown, then origin/type/shape metadata as currently appropriate.
3. Avoid escaping or reflowing normalized Markdown beyond safe composition separators.
4. Ensure undocumented symbols keep existing hover output.
5. Add hover tests with paragraphs, lists, and code spans from `[Doc]`.
## Acceptance Criteria
- Hover includes normalized authored Markdown for documented symbols.
- Hover does not include empty documentation sections.
- Markdown lists do not become accidental code blocks due to indentation.
- VSCode validation remains manual for this wave.
## Tests
- PBS editorial hover tests.
- LSP hover mapping tests that assert `MarkupKind.MARKDOWN` payloads.
## Affected Artifacts
- `PbsEditorialSupportService`
- `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler`
- `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping`
- Hover-related tests

View File

@ -0,0 +1,60 @@
---
id: PLN-0100
ticket: pbs-symbol-documentation-and-hover-markdown
title: Author Doc metadata for stdlib SDK and interface declarations
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Apply the new documentation model to repository-owned PBS stdlib, SDK, and interface declarations.
## Objective
Author English `[Doc(markdown = """...""")]` documentation for stdlib/API surfaces so editor assistance has useful content.
## Dependencies
- Depends on `PLN-0092`.
- Should run after `PLN-0096` so authored docs are validated by compiler tests.
- Benefits from `PLN-0099` for hover verification but does not require it to author source docs.
## Scope
- Add English Markdown documentation to stdlib, SDK, and interface declarations.
- Document parameters inside the owning declaration Markdown.
- Keep documentation concise and API-focused.
## Non-Goals
- Add `[Doc]` to parameters.
- Generate external documentation sites.
- Change stdlib semantics or host binding behavior.
## Execution Method
1. Inventory stdlib and SDK PBS files under `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/game/stdlib`.
2. Add `[Doc(markdown = """...""")]` to public named API declarations that users see in completion/hover.
3. Document parameter meaning inside callable Markdown when needed.
4. Ensure host/interface declarations use the same `Doc` shape.
5. Run stdlib compile and interface conformance tests.
## Acceptance Criteria
- Public stdlib/SDK declarations that are user-facing have concise English documentation.
- No parameters receive `[Doc]`.
- Existing stdlib behavior and host ABI bindings are unchanged.
## Tests
- `PbsGateUStdlibCompileTest`
- `PbsGateUSdkInterfaceConformanceTest`
- Relevant PBS frontend tests.
## Affected Artifacts
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/game/stdlib/1/**`
- Stdlib/interface compile tests

View File

@ -0,0 +1,58 @@
---
id: PLN-0101
ticket: pbs-symbol-documentation-and-hover-markdown
title: Protect runtime artifacts from Doc metadata lowering
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Guard the decision that `[Doc]` is compile/editor metadata and must not be lowered into runtime artifacts by default.
## Objective
Ensure documentation metadata does not affect IR, bytecode, cartridge output, stack behavior, or runtime validation.
## Dependencies
- Depends on `PLN-0097`.
- Should be validated before end-to-end conformance closure.
## Scope
- Confirm lowering ignores `Doc` metadata.
- Add regression tests comparing runtime-facing output with and without docs where practical.
- Protect constant pool and cartridge artifacts from documentation payload bloat.
## Non-Goals
- Define a future packaging format for documentation.
- Remove compiler/editor metadata before LSP consumption.
## Execution Method
1. Inspect PBS lowering paths under `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lowering`.
2. Ensure documented declarations lower identically to undocumented declarations for runtime-facing IR/backend artifacts.
3. Add golden or structural tests comparing compiled output for equivalent documented and undocumented programs.
4. Ensure large documentation payloads do not appear in runtime constant pools unless a future decision explicitly changes that.
## Acceptance Criteria
- Runtime-facing artifacts are unchanged by adding `[Doc]`.
- Compiler/editor metadata remains available before lowering for LSP/editor consumers.
- Golden artifact tests or equivalent regression tests cover this boundary.
## Tests
- `PbsGoldenArtifactsTest`
- Backend/lowering tests relevant to PBS IR output.
- Any artifact comparison test already used by the compiler frontend.
## Affected Artifacts
- PBS lowering package
- Golden artifact test resources if needed
- Compiler backend artifact tests

View File

@ -0,0 +1,73 @@
---
id: PLN-0102
ticket: pbs-symbol-documentation-and-hover-markdown
title: Add end-to-end Doc documentation conformance coverage
status: done
created: 2026-07-15
ref_decisions: [DEC-0039]
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
---
## Briefing
Close the feature with end-to-end tests that prove the whole `[Doc]` contract works across compiler and LSP boundaries.
## Objective
Validate that syntax, normalization, semantics, metadata attachment, LSP exposure, hover composition, stdlib docs, and runtime exclusion all satisfy `DEC-0039`.
## Dependencies
- Depends on `PLN-0092` through `PLN-0101`.
## Scope
- Add integrated compiler tests for valid and invalid `[Doc]`.
- Add LSP tests for hover documentation payloads.
- Add conformance coverage for normalization and runtime exclusion.
- Define a manual VSCode checklist for final visual validation.
## Non-Goals
- Automate VSCode UI rendering.
- Add new documentation syntax beyond `[Doc(markdown = """...""")]`.
- Reopen decision scope.
## Execution Method
1. Create one or more fixture PBS files using `[Doc]` on functions, methods, structs/classes when available, services, hosts, and interface declarations.
2. Add negative fixtures for parameters, duplicate `Doc`, empty payload, missing `markdown`, extra arguments, alias names, and invalid targets.
3. Add LSP tests proving hover transports Markdown and does not reparse source.
4. Add runtime artifact tests proving docs do not lower.
5. Record a short manual VSCode validation checklist covering paragraph rendering, list rendering, code spans, indentation behavior, and undocumented-symbol fallback.
## Acceptance Criteria
- All decision rules have at least one compiler or LSP test.
- Manual VSCode validation scope is documented but not automated.
- The feature can be implemented without returning to the agenda or decision for missing requirements.
## Tests
- Full relevant PBS frontend test suite.
- Relevant LSP module tests.
- Runtime/lowering golden tests.
- Manual VSCode checklist execution before final housekeeping.
## Manual VSCode Validation Checklist
This plan intentionally does not automate VSCode UI rendering. Before final housekeeping, validate manually that:
- hover renders a `Doc` paragraph below the symbol signature and kind;
- Markdown lists render from `- item` documentation lines;
- code spans such as `` `color` `` render as inline code;
- normalized indentation from `[Doc(markdown = """...""")]` does not leak leading source indentation into hover;
- undocumented symbols still show the existing hover fallback without an empty documentation block;
- completion documentation matches the same Markdown payload used by hover.
## Affected Artifacts
- PBS frontend tests and fixtures
- LSP tests and fixtures
- Golden/runtime artifact tests
- Optional manual validation note in the implementation PR or plan completion record

View File

@ -76,10 +76,18 @@ Mandatory declaration metadata on required declaration nodes includes:
2. declared signature/surface when applicable (parameters/return), 2. declared signature/surface when applicable (parameters/return),
3. declaration-level syntactic flags/attributes required by later phases, 3. declaration-level syntactic flags/attributes required by later phases,
4. stable `file/start/end` attribution, 4. stable `file/start/end` attribution,
5. lifecycle-marker metadata when a declaration carries `[Init]` or `[Frame]`. 5. lifecycle-marker metadata when a declaration carries `[Init]` or `[Frame]`,
6. documentation attribute metadata when a declaration carries `[Doc(markdown = """...""")]`.
Missing required attribution or metadata on mandatory nodes is non-conformant. Missing required attribution or metadata on mandatory nodes is non-conformant.
Documentation metadata rules:
- The AST MUST preserve the `Doc` attribute name, the `markdown` argument name, the raw documentation text block payload, and source attribution for the whole attribute and payload.
- The AST MUST distinguish documentation text block payloads from ordinary string literals.
- The AST MUST preserve enough attribution for later diagnostics to point at malformed or invalid `Doc` usage.
- AST construction MUST NOT normalize documentation text block content in a way that loses source-observable parse meaning before the semantic normalization step.
## 8. Mandatory Declaration Families ## 8. Mandatory Declaration Families
The v1 AST must represent, at minimum, declaration families for: The v1 AST must represent, at minimum, declaration families for:
@ -104,6 +112,8 @@ Lifecycle-observable declarations must preserve marker metadata explicitly:
- top-level `fn` marked with `[Frame]`, - top-level `fn` marked with `[Frame]`,
- and host method signatures marked with `[InitAllowed]`. - and host method signatures marked with `[InitAllowed]`.
Documentation-observable declarations must preserve `Doc` metadata explicitly on every accepted named API declaration surface that carries `[Doc(markdown = """...""")]`.
## 9. Mandatory Statement and Expression Families ## 9. Mandatory Statement and Expression Families
The v1 AST must represent statement/expression families for the supported core syntax slice, including at minimum: The v1 AST must represent statement/expression families for the supported core syntax slice, including at minimum:

View File

@ -214,6 +214,19 @@ Reserved-attribute diagnostics for host-backed asset lowering must also cover:
- out-of-range `AssetLowering.param` indexes, - out-of-range `AssetLowering.param` indexes,
- and `AssetLowering` targets whose selected parameter is not statically typed as `Addressable`. - and `AssetLowering` targets whose selected parameter is not statically typed as `Addressable`.
Documentation attribute diagnostics must cover:
- unterminated documentation text block syntax,
- `Doc` used without the required `markdown` argument,
- `Doc` with extra or unknown arguments,
- `Doc` with a non-documentation-text-block payload,
- duplicate `Doc` on the same declaration,
- `Doc` applied to a parameter or other non-documentable surface,
- normalized empty or whitespace-only documentation payload,
- and rejected aliases or alternate forms such as `text`, positional arguments, `html`, `format`, `Document`, `Documentation`, or `md"""..."""`.
Diagnostics for duplicate `Doc` attributes must be static semantic diagnostics and should include a related site for the conflicting declaration attribute when available.
At minimum, host-admission diagnostics must cover missing or malformed host capability metadata and unknown or undeclared capability names. At minimum, host-admission diagnostics must cover missing or malformed host capability metadata and unknown or undeclared capability names.
Only backend-originated failures that remain source-attributable and user-actionable belong to the PBS-facing diagnostics contract. Only backend-originated failures that remain source-attributable and user-actionable belong to the PBS-facing diagnostics contract.

View File

@ -114,6 +114,13 @@ String literals:
- delimited by `"`. - delimited by `"`.
- supported escapes: `\\`, `\"`, `\n`, `\r`, `\t`. - supported escapes: `\\`, `\"`, `\n`, `\r`, `\t`.
Documentation text block literals:
- delimited by `"""`.
- valid only as the value of the `markdown` argument in the reserved `[Doc(markdown = """...""")]` attribute surface.
- not part of the runtime expression literal surface in v1 core.
- preserve authored text for later documentation normalization.
Boolean literals: Boolean literals:
- `true`, `false`. - `true`, `false`.
@ -241,7 +248,8 @@ Attribute ::= '[' Identifier AttrArgs? ']'
AttrArgs ::= '(' AttrArgList? ')' AttrArgs ::= '(' AttrArgList? ')'
AttrArgList ::= AttrArg (',' AttrArg)* AttrArgList ::= AttrArg (',' AttrArg)*
AttrArg ::= Identifier '=' AttrValue AttrArg ::= Identifier '=' AttrValue
AttrValue ::= StringLit | IntLit | BoolLit AttrValue ::= StringLit | IntLit | BoolLit | DocTextBlock
DocTextBlock ::= '"""' DocTextBlockChar* '"""'
``` ```
Rules: Rules:
@ -250,11 +258,16 @@ Rules:
- Attributes do not introduce values, types, callables, or modules by themselves. - Attributes do not introduce values, types, callables, or modules by themselves.
- In v1 core, ordinary user-authored modules MUST reject attributes unless another specification explicitly permits them. - In v1 core, ordinary user-authored modules MUST reject attributes unless another specification explicitly permits them.
- Ordinary userland source MAY use `[Init]` and `[Frame]` only on top-level `fn` declarations. - Ordinary userland source MAY use `[Init]` and `[Frame]` only on top-level `fn` declarations.
- Ordinary userland source MAY use `[Doc(markdown = """...""")]` on named API declarations where the static semantics specification permits documentation.
- Reserved stdlib/toolchain interface modules MAY use attributes where explicitly allowed by syntax and semantics. - Reserved stdlib/toolchain interface modules MAY use attributes where explicitly allowed by syntax and semantics.
- Reserved stdlib/toolchain interface modules MAY additionally use `[InitAllowed]` only on `declare host` method signatures where another specification explicitly permits it. - Reserved stdlib/toolchain interface modules MAY additionally use `[InitAllowed]` only on `declare host` method signatures where another specification explicitly permits it.
- Attribute interpretation is compile-time only unless another specification explicitly lowers its effect into runtime-facing metadata. - Attribute interpretation is compile-time only unless another specification explicitly lowers its effect into runtime-facing metadata.
- Reserved attribute names used by builtin-type and intrinsic shells do not become ordinary value-level syntax. - Reserved attribute names used by builtin-type and intrinsic shells do not become ordinary value-level syntax.
- `Slot(...)` is not part of the v1 attribute surface; source that uses `Slot` must be rejected in syntax phase. - `Slot(...)` is not part of the v1 attribute surface; source that uses `Slot` must be rejected in syntax phase.
- `Doc` is a reserved documentation attribute name. Its only v1 syntactic shape is `[Doc(markdown = """...""")]`.
- The `Doc` attribute MUST NOT accept positional arguments, `text`, `html`, `format`, `Document`, `Documentation`, or prefixed text block forms such as `md"""..."""`.
- Documentation text blocks are not string literals and MUST NOT appear in ordinary expressions, parameter defaults, or non-`Doc` attribute arguments.
- Attribute ordering is syntactically insignificant; semantic validation determines duplicate or invalid attribute combinations.
### 6.2 Top-level declarations ### 6.2 Top-level declarations

View File

@ -80,7 +80,7 @@ Rules:
- Attributes are not first-class values and are not reflectable in v1 core. - Attributes are not first-class values and are not reflectable in v1 core.
- Attributes do not automatically survive into runtime or bytecode artifacts. - Attributes do not automatically survive into runtime or bytecode artifacts.
- An attribute affects runtime artifacts only when another specification defines an explicit lowering for its semantic effect. - An attribute affects runtime artifacts only when another specification defines an explicit lowering for its semantic effect.
- In v1 core, the normative reserved attributes are `Host`, `Capability`, `AssetLowering`, `BuiltinType`, `BuiltinConst`, `IntrinsicCall`, `Init`, `Frame`, and `InitAllowed`. - In v1 core, the normative reserved attributes are `Host`, `Capability`, `AssetLowering`, `BuiltinType`, `BuiltinConst`, `IntrinsicCall`, `Init`, `Frame`, `InitAllowed`, and `Doc`.
- `Host` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. - `Host` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body.
- `Capability` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. - `Capability` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body.
- `AssetLowering` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body. - `AssetLowering` is valid only on a host method signature declared directly inside a reserved stdlib/interface-module `declare host` body.
@ -100,6 +100,42 @@ Rules:
- Builtin metadata is consumed by the compiler during VM-owned builtin and intrinsic lowering rather than by host-binding lowering. - Builtin metadata is consumed by the compiler during VM-owned builtin and intrinsic lowering rather than by host-binding lowering.
- `Init` and `Frame` are consumed by lifecycle analysis and lowering rather than by ordinary runtime reflection. - `Init` and `Frame` are consumed by lifecycle analysis and lowering rather than by ordinary runtime reflection.
- `InitAllowed` is consumed by init-time host-admission analysis and does not become ordinary userland metadata. - `InitAllowed` is consumed by init-time host-admission analysis and does not become ordinary userland metadata.
- `Doc` is compile/editor documentation metadata and is consumed by compiler-owned editorial surfaces.
- `Doc` MUST NOT be lowered into bytecode, PBX, cartridge, or runtime-facing artifacts unless a future specification explicitly defines such lowering.
### 2.3.1 `Doc` documentation metadata
`Doc` attaches authored Markdown documentation to a named API declaration.
Canonical v1 shape:
```pbs
[Doc(markdown = """
Draws a sprite.
- `x`: screen x
- `y`: screen y
""")]
```
Rules:
- `Doc` MUST declare exactly one named argument, `markdown`.
- `Doc.markdown` MUST be a documentation text block.
- `Doc` MUST NOT accept aliases or alternate forms such as `text`, positional arguments, `html`, `format`, `Document`, `Documentation`, or `md"""..."""`.
- A declaration MUST NOT carry more than one `Doc` attribute.
- Duplicate `Doc` attributes are static semantic errors and must not be rejected only as parser errors.
- Attribute order is semantically irrelevant; `Doc` colocated with other valid attributes has the same meaning regardless of ordering.
- `Doc` is valid only on named API declarations that introduce named semantic symbols and can carry attributes.
- The v1 documentable surface includes functions, methods, classes/structs when present, services, hosts, builtin and stdlib declarations, and interface declarations.
- Parameters MUST NOT receive `Doc` in v1. Parameter documentation, when present, MUST be authored inside the Markdown payload of the owning declaration.
- Fields, enum cases, tuple fields, local variables, expressions, and other non-API declaration surfaces MUST NOT receive `Doc` unless a future decision explicitly classifies them as documentable named API declarations.
- Documentation text block content MUST be normalized before semantic empty-payload validation and before exposure to editor consumers.
- Normalization MUST remove an incidental leading empty line, remove an incidental trailing empty line, compute the common indentation of non-empty content lines, remove that common indentation, preserve internal line breaks, and preserve Markdown content after indentation normalization.
- A normalized empty or whitespace-only `Doc.markdown` payload is invalid.
- The authored format is a supported Markdown subset. Rendering, sanitization, and unsupported Markdown constructs are consumer responsibilities.
- Compiler semantic metadata MUST preserve normalized Markdown documentation as metadata of the documented symbol.
- LSP and editor consumers MUST consume compiler-resolved documentation metadata rather than reparsing source text.
### 2.4 Tuple shapes ### 2.4 Tuple shapes

View File

@ -354,6 +354,8 @@ Rules:
- Renaming a public stdlib or host-backed parameter is API compatibility-sensitive and MUST be reviewed with the same discipline as other public callable-shape changes. - Renaming a public stdlib or host-backed parameter is API compatibility-sensitive and MUST be reviewed with the same discipline as other public callable-shape changes.
- Interface-module loaders MUST NOT replace missing or unavailable public parameter names with generated ordinal placeholders such as `arg0` or `arg1`. - Interface-module loaders MUST NOT replace missing or unavailable public parameter names with generated ordinal placeholders such as `arg0` or `arg1`.
- Missing canonical parameter identity MUST be represented as incomplete semantic metadata, not as a fabricated public name. - Missing canonical parameter identity MUST be represented as incomplete semantic metadata, not as a fabricated public name.
- Stdlib, SDK, and interface-module API declarations SHOULD carry authored English documentation through `[Doc(markdown = """...""")]` once the documentation surface is implemented.
- Stdlib and SDK parameter documentation MUST be authored inside the owning declaration's Markdown payload; parameters MUST NOT carry `[Doc]`.
### 9.3 Interface-module restrictions ### 9.3 Interface-module restrictions
@ -441,6 +443,15 @@ Rules:
- Attributes are not exported as ordinary values, types, or runtime-reflectable objects. - Attributes are not exported as ordinary values, types, or runtime-reflectable objects.
- Attributes do not automatically survive into bytecode, PBX, or runtime metadata. - Attributes do not automatically survive into bytecode, PBX, or runtime metadata.
`Doc` is a reserved documentation attribute for compiler/editor metadata.
Rules:
- Stdlib, SDK, and interface modules MUST use the same `[Doc(markdown = """...""")]` surface as ordinary PBS source for authored API documentation.
- Authored source documentation takes precedence over imported or generated documentation for the same symbol.
- Conflicts between authored and imported/generated documentation MUST be resolved deterministically or reported through explicit diagnostics.
- `Doc` metadata enriches compiler/editor surfaces and MUST NOT change import resolution, host binding, ABI identity, executable lowering, or runtime behavior.
### 11.2 `Host` attribute ### 11.2 `Host` attribute
The canonical reserved host-binding attribute is: The canonical reserved host-binding attribute is:

View File

@ -0,0 +1,72 @@
package p.studio.compiler.pbs;
import java.util.ArrayList;
import java.util.List;
public final class PbsDocumentationTextBlockNormalizer {
private static final String DELIMITER = "\"\"\"";
private PbsDocumentationTextBlockNormalizer() {
}
public static String normalizeLexeme(final String lexeme) {
if (lexeme == null || !lexeme.startsWith(DELIMITER) || !lexeme.endsWith(DELIMITER) || lexeme.length() < 6) {
return "";
}
return normalizeContent(lexeme.substring(DELIMITER.length(), lexeme.length() - DELIMITER.length()));
}
public static String normalizeContent(final String rawContent) {
final var lines = splitLines(rawContent == null ? "" : rawContent.replace("\r\n", "\n").replace('\r', '\n'));
removeIncidentalBoundaryLines(lines);
final int commonIndent = commonIndent(lines);
if (commonIndent > 0) {
for (int i = 0; i < lines.size(); i++) {
final var line = lines.get(i);
if (line.isBlank()) {
lines.set(i, "");
} else {
lines.set(i, line.substring(Math.min(commonIndent, leadingWhitespace(line))));
}
}
}
return String.join("\n", lines);
}
private static List<String> splitLines(final String text) {
final var lines = new ArrayList<String>();
final var parts = text.split("\n", -1);
for (final var part : parts) {
lines.add(part);
}
return lines;
}
private static void removeIncidentalBoundaryLines(final List<String> lines) {
if (!lines.isEmpty() && lines.getFirst().isBlank()) {
lines.removeFirst();
}
if (!lines.isEmpty() && lines.getLast().isBlank()) {
lines.removeLast();
}
}
private static int commonIndent(final List<String> lines) {
var common = Integer.MAX_VALUE;
for (final var line : lines) {
if (line.isBlank()) {
continue;
}
common = Math.min(common, leadingWhitespace(line));
}
return common == Integer.MAX_VALUE ? 0 : common;
}
private static int leadingWhitespace(final String line) {
var count = 0;
while (count < line.length() && Character.isWhitespace(line.charAt(count))) {
count++;
}
return count;
}
}

View File

@ -50,7 +50,7 @@ public enum PbsSemanticKind {
public static PbsSemanticKind forToken(final PbsToken token) { public static PbsSemanticKind forToken(final PbsToken token) {
return switch (token.kind()) { return switch (token.kind()) {
case COMMENT -> COMMENT; case COMMENT -> COMMENT;
case STRING_LITERAL -> STRING; case STRING_LITERAL, DOC_TEXT_BLOCK -> STRING;
case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> NUMBER; case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> NUMBER;
case TRUE, FALSE, NONE, SOME, OK, ERR -> LITERAL; case TRUE, FALSE, NONE, SOME, OK, ERR -> LITERAL;
case VOID, OPTIONAL, RESULT -> BUILTIN_TYPE; case VOID, OPTIONAL, RESULT -> BUILTIN_TYPE;

View File

@ -78,6 +78,7 @@ public final class PbsAst {
} }
public sealed interface AttributeValue permits AttributeStringValue, public sealed interface AttributeValue permits AttributeStringValue,
AttributeDocTextBlockValue,
AttributeIntValue, AttributeIntValue,
AttributeBoolValue, AttributeBoolValue,
AttributeErrorValue { AttributeErrorValue {
@ -89,6 +90,11 @@ public final class PbsAst {
Span span) implements AttributeValue { Span span) implements AttributeValue {
} }
public record AttributeDocTextBlockValue(
String value,
Span span) implements AttributeValue {
}
public record AttributeIntValue( public record AttributeIntValue(
long value, long value,
Span span) implements AttributeValue { Span span) implements AttributeValue {
@ -132,6 +138,7 @@ public final class PbsAst {
ReturnKind returnKind, ReturnKind returnKind,
TypeRef returnType, TypeRef returnType,
TypeRef resultErrorType, TypeRef resultErrorType,
ReadOnlyList<Attribute> attributes,
LifecycleMarker lifecycleMarker, LifecycleMarker lifecycleMarker,
Block body, Block body,
Span span) implements TopDecl { Span span) implements TopDecl {
@ -143,18 +150,21 @@ public final class PbsAst {
ReadOnlyList<FunctionDecl> methods, ReadOnlyList<FunctionDecl> methods,
ReadOnlyList<CtorDecl> ctors, ReadOnlyList<CtorDecl> ctors,
boolean hasBody, boolean hasBody,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record ContractDecl( public record ContractDecl(
String name, String name,
ReadOnlyList<FunctionSignature> signatures, ReadOnlyList<FunctionSignature> signatures,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record HostDecl( public record HostDecl(
String name, String name,
ReadOnlyList<FunctionSignature> signatures, ReadOnlyList<FunctionSignature> signatures,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -169,18 +179,21 @@ public final class PbsAst {
public record ServiceDecl( public record ServiceDecl(
String name, String name,
ReadOnlyList<FunctionDecl> methods, ReadOnlyList<FunctionDecl> methods,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record ErrorDecl( public record ErrorDecl(
String name, String name,
ReadOnlyList<String> cases, ReadOnlyList<String> cases,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
public record EnumDecl( public record EnumDecl(
String name, String name,
ReadOnlyList<EnumCase> cases, ReadOnlyList<EnumCase> cases,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }
@ -190,6 +203,7 @@ public final class PbsAst {
ReturnKind returnKind, ReturnKind returnKind,
TypeRef returnType, TypeRef returnType,
TypeRef resultErrorType, TypeRef resultErrorType,
ReadOnlyList<Attribute> attributes,
Span span) implements TopDecl { Span span) implements TopDecl {
} }

View File

@ -3,5 +3,6 @@ package p.studio.compiler.pbs.lexer;
public enum LexErrors { public enum LexErrors {
E_LEX_INVALID_CHAR, E_LEX_INVALID_CHAR,
E_LEX_UNTERMINATED_STRING, E_LEX_UNTERMINATED_STRING,
E_LEX_UNTERMINATED_DOC_TEXT_BLOCK,
E_LEX_INVALID_STRING_ESCAPE, E_LEX_INVALID_STRING_ESCAPE,
} }

View File

@ -17,6 +17,7 @@ public final class PbsLexer {
IDENTIFIER, IDENTIFIER,
NUMBER, NUMBER,
STRING, STRING,
DOC_TEXT_BLOCK,
LINE_COMMENT LINE_COMMENT
} }
@ -52,6 +53,7 @@ public final class PbsLexer {
case IDENTIFIER -> scanIdentifierState(); case IDENTIFIER -> scanIdentifierState();
case NUMBER -> scanNumberState(); case NUMBER -> scanNumberState();
case STRING -> scanStringState(); case STRING -> scanStringState();
case DOC_TEXT_BLOCK -> scanDocTextBlockState();
case LINE_COMMENT -> scanLineCommentState(); case LINE_COMMENT -> scanLineCommentState();
} }
} }
@ -123,7 +125,15 @@ public final class PbsLexer {
} }
addToken(match('=') ? PbsTokenKind.SLASH_EQUAL : PbsTokenKind.SLASH); addToken(match('=') ? PbsTokenKind.SLASH_EQUAL : PbsTokenKind.SLASH);
} }
case '"' -> state = LexerState.STRING; case '"' -> {
if (peek() == '"' && peekNext() == '"') {
advance();
advance();
state = LexerState.DOC_TEXT_BLOCK;
return;
}
state = LexerState.STRING;
}
default -> { default -> {
if (isDigit(c)) { if (isDigit(c)) {
state = LexerState.NUMBER; state = LexerState.NUMBER;
@ -206,6 +216,23 @@ public final class PbsLexer {
state = LexerState.DEFAULT; state = LexerState.DEFAULT;
} }
private void scanDocTextBlockState() {
while (!isAtEnd()) {
if (peek() == '"' && peekNext() == '"' && peekAfterNext() == '"') {
advance();
advance();
advance();
addToken(PbsTokenKind.DOC_TEXT_BLOCK);
state = LexerState.DEFAULT;
return;
}
advance();
}
report(LexErrors.E_LEX_UNTERMINATED_DOC_TEXT_BLOCK, "Unterminated documentation text block");
state = LexerState.DEFAULT;
}
private void scanLineCommentState() { private void scanLineCommentState() {
while (!isAtEnd() && peek() != '\n') { while (!isAtEnd() && peek() != '\n') {
advance(); advance();
@ -250,6 +277,17 @@ public final class PbsLexer {
return source.codePointAt(nextIndex); return source.codePointAt(nextIndex);
} }
private int peekAfterNext() {
if (isAtEnd()) return '\0';
final int first = source.codePointAt(current);
final int secondIndex = current + Character.charCount(first);
if (secondIndex >= source.length()) return '\0';
final int second = source.codePointAt(secondIndex);
final int thirdIndex = secondIndex + Character.charCount(second);
if (thirdIndex >= source.length()) return '\0';
return source.codePointAt(thirdIndex);
}
private boolean isAtEnd() { private boolean isAtEnd() {
return current >= source.length(); return current >= source.length();
} }

View File

@ -17,6 +17,7 @@ public enum PbsTokenKind {
FLOAT_LITERAL, FLOAT_LITERAL,
BOUNDED_LITERAL, BOUNDED_LITERAL,
STRING_LITERAL, STRING_LITERAL,
DOC_TEXT_BLOCK,
COMMENT, COMMENT,
// Import keywords. // Import keywords.

View File

@ -84,6 +84,10 @@ final class PbsAttributeParser {
final var token = cursor.previous(); final var token = cursor.previous();
return new PbsAst.AttributeStringValue(token.lexeme(), context.span(token.start(), token.end())); return new PbsAst.AttributeStringValue(token.lexeme(), context.span(token.start(), token.end()));
} }
if (cursor.match(PbsTokenKind.DOC_TEXT_BLOCK)) {
final var token = cursor.previous();
return new PbsAst.AttributeDocTextBlockValue(token.lexeme(), context.span(token.start(), token.end()));
}
if (cursor.match(PbsTokenKind.INT_LITERAL)) { if (cursor.match(PbsTokenKind.INT_LITERAL)) {
final var token = cursor.previous(); final var token = cursor.previous();
final var parsedLong = parseLongOrNull(token.lexeme()); final var parsedLong = parseLongOrNull(token.lexeme());

View File

@ -108,28 +108,28 @@ final class PbsDeclarationParser {
final PbsToken declareToken, final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> pendingAttributes) { final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
if (cursor.match(PbsTokenKind.STRUCT)) { if (cursor.match(PbsTokenKind.STRUCT)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "struct declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "struct declarations");
return parseStructDeclaration(declareToken); return parseStructDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.CONTRACT)) { if (cursor.match(PbsTokenKind.CONTRACT)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "contract declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "contract declarations");
return parseContractDeclaration(declareToken); return parseContractDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.SERVICE)) { if (cursor.match(PbsTokenKind.SERVICE)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "service declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "service declarations");
return parseServiceDeclaration(declareToken); return parseServiceDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.ERROR)) { if (cursor.match(PbsTokenKind.ERROR)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "error declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "error declarations");
return parseErrorDeclaration(declareToken); return parseErrorDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.ENUM)) { if (cursor.match(PbsTokenKind.ENUM)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "enum declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "enum declarations");
return parseEnumDeclaration(declareToken); return parseEnumDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.CALLBACK)) { if (cursor.match(PbsTokenKind.CALLBACK)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "callback declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "callback declarations");
return parseCallbackDeclaration(declareToken); return parseCallbackDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.GLOBAL)) { if (cursor.match(PbsTokenKind.GLOBAL)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "global declarations"); rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "global declarations");
@ -139,14 +139,14 @@ final class PbsDeclarationParser {
return parseConstDeclaration(declareToken, pendingAttributes); return parseConstDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.HOST)) { if (cursor.match(PbsTokenKind.HOST)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "host declarations"); rejectUnsupportedDeclarationAttributes(pendingAttributes, "host declarations");
if (isNotInterfaceMode()) { if (isNotInterfaceMode()) {
final var end = declarationTerminatorDelegate.consume(); final var end = declarationTerminatorDelegate.consume();
report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION, report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
"'declare host' is reserved and not supported in ordinary source modules"); "'declare host' is reserved and not supported in ordinary source modules");
return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end)); return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end));
} }
return parseHostDeclaration(declareToken); return parseHostDeclaration(declareToken, pendingAttributes);
} }
if (cursor.match(PbsTokenKind.BUILTIN)) { if (cursor.match(PbsTokenKind.BUILTIN)) {
consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration"); consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration");
@ -211,14 +211,16 @@ final class PbsDeclarationParser {
span(implementsToken.start(), end)); span(implementsToken.start(), end));
} }
private PbsAst.HostDecl parseHostDeclaration(final PbsToken declareToken) { private PbsAst.HostDecl parseHostDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected host declaration name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected host declaration name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start host declaration body"); consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start host declaration body");
final var signatures = new ArrayList<PbsAst.FunctionSignature>(); final var signatures = new ArrayList<PbsAst.FunctionSignature>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
var attributes = ReadOnlyList.<PbsAst.Attribute>empty(); var signatureAttributes = ReadOnlyList.<PbsAst.Attribute>empty();
if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
attributes = attributeParser.parseAttributeList(); signatureAttributes = attributeParser.parseAttributeList();
} }
if (!cursor.match(PbsTokenKind.FN)) { if (!cursor.match(PbsTokenKind.FN)) {
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
@ -226,12 +228,13 @@ final class PbsDeclarationParser {
cursor.advance(); cursor.advance();
continue; continue;
} }
signatures.add(parseFunctionSignature(cursor.previous(), attributes)); signatures.add(parseFunctionSignature(cursor.previous(), signatureAttributes));
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end host declaration body"); final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end host declaration body");
return new PbsAst.HostDecl( return new PbsAst.HostDecl(
name.lexeme(), name.lexeme(),
ReadOnlyList.wrap(signatures), ReadOnlyList.wrap(signatures),
attributes,
span(declareToken.start(), rightBrace.end())); span(declareToken.start(), rightBrace.end()));
} }
@ -308,7 +311,9 @@ final class PbsDeclarationParser {
return fields; return fields;
} }
private PbsAst.StructDecl parseStructDeclaration(final PbsToken declareToken) { private PbsAst.StructDecl parseStructDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected struct name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected struct name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration");
final var fields = parseStructFields(); final var fields = parseStructFields();
@ -340,6 +345,7 @@ final class PbsDeclarationParser {
ReadOnlyList.wrap(methods), ReadOnlyList.wrap(methods),
ReadOnlyList.wrap(ctors), ReadOnlyList.wrap(ctors),
hasBody, hasBody,
attributes,
span(declareToken.start(), end)); span(declareToken.start(), end));
} }
@ -382,10 +388,17 @@ final class PbsDeclarationParser {
final var methods = new ArrayList<PbsAst.FunctionDecl>(); final var methods = new ArrayList<PbsAst.FunctionDecl>();
final var ctors = new ArrayList<PbsAst.CtorDecl>(); final var ctors = new ArrayList<PbsAst.CtorDecl>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
var attributes = ReadOnlyList.<PbsAst.Attribute>empty();
if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
attributes = attributeParser.parseAttributeList();
}
if (cursor.match(PbsTokenKind.FN)) { if (cursor.match(PbsTokenKind.FN)) {
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty())); methods.add(parseFunctionLike(cursor.previous(), attributes));
continue; continue;
} }
if (!attributes.isEmpty()) {
reportAttributesNotAllowed(attributes, "Attributes are valid only on struct methods in struct bodies");
}
if (cursor.match(PbsTokenKind.CTOR)) { if (cursor.match(PbsTokenKind.CTOR)) {
ctors.add(parseCtorDeclarationInBody(cursor.previous())); ctors.add(parseCtorDeclarationInBody(cursor.previous()));
continue; continue;
@ -411,17 +424,19 @@ final class PbsDeclarationParser {
span(ctorToken.start(), body.span().getEnd())); span(ctorToken.start(), body.span().getEnd()));
} }
private PbsAst.ContractDecl parseContractDeclaration(final PbsToken declareToken) { private PbsAst.ContractDecl parseContractDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected contract name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected contract name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body"); consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body");
final var signatures = new ArrayList<PbsAst.FunctionSignature>(); final var signatures = new ArrayList<PbsAst.FunctionSignature>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
var attributes = ReadOnlyList.<PbsAst.Attribute>empty(); var signatureAttributes = ReadOnlyList.<PbsAst.Attribute>empty();
if (cursor.check(PbsTokenKind.LEFT_BRACKET)) { if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
attributes = attributeParser.parseAttributeList(); signatureAttributes = attributeParser.parseAttributeList();
if (isOrdinaryMode()) { if (isOrdinaryMode()) {
reportAttributesNotAllowed(attributes, "Attributes are not allowed in ordinary .pbs source modules"); reportAttributesNotAllowed(signatureAttributes, "Attributes are not allowed in ordinary .pbs source modules");
attributes = ReadOnlyList.empty(); signatureAttributes = ReadOnlyList.empty();
} }
} }
if (!cursor.match(PbsTokenKind.FN)) { if (!cursor.match(PbsTokenKind.FN)) {
@ -430,30 +445,41 @@ final class PbsDeclarationParser {
cursor.advance(); cursor.advance();
continue; continue;
} }
signatures.add(parseFunctionSignature(cursor.previous(), attributes)); signatures.add(parseFunctionSignature(cursor.previous(), signatureAttributes));
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end contract body"); final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end contract body");
return new PbsAst.ContractDecl(name.lexeme(), ReadOnlyList.wrap(signatures), span(declareToken.start(), rightBrace.end())); return new PbsAst.ContractDecl(name.lexeme(), ReadOnlyList.wrap(signatures), attributes, span(declareToken.start(), rightBrace.end()));
} }
private PbsAst.ServiceDecl parseServiceDeclaration(final PbsToken declareToken) { private PbsAst.ServiceDecl parseServiceDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected service name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected service name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start service body"); consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start service body");
final var methods = new ArrayList<PbsAst.FunctionDecl>(); final var methods = new ArrayList<PbsAst.FunctionDecl>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
var methodAttributes = ReadOnlyList.<PbsAst.Attribute>empty();
if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
methodAttributes = attributeParser.parseAttributeList();
}
if (!cursor.match(PbsTokenKind.FN)) { if (!cursor.match(PbsTokenKind.FN)) {
if (!methodAttributes.isEmpty()) {
reportAttributesNotAllowed(methodAttributes, "Attributes are valid only on service methods in service bodies");
}
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE, report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
"Service body accepts only method declarations"); "Service body accepts only method declarations");
cursor.advance(); cursor.advance();
continue; continue;
} }
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty())); methods.add(parseFunctionLike(cursor.previous(), methodAttributes));
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body"); final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body");
return new PbsAst.ServiceDecl(name.lexeme(), ReadOnlyList.wrap(methods), span(declareToken.start(), rightBrace.end())); return new PbsAst.ServiceDecl(name.lexeme(), ReadOnlyList.wrap(methods), attributes, span(declareToken.start(), rightBrace.end()));
} }
private PbsAst.ErrorDecl parseErrorDeclaration(final PbsToken declareToken) { private PbsAst.ErrorDecl parseErrorDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected error name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected error name");
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start error body"); consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start error body");
final var cases = new ArrayList<String>(); final var cases = new ArrayList<String>();
@ -463,10 +489,12 @@ final class PbsDeclarationParser {
consume(PbsTokenKind.SEMICOLON, "Expected ';' after error case label"); consume(PbsTokenKind.SEMICOLON, "Expected ';' after error case label");
} }
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end error body"); final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end error body");
return new PbsAst.ErrorDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), rightBrace.end())); return new PbsAst.ErrorDecl(name.lexeme(), ReadOnlyList.wrap(cases), attributes, span(declareToken.start(), rightBrace.end()));
} }
private PbsAst.EnumDecl parseEnumDeclaration(final PbsToken declareToken) { private PbsAst.EnumDecl parseEnumDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected enum name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected enum name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name");
@ -522,7 +550,7 @@ final class PbsDeclarationParser {
final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after enum cases"); final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after enum cases");
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after enum declaration"); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after enum declaration");
return new PbsAst.EnumDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), Math.max(rightParen.end(), semicolon.end()))); return new PbsAst.EnumDecl(name.lexeme(), ReadOnlyList.wrap(cases), attributes, span(declareToken.start(), Math.max(rightParen.end(), semicolon.end())));
} }
private PbsAst.FunctionDecl parseFunctionLike( private PbsAst.FunctionDecl parseFunctionLike(
@ -543,6 +571,7 @@ final class PbsDeclarationParser {
returnSpec.kind(), returnSpec.kind(),
returnSpec.returnType(), returnSpec.returnType(),
returnSpec.resultErrorType(), returnSpec.resultErrorType(),
pendingAttributes,
lifecycleMarker, lifecycleMarker,
body, body,
span(fnToken.start(), body.span().getEnd())); span(fnToken.start(), body.span().getEnd()));
@ -585,7 +614,9 @@ final class PbsDeclarationParser {
return token; return token;
} }
private PbsAst.CallbackDecl parseCallbackDeclaration(final PbsToken declareToken) { private PbsAst.CallbackDecl parseCallbackDeclaration(
final PbsToken declareToken,
final ReadOnlyList<PbsAst.Attribute> attributes) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name"); final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name");
final var parameters = typeParser.parseParametersUntilRightParen(); final var parameters = typeParser.parseParametersUntilRightParen();
@ -600,6 +631,7 @@ final class PbsDeclarationParser {
returnSpec.kind(), returnSpec.kind(),
returnSpec.returnType(), returnSpec.returnType(),
returnSpec.resultErrorType(), returnSpec.resultErrorType(),
attributes,
span(declareToken.start(), semicolon.end())); span(declareToken.start(), semicolon.end()));
} }
@ -651,6 +683,20 @@ final class PbsDeclarationParser {
reportAttributesNotAllowed(attributes, "Attributes are not allowed before " + targetSurface); reportAttributesNotAllowed(attributes, "Attributes are not allowed before " + targetSurface);
} }
private void rejectUnsupportedDeclarationAttributes(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String targetSurface) {
for (final var attribute : attributes) {
if ("Doc".equals(attribute.name())) {
continue;
}
p.studio.compiler.source.diagnostics.Diagnostics.error(context.diagnostics(),
ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name(),
"Only [Doc] is allowed before " + targetSurface,
attribute.span());
}
}
private void reportAttributesNotAllowed( private void reportAttributesNotAllowed(
final ReadOnlyList<PbsAst.Attribute> attributes, final ReadOnlyList<PbsAst.Attribute> attributes,
final String message) { final String message) {
@ -676,14 +722,16 @@ final class PbsDeclarationParser {
return PbsAst.LifecycleMarker.NONE; return PbsAst.LifecycleMarker.NONE;
} }
if (!isOrdinaryMode()) {
reportAttributesNotAllowed(attributes, "Attributes are not allowed before top-level functions");
return PbsAst.LifecycleMarker.NONE;
}
PbsAst.LifecycleMarker marker = PbsAst.LifecycleMarker.NONE; PbsAst.LifecycleMarker marker = PbsAst.LifecycleMarker.NONE;
for (final var attribute : attributes) { for (final var attribute : attributes) {
final PbsAst.LifecycleMarker nextMarker; final PbsAst.LifecycleMarker nextMarker;
if ("Doc".equals(attribute.name())) {
continue;
}
if (!isOrdinaryMode()) {
reportAttributesNotAllowed(attributes, "Only [Doc] is allowed before interface executable methods");
return PbsAst.LifecycleMarker.NONE;
}
if ("Init".equals(attribute.name())) { if ("Init".equals(attribute.name())) {
nextMarker = PbsAst.LifecycleMarker.INIT; nextMarker = PbsAst.LifecycleMarker.INIT;
} else if ("Frame".equals(attribute.name())) { } else if ("Frame".equals(attribute.name())) {

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs.semantics; package p.studio.compiler.pbs.semantics;
import p.studio.compiler.models.SourceKind; import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.PbsDocumentationTextBlockNormalizer;
import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span; import p.studio.compiler.source.Span;
@ -19,6 +20,7 @@ public final class PbsDeclarationSemanticsValidator {
private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall"; private static final String ATTR_INTRINSIC_CALL = "IntrinsicCall";
private static final String ATTR_INIT_ALLOWED = "InitAllowed"; private static final String ATTR_INIT_ALLOWED = "InitAllowed";
private static final String ATTR_ASSET_LOWERING = "AssetLowering"; private static final String ATTR_ASSET_LOWERING = "AssetLowering";
private static final String ATTR_DOC = "Doc";
private static final Set<String> RESERVED_ATTRIBUTES = Set.of( private static final Set<String> RESERVED_ATTRIBUTES = Set.of(
ATTR_HOST, ATTR_HOST,
@ -27,7 +29,8 @@ public final class PbsDeclarationSemanticsValidator {
ATTR_BUILTIN_CONST, ATTR_BUILTIN_CONST,
ATTR_INTRINSIC_CALL, ATTR_INTRINSIC_CALL,
ATTR_INIT_ALLOWED, ATTR_INIT_ALLOWED,
ATTR_ASSET_LOWERING); ATTR_ASSET_LOWERING,
ATTR_DOC);
private final NameTable nameTable; private final NameTable nameTable;
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator(); private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
@ -60,6 +63,7 @@ public final class PbsDeclarationSemanticsValidator {
for (final var topDecl : ast.topDecls()) { for (final var topDecl : ast.topDecls()) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
validateDocAttributes(functionDecl.attributes(), "function declarations", diagnostics);
if (interfaceModule) { if (interfaceModule) {
reportInterfaceNonDeclarativeDecl( reportInterfaceNonDeclarativeDecl(
functionDecl.span(), functionDecl.span(),
@ -82,6 +86,7 @@ public final class PbsDeclarationSemanticsValidator {
} }
if (topDecl instanceof PbsAst.StructDecl structDecl) { if (topDecl instanceof PbsAst.StructDecl structDecl) {
validateDocAttributes(structDecl.attributes(), "struct declarations", diagnostics);
binder.registerType(structDecl.name(), structDecl.span(), "struct"); binder.registerType(structDecl.name(), structDecl.span(), "struct");
if (interfaceModule && (structDecl.hasBody() || !structDecl.methods().isEmpty() || !structDecl.ctors().isEmpty())) { if (interfaceModule && (structDecl.hasBody() || !structDecl.methods().isEmpty() || !structDecl.ctors().isEmpty())) {
reportInterfaceNonDeclarativeDecl( reportInterfaceNonDeclarativeDecl(
@ -94,6 +99,7 @@ public final class PbsDeclarationSemanticsValidator {
} }
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
validateDocAttributes(serviceDecl.attributes(), "service declarations", diagnostics);
binder.registerType(serviceDecl.name(), serviceDecl.span(), "service"); binder.registerType(serviceDecl.name(), serviceDecl.span(), "service");
binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton"); binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton");
binder.registerVisibleTopLevelSurface(serviceDecl.name(), serviceDecl.span(), "service"); binder.registerVisibleTopLevelSurface(serviceDecl.name(), serviceDecl.span(), "service");
@ -102,10 +108,12 @@ public final class PbsDeclarationSemanticsValidator {
} }
if (topDecl instanceof PbsAst.ContractDecl contractDecl) { if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
validateDocAttributes(contractDecl.attributes(), "contract declarations", diagnostics);
binder.registerType(contractDecl.name(), contractDecl.span(), "contract"); binder.registerType(contractDecl.name(), contractDecl.span(), "contract");
if (interfaceModule) { if (interfaceModule) {
for (final var signature : contractDecl.signatures()) { for (final var signature : contractDecl.signatures()) {
validateReservedAttributeTarget(signature.attributes(), "contract signature", diagnostics); validateDocAttributes(signature.attributes(), "contract signatures", diagnostics);
validateReservedAttributeTargetExceptDoc(signature.attributes(), "contract signature", diagnostics);
} }
} }
validateContractDeclaration(contractDecl, binder, rules); validateContractDeclaration(contractDecl, binder, rules);
@ -113,6 +121,7 @@ public final class PbsDeclarationSemanticsValidator {
} }
if (topDecl instanceof PbsAst.HostDecl hostDecl) { if (topDecl instanceof PbsAst.HostDecl hostDecl) {
validateDocAttributes(hostDecl.attributes(), "host declarations", diagnostics);
binder.registerHostOwner(hostDecl.name(), hostDecl.span(), "host owner"); binder.registerHostOwner(hostDecl.name(), hostDecl.span(), "host owner");
validateHostDeclaration(hostDecl, binder, rules, interfaceModule, diagnostics); validateHostDeclaration(hostDecl, binder, rules, interfaceModule, diagnostics);
continue; continue;
@ -125,18 +134,21 @@ public final class PbsDeclarationSemanticsValidator {
} }
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
validateDocAttributes(errorDecl.attributes(), "error declarations", diagnostics);
binder.registerType(errorDecl.name(), errorDecl.span(), "error"); binder.registerType(errorDecl.name(), errorDecl.span(), "error");
rules.validateErrorDeclaration(errorDecl); rules.validateErrorDeclaration(errorDecl);
continue; continue;
} }
if (topDecl instanceof PbsAst.EnumDecl enumDecl) { if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
validateDocAttributes(enumDecl.attributes(), "enum declarations", diagnostics);
binder.registerType(enumDecl.name(), enumDecl.span(), "enum"); binder.registerType(enumDecl.name(), enumDecl.span(), "enum");
rules.validateEnumDeclaration(enumDecl); rules.validateEnumDeclaration(enumDecl);
continue; continue;
} }
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
validateDocAttributes(callbackDecl.attributes(), "callback declarations", diagnostics);
binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback"); binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback");
rules.validateCallbackDeclaration(callbackDecl); rules.validateCallbackDeclaration(callbackDecl);
continue; continue;
@ -187,6 +199,7 @@ public final class PbsDeclarationSemanticsValidator {
false); false);
final var methodScope = PbsCallableScope.structMethods(ownerNameId); final var methodScope = PbsCallableScope.structMethods(ownerNameId);
for (final var method : structDecl.methods()) { for (final var method : structDecl.methods()) {
validateDocAttributes(method.attributes(), "struct methods", diagnostics);
validateCallable( validateCallable(
methodScope, methodScope,
method.name(), method.name(),
@ -226,6 +239,7 @@ public final class PbsDeclarationSemanticsValidator {
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
final var methodScope = PbsCallableScope.serviceMethods(nameTable.register(serviceDecl.name())); final var methodScope = PbsCallableScope.serviceMethods(nameTable.register(serviceDecl.name()));
for (final var method : serviceDecl.methods()) { for (final var method : serviceDecl.methods()) {
validateDocAttributes(method.attributes(), "service methods", diagnostics);
validateCallable( validateCallable(
methodScope, methodScope,
method.name(), method.name(),
@ -494,12 +508,13 @@ public final class PbsDeclarationSemanticsValidator {
final PbsAst.ConstDecl constDecl, final PbsAst.ConstDecl constDecl,
final boolean interfaceModule, final boolean interfaceModule,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
validateDocAttributes(constDecl.attributes(), "const declarations", diagnostics);
final var builtinConstAttributes = attributesNamed(constDecl.attributes(), ATTR_BUILTIN_CONST); final var builtinConstAttributes = attributesNamed(constDecl.attributes(), ATTR_BUILTIN_CONST);
for (final var attribute : constDecl.attributes()) { for (final var attribute : constDecl.attributes()) {
if (!isReservedAttribute(attribute.name())) { if (!isReservedAttribute(attribute.name())) {
continue; continue;
} }
if (ATTR_BUILTIN_CONST.equals(attribute.name())) { if (ATTR_BUILTIN_CONST.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
continue; continue;
} }
reportInvalidReservedAttributeTarget( reportInvalidReservedAttributeTarget(
@ -540,6 +555,7 @@ public final class PbsDeclarationSemanticsValidator {
private void validateHostSignatureAttributes( private void validateHostSignatureAttributes(
final PbsAst.FunctionSignature signature, final PbsAst.FunctionSignature signature,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
validateDocAttributes(signature.attributes(), "host signatures", diagnostics);
final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST); final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST);
final var initAllowedAttributes = attributesNamed(signature.attributes(), ATTR_INIT_ALLOWED); final var initAllowedAttributes = attributesNamed(signature.attributes(), ATTR_INIT_ALLOWED);
final var assetLoweringAttributes = attributesNamed(signature.attributes(), ATTR_ASSET_LOWERING); final var assetLoweringAttributes = attributesNamed(signature.attributes(), ATTR_ASSET_LOWERING);
@ -550,7 +566,8 @@ public final class PbsDeclarationSemanticsValidator {
if (ATTR_HOST.equals(attribute.name()) if (ATTR_HOST.equals(attribute.name())
|| ATTR_CAPABILITY.equals(attribute.name()) || ATTR_CAPABILITY.equals(attribute.name())
|| ATTR_INIT_ALLOWED.equals(attribute.name()) || ATTR_INIT_ALLOWED.equals(attribute.name())
|| ATTR_ASSET_LOWERING.equals(attribute.name())) { || ATTR_ASSET_LOWERING.equals(attribute.name())
|| ATTR_DOC.equals(attribute.name())) {
continue; continue;
} }
reportInvalidReservedAttributeTarget( reportInvalidReservedAttributeTarget(
@ -597,12 +614,13 @@ public final class PbsDeclarationSemanticsValidator {
private void validateBuiltinTypeAttribute( private void validateBuiltinTypeAttribute(
final PbsAst.BuiltinTypeDecl builtinTypeDecl, final PbsAst.BuiltinTypeDecl builtinTypeDecl,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
validateDocAttributes(builtinTypeDecl.attributes(), "builtin type declarations", diagnostics);
final var builtinTypeAttributes = attributesNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE); final var builtinTypeAttributes = attributesNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE);
for (final var attribute : builtinTypeDecl.attributes()) { for (final var attribute : builtinTypeDecl.attributes()) {
if (!isReservedAttribute(attribute.name())) { if (!isReservedAttribute(attribute.name())) {
continue; continue;
} }
if (ATTR_BUILTIN_TYPE.equals(attribute.name())) { if (ATTR_BUILTIN_TYPE.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
continue; continue;
} }
reportInvalidReservedAttributeTarget( reportInvalidReservedAttributeTarget(
@ -645,12 +663,13 @@ public final class PbsDeclarationSemanticsValidator {
private void validateIntrinsicCallAttributes( private void validateIntrinsicCallAttributes(
final PbsAst.FunctionSignature signature, final PbsAst.FunctionSignature signature,
final DiagnosticSink diagnostics) { final DiagnosticSink diagnostics) {
validateDocAttributes(signature.attributes(), "builtin method signatures", diagnostics);
final var intrinsicAttributes = attributesNamed(signature.attributes(), ATTR_INTRINSIC_CALL); final var intrinsicAttributes = attributesNamed(signature.attributes(), ATTR_INTRINSIC_CALL);
for (final var attribute : signature.attributes()) { for (final var attribute : signature.attributes()) {
if (!isReservedAttribute(attribute.name())) { if (!isReservedAttribute(attribute.name())) {
continue; continue;
} }
if (ATTR_INTRINSIC_CALL.equals(attribute.name())) { if (ATTR_INTRINSIC_CALL.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
continue; continue;
} }
reportInvalidReservedAttributeTarget( reportInvalidReservedAttributeTarget(
@ -770,6 +789,47 @@ public final class PbsDeclarationSemanticsValidator {
} }
} }
private void validateDocAttributes(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String targetSurface,
final DiagnosticSink diagnostics) {
final var docAttributes = attributesNamed(attributes, ATTR_DOC);
if (docAttributes.isEmpty()) {
return;
}
if (docAttributes.size() > 1) {
for (int i = 1; i < docAttributes.size(); i++) {
reportDuplicateReservedAttribute(docAttributes.get(i), ATTR_DOC, diagnostics);
}
}
validateDocAttributeShape(docAttributes.getFirst(), targetSurface, diagnostics);
}
private void validateDocAttributeShape(
final PbsAst.Attribute attribute,
final String targetSurface,
final DiagnosticSink diagnostics) {
final var args = validateNamedArguments(attribute, Set.of("markdown"), Set.of(), diagnostics);
if (args == null) {
return;
}
final var value = args.get("markdown");
if (!(value instanceof PbsAst.AttributeDocTextBlockValue docValue)) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
"Doc.markdown on %s must be a documentation text block".formatted(targetSurface),
attribute.span());
return;
}
final var normalized = PbsDocumentationTextBlockNormalizer.normalizeLexeme(docValue.value());
if (normalized.isBlank()) {
p.studio.compiler.source.diagnostics.Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name(),
"Doc.markdown on %s must not be empty".formatted(targetSurface),
attribute.span());
}
}
private Map<String, PbsAst.AttributeValue> validateNamedArguments( private Map<String, PbsAst.AttributeValue> validateNamedArguments(
final PbsAst.Attribute attribute, final PbsAst.Attribute attribute,
final Set<String> requiredNames, final Set<String> requiredNames,
@ -911,6 +971,21 @@ public final class PbsDeclarationSemanticsValidator {
} }
} }
private void validateReservedAttributeTargetExceptDoc(
final ReadOnlyList<PbsAst.Attribute> attributes,
final String targetSurface,
final DiagnosticSink diagnostics) {
for (final var attribute : attributes) {
if (!isReservedAttribute(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
continue;
}
reportInvalidReservedAttributeTarget(
attribute,
"Attribute '%s' is not valid on %s".formatted(attribute.name(), targetSurface),
diagnostics);
}
}
private void reportInvalidReservedAttributeTarget( private void reportInvalidReservedAttributeTarget(
final PbsAst.Attribute attribute, final PbsAst.Attribute attribute,
final String message, final String message,

View File

@ -6,12 +6,22 @@ public record PbsEditorialCompletionCandidate(
String label, String label,
PbsEditorialSymbolKind kind, PbsEditorialSymbolKind kind,
String detail, String detail,
String origin) { String origin,
String documentation) {
public PbsEditorialCompletionCandidate(
final String label,
final PbsEditorialSymbolKind kind,
final String detail,
final String origin) {
this(label, kind, detail, origin, "");
}
public PbsEditorialCompletionCandidate { public PbsEditorialCompletionCandidate {
label = requireText(label, "label"); label = requireText(label, "label");
kind = Objects.requireNonNull(kind, "kind"); kind = Objects.requireNonNull(kind, "kind");
detail = normalize(detail); detail = normalize(detail);
origin = normalize(origin); origin = normalize(origin);
documentation = normalize(documentation);
} }
private static String requireText( private static String requireText(

View File

@ -482,10 +482,11 @@ public final class PbsEditorialSupportService {
} }
for (final var method : structInfo.methods().entrySet()) { for (final var method : structInfo.methods().entrySet()) {
completions.put(method.getKey(), new PbsEditorialCompletionCandidate( completions.put(method.getKey(), new PbsEditorialCompletionCandidate(
method.getKey(), method.getKey(),
PbsEditorialSymbolKind.METHOD, PbsEditorialSymbolKind.METHOD,
formatCallableDetails(method.getValue()), formatCallableDetails(method.getValue()),
receiverType.name())); receiverType.name(),
documentationFromCallables(method.getValue())));
} }
} }
} else if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { } else if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) {
@ -494,10 +495,11 @@ public final class PbsEditorialSupportService {
: Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of()); : Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of());
for (final var method : methods.entrySet()) { for (final var method : methods.entrySet()) {
completions.put(method.getKey(), new PbsEditorialCompletionCandidate( completions.put(method.getKey(), new PbsEditorialCompletionCandidate(
method.getKey(), method.getKey(),
PbsEditorialSymbolKind.METHOD, PbsEditorialSymbolKind.METHOD,
formatCallableDetails(method.getValue()), formatCallableDetails(method.getValue()),
receiverType.name())); receiverType.name(),
documentationFromCallables(method.getValue())));
} }
} }
return List.copyOf(completions.values()); return List.copyOf(completions.values());
@ -587,7 +589,7 @@ public final class PbsEditorialSupportService {
formatCallableDetails(methods), formatCallableDetails(methods),
receiverType.name(), receiverType.name(),
signaturesFromCallables(methods), signaturesFromCallables(methods),
"")); documentationFromCallables(methods)));
} }
} }
if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) {
@ -602,7 +604,7 @@ public final class PbsEditorialSupportService {
formatCallableDetails(signatures), formatCallableDetails(signatures),
receiverType.name(), receiverType.name(),
signaturesFromCallables(signatures), signaturesFromCallables(signatures),
"")); documentationFromCallables(signatures)));
} }
} }
return Optional.empty(); return Optional.empty();
@ -663,7 +665,7 @@ public final class PbsEditorialSupportService {
detail, detail,
topDeclSymbol.origin(), topDeclSymbol.origin(),
signatures, signatures,
""); documentationFromTopDecl(topDeclSymbol.decl()));
} }
private PbsEditorialResolvedSymbol constructorSymbolFromTopDecl( private PbsEditorialResolvedSymbol constructorSymbolFromTopDecl(
@ -678,7 +680,68 @@ public final class PbsEditorialSupportService {
detail, detail,
topDeclSymbol.origin(), topDeclSymbol.origin(),
signatures, signatures,
""); documentationFromTopDecl(topDeclSymbol.decl()));
}
private String documentationFromCallables(final List<CallableSymbol> callables) {
for (final var callable : callables) {
if (!callable.documentation().isBlank()) {
return callable.documentation();
}
}
return "";
}
private String documentationFromTopDecl(final PbsAst.TopDecl topDecl) {
return documentationFromAttributes(attributesOf(topDecl));
}
private ReadOnlyList<PbsAst.Attribute> attributesOf(final PbsAst.TopDecl topDecl) {
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
return functionDecl.attributes();
}
if (topDecl instanceof PbsAst.StructDecl structDecl) {
return structDecl.attributes();
}
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
return builtinTypeDecl.attributes();
}
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
return serviceDecl.attributes();
}
if (topDecl instanceof PbsAst.HostDecl hostDecl) {
return hostDecl.attributes();
}
if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
return contractDecl.attributes();
}
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
return callbackDecl.attributes();
}
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
return enumDecl.attributes();
}
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
return errorDecl.attributes();
}
if (topDecl instanceof PbsAst.ConstDecl constDecl) {
return constDecl.attributes();
}
return ReadOnlyList.empty();
}
private String documentationFromAttributes(final ReadOnlyList<PbsAst.Attribute> attributes) {
for (final var attribute : attributes) {
if (!"Doc".equals(attribute.name()) || attribute.arguments().isEmpty()) {
continue;
}
final var argument = attribute.arguments().getFirst();
if (!"markdown".equals(argument.name()) || !(argument.value() instanceof PbsAst.AttributeDocTextBlockValue docValue)) {
continue;
}
return p.studio.compiler.pbs.PbsDocumentationTextBlockNormalizer.normalizeLexeme(docValue.value());
}
return "";
} }
private PbsEditorialCompletionCandidate completionFromTopDecl(final TopDeclSymbol topDeclSymbol) { private PbsEditorialCompletionCandidate completionFromTopDecl(final TopDeclSymbol topDeclSymbol) {
@ -690,7 +753,8 @@ public final class PbsEditorialSupportService {
topDeclSymbol.localName(), topDeclSymbol.localName(),
symbolKind(topDeclSymbol.decl()), symbolKind(topDeclSymbol.decl()),
detail, detail,
topDeclSymbol.origin()); topDeclSymbol.origin(),
documentationFromTopDecl(topDeclSymbol.decl()));
} }
private List<PbsEditorialSignature> signaturesForTopDecl(final PbsAst.TopDecl topDecl) { private List<PbsEditorialSignature> signaturesForTopDecl(final PbsAst.TopDecl topDecl) {
@ -1155,6 +1219,7 @@ public final class PbsEditorialSupportService {
functionDecl.returnKind(), functionDecl.returnKind(),
functionDecl.returnType(), functionDecl.returnType(),
functionDecl.resultErrorType(), functionDecl.resultErrorType(),
functionDecl.attributes(),
functionDecl.lifecycleMarker(), functionDecl.lifecycleMarker(),
functionDecl.body(), functionDecl.body(),
functionDecl.span()); functionDecl.span());
@ -1166,6 +1231,7 @@ public final class PbsEditorialSupportService {
structDecl.methods(), structDecl.methods(),
structDecl.ctors(), structDecl.ctors(),
structDecl.hasBody(), structDecl.hasBody(),
structDecl.attributes(),
structDecl.span()); structDecl.span());
} }
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
@ -1177,13 +1243,13 @@ public final class PbsEditorialSupportService {
builtinTypeDecl.span()); builtinTypeDecl.span());
} }
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
return new PbsAst.ServiceDecl(newName, serviceDecl.methods(), serviceDecl.span()); return new PbsAst.ServiceDecl(newName, serviceDecl.methods(), serviceDecl.attributes(), serviceDecl.span());
} }
if (topDecl instanceof PbsAst.HostDecl hostDecl) { if (topDecl instanceof PbsAst.HostDecl hostDecl) {
return new PbsAst.HostDecl(newName, hostDecl.signatures(), hostDecl.span()); return new PbsAst.HostDecl(newName, hostDecl.signatures(), hostDecl.attributes(), hostDecl.span());
} }
if (topDecl instanceof PbsAst.ContractDecl contractDecl) { if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
return new PbsAst.ContractDecl(newName, contractDecl.signatures(), contractDecl.span()); return new PbsAst.ContractDecl(newName, contractDecl.signatures(), contractDecl.attributes(), contractDecl.span());
} }
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
return new PbsAst.CallbackDecl( return new PbsAst.CallbackDecl(
@ -1192,13 +1258,14 @@ public final class PbsEditorialSupportService {
callbackDecl.returnKind(), callbackDecl.returnKind(),
callbackDecl.returnType(), callbackDecl.returnType(),
callbackDecl.resultErrorType(), callbackDecl.resultErrorType(),
callbackDecl.attributes(),
callbackDecl.span()); callbackDecl.span());
} }
if (topDecl instanceof PbsAst.EnumDecl enumDecl) { if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
return new PbsAst.EnumDecl(newName, enumDecl.cases(), enumDecl.span()); return new PbsAst.EnumDecl(newName, enumDecl.cases(), enumDecl.attributes(), enumDecl.span());
} }
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
return new PbsAst.ErrorDecl(newName, errorDecl.cases(), errorDecl.span()); return new PbsAst.ErrorDecl(newName, errorDecl.cases(), errorDecl.attributes(), errorDecl.span());
} }
if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
return new PbsAst.GlobalDecl(newName, globalDecl.explicitType(), globalDecl.initializer(), globalDecl.span()); return new PbsAst.GlobalDecl(newName, globalDecl.explicitType(), globalDecl.initializer(), globalDecl.span());

View File

@ -146,7 +146,8 @@ final class PbsFlowSemanticSupport {
List<CallableParameter> parameters, List<CallableParameter> parameters,
List<TypeView> inputTypes, List<TypeView> inputTypes,
TypeView outputType, TypeView outputType,
Span span) { Span span,
String documentation) {
} }
record CallableParameter( record CallableParameter(
@ -282,7 +283,8 @@ final class PbsFlowSemanticSupport {
method.returnKind(), method.returnKind(),
method.returnType(), method.returnType(),
method.resultErrorType(), method.resultErrorType(),
method.span())); method.span(),
documentationFromAttributes(method.attributes())));
} }
structs.put(structDecl.name(), new StructInfo(fields, methods)); structs.put(structDecl.name(), new StructInfo(fields, methods));
return; return;
@ -306,7 +308,8 @@ final class PbsFlowSemanticSupport {
signature.returnKind(), signature.returnKind(),
signature.returnType(), signature.returnType(),
signature.resultErrorType(), signature.resultErrorType(),
signature.span())); signature.span(),
documentationFromAttributes(signature.attributes())));
} }
structs.put(builtinTypeDecl.name(), new StructInfo(fields, methods)); structs.put(builtinTypeDecl.name(), new StructInfo(fields, methods));
return; return;
@ -321,7 +324,8 @@ final class PbsFlowSemanticSupport {
method.returnKind(), method.returnKind(),
method.returnType(), method.returnType(),
method.resultErrorType(), method.resultErrorType(),
method.span())); method.span(),
documentationFromAttributes(method.attributes())));
} }
services.put(serviceDecl.name(), new ServiceInfo(methods)); services.put(serviceDecl.name(), new ServiceInfo(methods));
serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name())); serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name()));
@ -337,7 +341,8 @@ final class PbsFlowSemanticSupport {
signature.returnKind(), signature.returnKind(),
signature.returnType(), signature.returnType(),
signature.resultErrorType(), signature.resultErrorType(),
signature.span())); signature.span(),
documentationFromAttributes(signature.attributes())));
} }
// Host owners are value singletons with callable members, same access shape as services. // Host owners are value singletons with callable members, same access shape as services.
services.put(hostDecl.name(), new ServiceInfo(methods)); services.put(hostDecl.name(), new ServiceInfo(methods));
@ -354,7 +359,8 @@ final class PbsFlowSemanticSupport {
signature.returnKind(), signature.returnKind(),
signature.returnType(), signature.returnType(),
signature.resultErrorType(), signature.resultErrorType(),
signature.span())); signature.span(),
documentationFromAttributes(signature.attributes())));
} }
contracts.put(contractDecl.name(), new ContractInfo(methods)); contracts.put(contractDecl.name(), new ContractInfo(methods));
return; return;
@ -367,12 +373,14 @@ final class PbsFlowSemanticSupport {
functionDecl.returnKind(), functionDecl.returnKind(),
functionDecl.returnType(), functionDecl.returnType(),
functionDecl.resultErrorType(), functionDecl.resultErrorType(),
functionDecl.span())); functionDecl.span(),
documentationFromAttributes(functionDecl.attributes())));
return; return;
} }
if (topDecl instanceof PbsAst.CallbackDecl( if (topDecl instanceof PbsAst.CallbackDecl(
String name, ReadOnlyList<PbsAst.Parameter> parameters, PbsAst.ReturnKind returnKind, String name, ReadOnlyList<PbsAst.Parameter> parameters, PbsAst.ReturnKind returnKind,
PbsAst.TypeRef returnType, PbsAst.TypeRef resultErrorType, Span span PbsAst.TypeRef returnType, PbsAst.TypeRef resultErrorType,
ReadOnlyList<PbsAst.Attribute> ignoredAttributes, Span span
)) { )) {
final var symbol = callableFrom( final var symbol = callableFrom(
name, name,
@ -380,7 +388,8 @@ final class PbsFlowSemanticSupport {
returnKind, returnKind,
returnType, returnType,
resultErrorType, resultErrorType,
span); span,
"");
callbacks.put(name, new CallbackSignature(symbol.inputTypes(), symbol.outputType())); callbacks.put(name, new CallbackSignature(symbol.inputTypes(), symbol.outputType()));
return; return;
} }
@ -477,7 +486,8 @@ final class PbsFlowSemanticSupport {
final PbsAst.ReturnKind returnKind, final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType, final PbsAst.TypeRef returnType,
final PbsAst.TypeRef resultErrorType, final PbsAst.TypeRef resultErrorType,
final Span span) { final Span span,
final String documentation) {
final var input = new ArrayList<TypeView>(parameters.size()); final var input = new ArrayList<TypeView>(parameters.size());
final var semanticParameters = new ArrayList<CallableParameter>(parameters.size()); final var semanticParameters = new ArrayList<CallableParameter>(parameters.size());
for (int index = 0; index < parameters.size(); index += 1) { for (int index = 0; index < parameters.size(); index += 1) {
@ -490,7 +500,21 @@ final class PbsFlowSemanticSupport {
type, type,
parameter.span())); parameter.span()));
} }
return new CallableSymbol(name, semanticParameters, input, callableReturn(returnKind, returnType, resultErrorType), span); return new CallableSymbol(name, semanticParameters, input, callableReturn(returnKind, returnType, resultErrorType), span, documentation);
}
private String documentationFromAttributes(final ReadOnlyList<PbsAst.Attribute> attributes) {
for (final var attribute : attributes) {
if (!"Doc".equals(attribute.name()) || attribute.arguments().isEmpty()) {
continue;
}
final var argument = attribute.arguments().getFirst();
if (!"markdown".equals(argument.name()) || !(argument.value() instanceof PbsAst.AttributeDocTextBlockValue docValue)) {
continue;
}
return p.studio.compiler.pbs.PbsDocumentationTextBlockNormalizer.normalizeLexeme(docValue.value());
}
return "";
} }
private TypeView callableReturn( private TypeView callableReturn(

View File

@ -1,61 +1,136 @@
[Doc(markdown = """
Represents a packed RGBA color.
""")]
[BuiltinType(name = "color", version = 1)] [BuiltinType(name = "color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub raw: int pub raw: int
) { ) {
[Doc(markdown = """
Builds a color from its packed integer representation.
- `raw`: packed color value.
""")]
[IntrinsicCall(name = "from_raw", version = 1)] [IntrinsicCall(name = "from_raw", version = 1)]
fn from_raw(raw: int) -> Color; fn from_raw(raw: int) -> Color;
[Doc(markdown = """
Builds an opaque color from red, green, and blue channels.
- `r`: red channel.
- `g`: green channel.
- `b`: blue channel.
""")]
[IntrinsicCall(name = "rgb", version = 1)] [IntrinsicCall(name = "rgb", version = 1)]
fn rgb(r: int, g: int, b: int) -> Color; fn rgb(r: int, g: int, b: int) -> Color;
[Doc(markdown = """
Builds a color from red, green, blue, and alpha channels.
- `r`: red channel.
- `g`: green channel.
- `b`: blue channel.
- `a`: alpha channel.
""")]
[IntrinsicCall(name = "rgba", version = 1)] [IntrinsicCall(name = "rgba", version = 1)]
fn rgba(r: int, g: int, b: int, a: int) -> Color; fn rgba(r: int, g: int, b: int, a: int) -> Color;
[Doc(markdown = """
Parses a CSS-style RGBA color string.
- `value`: color text in HTML/CSS format.
""")]
[IntrinsicCall(name = "html_rgba", version = 1)] [IntrinsicCall(name = "html_rgba", version = 1)]
fn html_rgba(value: str) -> Color; fn html_rgba(value: str) -> Color;
[Doc(markdown = """
Builds an opaque grayscale color.
- `value`: grayscale channel value.
""")]
[IntrinsicCall(name = "gray_scale", version = 1)] [IntrinsicCall(name = "gray_scale", version = 1)]
fn gray_scale(value: int) -> Color; fn gray_scale(value: int) -> Color;
[Doc(markdown = """
Returns the packed RGB portion of this color.
""")]
[IntrinsicCall(name = "hex", version = 1)] [IntrinsicCall(name = "hex", version = 1)]
fn hex() -> int; fn hex() -> int;
[Doc(markdown = """
Returns the alpha channel of this color.
""")]
[IntrinsicCall(name = "alpha", version = 1)] [IntrinsicCall(name = "alpha", version = 1)]
fn alpha() -> int; fn alpha() -> int;
} }
[Doc(markdown = """
Opaque black.
""")]
[BuiltinConst(target = "color", name = "black", version = 1)] [BuiltinConst(target = "color", name = "black", version = 1)]
declare const BLACK: Color; declare const BLACK: Color;
[Doc(markdown = """
Opaque white.
""")]
[BuiltinConst(target = "color", name = "white", version = 1)] [BuiltinConst(target = "color", name = "white", version = 1)]
declare const WHITE: Color; declare const WHITE: Color;
[Doc(markdown = """
Opaque red.
""")]
[BuiltinConst(target = "color", name = "red", version = 1)] [BuiltinConst(target = "color", name = "red", version = 1)]
declare const RED: Color; declare const RED: Color;
[Doc(markdown = """
Opaque green.
""")]
[BuiltinConst(target = "color", name = "green", version = 1)] [BuiltinConst(target = "color", name = "green", version = 1)]
declare const GREEN: Color; declare const GREEN: Color;
[Doc(markdown = """
Opaque blue.
""")]
[BuiltinConst(target = "color", name = "blue", version = 1)] [BuiltinConst(target = "color", name = "blue", version = 1)]
declare const BLUE: Color; declare const BLUE: Color;
[Doc(markdown = """
Opaque yellow.
""")]
[BuiltinConst(target = "color", name = "yellow", version = 1)] [BuiltinConst(target = "color", name = "yellow", version = 1)]
declare const YELLOW: Color; declare const YELLOW: Color;
[Doc(markdown = """
Opaque orange.
""")]
[BuiltinConst(target = "color", name = "orange", version = 1)] [BuiltinConst(target = "color", name = "orange", version = 1)]
declare const ORANGE: Color; declare const ORANGE: Color;
[Doc(markdown = """
Opaque indigo.
""")]
[BuiltinConst(target = "color", name = "indigo", version = 1)] [BuiltinConst(target = "color", name = "indigo", version = 1)]
declare const INDIGO: Color; declare const INDIGO: Color;
[Doc(markdown = """
Opaque gray.
""")]
[BuiltinConst(target = "color", name = "gray", version = 1)] [BuiltinConst(target = "color", name = "gray", version = 1)]
declare const GRAY: Color; declare const GRAY: Color;
[Doc(markdown = """
Opaque cyan.
""")]
[BuiltinConst(target = "color", name = "cyan", version = 1)] [BuiltinConst(target = "color", name = "cyan", version = 1)]
declare const CYAN: Color; declare const CYAN: Color;
[Doc(markdown = """
Opaque magenta.
""")]
[BuiltinConst(target = "color", name = "magenta", version = 1)] [BuiltinConst(target = "color", name = "magenta", version = 1)]
declare const MAGENTA: Color; declare const MAGENTA: Color;
[Doc(markdown = """
Fully transparent color.
""")]
[BuiltinConst(target = "color", name = "transparent", version = 1)] [BuiltinConst(target = "color", name = "transparent", version = 1)]
declare const TRANSPARENT: Color; declare const TRANSPARENT: Color;

View File

@ -1,35 +1,83 @@
[Doc(markdown = """
Low-level asset loading host bindings.
""")]
declare host LowAssets { declare host LowAssets {
[Doc(markdown = """
Starts loading an addressable asset into a slot.
- `addressable`: asset reference.
- `slot`: destination slot.
""")]
[Host(module = "asset", name = "load", version = 1)] [Host(module = "asset", name = "load", version = 1)]
[Capability(name = "asset")] [Capability(name = "asset")]
[AssetLowering(param = 0)] [AssetLowering(param = 0)]
fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int); fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int);
[Doc(markdown = """
Reads the status of an asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
[Host(module = "asset", name = "status", version = 1)] [Host(module = "asset", name = "status", version = 1)]
[Capability(name = "asset")] [Capability(name = "asset")]
fn status(loading_handle: int) -> int; fn status(loading_handle: int) -> int;
[Doc(markdown = """
Commits a completed asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
[Host(module = "asset", name = "commit", version = 1)] [Host(module = "asset", name = "commit", version = 1)]
[Capability(name = "asset")] [Capability(name = "asset")]
fn commit(loading_handle: int) -> int; fn commit(loading_handle: int) -> int;
[Doc(markdown = """
Cancels an asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
[Host(module = "asset", name = "cancel", version = 1)] [Host(module = "asset", name = "cancel", version = 1)]
[Capability(name = "asset")] [Capability(name = "asset")]
fn cancel(loading_handle: int) -> int; fn cancel(loading_handle: int) -> int;
} }
[Doc(markdown = """
High-level asset loading commands.
""")]
declare service Assets { declare service Assets {
[Doc(markdown = """
Starts loading an addressable asset into a slot.
- `addressable`: asset reference.
- `slot`: destination slot.
""")]
fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int) { fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int) {
return LowAssets.load(addressable, slot); return LowAssets.load(addressable, slot);
} }
[Doc(markdown = """
Reads the status of an asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
fn status(loading_handle: int) -> int { fn status(loading_handle: int) -> int {
return LowAssets.status(loading_handle); return LowAssets.status(loading_handle);
} }
[Doc(markdown = """
Commits a completed asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
fn commit(loading_handle: int) -> int { fn commit(loading_handle: int) -> int {
return LowAssets.commit(loading_handle); return LowAssets.commit(loading_handle);
} }
[Doc(markdown = """
Cancels an asset loading operation.
- `loading_handle`: handle returned by `load`.
""")]
fn cancel(loading_handle: int) -> int { fn cancel(loading_handle: int) -> int {
return LowAssets.cancel(loading_handle); return LowAssets.cancel(loading_handle);
} }

View File

@ -1,16 +1,46 @@
[Doc(markdown = """
Low-level scene composer host bindings.
""")]
declare host LowComposer { declare host LowComposer {
[Doc(markdown = """
Sets the scene camera position.
- `x`: camera x coordinate.
- `y`: camera y coordinate.
""")]
[Host(module = "composer", name = "set_camera", version = 1)] [Host(module = "composer", name = "set_camera", version = 1)]
[Capability(name = "composer")] [Capability(name = "composer")]
fn set_camera(x: int, y: int) -> void; fn set_camera(x: int, y: int) -> void;
[Doc(markdown = """
Binds a scene bank for composer emission.
- `scene_bank_id`: scene bank identifier.
""")]
[Host(module = "composer", name = "bind_scene", version = 1)] [Host(module = "composer", name = "bind_scene", version = 1)]
[Capability(name = "composer")] [Capability(name = "composer")]
fn bind_scene(scene_bank_id: int) -> int; fn bind_scene(scene_bank_id: int) -> int;
[Doc(markdown = """
Unbinds the active scene bank.
""")]
[Host(module = "composer", name = "unbind_scene", version = 1)] [Host(module = "composer", name = "unbind_scene", version = 1)]
[Capability(name = "composer")] [Capability(name = "composer")]
fn unbind_scene() -> int; fn unbind_scene() -> int;
[Doc(markdown = """
Emits a sprite command into the active scene.
- `glyph_id`: glyph identifier.
- `palette_id`: palette identifier.
- `x`: screen x coordinate.
- `y`: screen y coordinate.
- `layer`: render layer.
- `bank_id`: asset bank identifier.
- `flip_x`: mirror horizontally.
- `flip_y`: mirror vertically.
- `priority`: ordering priority.
""")]
[Host(module = "composer", name = "emit_sprite", version = 1)] [Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "composer")] [Capability(name = "composer")]
fn emit_sprite( fn emit_sprite(
@ -26,23 +56,53 @@ declare host LowComposer {
) -> int; ) -> int;
} }
[Doc(markdown = """
High-level scene composition commands.
""")]
declare service Composer declare service Composer
{ {
[Doc(markdown = """
Sets the scene camera position.
- `x`: camera x coordinate.
- `y`: camera y coordinate.
""")]
fn set_camera(x: int, y: int) -> void fn set_camera(x: int, y: int) -> void
{ {
LowComposer.set_camera(x, y); LowComposer.set_camera(x, y);
} }
[Doc(markdown = """
Binds a scene bank for composer emission.
- `scene_bank_id`: scene bank identifier.
""")]
fn bind_scene(scene_bank_id: int) -> int fn bind_scene(scene_bank_id: int) -> int
{ {
return LowComposer.bind_scene(scene_bank_id); return LowComposer.bind_scene(scene_bank_id);
} }
[Doc(markdown = """
Unbinds the active scene bank.
""")]
fn unbind_scene() -> int fn unbind_scene() -> int
{ {
return LowComposer.unbind_scene(); return LowComposer.unbind_scene();
} }
[Doc(markdown = """
Emits a sprite command into the active scene.
- `glyph_id`: glyph identifier.
- `palette_id`: palette identifier.
- `x`: screen x coordinate.
- `y`: screen y coordinate.
- `layer`: render layer.
- `bank_id`: asset bank identifier.
- `flip_x`: mirror horizontally.
- `flip_y`: mirror vertically.
- `priority`: ordering priority.
""")]
fn emit_sprite( fn emit_sprite(
glyph_id: int, glyph_id: int,
palette_id: int, palette_id: int,

View File

@ -1,67 +1,189 @@
import { Color } from @core:color; import { Color } from @core:color;
[Doc(markdown = """
Low-level 2D graphics host bindings.
""")]
declare host LowGfx { declare host LowGfx {
[Doc(markdown = """
Clears the render target.
- `color`: fill color.
""")]
[Host(module = "gfx2d", name = "clear", version = 1)] [Host(module = "gfx2d", name = "clear", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn clear(color: Color) -> void; fn clear(color: Color) -> void;
[Doc(markdown = """
Draws a filled rectangle.
- `x`: left coordinate.
- `y`: top coordinate.
- `w`: rectangle width.
- `h`: rectangle height.
- `color`: fill color.
""")]
[Host(module = "gfx2d", name = "fill_rect", version = 1)] [Host(module = "gfx2d", name = "fill_rect", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn fill_rect(x: int, y: int, w: int, h: int, color: Color) -> void; fn fill_rect(x: int, y: int, w: int, h: int, color: Color) -> void;
[Doc(markdown = """
Draws a line segment.
- `x1`: start x coordinate.
- `y1`: start y coordinate.
- `x2`: end x coordinate.
- `y2`: end y coordinate.
- `color`: line color.
""")]
[Host(module = "gfx2d", name = "draw_line", version = 1)] [Host(module = "gfx2d", name = "draw_line", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_line(x1: int, y1: int, x2: int, y2: int, color: Color) -> void; fn draw_line(x1: int, y1: int, x2: int, y2: int, color: Color) -> void;
[Doc(markdown = """
Draws a circle outline.
- `x`: center x coordinate.
- `y`: center y coordinate.
- `r`: radius.
- `color`: outline color.
""")]
[Host(module = "gfx2d", name = "draw_circle", version = 1)] [Host(module = "gfx2d", name = "draw_circle", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_circle(x: int, y: int, r: int, color: Color) -> void; fn draw_circle(x: int, y: int, r: int, color: Color) -> void;
[Doc(markdown = """
Draws a filled circle.
- `x`: center x coordinate.
- `y`: center y coordinate.
- `r`: radius.
- `border_color`: outline color.
- `fill_color`: fill color.
""")]
[Host(module = "gfx2d", name = "draw_disc", version = 1)] [Host(module = "gfx2d", name = "draw_disc", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_disc(x: int, y: int, r: int, border_color: Color, fill_color: Color) -> void; fn draw_disc(x: int, y: int, r: int, border_color: Color, fill_color: Color) -> void;
[Doc(markdown = """
Draws a filled rectangle with a border.
- `x`: left coordinate.
- `y`: top coordinate.
- `w`: rectangle width.
- `h`: rectangle height.
- `border_color`: border color.
- `fill_color`: fill color.
""")]
[Host(module = "gfx2d", name = "draw_square", version = 1)] [Host(module = "gfx2d", name = "draw_square", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_square(x: int, y: int, w: int, h: int, border_color: Color, fill_color: Color) -> void; fn draw_square(x: int, y: int, w: int, h: int, border_color: Color, fill_color: Color) -> void;
[Doc(markdown = """
Draws text at a screen position.
- `x`: left coordinate.
- `y`: baseline coordinate.
- `message`: text to draw.
- `color`: text color.
""")]
[Host(module = "gfx2d", name = "draw_text", version = 1)] [Host(module = "gfx2d", name = "draw_text", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_text(x: int, y: int, message: str, color: Color) -> void; fn draw_text(x: int, y: int, message: str, color: Color) -> void;
} }
[Doc(markdown = """
High-level 2D drawing commands.
""")]
declare service Gfx declare service Gfx
{ {
[Doc(markdown = """
Clears the render target.
- `color`: fill color.
""")]
fn clear(color: Color) -> void fn clear(color: Color) -> void
{ {
LowGfx.clear(color); LowGfx.clear(color);
} }
[Doc(markdown = """
Draws a filled rectangle.
- `x`: left coordinate.
- `y`: top coordinate.
- `w`: rectangle width.
- `h`: rectangle height.
- `color`: fill color.
""")]
fn fill_rect(x: int, y: int, w: int, h: int, color: Color) -> void fn fill_rect(x: int, y: int, w: int, h: int, color: Color) -> void
{ {
LowGfx.fill_rect(x, y, w, h, color); LowGfx.fill_rect(x, y, w, h, color);
} }
[Doc(markdown = """
Draws a line segment.
- `x1`: start x coordinate.
- `y1`: start y coordinate.
- `x2`: end x coordinate.
- `y2`: end y coordinate.
- `color`: line color.
""")]
fn draw_line(x1: int, y1: int, x2: int, y2: int, color: Color) -> void fn draw_line(x1: int, y1: int, x2: int, y2: int, color: Color) -> void
{ {
LowGfx.draw_line(x1, y1, x2, y2, color); LowGfx.draw_line(x1, y1, x2, y2, color);
} }
[Doc(markdown = """
Draws a circle outline.
- `x`: center x coordinate.
- `y`: center y coordinate.
- `r`: radius.
- `color`: outline color.
""")]
fn draw_circle(x: int, y: int, r: int, color: Color) -> void fn draw_circle(x: int, y: int, r: int, color: Color) -> void
{ {
LowGfx.draw_circle(x, y, r, color); LowGfx.draw_circle(x, y, r, color);
} }
[Doc(markdown = """
Draws a filled circle.
- `x`: center x coordinate.
- `y`: center y coordinate.
- `r`: radius.
- `border_color`: outline color.
- `fill_color`: fill color.
""")]
fn draw_disc(x: int, y: int, r: int, border_color: Color, fill_color: Color) -> void fn draw_disc(x: int, y: int, r: int, border_color: Color, fill_color: Color) -> void
{ {
LowGfx.draw_disc(x, y, r, border_color, fill_color); LowGfx.draw_disc(x, y, r, border_color, fill_color);
} }
[Doc(markdown = """
Draws a filled rectangle with a border.
- `x`: left coordinate.
- `y`: top coordinate.
- `w`: rectangle width.
- `h`: rectangle height.
- `border_color`: border color.
- `fill_color`: fill color.
""")]
fn draw_square(x: int, y: int, w: int, h: int, border_color: Color, fill_color: Color) -> void fn draw_square(x: int, y: int, w: int, h: int, border_color: Color, fill_color: Color) -> void
{ {
LowGfx.draw_square(x, y, w, h, border_color, fill_color); LowGfx.draw_square(x, y, w, h, border_color, fill_color);
} }
[Doc(markdown = """
Draws text at a screen position.
- `x`: left coordinate.
- `y`: baseline coordinate.
- `message`: text to draw.
- `color`: text color.
""")]
fn draw_text(x: int, y: int, message: str, color: Color) -> void fn draw_text(x: int, y: int, message: str, color: Color) -> void
{ {
LowGfx.draw_text(x, y, message, color); LowGfx.draw_text(x, y, message, color);

View File

@ -1,85 +1,169 @@
[Doc(markdown = """
Root input device provider.
""")]
[BuiltinType(name = "input", version = 1)] [BuiltinType(name = "input", version = 1)]
declare builtin type InputType() { declare builtin type InputType() {
[Doc(markdown = """
Returns the gamepad input state.
""")]
[IntrinsicCall(name = "pad", version = 1)] [IntrinsicCall(name = "pad", version = 1)]
fn pad() -> InputPad; fn pad() -> InputPad;
[Doc(markdown = """
Returns the touch input state.
""")]
[IntrinsicCall(name = "touch", version = 1)] [IntrinsicCall(name = "touch", version = 1)]
fn touch() -> InputTouch; fn touch() -> InputTouch;
} }
[Doc(markdown = """
Represents the current gamepad state.
""")]
[BuiltinType(name = "input.pad", version = 1)] [BuiltinType(name = "input.pad", version = 1)]
declare builtin type InputPad() { declare builtin type InputPad() {
[Doc(markdown = """
Returns the up directional button.
""")]
[IntrinsicCall(name = "up", version = 1)] [IntrinsicCall(name = "up", version = 1)]
fn up() -> InputButton; fn up() -> InputButton;
[Doc(markdown = """
Returns the down directional button.
""")]
[IntrinsicCall(name = "down", version = 1)] [IntrinsicCall(name = "down", version = 1)]
fn down() -> InputButton; fn down() -> InputButton;
[Doc(markdown = """
Returns the left directional button.
""")]
[IntrinsicCall(name = "left", version = 1)] [IntrinsicCall(name = "left", version = 1)]
fn left() -> InputButton; fn left() -> InputButton;
[Doc(markdown = """
Returns the right directional button.
""")]
[IntrinsicCall(name = "right", version = 1)] [IntrinsicCall(name = "right", version = 1)]
fn right() -> InputButton; fn right() -> InputButton;
[Doc(markdown = """
Returns the A action button.
""")]
[IntrinsicCall(name = "a", version = 1)] [IntrinsicCall(name = "a", version = 1)]
fn a() -> InputButton; fn a() -> InputButton;
[Doc(markdown = """
Returns the B action button.
""")]
[IntrinsicCall(name = "b", version = 1)] [IntrinsicCall(name = "b", version = 1)]
fn b() -> InputButton; fn b() -> InputButton;
[Doc(markdown = """
Returns the X action button.
""")]
[IntrinsicCall(name = "x", version = 1)] [IntrinsicCall(name = "x", version = 1)]
fn x() -> InputButton; fn x() -> InputButton;
[Doc(markdown = """
Returns the Y action button.
""")]
[IntrinsicCall(name = "y", version = 1)] [IntrinsicCall(name = "y", version = 1)]
fn y() -> InputButton; fn y() -> InputButton;
[Doc(markdown = """
Returns the left shoulder button.
""")]
[IntrinsicCall(name = "l", version = 1)] [IntrinsicCall(name = "l", version = 1)]
fn l() -> InputButton; fn l() -> InputButton;
[Doc(markdown = """
Returns the right shoulder button.
""")]
[IntrinsicCall(name = "r", version = 1)] [IntrinsicCall(name = "r", version = 1)]
fn r() -> InputButton; fn r() -> InputButton;
[Doc(markdown = """
Returns the start button.
""")]
[IntrinsicCall(name = "start", version = 1)] [IntrinsicCall(name = "start", version = 1)]
fn start() -> InputButton; fn start() -> InputButton;
[Doc(markdown = """
Returns the select button.
""")]
[IntrinsicCall(name = "select", version = 1)] [IntrinsicCall(name = "select", version = 1)]
fn select() -> InputButton; fn select() -> InputButton;
} }
[Doc(markdown = """
Represents the current touch input state.
""")]
[BuiltinType(name = "input.touch", version = 1)] [BuiltinType(name = "input.touch", version = 1)]
declare builtin type InputTouch() { declare builtin type InputTouch() {
[Doc(markdown = """
Returns the touch contact as a button state.
""")]
[IntrinsicCall(name = "button", version = 1)] [IntrinsicCall(name = "button", version = 1)]
fn button() -> InputButton; fn button() -> InputButton;
[Doc(markdown = """
Returns the current touch x coordinate.
""")]
[IntrinsicCall(name = "x", version = 1)] [IntrinsicCall(name = "x", version = 1)]
fn x() -> int; fn x() -> int;
[Doc(markdown = """
Returns the current touch y coordinate.
""")]
[IntrinsicCall(name = "y", version = 1)] [IntrinsicCall(name = "y", version = 1)]
fn y() -> int; fn y() -> int;
} }
[Doc(markdown = """
Represents one digital input button.
""")]
[BuiltinType(name = "input.button", version = 1)] [BuiltinType(name = "input.button", version = 1)]
declare builtin type InputButton() { declare builtin type InputButton() {
[Doc(markdown = """
Returns true on the frame where the button becomes down.
""")]
[IntrinsicCall(name = "pressed", version = 1)] [IntrinsicCall(name = "pressed", version = 1)]
fn pressed() -> bool; fn pressed() -> bool;
[Doc(markdown = """
Returns true on the frame where the button becomes up.
""")]
[IntrinsicCall(name = "released", version = 1)] [IntrinsicCall(name = "released", version = 1)]
fn released() -> bool; fn released() -> bool;
[Doc(markdown = """
Returns true while the button is currently down.
""")]
[IntrinsicCall(name = "down", version = 1)] [IntrinsicCall(name = "down", version = 1)]
fn down() -> bool; fn down() -> bool;
[Doc(markdown = """
Returns how long the button has been held.
""")]
[IntrinsicCall(name = "hold", version = 1)] [IntrinsicCall(name = "hold", version = 1)]
fn hold() -> int; fn hold() -> int;
} }
[Doc(markdown = """
High-level access to player input devices.
""")]
declare service Input declare service Input
{ {
[Doc(markdown = """
Returns the gamepad input state.
""")]
fn pad() -> InputPad fn pad() -> InputPad
{ {
return InputType.pad(); return InputType.pad();
} }
[Doc(markdown = """
Returns the touch input state.
""")]
fn touch() -> InputTouch fn touch() -> InputTouch
{ {
return InputType.touch(); return InputType.touch();

View File

@ -1,60 +1,134 @@
[Doc(markdown = """
Low-level logging host bindings.
""")]
declare host LowLog { declare host LowLog {
[Doc(markdown = """
Writes a log message at a numeric level.
- `level`: severity level.
- `message`: text to write.
""")]
[Host(module = "log", name = "write", version = 1)] [Host(module = "log", name = "write", version = 1)]
[Capability(name = "log")] [Capability(name = "log")]
fn write(level: int, message: str) -> void; fn write(level: int, message: str) -> void;
[Doc(markdown = """
Writes a tagged log message at a numeric level.
- `level`: severity level.
- `tag`: application-defined tag.
- `message`: text to write.
""")]
[Host(module = "log", name = "write_tag", version = 1)] [Host(module = "log", name = "write_tag", version = 1)]
[Capability(name = "log")] [Capability(name = "log")]
fn write_tag(level: int, tag: int, message: str) -> void; fn write_tag(level: int, tag: int, message: str) -> void;
} }
[Doc(markdown = """
High-level logging commands.
""")]
declare service Log declare service Log
{ {
[Doc(markdown = """
Writes a trace log message.
- `msg`: text to write.
""")]
fn trace(msg: str) -> void fn trace(msg: str) -> void
{ {
LowLog.write(0, msg); LowLog.write(0, msg);
} }
[Doc(markdown = """
Writes a debug log message.
- `msg`: text to write.
""")]
fn debug(msg: str) -> void fn debug(msg: str) -> void
{ {
LowLog.write(1, msg); LowLog.write(1, msg);
} }
[Doc(markdown = """
Writes an informational log message.
- `msg`: text to write.
""")]
fn info(msg: str) -> void fn info(msg: str) -> void
{ {
LowLog.write(2, msg); LowLog.write(2, msg);
} }
[Doc(markdown = """
Writes a warning log message.
- `msg`: text to write.
""")]
fn warn(msg: str) -> void fn warn(msg: str) -> void
{ {
LowLog.write(3, msg); LowLog.write(3, msg);
} }
[Doc(markdown = """
Writes a failure log message.
- `msg`: text to write.
""")]
fn failure(msg: str) -> void fn failure(msg: str) -> void
{ {
LowLog.write(4, msg); LowLog.write(4, msg);
} }
[Doc(markdown = """
Writes a tagged trace log message.
- `tag`: application-defined tag.
- `msg`: text to write.
""")]
fn trace(tag: int, msg: str) -> void fn trace(tag: int, msg: str) -> void
{ {
LowLog.write_tag(0, tag, msg); LowLog.write_tag(0, tag, msg);
} }
[Doc(markdown = """
Writes a tagged debug log message.
- `tag`: application-defined tag.
- `msg`: text to write.
""")]
fn debug(tag: int, msg: str) -> void fn debug(tag: int, msg: str) -> void
{ {
LowLog.write_tag(1, tag, msg); LowLog.write_tag(1, tag, msg);
} }
[Doc(markdown = """
Writes a tagged informational log message.
- `tag`: application-defined tag.
- `msg`: text to write.
""")]
fn info(tag: int, msg: str) -> void fn info(tag: int, msg: str) -> void
{ {
LowLog.write_tag(2, tag, msg); LowLog.write_tag(2, tag, msg);
} }
[Doc(markdown = """
Writes a tagged warning log message.
- `tag`: application-defined tag.
- `msg`: text to write.
""")]
fn warn(tag: int, msg: str) -> void fn warn(tag: int, msg: str) -> void
{ {
LowLog.write_tag(3, tag, msg); LowLog.write_tag(3, tag, msg);
} }
[Doc(markdown = """
Writes a tagged failure log message.
- `tag`: application-defined tag.
- `msg`: text to write.
""")]
fn failure(tag: int, msg: str) -> void fn failure(tag: int, msg: str) -> void
{ {
LowLog.write_tag(4, tag, msg); LowLog.write_tag(4, tag, msg);

View File

@ -0,0 +1,54 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PbsDocumentationTextBlockNormalizerTest {
@Test
void shouldNormalizeIncidentalBoundaryLinesAndIndentation() {
final var lexeme = "\"\"\"\n" +
" Draws a sprite.\n" +
"\n" +
" - `x`: screen x\n" +
" - `y`: screen y\n" +
" \"\"\"";
final var normalized = PbsDocumentationTextBlockNormalizer.normalizeLexeme(lexeme);
assertEquals("Draws a sprite.\n\n- `x`: screen x\n- `y`: screen y", normalized);
}
@Test
void shouldPreserveInternalLineBreaksAndMarkdownContent() {
final var content = "\n" +
" Paragraph one.\n" +
" \n" +
" `code_span()`\n" +
" Paragraph two.\n";
final var normalized = PbsDocumentationTextBlockNormalizer.normalizeContent(content);
assertEquals("Paragraph one.\n\n`code_span()`\nParagraph two.", normalized);
}
@Test
void shouldReturnEmptyStringForWhitespaceOnlyPayload() {
final var normalized = PbsDocumentationTextBlockNormalizer.normalizeLexeme("\"\"\"\n \n \t\n\"\"\"");
assertEquals("", normalized.trim());
}
@Test
void shouldNormalizeCarriageReturnLineEndings() {
final var normalized = PbsDocumentationTextBlockNormalizer.normalizeContent("\r\n\tLine one\r\n\tLine two\r\n");
assertEquals("Line one\nLine two", normalized);
}
@Test
void shouldReturnEmptyStringForMalformedLexeme() {
assertEquals("", PbsDocumentationTextBlockNormalizer.normalizeLexeme("\"not a text block\""));
}
}

View File

@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.Addressable; import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.FESurfaceContext; import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.HostAdmissionContext; import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRBackendFile;
import p.studio.compiler.models.IRHiddenGlobalKind; import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRGlobalVisibility; import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRSyntheticCallableKind; import p.studio.compiler.models.IRSyntheticCallableKind;
@ -52,6 +54,57 @@ class PbsFrontendCompilerTest {
assertEquals(1, functions.get(1).parameterCount()); assertEquals(1, functions.get(1).parameterCount());
} }
@Test
void shouldNotLowerDocMetadataIntoRuntimeBackendArtifacts() {
final var undocumentedSource = """
declare global SCORE: int = 0;
fn add_one(value: int) -> int {
return value + 1;
}
[Init]
fn boot() -> void {
SCORE = add_one(41);
return;
}
""";
final var marker = "THIS_DOC_PAYLOAD_MUST_NOT_APPEAR_IN_RUNTIME_ARTIFACTS";
final var documentedSource = """
declare global SCORE: int = 0;
[Doc(markdown = \"""
Adds one to a value.
This deliberately large documentation payload is editor metadata only:
THIS_DOC_PAYLOAD_MUST_NOT_APPEAR_IN_RUNTIME_ARTIFACTS.
\""")]
fn add_one(value: int) -> int {
return value + 1;
}
[Doc(markdown = \"""
Initializes the score global.
\""")]
[Init]
fn boot() -> void {
SCORE = add_one(41);
return;
}
""";
final var compiler = new PbsFrontendCompiler();
final var undocumentedDiagnostics = DiagnosticSink.empty();
final var documentedDiagnostics = DiagnosticSink.empty();
final var undocumentedBackend = compiler.compileFile(new FileId(20), undocumentedSource, undocumentedDiagnostics);
final var documentedBackend = compiler.compileFile(new FileId(20), documentedSource, documentedDiagnostics);
assertTrue(undocumentedDiagnostics.isEmpty(), diagnosticsSummary(undocumentedDiagnostics));
assertTrue(documentedDiagnostics.isEmpty(), diagnosticsSummary(documentedDiagnostics));
assertEquals(runtimeFingerprint(undocumentedBackend), runtimeFingerprint(documentedBackend));
assertFalse(String.valueOf(documentedBackend).contains(marker));
}
@Test @Test
void shouldExposeGlobalsAndSyntheticLifecycleArtifactsInBackendFile() { void shouldExposeGlobalsAndSyntheticLifecycleArtifactsInBackendFile() {
final var source = """ final var source = """
@ -812,4 +865,62 @@ class PbsFrontendCompilerTest {
assertEquals(1L, pixel.fields().get(1).slotStart()); assertEquals(1L, pixel.fields().get(1).slotStart());
assertEquals(2L, pixel.fields().get(2).slotStart()); assertEquals(2L, pixel.fields().get(2).slotStart());
} }
private static String runtimeFingerprint(final IRBackendFile backend) {
final var out = new StringBuilder();
out.append("functions:");
backend.functions().forEach(function -> out
.append(function.name()).append('/')
.append(function.parameterCount()).append('/')
.append(function.hasExplicitReturnType()).append(';'));
out.append("synthetic:");
backend.syntheticFunctions().forEach(function -> out
.append(function.moduleId()).append('/')
.append(function.callableName()).append('/')
.append(function.kind()).append(';'));
out.append("globals:");
backend.globals().forEach(global -> out
.append(global.moduleId()).append('/')
.append(global.name()).append('/')
.append(global.declaredTypeSurface()).append('/')
.append(global.slot()).append('/')
.append(global.visibility()).append('/')
.append(global.hiddenKind()).append(';'));
out.append("executables:");
backend.executableFunctions().forEach(function -> {
out.append(function.moduleId()).append('/')
.append(function.callableName()).append('/')
.append(function.callableId().getIndex()).append('/')
.append(function.paramSlots()).append('/')
.append(function.localSlots()).append('/')
.append(function.returnSlots()).append('/')
.append(function.maxStackSlots()).append('{');
function.instructions().forEach(instruction -> appendInstructionFingerprint(out, instruction));
out.append("};");
});
out.append("modules:").append(backend.modulePool()).append(';');
out.append("callables:").append(backend.callableSignatures()).append(';');
out.append("intrinsics:").append(backend.intrinsicPool()).append(';');
return out.toString();
}
private static void appendInstructionFingerprint(
final StringBuilder out,
final IRBackendExecutableFunction.Instruction instruction) {
out.append(instruction.kind()).append('(')
.append(instruction.calleeModuleId()).append(',')
.append(instruction.calleeCallableName()).append(',')
.append(instruction.calleeCallableId() == null ? "" : instruction.calleeCallableId().getIndex()).append(',')
.append(instruction.hostCall()).append(',')
.append(instruction.intrinsicCall()).append(',')
.append(instruction.label()).append(',')
.append(instruction.targetLabel()).append(',')
.append(instruction.expectedArgSlots()).append(',')
.append(instruction.expectedRetSlots())
.append(");");
}
private static String diagnosticsSummary(final DiagnosticSink diagnostics) {
return diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString();
}
} }

View File

@ -113,6 +113,55 @@ class PbsLexerTest {
diagnostics.stream().findFirst().orElseThrow().getCode()); diagnostics.stream().findFirst().orElseThrow().getCode());
} }
@Test
void shouldLexDocumentationTextBlock() {
final var source = """
[Doc(markdown = \"""
Draws a sprite.
- `x`: screen x
- `y`: screen y
\""")]
""";
final var diagnostics = DiagnosticSink.empty();
final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);
final var docTextBlock = tokens.stream()
.filter(t -> t.kind() == PbsTokenKind.DOC_TEXT_BLOCK)
.findFirst()
.orElseThrow();
assertTrue(docTextBlock.lexeme().startsWith("\"\"\""));
assertTrue(docTextBlock.lexeme().endsWith("\"\"\""));
assertTrue(docTextBlock.lexeme().contains("Draws a sprite."));
assertTrue(docTextBlock.lexeme().contains("- `x`: screen x"));
assertTrue(diagnostics.isEmpty(), "Documentation text block should lex without diagnostics");
}
@Test
void shouldAllowSingleAndDoubleQuotesInsideDocumentationTextBlock() {
final var source = "[Doc(markdown = \"\"\"Uses \"quoted\" text and 'single' quotes.\"\"\")]";
final var diagnostics = DiagnosticSink.empty();
final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics);
final var kinds = tokens.stream().map(PbsToken::kind).toList();
assertTrue(kinds.contains(PbsTokenKind.DOC_TEXT_BLOCK));
assertTrue(diagnostics.isEmpty(), "Quotes inside documentation text block should not need escaping");
}
@Test
void shouldReportUnterminatedDocumentationTextBlock() {
final var source = "[Doc(markdown = \"\"\"unterminated)]";
final var diagnostics = DiagnosticSink.empty();
PbsLexer.lex(source, new FileId(0), diagnostics);
assertTrue(diagnostics.hasErrors(), "Lexer should report unterminated documentation text blocks");
assertEquals(LexErrors.E_LEX_UNTERMINATED_DOC_TEXT_BLOCK.name(),
diagnostics.stream().findFirst().orElseThrow().getCode());
}
@Test @Test
void shouldReportInvalidStringEscape() { void shouldReportInvalidStringEscape() {
final var source = "\"bad\\q\""; final var source = "\"bad\\q\"";

View File

@ -339,6 +339,82 @@ class PbsParserTest {
assertEquals(1, constDecl.attributes().size()); assertEquals(1, constDecl.attributes().size());
} }
@Test
void shouldParseDocumentationTextBlockAttributeValuesInInterfaceModuleMode() {
final var source = """
[Doc(markdown = \"""
Represents a packed color.
\""")]
[BuiltinType(name = "color", version = 1)]
declare builtin type Color(
pub raw: int
) {
[Doc(markdown = \"""
Packs this color into an integer.
\""")]
[IntrinsicCall(name = "color.pack", version = 1)]
fn pack() -> int;
}
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final PbsAst.File ast = PbsParser.parse(
PbsLexer.lex(source, fileId, diagnostics),
fileId,
diagnostics,
PbsParser.ParseMode.INTERFACE_MODULE);
assertTrue(diagnostics.isEmpty(), "Parser should accept documentation text block attribute values");
final var builtinType = assertInstanceOf(PbsAst.BuiltinTypeDecl.class, ast.topDecls().getFirst());
final var typeDoc = builtinType.attributes().getFirst().arguments().getFirst().value();
assertInstanceOf(PbsAst.AttributeDocTextBlockValue.class, typeDoc);
assertTrue(((PbsAst.AttributeDocTextBlockValue) typeDoc).value().contains("Represents a packed color."));
final var signatureDoc = builtinType.signatures().getFirst().attributes().getFirst().arguments().getFirst().value();
assertInstanceOf(PbsAst.AttributeDocTextBlockValue.class, signatureDoc);
assertTrue(((PbsAst.AttributeDocTextBlockValue) signatureDoc).value().startsWith("\"\"\""));
assertTrue(((PbsAst.AttributeDocTextBlockValue) signatureDoc).value().endsWith("\"\"\""));
}
@Test
void shouldPreserveDocumentationAttributesOnStructAndServiceMethods() {
final var source = """
declare struct Sprite() {
[Doc(markdown = \"""
Draws this sprite.
\""")]
fn draw() -> void {}
}
[Doc(markdown = \"""
Provides drawing commands.
\""")]
declare service Gfx {
[Doc(markdown = \"""
Clears the screen.
\""")]
fn clear() -> void {}
}
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final PbsAst.File ast = PbsParser.parse(
PbsLexer.lex(source, fileId, diagnostics),
fileId,
diagnostics,
PbsParser.ParseMode.ORDINARY);
assertTrue(diagnostics.isEmpty(), "Parser should accept Doc attributes on struct and service methods");
final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().getFirst());
assertEquals("Doc", structDecl.methods().getFirst().attributes().getFirst().name());
final var serviceDecl = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(1));
assertEquals("Doc", serviceDecl.attributes().getFirst().name());
assertEquals("Doc", serviceDecl.methods().getFirst().attributes().getFirst().name());
}
@Test @Test
void shouldRecoverFromMalformedReservedDeclarationsInInterfaceModuleMode() { void shouldRecoverFromMalformedReservedDeclarationsInInterfaceModuleMode() {
final var source = """ final var source = """

View File

@ -123,6 +123,49 @@ final class PbsEditorialSupportServiceTest {
assertEquals("Input", hostMethodHover.origin()); assertEquals("Input", hostMethodHover.origin());
} }
@Test
void shouldExposeDocMetadataOnTopLevelSymbolHover() {
final var source = """
[Doc(markdown = \"""
Runs the program.
- no parameters
\""")]
fn run() -> void { return; }
""";
final var ast = parseOrdinary(source);
final var hover = requireHover(source, ast, ReadOnlyList.empty(), "run");
assertEquals(PbsEditorialSymbolKind.FUNCTION, hover.kind());
assertEquals("Runs the program.\n\n- no parameters", hover.documentation());
}
@Test
void shouldExposeDocMetadataOnMemberHover() {
final var source = """
import { Gfx } from @sdk:gfx;
fn main() -> void {
Gfx.draw_pixel(1, 2, 3);
}
""";
final var ast = parseOrdinary(source);
final var supplementalTopDecls = supplementalTopDecls("""
declare host Gfx {
[Doc(markdown = \"""
Draws a single pixel.
\""")]
fn draw_pixel(x: int, y: int, color: int) -> void;
}
""");
final var hover = requireHover(source, ast, supplementalTopDecls, "draw_pixel");
assertEquals(PbsEditorialSymbolKind.METHOD, hover.kind());
assertEquals("Draws a single pixel.", hover.documentation());
}
@Test @Test
void shouldResolveSignatureHelpForFunctionMethodAndConstructorCalls() { void shouldResolveSignatureHelpForFunctionMethodAndConstructorCalls() {
final var source = """ final var source = """

View File

@ -14,15 +14,27 @@ class PbsInterfaceModuleSemanticsTest {
@Test @Test
void shouldAcceptValidInterfaceModuleReservedMetadataShapes() { void shouldAcceptValidInterfaceModuleReservedMetadataShapes() {
final var source = """ final var source = """
[Doc(markdown = \"""
Packed color value.
\""")]
[BuiltinType(name = "color", version = 1)] [BuiltinType(name = "color", version = 1)]
declare builtin type Color( declare builtin type Color(
pub raw: int pub raw: int
) { ) {
[Doc(markdown = \"""
Packs the color.
\""")]
[IntrinsicCall(name = "pack", version = 1)] [IntrinsicCall(name = "pack", version = 1)]
fn pack() -> int; fn pack() -> int;
} }
[Doc(markdown = \"""
Graphics host API.
\""")]
declare host Gfx { declare host Gfx {
[Doc(markdown = \"""
Draws one pixel.
\""")]
[Host(module = "gfx2d", name = "draw_pixel", version = 1)] [Host(module = "gfx2d", name = "draw_pixel", version = 1)]
[Capability(name = "gfx2d")] [Capability(name = "gfx2d")]
fn draw_pixel(x: int, y: int, c: Color) -> void; fn draw_pixel(x: int, y: int, c: Color) -> void;
@ -43,6 +55,26 @@ class PbsInterfaceModuleSemanticsTest {
d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()))); d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
} }
@Test
void shouldRejectDocOnBuiltinFields() {
final var source = """
[BuiltinType(name = "color", version = 1)]
declare builtin type Color(
[Doc(markdown = \"""
Raw color bits.
\""")]
pub raw: int
) {
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics, SourceKind.SDK_INTERFACE);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name())));
}
@Test @Test
void shouldRejectInvalidReservedAttributeTargetsAndShapesInInterfaceModule() { void shouldRejectInvalidReservedAttributeTargetsAndShapesInInterfaceModule() {
final var source = """ final var source = """

View File

@ -9,6 +9,106 @@ import static org.junit.jupiter.api.Assertions.*;
class PbsSemanticsDeclarationsTest { class PbsSemanticsDeclarationsTest {
@Test
void shouldAcceptDocOnNamedApiDeclarations() {
final var source = """
[Doc(markdown = \"""
Runs the program.
\""")]
fn run() -> void { return; }
[Doc(markdown = \"""
Holds player state.
\""")]
declare struct Player(x: int) {
[Doc(markdown = \"""
Updates player state.
\""")]
fn update() -> void { return; }
}
[Doc(markdown = \"""
Game services.
\""")]
declare service Game {
[Doc(markdown = \"""
Runs one game tick.
\""")]
fn tick() -> void { return; }
}
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
assertFalse(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_RESERVED_ATTRIBUTE_TARGET.name())));
}
@Test
void shouldRejectMalformedDuplicateAndEmptyDocAttributes() {
final var source = """
[Doc(markdown = \"""
First.
\""")]
[Doc(markdown = \"""
Second.
\""")]
fn duplicated() -> void { return; }
[Doc(text = \"""
Wrong argument.
\""")]
fn malformed() -> void { return; }
[Doc(markdown = \"""
\""")]
fn empty() -> void { return; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_DUPLICATE_RESERVED_ATTRIBUTE.name())));
final var malformedCount = diagnostics.stream()
.filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name()))
.count();
assertTrue(malformedCount >= 2);
}
@Test
void shouldRejectDocAliasesExtraArgumentsAndParameterTargets() {
final var source = """
[Document(markdown = \"""
Alias should not be accepted.
\""")]
fn alias() -> void { return; }
[Doc(markdown = \"""
Has an extra argument.
\""", format = \"""
markdown
\""")]
fn extra() -> void { return; }
fn parameter([Doc(markdown = \"""
Parameter docs are not allowed.
\""")] value: int) -> int { return value; }
""";
final var diagnostics = DiagnosticSink.empty();
new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals("E_PARSE_ATTRIBUTES_NOT_ALLOWED")));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_MALFORMED_RESERVED_ATTRIBUTE.name())));
}
@Test @Test
void shouldAllowFunctionOverloadsWithDifferentShapes() { void shouldAllowFunctionOverloadsWithDifferentShapes() {
final var source = """ final var source = """

View File

@ -288,7 +288,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
candidate.label(), candidate.label(),
mapCompletionKind(candidate.kind()), mapCompletionKind(candidate.kind()),
candidate.detail(), candidate.detail(),
candidate.origin()); candidate.documentation().isBlank() ? candidate.origin() : candidate.documentation());
} }
private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) { private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) {

View File

@ -195,6 +195,8 @@ class CompilerLanguageServiceBridgeTest {
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
assertTrue(clear565Completion.detail().contains("clear(color:")); assertTrue(clear565Completion.detail().contains("clear(color:"));
assertTrue(clear565Completion.documentation().contains("Clears the render target."));
assertTrue(clear565Completion.documentation().contains("- `color`: fill color."));
assertFalse(clear565Completion.detail().contains("arg0")); assertFalse(clear565Completion.detail().contains("arg0"));
final var hover = bridge.hover( final var hover = bridge.hover(
@ -204,6 +206,8 @@ class CompilerLanguageServiceBridgeTest {
memberPosition.line(), memberPosition.line(),
memberPosition.character()); memberPosition.character());
assertTrue(hover.markdown().contains("clear(color:")); assertTrue(hover.markdown().contains("clear(color:"));
assertTrue(hover.markdown().contains("Clears the render target."));
assertTrue(hover.markdown().contains("- `color`: fill color."));
assertFalse(hover.markdown().contains("arg0")); assertFalse(hover.markdown().contains("arg0"));
final var signatureHelp = bridge.signatureHelp( final var signatureHelp = bridge.signatureHelp(
@ -217,6 +221,56 @@ class CompilerLanguageServiceBridgeTest {
assertFalse(signatureHelp.signatures().getFirst().label().contains("arg0")); assertFalse(signatureHelp.signatures().getFirst().label().contains("arg0"));
} }
@Test
void hoverAndCompletionExposeCompilerResolvedDocMarkdown() {
final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize())
.resolve("test-projects")
.resolve("main")
.toAbsolutePath()
.normalize();
final Path documentPath = projectRoot.resolve("src").resolve("main.pbs");
final String overlay = """
[Doc(markdown = \"""
Computes the answer.
- returns `42`
- has no parameters
\""")]
fn helper() -> int { return 42; }
fn frame() -> void {
helper();
}
""";
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot);
final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay);
final var helperPosition = mapper.positionOf(overlay.indexOf("helper();"));
final var hover = bridge.hover(
context,
documentPath.toUri().toString(),
overlay,
helperPosition.line(),
helperPosition.character());
assertTrue(hover.markdown().contains("Computes the answer."));
assertTrue(hover.markdown().contains("- returns `42`"));
assertTrue(hover.markdown().indexOf("**function**") < hover.markdown().indexOf("Computes the answer."));
final var completion = bridge.completion(
context,
documentPath.toUri().toString(),
overlay,
helperPosition.line(),
helperPosition.character());
final var helper = completion.items().stream()
.filter(item -> item.label().equals("helper"))
.findFirst()
.orElseThrow();
assertTrue(helper.documentation().contains("Computes the answer."));
assertTrue(helper.documentation().contains("- returns `42`"));
}
@Test @Test
void describeServerPublishesFrontendVisualThemes() { void describeServerPublishesFrontendVisualThemes() {
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();