Compare commits

...

10 Commits

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

View File

@ -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"}]}

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -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

View File

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

View File

@ -50,7 +50,7 @@ public enum PbsSemanticKind {
public static PbsSemanticKind forToken(final PbsToken token) {
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;

View File

@ -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 {
}

View File

@ -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,
}

View File

@ -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();
}

View File

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

View File

@ -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());

View File

@ -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())) {

View File

@ -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,

View File

@ -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(

View File

@ -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());

View File

@ -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(

View File

@ -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;

View File

@ -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);
}

View File

@ -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,

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

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

View File

@ -4,6 +4,8 @@ import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.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();
}
}

View File

@ -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\"";

View File

@ -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 = """

View File

@ -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 = """

View File

@ -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 = """

View File

@ -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 = """

View File

@ -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) {

View File

@ -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();