Compare commits
10 Commits
917b0e1b9a
...
725c28b3f5
| Author | SHA1 | Date | |
|---|---|---|---|
| 725c28b3f5 | |||
| d8db99eb81 | |||
| 63215ac02f | |||
| e7647165bc | |||
| f32a25eb59 | |||
| d680504c31 | |||
| aeea8ca900 | |||
| 05dd37a30a | |||
| f700e400d5 | |||
| 2b25364252 |
@ -1,7 +1,7 @@
|
||||
{"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-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":"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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","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":"open","created_at":"2026-07-15","updated_at":"2026-07-15","ref_decisions":["DEC-0039"]}],"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-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"}]}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0093
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Implement PBS lexer support for documentation text blocks
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0094
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Extend PBS attribute parser and AST for Doc markdown payloads
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0095
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Implement documentation text block normalization
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0096
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Validate Doc attribute semantics and diagnostics
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0097
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Attach Doc metadata to PBS semantic symbols
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0098
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Expose Doc metadata through PBS editorial and LSP surfaces
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0099
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Render Doc markdown in hover composition
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0100
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Author Doc metadata for stdlib SDK and interface declarations
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0101
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Protect runtime artifacts from Doc metadata lowering
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0102
|
||||
ticket: pbs-symbol-documentation-and-hover-markdown
|
||||
title: Add end-to-end Doc documentation conformance coverage
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-15
|
||||
ref_decisions: [DEC-0039]
|
||||
tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown]
|
||||
@ -54,6 +54,17 @@ Validate that syntax, normalization, semantics, metadata attachment, LSP exposur
|
||||
- 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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -50,7 +50,7 @@ public enum PbsSemanticKind {
|
||||
public static PbsSemanticKind forToken(final PbsToken token) {
|
||||
return switch (token.kind()) {
|
||||
case COMMENT -> COMMENT;
|
||||
case STRING_LITERAL -> STRING;
|
||||
case STRING_LITERAL, DOC_TEXT_BLOCK -> STRING;
|
||||
case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> NUMBER;
|
||||
case TRUE, FALSE, NONE, SOME, OK, ERR -> LITERAL;
|
||||
case VOID, OPTIONAL, RESULT -> BUILTIN_TYPE;
|
||||
|
||||
@ -78,6 +78,7 @@ public final class PbsAst {
|
||||
}
|
||||
|
||||
public sealed interface AttributeValue permits AttributeStringValue,
|
||||
AttributeDocTextBlockValue,
|
||||
AttributeIntValue,
|
||||
AttributeBoolValue,
|
||||
AttributeErrorValue {
|
||||
@ -89,6 +90,11 @@ public final class PbsAst {
|
||||
Span span) implements AttributeValue {
|
||||
}
|
||||
|
||||
public record AttributeDocTextBlockValue(
|
||||
String value,
|
||||
Span span) implements AttributeValue {
|
||||
}
|
||||
|
||||
public record AttributeIntValue(
|
||||
long value,
|
||||
Span span) implements AttributeValue {
|
||||
@ -132,6 +138,7 @@ public final class PbsAst {
|
||||
ReturnKind returnKind,
|
||||
TypeRef returnType,
|
||||
TypeRef resultErrorType,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
LifecycleMarker lifecycleMarker,
|
||||
Block body,
|
||||
Span span) implements TopDecl {
|
||||
@ -143,18 +150,21 @@ public final class PbsAst {
|
||||
ReadOnlyList<FunctionDecl> methods,
|
||||
ReadOnlyList<CtorDecl> ctors,
|
||||
boolean hasBody,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
public record ContractDecl(
|
||||
String name,
|
||||
ReadOnlyList<FunctionSignature> signatures,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
public record HostDecl(
|
||||
String name,
|
||||
ReadOnlyList<FunctionSignature> signatures,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
@ -169,18 +179,21 @@ public final class PbsAst {
|
||||
public record ServiceDecl(
|
||||
String name,
|
||||
ReadOnlyList<FunctionDecl> methods,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
public record ErrorDecl(
|
||||
String name,
|
||||
ReadOnlyList<String> cases,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
public record EnumDecl(
|
||||
String name,
|
||||
ReadOnlyList<EnumCase> cases,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
@ -190,6 +203,7 @@ public final class PbsAst {
|
||||
ReturnKind returnKind,
|
||||
TypeRef returnType,
|
||||
TypeRef resultErrorType,
|
||||
ReadOnlyList<Attribute> attributes,
|
||||
Span span) implements TopDecl {
|
||||
}
|
||||
|
||||
|
||||
@ -3,5 +3,6 @@ package p.studio.compiler.pbs.lexer;
|
||||
public enum LexErrors {
|
||||
E_LEX_INVALID_CHAR,
|
||||
E_LEX_UNTERMINATED_STRING,
|
||||
E_LEX_UNTERMINATED_DOC_TEXT_BLOCK,
|
||||
E_LEX_INVALID_STRING_ESCAPE,
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ public final class PbsLexer {
|
||||
IDENTIFIER,
|
||||
NUMBER,
|
||||
STRING,
|
||||
DOC_TEXT_BLOCK,
|
||||
LINE_COMMENT
|
||||
}
|
||||
|
||||
@ -52,6 +53,7 @@ public final class PbsLexer {
|
||||
case IDENTIFIER -> scanIdentifierState();
|
||||
case NUMBER -> scanNumberState();
|
||||
case STRING -> scanStringState();
|
||||
case DOC_TEXT_BLOCK -> scanDocTextBlockState();
|
||||
case LINE_COMMENT -> scanLineCommentState();
|
||||
}
|
||||
}
|
||||
@ -123,7 +125,15 @@ public final class PbsLexer {
|
||||
}
|
||||
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 -> {
|
||||
if (isDigit(c)) {
|
||||
state = LexerState.NUMBER;
|
||||
@ -206,6 +216,23 @@ public final class PbsLexer {
|
||||
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() {
|
||||
while (!isAtEnd() && peek() != '\n') {
|
||||
advance();
|
||||
@ -250,6 +277,17 @@ public final class PbsLexer {
|
||||
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() {
|
||||
return current >= source.length();
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ public enum PbsTokenKind {
|
||||
FLOAT_LITERAL,
|
||||
BOUNDED_LITERAL,
|
||||
STRING_LITERAL,
|
||||
DOC_TEXT_BLOCK,
|
||||
COMMENT,
|
||||
|
||||
// Import keywords.
|
||||
|
||||
@ -84,6 +84,10 @@ final class PbsAttributeParser {
|
||||
final var token = cursor.previous();
|
||||
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)) {
|
||||
final var token = cursor.previous();
|
||||
final var parsedLong = parseLongOrNull(token.lexeme());
|
||||
|
||||
@ -108,28 +108,28 @@ final class PbsDeclarationParser {
|
||||
final PbsToken declareToken,
|
||||
final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
|
||||
if (cursor.match(PbsTokenKind.STRUCT)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "struct declarations");
|
||||
return parseStructDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "struct declarations");
|
||||
return parseStructDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.CONTRACT)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "contract declarations");
|
||||
return parseContractDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "contract declarations");
|
||||
return parseContractDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.SERVICE)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "service declarations");
|
||||
return parseServiceDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "service declarations");
|
||||
return parseServiceDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.ERROR)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "error declarations");
|
||||
return parseErrorDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "error declarations");
|
||||
return parseErrorDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.ENUM)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "enum declarations");
|
||||
return parseEnumDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "enum declarations");
|
||||
return parseEnumDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.CALLBACK)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "callback declarations");
|
||||
return parseCallbackDeclaration(declareToken);
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "callback declarations");
|
||||
return parseCallbackDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.GLOBAL)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "global declarations");
|
||||
@ -139,14 +139,14 @@ final class PbsDeclarationParser {
|
||||
return parseConstDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.HOST)) {
|
||||
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "host declarations");
|
||||
rejectUnsupportedDeclarationAttributes(pendingAttributes, "host declarations");
|
||||
if (isNotInterfaceMode()) {
|
||||
final var end = declarationTerminatorDelegate.consume();
|
||||
report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
|
||||
"'declare host' is reserved and not supported in ordinary source modules");
|
||||
return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end));
|
||||
}
|
||||
return parseHostDeclaration(declareToken);
|
||||
return parseHostDeclaration(declareToken, pendingAttributes);
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.BUILTIN)) {
|
||||
consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration");
|
||||
@ -211,14 +211,16 @@ final class PbsDeclarationParser {
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start host declaration body");
|
||||
final var signatures = new ArrayList<PbsAst.FunctionSignature>();
|
||||
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)) {
|
||||
attributes = attributeParser.parseAttributeList();
|
||||
signatureAttributes = attributeParser.parseAttributeList();
|
||||
}
|
||||
if (!cursor.match(PbsTokenKind.FN)) {
|
||||
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
|
||||
@ -226,12 +228,13 @@ final class PbsDeclarationParser {
|
||||
cursor.advance();
|
||||
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");
|
||||
return new PbsAst.HostDecl(
|
||||
name.lexeme(),
|
||||
ReadOnlyList.wrap(signatures),
|
||||
attributes,
|
||||
span(declareToken.start(), rightBrace.end()));
|
||||
}
|
||||
|
||||
@ -308,7 +311,9 @@ final class PbsDeclarationParser {
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration");
|
||||
final var fields = parseStructFields();
|
||||
@ -340,6 +345,7 @@ final class PbsDeclarationParser {
|
||||
ReadOnlyList.wrap(methods),
|
||||
ReadOnlyList.wrap(ctors),
|
||||
hasBody,
|
||||
attributes,
|
||||
span(declareToken.start(), end));
|
||||
}
|
||||
|
||||
@ -382,10 +388,17 @@ final class PbsDeclarationParser {
|
||||
final var methods = new ArrayList<PbsAst.FunctionDecl>();
|
||||
final var ctors = new ArrayList<PbsAst.CtorDecl>();
|
||||
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)) {
|
||||
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
|
||||
methods.add(parseFunctionLike(cursor.previous(), attributes));
|
||||
continue;
|
||||
}
|
||||
if (!attributes.isEmpty()) {
|
||||
reportAttributesNotAllowed(attributes, "Attributes are valid only on struct methods in struct bodies");
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.CTOR)) {
|
||||
ctors.add(parseCtorDeclarationInBody(cursor.previous()));
|
||||
continue;
|
||||
@ -411,17 +424,19 @@ final class PbsDeclarationParser {
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body");
|
||||
final var signatures = new ArrayList<PbsAst.FunctionSignature>();
|
||||
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)) {
|
||||
attributes = attributeParser.parseAttributeList();
|
||||
signatureAttributes = attributeParser.parseAttributeList();
|
||||
if (isOrdinaryMode()) {
|
||||
reportAttributesNotAllowed(attributes, "Attributes are not allowed in ordinary .pbs source modules");
|
||||
attributes = ReadOnlyList.empty();
|
||||
reportAttributesNotAllowed(signatureAttributes, "Attributes are not allowed in ordinary .pbs source modules");
|
||||
signatureAttributes = ReadOnlyList.empty();
|
||||
}
|
||||
}
|
||||
if (!cursor.match(PbsTokenKind.FN)) {
|
||||
@ -430,30 +445,41 @@ final class PbsDeclarationParser {
|
||||
cursor.advance();
|
||||
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");
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start service body");
|
||||
final var methods = new ArrayList<PbsAst.FunctionDecl>();
|
||||
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 (!methodAttributes.isEmpty()) {
|
||||
reportAttributesNotAllowed(methodAttributes, "Attributes are valid only on service methods in service bodies");
|
||||
}
|
||||
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
|
||||
"Service body accepts only method declarations");
|
||||
cursor.advance();
|
||||
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");
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start error body");
|
||||
final var cases = new ArrayList<String>();
|
||||
@ -463,10 +489,12 @@ final class PbsDeclarationParser {
|
||||
consume(PbsTokenKind.SEMICOLON, "Expected ';' after error case label");
|
||||
}
|
||||
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");
|
||||
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 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(
|
||||
@ -543,6 +571,7 @@ final class PbsDeclarationParser {
|
||||
returnSpec.kind(),
|
||||
returnSpec.returnType(),
|
||||
returnSpec.resultErrorType(),
|
||||
pendingAttributes,
|
||||
lifecycleMarker,
|
||||
body,
|
||||
span(fnToken.start(), body.span().getEnd()));
|
||||
@ -585,7 +614,9 @@ final class PbsDeclarationParser {
|
||||
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");
|
||||
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name");
|
||||
final var parameters = typeParser.parseParametersUntilRightParen();
|
||||
@ -600,6 +631,7 @@ final class PbsDeclarationParser {
|
||||
returnSpec.kind(),
|
||||
returnSpec.returnType(),
|
||||
returnSpec.resultErrorType(),
|
||||
attributes,
|
||||
span(declareToken.start(), semicolon.end()));
|
||||
}
|
||||
|
||||
@ -651,6 +683,20 @@ final class PbsDeclarationParser {
|
||||
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(
|
||||
final ReadOnlyList<PbsAst.Attribute> attributes,
|
||||
final String message) {
|
||||
@ -676,14 +722,16 @@ final class PbsDeclarationParser {
|
||||
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;
|
||||
for (final var attribute : attributes) {
|
||||
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())) {
|
||||
nextMarker = PbsAst.LifecycleMarker.INIT;
|
||||
} else if ("Frame".equals(attribute.name())) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.compiler.pbs.semantics;
|
||||
|
||||
import p.studio.compiler.models.SourceKind;
|
||||
import p.studio.compiler.pbs.PbsDocumentationTextBlockNormalizer;
|
||||
import p.studio.compiler.pbs.PbsFrontendCompiler;
|
||||
import p.studio.compiler.pbs.ast.PbsAst;
|
||||
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_INIT_ALLOWED = "InitAllowed";
|
||||
private static final String ATTR_ASSET_LOWERING = "AssetLowering";
|
||||
private static final String ATTR_DOC = "Doc";
|
||||
|
||||
private static final Set<String> RESERVED_ATTRIBUTES = Set.of(
|
||||
ATTR_HOST,
|
||||
@ -27,7 +29,8 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
ATTR_BUILTIN_CONST,
|
||||
ATTR_INTRINSIC_CALL,
|
||||
ATTR_INIT_ALLOWED,
|
||||
ATTR_ASSET_LOWERING);
|
||||
ATTR_ASSET_LOWERING,
|
||||
ATTR_DOC);
|
||||
|
||||
private final NameTable nameTable;
|
||||
private final PbsConstSemanticsValidator constSemanticsValidator = new PbsConstSemanticsValidator();
|
||||
@ -60,6 +63,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
|
||||
for (final var topDecl : ast.topDecls()) {
|
||||
if (topDecl instanceof PbsAst.FunctionDecl functionDecl) {
|
||||
validateDocAttributes(functionDecl.attributes(), "function declarations", diagnostics);
|
||||
if (interfaceModule) {
|
||||
reportInterfaceNonDeclarativeDecl(
|
||||
functionDecl.span(),
|
||||
@ -82,6 +86,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.StructDecl structDecl) {
|
||||
validateDocAttributes(structDecl.attributes(), "struct declarations", diagnostics);
|
||||
binder.registerType(structDecl.name(), structDecl.span(), "struct");
|
||||
if (interfaceModule && (structDecl.hasBody() || !structDecl.methods().isEmpty() || !structDecl.ctors().isEmpty())) {
|
||||
reportInterfaceNonDeclarativeDecl(
|
||||
@ -94,6 +99,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
|
||||
validateDocAttributes(serviceDecl.attributes(), "service declarations", diagnostics);
|
||||
binder.registerType(serviceDecl.name(), serviceDecl.span(), "service");
|
||||
binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton");
|
||||
binder.registerVisibleTopLevelSurface(serviceDecl.name(), serviceDecl.span(), "service");
|
||||
@ -102,10 +108,12 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.ContractDecl contractDecl) {
|
||||
validateDocAttributes(contractDecl.attributes(), "contract declarations", diagnostics);
|
||||
binder.registerType(contractDecl.name(), contractDecl.span(), "contract");
|
||||
if (interfaceModule) {
|
||||
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);
|
||||
@ -113,6 +121,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.HostDecl hostDecl) {
|
||||
validateDocAttributes(hostDecl.attributes(), "host declarations", diagnostics);
|
||||
binder.registerHostOwner(hostDecl.name(), hostDecl.span(), "host owner");
|
||||
validateHostDeclaration(hostDecl, binder, rules, interfaceModule, diagnostics);
|
||||
continue;
|
||||
@ -125,18 +134,21 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.ErrorDecl errorDecl) {
|
||||
validateDocAttributes(errorDecl.attributes(), "error declarations", diagnostics);
|
||||
binder.registerType(errorDecl.name(), errorDecl.span(), "error");
|
||||
rules.validateErrorDeclaration(errorDecl);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.EnumDecl enumDecl) {
|
||||
validateDocAttributes(enumDecl.attributes(), "enum declarations", diagnostics);
|
||||
binder.registerType(enumDecl.name(), enumDecl.span(), "enum");
|
||||
rules.validateEnumDeclaration(enumDecl);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
|
||||
validateDocAttributes(callbackDecl.attributes(), "callback declarations", diagnostics);
|
||||
binder.registerType(callbackDecl.name(), callbackDecl.span(), "callback");
|
||||
rules.validateCallbackDeclaration(callbackDecl);
|
||||
continue;
|
||||
@ -187,6 +199,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
false);
|
||||
final var methodScope = PbsCallableScope.structMethods(ownerNameId);
|
||||
for (final var method : structDecl.methods()) {
|
||||
validateDocAttributes(method.attributes(), "struct methods", diagnostics);
|
||||
validateCallable(
|
||||
methodScope,
|
||||
method.name(),
|
||||
@ -226,6 +239,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
final DiagnosticSink diagnostics) {
|
||||
final var methodScope = PbsCallableScope.serviceMethods(nameTable.register(serviceDecl.name()));
|
||||
for (final var method : serviceDecl.methods()) {
|
||||
validateDocAttributes(method.attributes(), "service methods", diagnostics);
|
||||
validateCallable(
|
||||
methodScope,
|
||||
method.name(),
|
||||
@ -494,12 +508,13 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
final PbsAst.ConstDecl constDecl,
|
||||
final boolean interfaceModule,
|
||||
final DiagnosticSink diagnostics) {
|
||||
validateDocAttributes(constDecl.attributes(), "const declarations", diagnostics);
|
||||
final var builtinConstAttributes = attributesNamed(constDecl.attributes(), ATTR_BUILTIN_CONST);
|
||||
for (final var attribute : constDecl.attributes()) {
|
||||
if (!isReservedAttribute(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
if (ATTR_BUILTIN_CONST.equals(attribute.name())) {
|
||||
if (ATTR_BUILTIN_CONST.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
reportInvalidReservedAttributeTarget(
|
||||
@ -540,6 +555,7 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
private void validateHostSignatureAttributes(
|
||||
final PbsAst.FunctionSignature signature,
|
||||
final DiagnosticSink diagnostics) {
|
||||
validateDocAttributes(signature.attributes(), "host signatures", diagnostics);
|
||||
final var hostAttributes = attributesNamed(signature.attributes(), ATTR_HOST);
|
||||
final var initAllowedAttributes = attributesNamed(signature.attributes(), ATTR_INIT_ALLOWED);
|
||||
final var assetLoweringAttributes = attributesNamed(signature.attributes(), ATTR_ASSET_LOWERING);
|
||||
@ -550,7 +566,8 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
if (ATTR_HOST.equals(attribute.name())
|
||||
|| ATTR_CAPABILITY.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;
|
||||
}
|
||||
reportInvalidReservedAttributeTarget(
|
||||
@ -597,12 +614,13 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
private void validateBuiltinTypeAttribute(
|
||||
final PbsAst.BuiltinTypeDecl builtinTypeDecl,
|
||||
final DiagnosticSink diagnostics) {
|
||||
validateDocAttributes(builtinTypeDecl.attributes(), "builtin type declarations", diagnostics);
|
||||
final var builtinTypeAttributes = attributesNamed(builtinTypeDecl.attributes(), ATTR_BUILTIN_TYPE);
|
||||
for (final var attribute : builtinTypeDecl.attributes()) {
|
||||
if (!isReservedAttribute(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
if (ATTR_BUILTIN_TYPE.equals(attribute.name())) {
|
||||
if (ATTR_BUILTIN_TYPE.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
reportInvalidReservedAttributeTarget(
|
||||
@ -645,12 +663,13 @@ public final class PbsDeclarationSemanticsValidator {
|
||||
private void validateIntrinsicCallAttributes(
|
||||
final PbsAst.FunctionSignature signature,
|
||||
final DiagnosticSink diagnostics) {
|
||||
validateDocAttributes(signature.attributes(), "builtin method signatures", diagnostics);
|
||||
final var intrinsicAttributes = attributesNamed(signature.attributes(), ATTR_INTRINSIC_CALL);
|
||||
for (final var attribute : signature.attributes()) {
|
||||
if (!isReservedAttribute(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
if (ATTR_INTRINSIC_CALL.equals(attribute.name())) {
|
||||
if (ATTR_INTRINSIC_CALL.equals(attribute.name()) || ATTR_DOC.equals(attribute.name())) {
|
||||
continue;
|
||||
}
|
||||
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(
|
||||
final PbsAst.Attribute attribute,
|
||||
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(
|
||||
final PbsAst.Attribute attribute,
|
||||
final String message,
|
||||
|
||||
@ -6,12 +6,22 @@ public record PbsEditorialCompletionCandidate(
|
||||
String label,
|
||||
PbsEditorialSymbolKind kind,
|
||||
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 {
|
||||
label = requireText(label, "label");
|
||||
kind = Objects.requireNonNull(kind, "kind");
|
||||
detail = normalize(detail);
|
||||
origin = normalize(origin);
|
||||
documentation = normalize(documentation);
|
||||
}
|
||||
|
||||
private static String requireText(
|
||||
|
||||
@ -482,10 +482,11 @@ public final class PbsEditorialSupportService {
|
||||
}
|
||||
for (final var method : structInfo.methods().entrySet()) {
|
||||
completions.put(method.getKey(), new PbsEditorialCompletionCandidate(
|
||||
method.getKey(),
|
||||
PbsEditorialSymbolKind.METHOD,
|
||||
formatCallableDetails(method.getValue()),
|
||||
receiverType.name()));
|
||||
method.getKey(),
|
||||
PbsEditorialSymbolKind.METHOD,
|
||||
formatCallableDetails(method.getValue()),
|
||||
receiverType.name(),
|
||||
documentationFromCallables(method.getValue())));
|
||||
}
|
||||
}
|
||||
} 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());
|
||||
for (final var method : methods.entrySet()) {
|
||||
completions.put(method.getKey(), new PbsEditorialCompletionCandidate(
|
||||
method.getKey(),
|
||||
PbsEditorialSymbolKind.METHOD,
|
||||
formatCallableDetails(method.getValue()),
|
||||
receiverType.name()));
|
||||
method.getKey(),
|
||||
PbsEditorialSymbolKind.METHOD,
|
||||
formatCallableDetails(method.getValue()),
|
||||
receiverType.name(),
|
||||
documentationFromCallables(method.getValue())));
|
||||
}
|
||||
}
|
||||
return List.copyOf(completions.values());
|
||||
@ -587,7 +589,7 @@ public final class PbsEditorialSupportService {
|
||||
formatCallableDetails(methods),
|
||||
receiverType.name(),
|
||||
signaturesFromCallables(methods),
|
||||
""));
|
||||
documentationFromCallables(methods)));
|
||||
}
|
||||
}
|
||||
if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) {
|
||||
@ -602,7 +604,7 @@ public final class PbsEditorialSupportService {
|
||||
formatCallableDetails(signatures),
|
||||
receiverType.name(),
|
||||
signaturesFromCallables(signatures),
|
||||
""));
|
||||
documentationFromCallables(signatures)));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
@ -663,7 +665,7 @@ public final class PbsEditorialSupportService {
|
||||
detail,
|
||||
topDeclSymbol.origin(),
|
||||
signatures,
|
||||
"");
|
||||
documentationFromTopDecl(topDeclSymbol.decl()));
|
||||
}
|
||||
|
||||
private PbsEditorialResolvedSymbol constructorSymbolFromTopDecl(
|
||||
@ -678,7 +680,68 @@ public final class PbsEditorialSupportService {
|
||||
detail,
|
||||
topDeclSymbol.origin(),
|
||||
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) {
|
||||
@ -690,7 +753,8 @@ public final class PbsEditorialSupportService {
|
||||
topDeclSymbol.localName(),
|
||||
symbolKind(topDeclSymbol.decl()),
|
||||
detail,
|
||||
topDeclSymbol.origin());
|
||||
topDeclSymbol.origin(),
|
||||
documentationFromTopDecl(topDeclSymbol.decl()));
|
||||
}
|
||||
|
||||
private List<PbsEditorialSignature> signaturesForTopDecl(final PbsAst.TopDecl topDecl) {
|
||||
@ -1155,6 +1219,7 @@ public final class PbsEditorialSupportService {
|
||||
functionDecl.returnKind(),
|
||||
functionDecl.returnType(),
|
||||
functionDecl.resultErrorType(),
|
||||
functionDecl.attributes(),
|
||||
functionDecl.lifecycleMarker(),
|
||||
functionDecl.body(),
|
||||
functionDecl.span());
|
||||
@ -1166,6 +1231,7 @@ public final class PbsEditorialSupportService {
|
||||
structDecl.methods(),
|
||||
structDecl.ctors(),
|
||||
structDecl.hasBody(),
|
||||
structDecl.attributes(),
|
||||
structDecl.span());
|
||||
}
|
||||
if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) {
|
||||
@ -1177,13 +1243,13 @@ public final class PbsEditorialSupportService {
|
||||
builtinTypeDecl.span());
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
return new PbsAst.CallbackDecl(
|
||||
@ -1192,13 +1258,14 @@ public final class PbsEditorialSupportService {
|
||||
callbackDecl.returnKind(),
|
||||
callbackDecl.returnType(),
|
||||
callbackDecl.resultErrorType(),
|
||||
callbackDecl.attributes(),
|
||||
callbackDecl.span());
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
return new PbsAst.GlobalDecl(newName, globalDecl.explicitType(), globalDecl.initializer(), globalDecl.span());
|
||||
|
||||
@ -146,7 +146,8 @@ final class PbsFlowSemanticSupport {
|
||||
List<CallableParameter> parameters,
|
||||
List<TypeView> inputTypes,
|
||||
TypeView outputType,
|
||||
Span span) {
|
||||
Span span,
|
||||
String documentation) {
|
||||
}
|
||||
|
||||
record CallableParameter(
|
||||
@ -282,7 +283,8 @@ final class PbsFlowSemanticSupport {
|
||||
method.returnKind(),
|
||||
method.returnType(),
|
||||
method.resultErrorType(),
|
||||
method.span()));
|
||||
method.span(),
|
||||
documentationFromAttributes(method.attributes())));
|
||||
}
|
||||
structs.put(structDecl.name(), new StructInfo(fields, methods));
|
||||
return;
|
||||
@ -306,7 +308,8 @@ final class PbsFlowSemanticSupport {
|
||||
signature.returnKind(),
|
||||
signature.returnType(),
|
||||
signature.resultErrorType(),
|
||||
signature.span()));
|
||||
signature.span(),
|
||||
documentationFromAttributes(signature.attributes())));
|
||||
}
|
||||
structs.put(builtinTypeDecl.name(), new StructInfo(fields, methods));
|
||||
return;
|
||||
@ -321,7 +324,8 @@ final class PbsFlowSemanticSupport {
|
||||
method.returnKind(),
|
||||
method.returnType(),
|
||||
method.resultErrorType(),
|
||||
method.span()));
|
||||
method.span(),
|
||||
documentationFromAttributes(method.attributes())));
|
||||
}
|
||||
services.put(serviceDecl.name(), new ServiceInfo(methods));
|
||||
serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name()));
|
||||
@ -337,7 +341,8 @@ final class PbsFlowSemanticSupport {
|
||||
signature.returnKind(),
|
||||
signature.returnType(),
|
||||
signature.resultErrorType(),
|
||||
signature.span()));
|
||||
signature.span(),
|
||||
documentationFromAttributes(signature.attributes())));
|
||||
}
|
||||
// Host owners are value singletons with callable members, same access shape as services.
|
||||
services.put(hostDecl.name(), new ServiceInfo(methods));
|
||||
@ -354,7 +359,8 @@ final class PbsFlowSemanticSupport {
|
||||
signature.returnKind(),
|
||||
signature.returnType(),
|
||||
signature.resultErrorType(),
|
||||
signature.span()));
|
||||
signature.span(),
|
||||
documentationFromAttributes(signature.attributes())));
|
||||
}
|
||||
contracts.put(contractDecl.name(), new ContractInfo(methods));
|
||||
return;
|
||||
@ -367,12 +373,14 @@ final class PbsFlowSemanticSupport {
|
||||
functionDecl.returnKind(),
|
||||
functionDecl.returnType(),
|
||||
functionDecl.resultErrorType(),
|
||||
functionDecl.span()));
|
||||
functionDecl.span(),
|
||||
documentationFromAttributes(functionDecl.attributes())));
|
||||
return;
|
||||
}
|
||||
if (topDecl instanceof PbsAst.CallbackDecl(
|
||||
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(
|
||||
name,
|
||||
@ -380,7 +388,8 @@ final class PbsFlowSemanticSupport {
|
||||
returnKind,
|
||||
returnType,
|
||||
resultErrorType,
|
||||
span);
|
||||
span,
|
||||
"");
|
||||
callbacks.put(name, new CallbackSignature(symbol.inputTypes(), symbol.outputType()));
|
||||
return;
|
||||
}
|
||||
@ -477,7 +486,8 @@ final class PbsFlowSemanticSupport {
|
||||
final PbsAst.ReturnKind returnKind,
|
||||
final PbsAst.TypeRef returnType,
|
||||
final PbsAst.TypeRef resultErrorType,
|
||||
final Span span) {
|
||||
final Span span,
|
||||
final String documentation) {
|
||||
final var input = new ArrayList<TypeView>(parameters.size());
|
||||
final var semanticParameters = new ArrayList<CallableParameter>(parameters.size());
|
||||
for (int index = 0; index < parameters.size(); index += 1) {
|
||||
@ -490,7 +500,21 @@ final class PbsFlowSemanticSupport {
|
||||
type,
|
||||
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(
|
||||
|
||||
@ -1,61 +1,136 @@
|
||||
[Doc(markdown = """
|
||||
Represents a packed RGBA color.
|
||||
""")]
|
||||
[BuiltinType(name = "color", version = 1)]
|
||||
declare builtin type Color(
|
||||
pub raw: int
|
||||
) {
|
||||
[Doc(markdown = """
|
||||
Builds a color from its packed integer representation.
|
||||
|
||||
- `raw`: packed color value.
|
||||
""")]
|
||||
[IntrinsicCall(name = "from_raw", version = 1)]
|
||||
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)]
|
||||
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)]
|
||||
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)]
|
||||
fn html_rgba(value: str) -> Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Builds an opaque grayscale color.
|
||||
|
||||
- `value`: grayscale channel value.
|
||||
""")]
|
||||
[IntrinsicCall(name = "gray_scale", version = 1)]
|
||||
fn gray_scale(value: int) -> Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the packed RGB portion of this color.
|
||||
""")]
|
||||
[IntrinsicCall(name = "hex", version = 1)]
|
||||
fn hex() -> int;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the alpha channel of this color.
|
||||
""")]
|
||||
[IntrinsicCall(name = "alpha", version = 1)]
|
||||
fn alpha() -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque black.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "black", version = 1)]
|
||||
declare const BLACK: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque white.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "white", version = 1)]
|
||||
declare const WHITE: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque red.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "red", version = 1)]
|
||||
declare const RED: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque green.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "green", version = 1)]
|
||||
declare const GREEN: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque blue.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "blue", version = 1)]
|
||||
declare const BLUE: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque yellow.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "yellow", version = 1)]
|
||||
declare const YELLOW: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque orange.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "orange", version = 1)]
|
||||
declare const ORANGE: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque indigo.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "indigo", version = 1)]
|
||||
declare const INDIGO: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque gray.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "gray", version = 1)]
|
||||
declare const GRAY: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque cyan.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "cyan", version = 1)]
|
||||
declare const CYAN: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Opaque magenta.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "magenta", version = 1)]
|
||||
declare const MAGENTA: Color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Fully transparent color.
|
||||
""")]
|
||||
[BuiltinConst(target = "color", name = "transparent", version = 1)]
|
||||
declare const TRANSPARENT: Color;
|
||||
|
||||
@ -1,35 +1,83 @@
|
||||
[Doc(markdown = """
|
||||
Low-level asset loading host bindings.
|
||||
""")]
|
||||
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)]
|
||||
[Capability(name = "asset")]
|
||||
[AssetLowering(param = 0)]
|
||||
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)]
|
||||
[Capability(name = "asset")]
|
||||
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)]
|
||||
[Capability(name = "asset")]
|
||||
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)]
|
||||
[Capability(name = "asset")]
|
||||
fn cancel(loading_handle: int) -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
High-level asset loading commands.
|
||||
""")]
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
return LowAssets.commit(loading_handle);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Cancels an asset loading operation.
|
||||
|
||||
- `loading_handle`: handle returned by `load`.
|
||||
""")]
|
||||
fn cancel(loading_handle: int) -> int {
|
||||
return LowAssets.cancel(loading_handle);
|
||||
}
|
||||
|
||||
@ -1,16 +1,46 @@
|
||||
[Doc(markdown = """
|
||||
Low-level scene composer host bindings.
|
||||
""")]
|
||||
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)]
|
||||
[Capability(name = "composer")]
|
||||
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)]
|
||||
[Capability(name = "composer")]
|
||||
fn bind_scene(scene_bank_id: int) -> int;
|
||||
|
||||
[Doc(markdown = """
|
||||
Unbinds the active scene bank.
|
||||
""")]
|
||||
[Host(module = "composer", name = "unbind_scene", version = 1)]
|
||||
[Capability(name = "composer")]
|
||||
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)]
|
||||
[Capability(name = "composer")]
|
||||
fn emit_sprite(
|
||||
@ -26,23 +56,53 @@ declare host LowComposer {
|
||||
) -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
High-level scene composition commands.
|
||||
""")]
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
return LowComposer.bind_scene(scene_bank_id);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Unbinds the active scene bank.
|
||||
""")]
|
||||
fn unbind_scene() -> int
|
||||
{
|
||||
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(
|
||||
glyph_id: int,
|
||||
palette_id: int,
|
||||
|
||||
@ -1,67 +1,189 @@
|
||||
import { Color } from @core:color;
|
||||
|
||||
[Doc(markdown = """
|
||||
Low-level 2D graphics host bindings.
|
||||
""")]
|
||||
declare host LowGfx {
|
||||
[Doc(markdown = """
|
||||
Clears the render target.
|
||||
|
||||
- `color`: fill color.
|
||||
""")]
|
||||
[Host(module = "gfx2d", name = "clear", version = 1)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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)]
|
||||
[Capability(name = "gfx2d")]
|
||||
fn draw_text(x: int, y: int, message: str, color: Color) -> void;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
High-level 2D drawing commands.
|
||||
""")]
|
||||
declare service Gfx
|
||||
{
|
||||
[Doc(markdown = """
|
||||
Clears the render target.
|
||||
|
||||
- `color`: fill color.
|
||||
""")]
|
||||
fn clear(color: Color) -> void
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
LowGfx.draw_text(x, y, message, color);
|
||||
|
||||
@ -1,85 +1,169 @@
|
||||
[Doc(markdown = """
|
||||
Root input device provider.
|
||||
""")]
|
||||
[BuiltinType(name = "input", version = 1)]
|
||||
declare builtin type InputType() {
|
||||
[Doc(markdown = """
|
||||
Returns the gamepad input state.
|
||||
""")]
|
||||
[IntrinsicCall(name = "pad", version = 1)]
|
||||
fn pad() -> InputPad;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the touch input state.
|
||||
""")]
|
||||
[IntrinsicCall(name = "touch", version = 1)]
|
||||
fn touch() -> InputTouch;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Represents the current gamepad state.
|
||||
""")]
|
||||
[BuiltinType(name = "input.pad", version = 1)]
|
||||
declare builtin type InputPad() {
|
||||
[Doc(markdown = """
|
||||
Returns the up directional button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "up", version = 1)]
|
||||
fn up() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the down directional button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "down", version = 1)]
|
||||
fn down() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the left directional button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "left", version = 1)]
|
||||
fn left() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the right directional button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "right", version = 1)]
|
||||
fn right() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the A action button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "a", version = 1)]
|
||||
fn a() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the B action button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "b", version = 1)]
|
||||
fn b() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the X action button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "x", version = 1)]
|
||||
fn x() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the Y action button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "y", version = 1)]
|
||||
fn y() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the left shoulder button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "l", version = 1)]
|
||||
fn l() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the right shoulder button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "r", version = 1)]
|
||||
fn r() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the start button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "start", version = 1)]
|
||||
fn start() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the select button.
|
||||
""")]
|
||||
[IntrinsicCall(name = "select", version = 1)]
|
||||
fn select() -> InputButton;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Represents the current touch input state.
|
||||
""")]
|
||||
[BuiltinType(name = "input.touch", version = 1)]
|
||||
declare builtin type InputTouch() {
|
||||
[Doc(markdown = """
|
||||
Returns the touch contact as a button state.
|
||||
""")]
|
||||
[IntrinsicCall(name = "button", version = 1)]
|
||||
fn button() -> InputButton;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the current touch x coordinate.
|
||||
""")]
|
||||
[IntrinsicCall(name = "x", version = 1)]
|
||||
fn x() -> int;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the current touch y coordinate.
|
||||
""")]
|
||||
[IntrinsicCall(name = "y", version = 1)]
|
||||
fn y() -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Represents one digital input button.
|
||||
""")]
|
||||
[BuiltinType(name = "input.button", version = 1)]
|
||||
declare builtin type InputButton() {
|
||||
[Doc(markdown = """
|
||||
Returns true on the frame where the button becomes down.
|
||||
""")]
|
||||
[IntrinsicCall(name = "pressed", version = 1)]
|
||||
fn pressed() -> bool;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns true on the frame where the button becomes up.
|
||||
""")]
|
||||
[IntrinsicCall(name = "released", version = 1)]
|
||||
fn released() -> bool;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns true while the button is currently down.
|
||||
""")]
|
||||
[IntrinsicCall(name = "down", version = 1)]
|
||||
fn down() -> bool;
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns how long the button has been held.
|
||||
""")]
|
||||
[IntrinsicCall(name = "hold", version = 1)]
|
||||
fn hold() -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
High-level access to player input devices.
|
||||
""")]
|
||||
declare service Input
|
||||
{
|
||||
[Doc(markdown = """
|
||||
Returns the gamepad input state.
|
||||
""")]
|
||||
fn pad() -> InputPad
|
||||
{
|
||||
return InputType.pad();
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Returns the touch input state.
|
||||
""")]
|
||||
fn touch() -> InputTouch
|
||||
{
|
||||
return InputType.touch();
|
||||
|
||||
@ -1,60 +1,134 @@
|
||||
[Doc(markdown = """
|
||||
Low-level logging host bindings.
|
||||
""")]
|
||||
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)]
|
||||
[Capability(name = "log")]
|
||||
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)]
|
||||
[Capability(name = "log")]
|
||||
fn write_tag(level: int, tag: int, message: str) -> void;
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
High-level logging commands.
|
||||
""")]
|
||||
declare service Log
|
||||
{
|
||||
[Doc(markdown = """
|
||||
Writes a trace log message.
|
||||
|
||||
- `msg`: text to write.
|
||||
""")]
|
||||
fn trace(msg: str) -> void
|
||||
{
|
||||
LowLog.write(0, msg);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Writes a debug log message.
|
||||
|
||||
- `msg`: text to write.
|
||||
""")]
|
||||
fn debug(msg: str) -> void
|
||||
{
|
||||
LowLog.write(1, msg);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Writes an informational log message.
|
||||
|
||||
- `msg`: text to write.
|
||||
""")]
|
||||
fn info(msg: str) -> void
|
||||
{
|
||||
LowLog.write(2, msg);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Writes a warning log message.
|
||||
|
||||
- `msg`: text to write.
|
||||
""")]
|
||||
fn warn(msg: str) -> void
|
||||
{
|
||||
LowLog.write(3, msg);
|
||||
}
|
||||
|
||||
[Doc(markdown = """
|
||||
Writes a failure log message.
|
||||
|
||||
- `msg`: text to write.
|
||||
""")]
|
||||
fn failure(msg: str) -> void
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
LowLog.write_tag(4, tag, msg);
|
||||
|
||||
@ -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\""));
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
|
||||
import p.studio.compiler.messages.Addressable;
|
||||
import p.studio.compiler.messages.FESurfaceContext;
|
||||
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.IRGlobalVisibility;
|
||||
import p.studio.compiler.models.IRSyntheticCallableKind;
|
||||
@ -52,6 +54,57 @@ class PbsFrontendCompilerTest {
|
||||
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
|
||||
void shouldExposeGlobalsAndSyntheticLifecycleArtifactsInBackendFile() {
|
||||
final var source = """
|
||||
@ -812,4 +865,62 @@ class PbsFrontendCompilerTest {
|
||||
assertEquals(1L, pixel.fields().get(1).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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,55 @@ class PbsLexerTest {
|
||||
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
|
||||
void shouldReportInvalidStringEscape() {
|
||||
final var source = "\"bad\\q\"";
|
||||
|
||||
@ -339,6 +339,82 @@ class PbsParserTest {
|
||||
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
|
||||
void shouldRecoverFromMalformedReservedDeclarationsInInterfaceModuleMode() {
|
||||
final var source = """
|
||||
|
||||
@ -123,6 +123,49 @@ final class PbsEditorialSupportServiceTest {
|
||||
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
|
||||
void shouldResolveSignatureHelpForFunctionMethodAndConstructorCalls() {
|
||||
final var source = """
|
||||
|
||||
@ -14,15 +14,27 @@ class PbsInterfaceModuleSemanticsTest {
|
||||
@Test
|
||||
void shouldAcceptValidInterfaceModuleReservedMetadataShapes() {
|
||||
final var source = """
|
||||
[Doc(markdown = \"""
|
||||
Packed color value.
|
||||
\""")]
|
||||
[BuiltinType(name = "color", version = 1)]
|
||||
declare builtin type Color(
|
||||
pub raw: int
|
||||
) {
|
||||
[Doc(markdown = \"""
|
||||
Packs the color.
|
||||
\""")]
|
||||
[IntrinsicCall(name = "pack", version = 1)]
|
||||
fn pack() -> int;
|
||||
}
|
||||
|
||||
[Doc(markdown = \"""
|
||||
Graphics host API.
|
||||
\""")]
|
||||
declare host Gfx {
|
||||
[Doc(markdown = \"""
|
||||
Draws one pixel.
|
||||
\""")]
|
||||
[Host(module = "gfx2d", name = "draw_pixel", version = 1)]
|
||||
[Capability(name = "gfx2d")]
|
||||
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())));
|
||||
}
|
||||
|
||||
@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
|
||||
void shouldRejectInvalidReservedAttributeTargetsAndShapesInInterfaceModule() {
|
||||
final var source = """
|
||||
|
||||
@ -9,6 +9,106 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
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
|
||||
void shouldAllowFunctionOverloadsWithDifferentShapes() {
|
||||
final var source = """
|
||||
|
||||
@ -288,7 +288,7 @@ public final class CompilerLanguageServiceBridge implements LanguageServiceBridg
|
||||
candidate.label(),
|
||||
mapCompletionKind(candidate.kind()),
|
||||
candidate.detail(),
|
||||
candidate.origin());
|
||||
candidate.documentation().isBlank() ? candidate.origin() : candidate.documentation());
|
||||
}
|
||||
|
||||
private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) {
|
||||
|
||||
@ -195,6 +195,8 @@ class CompilerLanguageServiceBridgeTest {
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
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"));
|
||||
|
||||
final var hover = bridge.hover(
|
||||
@ -204,6 +206,8 @@ class CompilerLanguageServiceBridgeTest {
|
||||
memberPosition.line(),
|
||||
memberPosition.character());
|
||||
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"));
|
||||
|
||||
final var signatureHelp = bridge.signatureHelp(
|
||||
@ -217,6 +221,56 @@ class CompilerLanguageServiceBridgeTest {
|
||||
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
|
||||
void describeServerPublishesFrontendVisualThemes() {
|
||||
final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user