diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 55fca3b9..6cfa6845 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -15,4 +15,4 @@ {"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} {"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]} {"type":"discussion","id":"DSC-0016","status":"open","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-studio-editor-scope-guides-and-brace-anchoring.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0014","file":"DEC-0014-studio-editor-active-scope-and-structural-anchors.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"plans":[{"id":"PLN-0030","file":"PLN-0030-studio-active-container-and-active-scope-gutter-wave-1.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0031","file":"PLN-0031-studio-structural-anchor-semantic-surface-specification.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"},{"id":"PLN-0032","file":"PLN-0032-frontend-structural-anchor-payloads-and-anchor-aware-tests.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}],"lessons":[]} -{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"open","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"open","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0017","status":"open","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[{"id":"AGD-0018","file":"AGD-0018-studio-editor-inline-type-hints-for-let-bindings.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03"}],"decisions":[{"id":"DEC-0015","file":"DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md","status":"accepted","created_at":"2026-04-03","updated_at":"2026-04-03","ref_agenda":"AGD-0018"}],"plans":[{"id":"PLN-0033","file":"PLN-0033-inline-hint-spec-and-contract-propagation.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0034","file":"PLN-0034-lsp-inline-hint-transport-contract.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0035","file":"PLN-0035-pbs-inline-type-hint-payload-production.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]},{"id":"PLN-0036","file":"PLN-0036-studio-inline-hint-rendering-and-rollout.md","status":"open","created_at":"2026-04-03","updated_at":"2026-04-03","ref_decisions":["DEC-0015"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0035-pbs-inline-type-hint-payload-production.md b/discussion/workflow/plans/PLN-0035-pbs-inline-type-hint-payload-production.md new file mode 100644 index 00000000..85554199 --- /dev/null +++ b/discussion/workflow/plans/PLN-0035-pbs-inline-type-hint-payload-production.md @@ -0,0 +1,110 @@ +--- +id: PLN-0035 +ticket: studio-editor-inline-type-hints-for-let-bindings +title: PBS inline type hint payload production +status: done +created: 2026-04-03 +completed: 2026-04-03 +tags: [compiler, pbs, inline-hints, type-inference, lsp] +--- + +## Objective + +Produce frontend-owned inline hint payloads for PBS inferred local bindings, starting with `let` bindings whose types are omitted in source. + +## Background + +DEC-0015 explicitly leaves hint eligibility frontend-owned. The initial user need comes from PBS inferred `let` bindings, and PBS already computes the inferred type during flow analysis. That semantic fact now needs to be surfaced as frontend-produced hint payload rather than remaining internal-only analysis state. + +## Scope + +### Included +- Surface PBS-produced inline type hints for inferred `let` bindings. +- Reuse existing flow-analysis type information rather than adding parallel inference. +- Preserve valid hints under partial degradation wherever the frontend can still produce them. +- Add PBS/LSP-facing tests for hint eligibility and payload correctness. + +### Excluded +- Host rendering policy. +- Non-PBS frontend hint production. +- Hint categories not chosen by PBS. + +## Execution Steps + +### Step 1 - Identify stable anchor and payload sources in PBS analysis + +**What:** Determine the exact PBS semantic source for inferred binding type and anchor location. + +**How:** Trace `let` analysis through the PBS flow-analysis pipeline and identify: +- where inferred local binding type becomes stable; +- how to render that type as display text; +- which source position should anchor the hint; +- what happens when the binding has explicit type text and therefore should not receive an inferred-type hint. + +This step must avoid recomputing types outside the existing semantic pipeline. + +**File(s):** +- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/` +- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/` + +### Step 2 - Emit frontend-owned hint payloads for eligible bindings + +**What:** Make PBS produce inline hint payloads for eligible inferred bindings. + +**How:** Extend the PBS semantic-read production path so it emits frontend-owned hint entries for `let` bindings with omitted type syntax and stable inferred type results. + +The production path must: +- suppress hints when PBS does not want one; +- avoid emitting hints for explicitly typed bindings; +- preserve valid hints even if unrelated parts of the document degrade. + +**File(s):** +- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/` +- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/` +- any accepted frontend semantic contract surface used by `prometeu-lsp` + +### Step 3 - Add PBS-focused hint production tests + +**What:** Lock PBS hint policy and payload behavior with tests. + +**How:** Add tests for: +- inferred `let` bindings receiving hints; +- explicit `let name: Type = ...` bindings receiving no inferred-type hint; +- payload text stability for scalar, struct, optional, result, tuple, or other supported type forms chosen by PBS; +- partial-degradation cases where unaffected bindings still produce hints. + +**File(s):** +- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/` +- `prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/` where end-to-end analyze assertions are required + +## Test Requirements + +### Unit Tests +- PBS semantic tests for inferred binding hint eligibility. +- Type-surface formatting tests if payload display text is factored into a helper. + +### Integration Tests +- LSP analyze tests verifying PBS-produced hints appear in transported results. +- Partial-degradation tests preserving unaffected hints. + +### Manual Verification +- Open a PBS document with inferred `let` bindings and inspect the LSP analyze payload. +- Confirm no hints appear for explicit type annotations. + +## Acceptance Criteria + +- [ ] PBS produces frontend-owned inline hint payloads for eligible inferred `let` bindings. +- [ ] Explicitly typed bindings do not receive inferred-type hints. +- [ ] Valid hints survive unrelated local degradation when PBS can still analyze those bindings. +- [ ] Tests lock the frontend-owned eligibility policy. + +## Dependencies + +- Accepted decision `DEC-0015-studio-editor-inline-type-hints-contract-and-rendering-model.md` +- `PLN-0033-inline-hint-spec-and-contract-propagation.md` +- `PLN-0034-lsp-inline-hint-transport-contract.md` + +## Risks + +- If inferred-type display text is not normalized, hint output may become unstable across equivalent semantic forms. +- If hint production is attached too late in the pipeline, degraded documents may lose more valid hints than necessary. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index 2d4424cd..a811fe41 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -343,6 +343,7 @@ public final class PbsAst { public record LetStatement( boolean isConst, String name, + Span nameSpan, TypeRef explicitType, Expression initializer, Span span) implements Statement { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBlockParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBlockParser.java index c710287f..96da307b 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBlockParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBlockParser.java @@ -118,6 +118,7 @@ final class PbsBlockParser { return new PbsAst.LetStatement( isConst, name.lexeme(), + context.span(name.start(), name.end()), explicitType, initializer, context.span(letToken.start(), semicolon.end())); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java index 17f0bc3b..8ace234d 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java @@ -5,9 +5,12 @@ import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Model; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Scope; import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView; +import p.studio.compiler.services.PbsInlineHintSurface; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.utilities.structures.ReadOnlyList; +import java.util.ArrayList; + final class PbsFlowBodyAnalyzer { private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps); @@ -16,24 +19,75 @@ final class PbsFlowBodyAnalyzer { typeOps, expressionAnalyzer, this::analyzeBlock); - private final PbsFlowStatementAnalyzer statementAnalyzer = new PbsFlowStatementAnalyzer( - typeOps, - expressionAnalyzer, - assignmentAnalyzer::analyzeAssignmentStatement); private final PbsFlowCallableBodyAnalyzer callableBodyAnalyzer = new PbsFlowCallableBodyAnalyzer( typeOps, completionAnalyzer, this::analyzeBlock); + private PbsFlowStatementAnalyzer.InlineHintCollector inlineHintCollector = PbsFlowStatementAnalyzer.InlineHintCollector.noop(); public void validate( final PbsAst.File ast, final ReadOnlyList supplementalTopDecls, final FESurfaceContext feSurfaceContext, final DiagnosticSink diagnostics) { - final var model = Model.from(ast, supplementalTopDecls, feSurfaceContext, diagnostics); + final var previousCollector = inlineHintCollector; + inlineHintCollector = PbsFlowStatementAnalyzer.InlineHintCollector.noop(); + try { + final var model = Model.from(ast, supplementalTopDecls, feSurfaceContext, diagnostics); - for (final var topDecl : ast.topDecls()) { - validateTopDecl(topDecl, model, diagnostics); + for (final var topDecl : ast.topDecls()) { + validateTopDecl(topDecl, model, diagnostics); + } + } finally { + inlineHintCollector = previousCollector; + } + } + + public ReadOnlyList collectInlineHints( + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final DiagnosticSink diagnostics) { + final var collector = new InlineHintsCollector(); + final var previousCollector = inlineHintCollector; + inlineHintCollector = collector; + try { + final var model = Model.from(ast, supplementalTopDecls, feSurfaceContext, diagnostics); + + for (final var topDecl : ast.topDecls()) { + validateTopDecl(topDecl, model, diagnostics); + } + } finally { + inlineHintCollector = previousCollector; + } + return collector.hints(); + } + + private PbsFlowStatementAnalyzer statementAnalyzer() { + return new PbsFlowStatementAnalyzer( + typeOps, + expressionAnalyzer, + assignmentAnalyzer::analyzeAssignmentStatement, + inlineHintCollector); + } + + private static final class InlineHintsCollector implements PbsFlowStatementAnalyzer.InlineHintCollector { + private static final String TYPE_HINT_CATEGORY = "type"; + + private final ArrayList hints = new ArrayList<>(); + private final PbsInlineHintSurfaceFormatter formatter = new PbsInlineHintSurfaceFormatter(); + + @Override + public void collect(final PbsAst.LetStatement letStatement, final TypeView inferredType) { + final var label = formatter.format(inferredType); + if (label == null || letStatement.nameSpan() == null || letStatement.nameSpan().isNone()) { + return; + } + hints.add(new PbsInlineHintSurface(letStatement.nameSpan(), label, TYPE_HINT_CATEGORY)); + } + + ReadOnlyList hints() { + return ReadOnlyList.wrap(hints); } } @@ -194,6 +248,6 @@ final class PbsFlowBodyAnalyzer { final PbsAst.Block block, final PbsFlowBodyContext context, final boolean valueContext) { - return statementAnalyzer.analyzeBlock(block, context, valueContext); + return statementAnalyzer().analyzeBlock(block, context, valueContext); } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java index e459aad7..c340005d 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java @@ -2,6 +2,7 @@ package p.studio.compiler.pbs.semantics; import p.studio.compiler.messages.FESurfaceContext; import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.services.PbsInlineHintSurface; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.utilities.structures.ReadOnlyList; @@ -19,4 +20,12 @@ public final class PbsFlowSemanticsValidator { final DiagnosticSink diagnostics) { flowBodyAnalyzer.validate(ast, supplementalTopDecls, feSurfaceContext, diagnostics); } + + public ReadOnlyList collectInlineHints( + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final DiagnosticSink diagnostics) { + return flowBodyAnalyzer.collectInlineHints(ast, supplementalTopDecls, feSurfaceContext, diagnostics); + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java index 8b88693d..58372b3f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowStatementAnalyzer.java @@ -11,17 +11,30 @@ final class PbsFlowStatementAnalyzer { void analyze(PbsAst.AssignStatement assignStatement, PbsFlowBodyContext context); } + @FunctionalInterface + interface InlineHintCollector { + void collect(PbsAst.LetStatement letStatement, TypeView inferredType); + + static InlineHintCollector noop() { + return (letStatement, inferredType) -> { + }; + } + } + private final PbsFlowTypeOps typeOps; private final PbsFlowExpressionAnalyzer expressionAnalyzer; private final AssignmentStatementAnalyzer assignmentStatementAnalyzer; + private final InlineHintCollector inlineHintCollector; PbsFlowStatementAnalyzer( final PbsFlowTypeOps typeOps, final PbsFlowExpressionAnalyzer expressionAnalyzer, - final AssignmentStatementAnalyzer assignmentStatementAnalyzer) { + final AssignmentStatementAnalyzer assignmentStatementAnalyzer, + final InlineHintCollector inlineHintCollector) { this.typeOps = typeOps; this.expressionAnalyzer = expressionAnalyzer; this.assignmentStatementAnalyzer = assignmentStatementAnalyzer; + this.inlineHintCollector = inlineHintCollector; } TypeView analyzeBlock( @@ -96,7 +109,11 @@ final class PbsFlowStatementAnalyzer { ExprUse.VALUE, true, this::analyzeNestedBlock); - scope.bind(letStatement.name(), expected == null ? initializer.type() : expected, !letStatement.isConst()); + final var resolvedType = expected == null ? initializer.type() : expected; + scope.bind(letStatement.name(), resolvedType, !letStatement.isConst()); + if (letStatement.explicitType() == null) { + inlineHintCollector.collect(letStatement, resolvedType); + } return; } if (statement instanceof PbsAst.ReturnStatement returnStatement) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsInlineHintSurfaceFormatter.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsInlineHintSurfaceFormatter.java new file mode 100644 index 00000000..97ffffe6 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsInlineHintSurfaceFormatter.java @@ -0,0 +1,62 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TupleField; +import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.TypeView; + +final class PbsInlineHintSurfaceFormatter { + String format(final TypeView type) { + if (type == null) { + return null; + } + return switch (type.kind()) { + case UNKNOWN, ASSET_NAMESPACE -> null; + case UNIT -> "void"; + case INT, FLOAT, BOOL, STR -> type.name(); + case STRUCT, + SERVICE, + CONTRACT, + CALLBACK, + ENUM, + ERROR, + TYPE_REF, + ADDRESSABLE -> type.name(); + case OPTIONAL -> { + final var inner = format(type.inner()); + yield inner == null ? null : "optional " + inner; + } + case RESULT -> { + final var errorType = format(type.errorType()); + final var payloadType = format(type.inner()); + if (errorType == null) { + yield null; + } + yield payloadType == null + ? "result<%s>".formatted(errorType) + : "result<%s> %s".formatted(errorType, payloadType); + } + case TUPLE -> formatTuple(type.tupleFields()); + }; + } + + private String formatTuple(final java.util.List fields) { + if (fields.isEmpty()) { + return "()"; + } + final var builder = new StringBuilder("("); + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + builder.append(", "); + } + final var field = fields.get(i); + final var fieldType = format(field.type()); + if (fieldType == null) { + return null; + } + builder.append(field.label()) + .append(": ") + .append(fieldType); + } + builder.append(')'); + return builder.toString(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 68225aa3..27341fff 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -13,6 +13,7 @@ import p.studio.compiler.models.IRHiddenGlobalKind; import p.studio.compiler.models.IRSyntheticCallableKind; import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.pbs.PbsReservedMetadataExtractor; +import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator; import p.studio.compiler.pbs.stdlib.InterfaceModuleLoader; import p.studio.compiler.pbs.stdlib.ResourceStdlibEnvironmentResolver; import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; @@ -112,16 +113,52 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { final FrontendPhaseContext ctx, final DiagnosticSink diagnostics, final BuildingIssueSink issues) { + return semanticReadSurface(ctx, diagnostics, issues).supplementalTopDeclsByFile(); + } + + public static Map> inlineHintsByFile( + final FrontendPhaseContext ctx, + final DiagnosticSink diagnostics, + final BuildingIssueSink issues) { + return semanticReadSurface(ctx, diagnostics, issues).inlineHintsByFile(); + } + + public static PbsSemanticReadSurface semanticReadSurface( + final FrontendPhaseContext ctx, + final DiagnosticSink diagnostics, + final BuildingIssueSink issues) { final var service = new PBSFrontendPhaseService(); final var assembly = service.moduleAssemblyService.assemble(ctx, ctx.nameTable(), diagnostics, issues); final var importedSemanticContexts = service.importedSemanticContextService.build( assembly.parsedSourceFiles(), assembly.moduleTable()); + final var flowSemanticsValidator = new PbsFlowSemanticsValidator(); final Map> supplementalTopDeclsByFile = new LinkedHashMap<>(); + final Map> inlineHintsByFile = new LinkedHashMap<>(); for (final var entry : importedSemanticContexts.entrySet()) { supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls()); } - return Map.copyOf(supplementalTopDeclsByFile); + for (final var parsedSourceFile : assembly.parsedSourceFiles()) { + final var importedSemanticContext = importedSemanticContexts.getOrDefault( + parsedSourceFile.fileId(), + PbsImportedSemanticContext.empty()); + final var inlineHints = flowSemanticsValidator.collectInlineHints( + parsedSourceFile.ast(), + importedSemanticContext.supplementalTopDecls(), + ctx.feSurfaceContext(), + diagnostics); + if (!inlineHints.isEmpty()) { + inlineHintsByFile.put(parsedSourceFile.fileId(), inlineHints); + } + } + return new PbsSemanticReadSurface( + Map.copyOf(supplementalTopDeclsByFile), + Map.copyOf(inlineHintsByFile)); + } + + public record PbsSemanticReadSurface( + Map> supplementalTopDeclsByFile, + Map> inlineHintsByFile) { } private IRBackend mergeCompiledSources( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsInlineHintSurface.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsInlineHintSurface.java new file mode 100644 index 00000000..7704c293 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PbsInlineHintSurface.java @@ -0,0 +1,23 @@ +package p.studio.compiler.services; + +import p.studio.compiler.source.Span; + +import java.util.Objects; + +public record PbsInlineHintSurface( + Span anchorSpan, + String label, + String category) { + + public PbsInlineHintSurface { + anchorSpan = Objects.requireNonNull(anchorSpan, "anchorSpan"); + label = Objects.requireNonNull(label, "label").trim(); + category = Objects.requireNonNull(category, "category").trim(); + if (label.isBlank()) { + throw new IllegalArgumentException("label cannot be blank"); + } + if (category.isBlank()) { + throw new IllegalArgumentException("category cannot be blank"); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index 0c428e4a..c01bb737 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -194,6 +194,29 @@ class PbsParserTest { assertFalse(diagnostics.isEmpty(), "Parser should reject reserved keyword 'error' as callable/member name"); } + @Test + void shouldTrackLetNameSpanSeparatelyFromWholeStatementSpan() { + final var source = """ + fn main() -> void { + let inferred = 1; + return; + } + """; + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + + final PbsAst.File ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + final var functionDecl = assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().getFirst()); + final var letStatement = assertInstanceOf(PbsAst.LetStatement.class, functionDecl.body().statements().getFirst()); + assertEquals("inferred", letStatement.name()); + assertEquals(source.indexOf("inferred"), letStatement.nameSpan().getStart()); + assertEquals(source.indexOf("inferred") + "inferred".length(), letStatement.nameSpan().getEnd()); + assertTrue(letStatement.span().getStart() < letStatement.nameSpan().getStart()); + assertTrue(letStatement.span().getEnd() > letStatement.nameSpan().getEnd()); + } + @Test void shouldAcceptStructFieldTrailingComma() { final var source = "declare struct S(a: int,);"; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java index 24525573..ec9fa894 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java @@ -20,6 +20,7 @@ import p.studio.compiler.pbs.stdlib.StdlibEnvironment; import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver; import p.studio.compiler.pbs.stdlib.StdlibModuleSource; import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.ProjectId; import p.studio.compiler.source.tables.FileTable; import p.studio.compiler.source.tables.ProjectTable; @@ -1868,13 +1869,127 @@ class PBSFrontendPhaseServiceTest { allowedDiagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); } - private void registerFile( + @Test + void shouldProduceInlineHintsForEligibleInferredLetBindings() throws IOException { + final var projectRoot = tempDir.resolve("project-inline-hints"); + final var sourceRoot = projectRoot.resolve("src"); + Files.createDirectories(sourceRoot); + + final var sourceFile = sourceRoot.resolve("main.pbs"); + final var modBarrel = sourceRoot.resolve("mod.barrel"); + Files.writeString(sourceFile, """ + declare struct Player(score: int); + declare error Failure { + Boom; + } + + fn fetch_pair() -> result (left: int, right: int) { + return ok((left: 1, right: 2)); + } + + fn main() -> void { + let scalar = 1; + let player = new Player(1); + let maybe = some(1); + let pair = (left: 1, right: 2); + let outcome = fetch_pair(); + return; + } + """); + Files.writeString(modBarrel, "pub fn main() -> void;"); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("app") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .build()); + + final var sourceFileId = registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, modBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId)))); + final var diagnostics = DiagnosticSink.empty(); + + final var inlineHintsByFile = PBSFrontendPhaseService.inlineHintsByFile( + ctx, + diagnostics, + BuildingIssueSink.empty()); + final var sourceHints = inlineHintsByFile.get(sourceFileId); + assertNotNull(sourceHints, () -> "diagnostics=" + + diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList() + + ", keys=" + inlineHintsByFile.keySet()); + final var labels = sourceHints.asList().stream() + .map(PbsInlineHintSurface::label) + .toList(); + + assertEquals(List.of( + "int", + "Player", + "optional int", + "(left: int, right: int)", + "result (left: int, right: int)"), + labels); + assertEquals(0, diagnostics.errorCount(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString()); + } + + @Test + void shouldSuppressInlineHintsForExplicitlyTypedLetBindings() throws IOException { + final var projectRoot = tempDir.resolve("project-inline-hints-explicit"); + final var sourceRoot = projectRoot.resolve("src"); + Files.createDirectories(sourceRoot); + + final var sourceFile = sourceRoot.resolve("main.pbs"); + final var modBarrel = sourceRoot.resolve("mod.barrel"); + Files.writeString(sourceFile, """ + fn main() -> void { + let inferred = 1; + let explicit: int = 2; + return; + } + """); + Files.writeString(modBarrel, "pub fn main() -> void;"); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("app") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .build()); + + final var sourceFileId = registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, modBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId)))); + final var diagnostics = DiagnosticSink.empty(); + + final var inlineHints = PBSFrontendPhaseService.inlineHintsByFile( + ctx, + diagnostics, + BuildingIssueSink.empty()).get(sourceFileId); + + assertEquals(1, inlineHints.size()); + assertEquals("int", inlineHints.getFirst().label()); + assertEquals("type", inlineHints.getFirst().category()); + } + + private FileId registerFile( final ProjectId projectId, final Path projectRoot, final Path file, final FileTable fileTable) throws IOException { final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); - fileTable.register(new SourceHandle( + return fileTable.register(new SourceHandle( projectId, projectRoot.relativize(file), file, diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java index 7012b2cd..0163745c 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java @@ -14,11 +14,13 @@ import p.studio.compiler.source.diagnostics.Diagnostic; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.services.PbsInlineHintSurface; import p.studio.compiler.workspaces.AssetSurfaceContextLoader; import p.studio.compiler.workspaces.PipelineStage; import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage; import p.studio.lsp.dtos.LspDiagnosticDTO; +import p.studio.lsp.dtos.LspInlineHintDTO; import p.studio.lsp.dtos.LspRangeDTO; import p.studio.lsp.dtos.LspSemanticPresentationDTO; import p.studio.lsp.messages.*; @@ -154,6 +156,10 @@ final class LspSemanticReadPhase { final Map> importedSupplementalTopDeclsByFile = importedSupplementalTopDeclsByFile( snapshot.frontendSpec(), context); + final Map> inlineHintsByDocument = inlineHintsByDocument( + snapshot, + snapshot.frontendSpec(), + context); final List indexedDocuments = new ArrayList<>(); for (final FileId fileId : snapshot.fileTable()) { final SourceHandle sourceHandle = snapshot.fileTable().get(fileId); @@ -181,7 +187,7 @@ final class LspSemanticReadPhase { semanticPresentation(snapshot.frontendSpec()), diagnosticsByDocument, semanticIndex.semanticHighlightsByDocument(), - Map.of(), + inlineHintsByDocument, semanticIndex.documentSymbolsByDocument(), semanticIndex.structuralAnchorsByDocument(), semanticIndex.workspaceSymbols(), @@ -192,9 +198,42 @@ final class LspSemanticReadPhase { private static Map> importedSupplementalTopDeclsByFile( final FrontendSpec frontendSpec, final BuilderPipelineContext context) { - if (context.resolvedWorkspace == null || context.fileTable == null || !"pbs".equals(frontendSpec.getLanguageId())) { + final var surface = semanticReadSurface(frontendSpec, context); + final Map> byFile = new LinkedHashMap<>(); + for (final var entry : surface.supplementalTopDeclsByFile().entrySet()) { + byFile.put(entry.getKey(), entry.getValue().asList()); + } + return Map.copyOf(byFile); + } + + private static Map> inlineHintsByDocument( + final AnalysisSnapshot snapshot, + final FrontendSpec frontendSpec, + final BuilderPipelineContext context) { + if (snapshot.fileTable() == null) { return Map.of(); } + final var surface = semanticReadSurface(frontendSpec, context); + final Map> byDocument = new LinkedHashMap<>(); + for (final var entry : surface.inlineHintsByFile().entrySet()) { + final SourceHandle sourceHandle = snapshot.fileTable().get(entry.getKey()); + final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize(); + final var hints = entry.getValue().asList().stream() + .map(LspSemanticReadPhase::toInlineHintDTO) + .toList(); + if (!hints.isEmpty()) { + byDocument.put(documentPath, hints); + } + } + return Map.copyOf(byDocument); + } + + private static PBSFrontendPhaseService.PbsSemanticReadSurface semanticReadSurface( + final FrontendSpec frontendSpec, + final BuilderPipelineContext context) { + if (context.resolvedWorkspace == null || context.fileTable == null || !"pbs".equals(frontendSpec.getLanguageId())) { + return new PBSFrontendPhaseService.PbsSemanticReadSurface(Map.of(), Map.of()); + } final FESurfaceContext feSurfaceContext = new AssetSurfaceContextLoader().load(context.resolvedWorkspace.mainProject().getRootPath()); final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( context.resolvedWorkspace.graph().projectTable(), @@ -205,15 +244,19 @@ final class LspSemanticReadPhase { feSurfaceContext); final var diagnostics = DiagnosticSink.empty(); final var issues = BuildingIssueSink.empty(); - final var supplementalTopDecls = PBSFrontendPhaseService.importedSupplementalTopDeclsByFile( + return PBSFrontendPhaseService.semanticReadSurface( frontendPhaseContext, diagnostics, issues); - final Map> byFile = new LinkedHashMap<>(); - for (final var entry : supplementalTopDecls.entrySet()) { - byFile.put(entry.getKey(), entry.getValue().asList()); - } - return Map.copyOf(byFile); + } + + private static LspInlineHintDTO toInlineHintDTO(final PbsInlineHintSurface hint) { + return new LspInlineHintDTO( + new LspRangeDTO( + (int) hint.anchorSpan().getStart(), + (int) hint.anchorSpan().getEnd()), + hint.label(), + hint.category()); } private record IndexedDocument( diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java index d36c2829..cefb212f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java @@ -89,7 +89,7 @@ final class LspServiceImplTest { assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper")); assertEquals(List.of("/themes/pbs/semantic-highlighting.css"), analysis.semanticPresentation().resources()); assertTrue(analysis.semanticPresentation().semanticKeys().contains("pbs-function")); - assertTrue(analysis.inlineHints().isEmpty()); + assertFalse(analysis.inlineHints().isEmpty(), analysis.toString()); assertTrue( analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")), @@ -209,7 +209,8 @@ final class LspServiceImplTest { void analyzeDocumentReturnsDedicatedInlineHintTransportSurface() throws Exception { final Path projectRoot = createProject(); final Path mainFile = projectRoot.resolve("src/main.pbs"); - Files.writeString(mainFile, "fn main() -> void { let value = 1; }\n"); + final String source = "fn main() -> void { let value = 1; }\n"; + Files.writeString(mainFile, source); final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); final LspService service = new LspServiceImpl( @@ -218,7 +219,39 @@ final class LspServiceImplTest { final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - assertTrue(analysis.inlineHints().isEmpty(), analysis.toString()); + assertEquals(1, analysis.inlineHints().size(), analysis.toString()); + assertEquals("int", analysis.inlineHints().getFirst().label()); + assertEquals("type", analysis.inlineHints().getFirst().category()); + assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor())); + } + + @Test + void analyzeDocumentPreservesValidInlineHintsUnderPartialDegradation() throws Exception { + final Path projectRoot = createProject(); + final Path mainFile = projectRoot.resolve("src/main.pbs"); + final String source = """ + fn ok() -> void { + let value = 1; + return; + } + + fn broken( -> void { + let missing = 2; + } + """; + Files.writeString(mainFile, source); + + final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); + final LspService service = new LspServiceImpl( + new LspProjectContext("Example", "pbs", projectRoot), + vfs); + + final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); + + assertFalse(analysis.diagnostics().isEmpty(), analysis.toString()); + assertEquals(1, analysis.inlineHints().size(), analysis.toString()); + assertEquals("int", analysis.inlineHints().getFirst().label()); + assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor())); } private Path createProject() throws Exception { @@ -315,6 +348,12 @@ final class LspServiceImplTest { return source.substring(start, end); } + private static String sourceSlice( + final String source, + final p.studio.lsp.dtos.LspRangeDTO range) { + return spanContent(source, range.startOffset(), range.endOffset()); + } + private static final class OverlayVfsProjectDocument implements VfsProjectDocument { private final VfsProjectDocument delegate; private final Path overlayPath;