diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java new file mode 100644 index 00000000..4001004b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java @@ -0,0 +1,30 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.Objects; + +public record PbsEditorialCompletionCandidate( + String label, + PbsEditorialSymbolKind kind, + String detail, + String origin) { + public PbsEditorialCompletionCandidate { + label = requireText(label, "label"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + origin = normalize(origin); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java new file mode 100644 index 00000000..43abee2b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java @@ -0,0 +1,35 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialResolvedSymbol( + String displayName, + PbsEditorialSymbolKind kind, + String detail, + String origin, + List signatures, + String documentation) { + public PbsEditorialResolvedSymbol { + displayName = requireText(displayName, "displayName"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + origin = normalize(origin); + signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures")); + documentation = normalize(documentation); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java new file mode 100644 index 00000000..43bb9a89 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java @@ -0,0 +1,27 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialSignature( + String callableName, + String label, + List parameterLabels, + String returnLabel) { + public PbsEditorialSignature { + callableName = requireText(callableName, "callableName"); + label = requireText(label, "label"); + parameterLabels = List.copyOf(Objects.requireNonNull(parameterLabels, "parameterLabels")); + returnLabel = requireText(returnLabel, "returnLabel"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java new file mode 100644 index 00000000..e632e2ed --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java @@ -0,0 +1,22 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialSignatureHelp( + List signatures, + int activeSignature, + int activeParameter) { + public PbsEditorialSignatureHelp { + signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures")); + if (signatures.isEmpty()) { + throw new IllegalArgumentException("signatures must not be empty"); + } + if (activeSignature < 0 || activeSignature >= signatures.size()) { + throw new IllegalArgumentException("activeSignature must reference one of the provided signatures"); + } + if (activeParameter < 0) { + throw new IllegalArgumentException("activeParameter must be >= 0"); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java new file mode 100644 index 00000000..dc4c839c --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java @@ -0,0 +1,1303 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.messages.FESurfaceContext; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.*; + +import static p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.*; + +public final class PbsEditorialSupportService { + private static final Set BUILTIN_TYPE_NAMES = Set.of("int", "float", "bool", "str", "string", "i32", "i64", "f32", "f64"); + private static final List KEYWORDS = List.of( + "fn", "declare", "let", "const", "global", "struct", "service", "host", "contract", "enum", "error", "callback", + "if", "else", "switch", "for", "until", "step", "while", "break", "continue", "return", + "new", "bind", "apply", "handle", "some", "none", "ok", "err", "true", "false"); + + public List completion( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return completion(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public List completion( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final var scopeState = context.scopeStateAt(offset); + final Integer previousTokenIndex = context.previousSignificantTokenIndexAt(offset); + if (previousTokenIndex != null && context.token(previousTokenIndex).kind() == PbsTokenKind.DOT) { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState); + return context.memberCompletions(receiverType); + } + return context.generalCompletions(scopeState); + } + + public Optional hover( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return hover(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public Optional hover( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final Integer tokenIndex = context.identifierTokenIndexAt(offset); + if (tokenIndex == null) { + return Optional.empty(); + } + final var scopeState = context.scopeStateAt(offset); + final var token = context.token(tokenIndex); + final Integer previousTokenIndex = context.previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && context.token(previousTokenIndex).kind() == PbsTokenKind.DOT) { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState); + return context.resolveMemberHover(receiverType, token.lexeme()); + } + return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState); + } + + public Optional signatureHelp( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return signatureHelp(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public Optional signatureHelp( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final var callSite = context.callSiteAt(offset); + if (callSite == null) { + return Optional.empty(); + } + final var scopeState = context.scopeStateAt(offset); + final List signatures = switch (callSite.kind()) { + case FUNCTION -> context.resolveCallableSignatures(callSite.name(), scopeState); + case MEMBER -> { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(callSite.memberDotTokenIndex(), scopeState); + yield context.resolveMemberSignatures(receiverType, callSite.name()); + } + case CONSTRUCTOR -> context.resolveConstructorSignatures(callSite.name()); + }; + if (signatures.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter())); + } + + private DocumentContext buildContext( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext) { + return new DocumentContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + } + + private static final class DocumentContext { + private final String sourceText; + private final PbsAst.File ast; + private final ReadOnlyList editorialSupplementalTopDecls; + private final ReadOnlyList tokens; + private final Model model; + private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); + private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps); + private final PbsInlineHintSurfaceFormatter typeFormatter = new PbsInlineHintSurfaceFormatter(); + private final Map topLevelSymbols; + private final Map importedSymbols; + private final Map supplementalSymbols; + private final Map aliasedTypeRefs; + + private DocumentContext( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext) { + this.sourceText = sourceText == null ? "" : sourceText; + this.ast = Objects.requireNonNull(ast, "ast"); + final ArrayList expandedSupplementals = new ArrayList<>(supplementalTopDecls == null + ? List.of() + : supplementalTopDecls.asList()); + this.importedSymbols = buildImportedSymbols(expandedSupplementals); + this.editorialSupplementalTopDecls = ReadOnlyList.wrap(expandedSupplementals); + this.tokens = PbsLexer.lex(this.sourceText, FileId.none(), DiagnosticSink.empty()); + this.model = Model.from( + ast, + editorialSupplementalTopDecls, + feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext, + DiagnosticSink.empty()); + this.topLevelSymbols = buildTopLevelSymbols(); + this.supplementalSymbols = buildSupplementalSymbols(); + this.aliasedTypeRefs = buildAliasedTypeRefs(); + } + + private Map buildTopLevelSymbols() { + final Map symbols = new LinkedHashMap<>(); + for (final var topDecl : ast.topDecls()) { + final String name = topDeclName(topDecl); + if (name == null || symbols.containsKey(name)) { + continue; + } + symbols.put(name, new TopDeclSymbol(name, topDecl, "current module")); + } + return Map.copyOf(symbols); + } + + private Map buildImportedSymbols(final ArrayList expandedSupplementals) { + final Map symbols = new LinkedHashMap<>(); + for (final var importDecl : ast.imports()) { + final String moduleLabel = formatModuleRef(importDecl.moduleRef()); + for (final var item : importDecl.items()) { + final String localName = item.alias() == null || item.alias().isBlank() + ? item.name() + : item.alias(); + final PbsAst.TopDecl target = findSupplementalTopDecl(item.name(), expandedSupplementals); + if (target == null) { + continue; + } + symbols.put(localName, new TopDeclSymbol(localName, cloneTopDeclWithName(target, localName), moduleLabel)); + if (!localName.equals(item.name())) { + expandedSupplementals.add(cloneTopDeclWithName(target, localName)); + } + } + } + return Map.copyOf(symbols); + } + + private Map buildSupplementalSymbols() { + final Map symbols = new LinkedHashMap<>(); + for (final var topDecl : editorialSupplementalTopDecls) { + final String name = topDeclName(topDecl); + if (name == null || importedSymbols.containsKey(name) || symbols.containsKey(name)) { + continue; + } + symbols.put(name, new TopDeclSymbol(name, topDecl, "imported context")); + } + return Map.copyOf(symbols); + } + + private Map buildAliasedTypeRefs() { + final Map typeRefs = new HashMap<>(); + for (final var entry : importedSymbols.entrySet()) { + if (entry.getValue().decl() instanceof PbsAst.StructDecl structDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(structDecl.name(), structDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(builtinTypeDecl.name(), builtinTypeDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ServiceDecl serviceDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(serviceDecl.name(), serviceDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.HostDecl hostDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(hostDecl.name(), hostDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ContractDecl contractDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(contractDecl.name(), contractDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.EnumDecl enumDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(enumDecl.name(), enumDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ErrorDecl errorDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(errorDecl.name(), errorDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.CallbackDecl callbackDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(callbackDecl.name(), callbackDecl.span())); + } + } + return Map.copyOf(typeRefs); + } + + private ScopeState scopeStateAt(final int offset) { + final CallableFrame callableFrame = callableFrameAt(offset); + if (callableFrame == null) { + return new ScopeState(new Scope(), null, TypeView.unit(), null, Set.of()); + } + final Scope scope = new Scope(); + for (final var parameter : callableFrame.parameters()) { + scope.bind(parameter.name(), typeOps.typeFromTypeRef(parameter.typeRef(), model, callableFrame.receiverType()), true); + } + final Scope resolvedScope = populateScopeUntilOffset(callableFrame.body(), scope, callableFrame, offset); + return new ScopeState( + resolvedScope, + callableFrame.receiverType(), + callableFrame.returnType(), + callableFrame.resultErrorName(), + callableFrame.parameterNames()); + } + + private Scope populateScopeUntilOffset( + final PbsAst.Block block, + final Scope seedScope, + final CallableFrame frame, + final int offset) { + final Scope scope = seedScope.copy(); + for (final var statement : block.statements()) { + if (statement.span().getStart() >= offset) { + break; + } + if (statement.span().contains(offset)) { + if (statement instanceof PbsAst.IfStatement ifStatement) { + if (ifStatement.thenBlock().span().contains(offset)) { + return populateScopeUntilOffset(ifStatement.thenBlock(), scope, frame, offset); + } + if (ifStatement.elseBlock() != null && ifStatement.elseBlock().span().contains(offset)) { + return populateScopeUntilOffset(ifStatement.elseBlock(), scope, frame, offset); + } + if (ifStatement.elseIf() != null && ifStatement.elseIf().span().contains(offset)) { + final var nestedBlock = new PbsAst.Block(ReadOnlyList.wrap(List.of(ifStatement.elseIf())), null, ifStatement.elseIf().span()); + return populateScopeUntilOffset(nestedBlock, scope, frame, offset); + } + } else if (statement instanceof PbsAst.ForStatement forStatement) { + if (forStatement.body().span().contains(offset)) { + final Scope loopScope = scope.copy(); + loopScope.bind(forStatement.iteratorName(), typeOps.typeFromTypeRef(forStatement.iteratorType(), model, frame.receiverType())); + return populateScopeUntilOffset(forStatement.body(), loopScope, frame, offset); + } + } else if (statement instanceof PbsAst.WhileStatement whileStatement && whileStatement.body().span().contains(offset)) { + return populateScopeUntilOffset(whileStatement.body(), scope, frame, offset); + } + return scope; + } + applyStatementEffects(statement, scope, frame); + } + return scope; + } + + private void applyStatementEffects( + final PbsAst.Statement statement, + final Scope scope, + final CallableFrame frame) { + if (statement instanceof PbsAst.LetStatement letStatement) { + final PbsAst.TypeRef explicitType = aliasExpandedTypeRef(letStatement.explicitType()); + final TypeView expectedType = explicitType == null + ? null + : typeOps.typeFromTypeRef(explicitType, model, frame.receiverType()); + final TypeView inferredType = analyzeExpressionType( + letStatement.initializer(), + scope, + expectedType, + frame.returnType(), + frame.resultErrorName(), + frame.receiverType()); + final TypeView resolvedType = expectedType == null ? inferredType : expectedType; + scope.bind(letStatement.name(), resolvedType, !letStatement.isConst()); + } + } + + private TypeView analyzeExpressionType( + final PbsAst.Expression expression, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType) { + return expressionAnalyzer.analyzeExpression( + expression, + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + DiagnosticSink.empty(), + ExprUse.VALUE, + true, + this::analyzeNestedBlock).type(); + } + + private TypeView analyzeNestedBlock( + final PbsAst.Block block, + final Scope outerScope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model ignoredModel, + final DiagnosticSink diagnostics, + final boolean valueContext) { + final Scope scoped = outerScope.copy(); + for (final var statement : block.statements()) { + if (statement instanceof PbsAst.LetStatement letStatement) { + final PbsAst.TypeRef explicitType = aliasExpandedTypeRef(letStatement.explicitType()); + final TypeView expectedType = explicitType == null + ? null + : typeOps.typeFromTypeRef(explicitType, model, receiverType); + final TypeView inferredType = expressionAnalyzer.analyzeExpression( + letStatement.initializer(), + scoped, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true, + this::analyzeNestedBlock).type(); + scoped.bind(letStatement.name(), expectedType == null ? inferredType : expectedType, !letStatement.isConst()); + } + } + if (block.tailExpression() == null) { + return TypeView.unit(); + } + return expressionAnalyzer.analyzeExpression( + block.tailExpression(), + scoped, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + valueContext, + this::analyzeNestedBlock).type(); + } + + private CallableFrame callableFrameAt(final int offset) { + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl && functionDecl.span().contains(offset)) { + return new CallableFrame( + functionDecl.parameters(), + functionDecl.body(), + null, + typeOps.callableReturnType( + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + model), + errorName(functionDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.StructDecl structDecl && structDecl.span().contains(offset)) { + final TypeView receiverType = TypeView.struct(structDecl.name()); + for (final var method : structDecl.methods()) { + if (method.span().contains(offset)) { + return new CallableFrame( + method.parameters(), + method.body(), + receiverType, + typeOps.callableReturnType( + method.returnKind(), + method.returnType(), + method.resultErrorType(), + model), + errorName(method.resultErrorType())); + } + } + for (final var ctor : structDecl.ctors()) { + if (ctor.span().contains(offset)) { + return new CallableFrame( + ctor.parameters(), + ctor.body(), + receiverType, + TypeView.unit(), + null); + } + } + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl && serviceDecl.span().contains(offset)) { + final TypeView receiverType = TypeView.service(serviceDecl.name()); + for (final var method : serviceDecl.methods()) { + if (method.span().contains(offset)) { + return new CallableFrame( + method.parameters(), + method.body(), + receiverType, + typeOps.callableReturnType( + method.returnKind(), + method.returnType(), + method.resultErrorType(), + model), + errorName(method.resultErrorType())); + } + } + } + } + return null; + } + + private List generalCompletions(final ScopeState scopeState) { + final Map completions = new LinkedHashMap<>(); + for (final var keyword : KEYWORDS) { + completions.put(keyword, new PbsEditorialCompletionCandidate(keyword, PbsEditorialSymbolKind.KEYWORD, "", "")); + } + for (final var parameterName : scopeState.parameterNames()) { + final TypeView type = scopeState.scope().resolve(parameterName); + completions.put(parameterName, new PbsEditorialCompletionCandidate( + parameterName, + PbsEditorialSymbolKind.PARAMETER, + formatType(type), + "local parameter")); + } + for (final var entry : scopeState.scope().entries().entrySet()) { + if (scopeState.parameterNames().contains(entry.getKey())) { + continue; + } + completions.put(entry.getKey(), new PbsEditorialCompletionCandidate( + entry.getKey(), + PbsEditorialSymbolKind.LOCAL, + formatType(entry.getValue().type()), + entry.getValue().mutable() ? "local mutable binding" : "local const binding")); + } + for (final var entry : importedSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + for (final var entry : supplementalSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + for (final var entry : topLevelSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + return List.copyOf(completions.values()); + } + + private List memberCompletions(final TypeView receiverType) { + if (receiverType == null) { + return List.of(); + } + final Map completions = new LinkedHashMap<>(); + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo != null) { + for (final var field : structInfo.fields().entrySet()) { + completions.put(field.getKey(), new PbsEditorialCompletionCandidate( + field.getKey(), + PbsEditorialSymbolKind.FIELD, + formatType(field.getValue().type()), + receiverType.name())); + } + for (final var method : structInfo.methods().entrySet()) { + completions.put(method.getKey(), new PbsEditorialCompletionCandidate( + method.getKey(), + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(method.getValue()), + receiverType.name())); + } + } + } else if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { + final Map> methods = receiverType.kind() == Kind.SERVICE + ? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of()) + : 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())); + } + } + return List.copyOf(completions.values()); + } + + private Optional resolveSymbolHover( + final String symbolName, + final int tokenIndex, + final ScopeState scopeState) { + if (scopeState.parameterNames().contains(symbolName)) { + final TypeView type = scopeState.scope().resolve(symbolName); + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.PARAMETER, + formatType(type), + "local parameter", + List.of(), + "")); + } + final Scope.LocalSymbol localSymbol = scopeState.scope().entries().get(symbolName); + if (localSymbol != null) { + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.LOCAL, + formatType(localSymbol.type()), + localSymbol.mutable() ? "local mutable binding" : "local const binding", + List.of(), + "")); + } + final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.NEW) { + final TopDeclSymbol topDeclSymbol = resolveTopDeclSymbol(symbolName); + if (topDeclSymbol != null) { + return Optional.of(constructorSymbolFromTopDecl(topDeclSymbol, resolveConstructorSignatures(symbolName))); + } + } + final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName); + if (importedSymbol != null) { + return Optional.of(symbolFromTopDecl(importedSymbol, signaturesForTopDecl(importedSymbol.decl()))); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(symbolName); + if (supplementalSymbol != null) { + return Optional.of(symbolFromTopDecl(supplementalSymbol, signaturesForTopDecl(supplementalSymbol.decl()))); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName); + if (topLevelSymbol != null) { + return Optional.of(symbolFromTopDecl(topLevelSymbol, signaturesForTopDecl(topLevelSymbol.decl()))); + } + if (BUILTIN_TYPE_NAMES.contains(symbolName)) { + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.BUILTIN_TYPE, + "builtin scalar type", + "", + List.of(), + "")); + } + return Optional.empty(); + } + + private Optional resolveMemberHover( + final TypeView receiverType, + final String memberName) { + if (receiverType == null) { + return Optional.empty(); + } + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo == null) { + return Optional.empty(); + } + final StructFieldInfo field = structInfo.fields().get(memberName); + if (field != null) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.FIELD, + formatType(field.type()), + receiverType.name(), + List.of(), + "")); + } + final List methods = structInfo.methods().get(memberName); + if (methods != null && !methods.isEmpty()) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(methods), + receiverType.name(), + signaturesFromCallables(methods), + "")); + } + } + if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { + final Map> methods = receiverType.kind() == Kind.SERVICE + ? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of()) + : Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of()); + final List signatures = methods.get(memberName); + if (signatures != null && !signatures.isEmpty()) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(signatures), + receiverType.name(), + signaturesFromCallables(signatures), + "")); + } + } + return Optional.empty(); + } + + private List resolveCallableSignatures( + final String callableName, + final ScopeState scopeState) { + final TopDeclSymbol importedSymbol = importedSymbols.get(callableName); + if (importedSymbol != null) { + return signaturesForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(callableName); + if (topLevelSymbol != null) { + return signaturesForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(callableName); + if (supplementalSymbol != null) { + return signaturesForTopDecl(supplementalSymbol.decl()); + } + return signaturesFromCallables(model.topLevelCallables.getOrDefault(callableName, List.of())); + } + + private List resolveMemberSignatures( + final TypeView receiverType, + final String memberName) { + return resolveMemberHover(receiverType, memberName) + .map(PbsEditorialResolvedSymbol::signatures) + .orElse(List.of()); + } + + private List resolveConstructorSignatures(final String typeName) { + final TopDeclSymbol importedSymbol = importedSymbols.get(typeName); + if (importedSymbol != null) { + return constructorSignaturesForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(typeName); + if (topLevelSymbol != null) { + return constructorSignaturesForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(typeName); + if (supplementalSymbol != null) { + return constructorSignaturesForTopDecl(supplementalSymbol.decl()); + } + return List.of(); + } + + private PbsEditorialResolvedSymbol symbolFromTopDecl( + final TopDeclSymbol topDeclSymbol, + final List signatures) { + final PbsEditorialSymbolKind kind = symbolKind(topDeclSymbol.decl()); + final String detail = signatures.isEmpty() + ? kind.name().toLowerCase(Locale.ROOT).replace('_', ' ') + : signatures.getFirst().label(); + return new PbsEditorialResolvedSymbol( + topDeclSymbol.localName(), + kind, + detail, + topDeclSymbol.origin(), + signatures, + ""); + } + + private PbsEditorialResolvedSymbol constructorSymbolFromTopDecl( + final TopDeclSymbol topDeclSymbol, + final List signatures) { + final String detail = signatures.isEmpty() + ? "constructor" + : signatures.getFirst().label(); + return new PbsEditorialResolvedSymbol( + topDeclSymbol.localName(), + PbsEditorialSymbolKind.CONSTRUCTOR, + detail, + topDeclSymbol.origin(), + signatures, + ""); + } + + private PbsEditorialCompletionCandidate completionFromTopDecl(final TopDeclSymbol topDeclSymbol) { + final List signatures = signaturesForTopDecl(topDeclSymbol.decl()); + final String detail = signatures.isEmpty() + ? symbolKind(topDeclSymbol.decl()).name().toLowerCase(Locale.ROOT).replace('_', ' ') + : signatures.getFirst().label(); + return new PbsEditorialCompletionCandidate( + topDeclSymbol.localName(), + symbolKind(topDeclSymbol.decl()), + detail, + topDeclSymbol.origin()); + } + + private List signaturesForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return List.of(signatureFromFunction(functionDecl.name(), functionDecl.parameters(), functionDecl.returnKind(), functionDecl.returnType(), functionDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return List.of(signatureFromFunction(callbackDecl.name(), callbackDecl.parameters(), callbackDecl.returnKind(), callbackDecl.returnType(), callbackDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.methods().stream() + .map(method -> signatureFromFunction(method.name(), method.parameters(), method.returnKind(), method.returnType(), method.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + return List.of(); + } + + private List constructorSignaturesForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + if (structDecl.ctors().isEmpty()) { + return List.of(new PbsEditorialSignature(structDecl.name(), structDecl.name() + "()", List.of(), "unit")); + } + return structDecl.ctors().stream() + .map(ctor -> signatureFromCtor(structDecl.name(), ctor)) + .toList(); + } + return List.of(); + } + + private List signaturesFromCallables(final List callables) { + if (callables == null || callables.isEmpty()) { + return List.of(); + } + final ArrayList signatures = new ArrayList<>(callables.size()); + for (final var callable : callables) { + final ArrayList parameterLabels = new ArrayList<>(callable.inputTypes().size()); + for (int index = 0; index < callable.inputTypes().size(); index += 1) { + parameterLabels.add("arg" + index + ": " + formatType(callable.inputTypes().get(index))); + } + final String label = callable.name() + "(" + String.join(", ", parameterLabels) + ") -> " + formatType(callable.outputType()); + signatures.add(new PbsEditorialSignature(callable.name(), label, parameterLabels, formatType(callable.outputType()))); + } + return List.copyOf(signatures); + } + + private PbsEditorialSignature signatureFromFunction( + final String callableName, + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + final ArrayList parameterLabels = new ArrayList<>(parameters.size()); + for (final var parameter : parameters) { + parameterLabels.add(parameter.name() + ": " + formatTypeRef(aliasExpandedTypeRef(parameter.typeRef()))); + } + final String returnLabel = formatReturnType(returnKind, aliasExpandedTypeRef(returnType), aliasExpandedTypeRef(resultErrorType)); + return new PbsEditorialSignature( + callableName, + callableName + "(" + String.join(", ", parameterLabels) + ") -> " + returnLabel, + parameterLabels, + returnLabel); + } + + private PbsEditorialSignature signatureFromCtor( + final String ownerTypeName, + final PbsAst.CtorDecl ctorDecl) { + final ArrayList parameterLabels = new ArrayList<>(ctorDecl.parameters().size()); + for (final var parameter : ctorDecl.parameters()) { + parameterLabels.add(parameter.name() + ": " + formatTypeRef(aliasExpandedTypeRef(parameter.typeRef()))); + } + return new PbsEditorialSignature( + ownerTypeName, + ownerTypeName + "(" + String.join(", ", parameterLabels) + ")", + parameterLabels, + "unit"); + } + + private String formatCallableDetails(final List callables) { + if (callables == null || callables.isEmpty()) { + return ""; + } + return signaturesFromCallables(callables).getFirst().label(); + } + + private String formatType(final TypeView type) { + final String formatted = typeFormatter.format(type); + return formatted == null ? "unknown" : formatted; + } + + private String formatTypeRef(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return "unit"; + } + return switch (typeRef.kind()) { + case UNIT -> "unit"; + case SELF -> "Self"; + case SIMPLE -> typeRef.name(); + case OPTIONAL -> formatTypeRef(typeRef.inner()) + "?"; + case GROUP -> formatTypeRef(typeRef.inner()); + case NAMED_TUPLE -> { + final ArrayList fields = new ArrayList<>(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(field.label() + ": " + formatTypeRef(field.typeRef())); + } + yield "(" + String.join(", ", fields) + ")"; + } + case ERROR -> "unknown"; + }; + } + + private String formatReturnType( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + return switch (returnKind) { + case INFERRED_UNIT, EXPLICIT_UNIT -> "unit"; + case PLAIN -> formatTypeRef(returnType); + case RESULT -> "result<" + formatTypeRef(resultErrorType) + ", " + formatTypeRef(returnType) + ">"; + }; + } + + private TypeView resolveReceiverTypeBeforeDot( + final int dotTokenIndex, + final ScopeState scopeState) { + final List segments = resolveChainSegmentsEndingAt(dotTokenIndex - 1); + if (segments.isEmpty()) { + return TypeView.unknown(); + } + TypeView current = resolveBaseChainType(segments.getFirst(), scopeState); + for (int index = 1; index < segments.size(); index += 1) { + current = resolveChainMemberType(current, segments.get(index)); + } + return current; + } + + private TypeView resolveBaseChainType( + final String baseName, + final ScopeState scopeState) { + if ("this".equals(baseName)) { + return scopeState.receiverType() == null ? TypeView.unknown() : scopeState.receiverType(); + } + final TypeView localType = scopeState.scope().resolve(baseName); + if (localType != null) { + return localType; + } + final TopDeclSymbol importedSymbol = importedSymbols.get(baseName); + if (importedSymbol != null) { + return typeForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(baseName); + if (topLevelSymbol != null) { + return typeForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(baseName); + if (supplementalSymbol != null) { + return typeForTopDecl(supplementalSymbol.decl()); + } + if ("assets".equals(baseName)) { + return TypeView.assetNamespace("assets"); + } + return TypeView.unknown(); + } + + private TypeView resolveChainMemberType( + final TypeView receiverType, + final String memberName) { + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo == null) { + return TypeView.unknown(); + } + final StructFieldInfo field = structInfo.fields().get(memberName); + return field == null ? TypeView.unknown() : field.type(); + } + if (receiverType.kind() == Kind.ASSET_NAMESPACE) { + final String candidate = receiverType.name() + "." + memberName; + if (model.resolveAssetId(candidate) != null) { + return TypeView.addressable(candidate); + } + if (model.isAssetNamespace(candidate)) { + return TypeView.assetNamespace(candidate); + } + } + return TypeView.unknown(); + } + + private TypeView typeForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return TypeView.struct(structDecl.name()); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return TypeView.struct(builtinTypeDecl.name()); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return TypeView.service(serviceDecl.name()); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return TypeView.service(hostDecl.name()); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return TypeView.contract(contractDecl.name()); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return TypeView.enumType(enumDecl.name()); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return TypeView.error(errorDecl.name()); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return typeOps.typeFromTypeRef(PbsAst.TypeRef.simple(callbackDecl.name(), callbackDecl.span()), model, null); + } + return TypeView.unknown(); + } + + private CallSite callSiteAt(final int offset) { + int depth = 0; + int activeParameter = 0; + for (int index = significantTokenCount() - 1; index >= 0; index -= 1) { + final PbsToken token = significantToken(index); + if (token.start() >= offset) { + continue; + } + if (token.kind() == PbsTokenKind.RIGHT_PAREN) { + depth += 1; + continue; + } + if (token.kind() == PbsTokenKind.LEFT_PAREN) { + if (depth == 0) { + final Integer calleeIndex = previousSignificantTokenIndex(tokenIndex(token)); + if (calleeIndex == null) { + return null; + } + final PbsToken calleeToken = token(calleeIndex); + if (calleeToken.kind() != PbsTokenKind.IDENTIFIER) { + return null; + } + final Integer maybeDotIndex = previousSignificantTokenIndex(calleeIndex); + if (maybeDotIndex != null && token(maybeDotIndex).kind() == PbsTokenKind.DOT) { + return new CallSite(CallSiteKind.MEMBER, calleeToken.lexeme(), activeParameter, maybeDotIndex); + } + if (maybeDotIndex != null && token(maybeDotIndex).kind() == PbsTokenKind.NEW) { + return new CallSite(CallSiteKind.CONSTRUCTOR, calleeToken.lexeme(), activeParameter, -1); + } + return new CallSite(CallSiteKind.FUNCTION, calleeToken.lexeme(), activeParameter, -1); + } + depth -= 1; + continue; + } + if (depth == 0 && token.kind() == PbsTokenKind.COMMA) { + activeParameter += 1; + } + } + return null; + } + + private Integer identifierTokenIndexAt(final int offset) { + for (int index = 0; index < tokens.size(); index += 1) { + final PbsToken token = tokens.get(index); + if (token.kind() == PbsTokenKind.IDENTIFIER && token.start() <= offset && token.end() > offset) { + return index; + } + } + return null; + } + + private Integer previousSignificantTokenIndexAt(final int offset) { + for (int index = tokens.size() - 1; index >= 0; index -= 1) { + final PbsToken token = tokens.get(index); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + if (token.end() <= offset) { + return index; + } + } + return null; + } + + private Integer previousSignificantTokenIndex(final int index) { + for (int cursor = index - 1; cursor >= 0; cursor -= 1) { + final PbsToken token = tokens.get(cursor); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + return cursor; + } + return null; + } + + private List resolveChainSegmentsEndingAt(final int endTokenIndex) { + final ArrayList reversedSegments = new ArrayList<>(); + int cursor = endTokenIndex; + boolean expectIdentifier = true; + while (cursor >= 0) { + final PbsToken token = tokens.get(cursor); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + cursor -= 1; + continue; + } + if (expectIdentifier) { + if (token.kind() == PbsTokenKind.IDENTIFIER) { + reversedSegments.add(token.lexeme()); + expectIdentifier = false; + cursor -= 1; + continue; + } + if (token.kind() == PbsTokenKind.THIS) { + reversedSegments.add("this"); + } + break; + } + if (token.kind() != PbsTokenKind.DOT) { + break; + } + expectIdentifier = true; + cursor -= 1; + } + Collections.reverse(reversedSegments); + return List.copyOf(reversedSegments); + } + + private int tokenIndex(final PbsToken needle) { + for (int index = 0; index < tokens.size(); index += 1) { + if (tokens.get(index) == needle) { + return index; + } + } + return -1; + } + + private int significantTokenCount() { + int count = 0; + for (final var token : tokens) { + if (token.kind() != PbsTokenKind.COMMENT && token.kind() != PbsTokenKind.EOF) { + count += 1; + } + } + return count; + } + + private PbsToken significantToken(final int significantIndex) { + int cursor = 0; + for (final var token : tokens) { + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + if (cursor == significantIndex) { + return token; + } + cursor += 1; + } + throw new IndexOutOfBoundsException("No significant token at index " + significantIndex); + } + + private PbsToken token(final int index) { + return tokens.get(index); + } + + private PbsAst.TypeRef aliasExpandedTypeRef(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return null; + } + return switch (typeRef.kind()) { + case SIMPLE -> aliasedTypeRefs.getOrDefault(typeRef.name(), typeRef); + case OPTIONAL -> PbsAst.TypeRef.optional(aliasExpandedTypeRef(typeRef.inner()), typeRef.span()); + case GROUP -> PbsAst.TypeRef.group(aliasExpandedTypeRef(typeRef.inner()), typeRef.span()); + case NAMED_TUPLE -> { + final ArrayList fields = new ArrayList<>(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(new PbsAst.NamedTypeField(field.label(), aliasExpandedTypeRef(field.typeRef()), field.span())); + } + yield new PbsAst.TypeRef(PbsAst.TypeRefKind.NAMED_TUPLE, typeRef.name(), ReadOnlyList.wrap(fields), null, typeRef.span()); + } + case UNIT, SELF, ERROR -> typeRef; + }; + } + + private PbsAst.TopDecl findSupplementalTopDecl( + final String name, + final List supplementals) { + for (final var topDecl : supplementals) { + if (name.equals(topDeclName(topDecl))) { + return topDecl; + } + } + return null; + } + + private String topDeclName(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return functionDecl.name(); + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return structDecl.name(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.name(); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.name(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.name(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.name(); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return callbackDecl.name(); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return enumDecl.name(); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return errorDecl.name(); + } + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + return globalDecl.name(); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return constDecl.name(); + } + return null; + } + + private String formatModuleRef(final PbsAst.ModuleRef moduleRef) { + if (moduleRef == null) { + return ""; + } + if (moduleRef.pathSegments().isEmpty()) { + return "@%s:".formatted(moduleRef.project()); + } + return "@%s:%s".formatted(moduleRef.project(), String.join("/", moduleRef.pathSegments().asList())); + } + + private PbsAst.TopDecl cloneTopDeclWithName( + final PbsAst.TopDecl topDecl, + final String newName) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return new PbsAst.FunctionDecl( + newName, + functionDecl.parameters(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + functionDecl.lifecycleMarker(), + functionDecl.body(), + functionDecl.span()); + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return new PbsAst.StructDecl( + newName, + structDecl.fields(), + structDecl.methods(), + structDecl.ctors(), + structDecl.hasBody(), + structDecl.span()); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return new PbsAst.BuiltinTypeDecl( + newName, + builtinTypeDecl.fields(), + builtinTypeDecl.signatures(), + builtinTypeDecl.attributes(), + builtinTypeDecl.span()); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return new PbsAst.ServiceDecl(newName, serviceDecl.methods(), serviceDecl.span()); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return new PbsAst.HostDecl(newName, hostDecl.signatures(), hostDecl.span()); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return new PbsAst.ContractDecl(newName, contractDecl.signatures(), contractDecl.span()); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return new PbsAst.CallbackDecl( + newName, + callbackDecl.parameters(), + callbackDecl.returnKind(), + callbackDecl.returnType(), + callbackDecl.resultErrorType(), + callbackDecl.span()); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return new PbsAst.EnumDecl(newName, enumDecl.cases(), enumDecl.span()); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return new PbsAst.ErrorDecl(newName, errorDecl.cases(), errorDecl.span()); + } + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + return new PbsAst.GlobalDecl(newName, globalDecl.explicitType(), globalDecl.initializer(), globalDecl.span()); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return new PbsAst.ConstDecl(newName, constDecl.explicitType(), constDecl.initializer(), constDecl.attributes(), constDecl.span()); + } + return topDecl; + } + + private TopDeclSymbol resolveTopDeclSymbol(final String symbolName) { + final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName); + if (importedSymbol != null) { + return importedSymbol; + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName); + if (topLevelSymbol != null) { + return topLevelSymbol; + } + return supplementalSymbols.get(symbolName); + } + + private PbsEditorialSymbolKind symbolKind(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl) { + return PbsEditorialSymbolKind.FUNCTION; + } + if (topDecl instanceof PbsAst.StructDecl) { + return PbsEditorialSymbolKind.STRUCT; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl) { + return PbsEditorialSymbolKind.BUILTIN_TYPE; + } + if (topDecl instanceof PbsAst.ServiceDecl) { + return PbsEditorialSymbolKind.SERVICE; + } + if (topDecl instanceof PbsAst.HostDecl) { + return PbsEditorialSymbolKind.HOST; + } + if (topDecl instanceof PbsAst.ContractDecl) { + return PbsEditorialSymbolKind.CONTRACT; + } + if (topDecl instanceof PbsAst.CallbackDecl) { + return PbsEditorialSymbolKind.CALLBACK; + } + if (topDecl instanceof PbsAst.EnumDecl) { + return PbsEditorialSymbolKind.ENUM; + } + if (topDecl instanceof PbsAst.ErrorDecl) { + return PbsEditorialSymbolKind.ERROR; + } + if (topDecl instanceof PbsAst.GlobalDecl) { + return PbsEditorialSymbolKind.GLOBAL; + } + if (topDecl instanceof PbsAst.ConstDecl) { + return PbsEditorialSymbolKind.CONST; + } + throw new IllegalArgumentException("Unsupported top declaration: " + topDecl.getClass().getSimpleName()); + } + + private String errorName(final PbsAst.TypeRef typeRef) { + return typeRef == null ? null : typeRef.name(); + } + } + + private record TopDeclSymbol( + String localName, + PbsAst.TopDecl decl, + String origin) { + } + + private record CallableFrame( + ReadOnlyList parameters, + PbsAst.Block body, + TypeView receiverType, + TypeView returnType, + String resultErrorName) { + private Set parameterNames() { + final Set names = new LinkedHashSet<>(); + for (final var parameter : parameters) { + names.add(parameter.name()); + } + return Set.copyOf(names); + } + } + + private record ScopeState( + Scope scope, + TypeView receiverType, + TypeView returnType, + String resultErrorName, + Set parameterNames) { + } + + private enum CallSiteKind { + FUNCTION, + MEMBER, + CONSTRUCTOR + } + + private record CallSite( + CallSiteKind kind, + String name, + int activeParameter, + int memberDotTokenIndex) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java new file mode 100644 index 00000000..f684dd38 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java @@ -0,0 +1,21 @@ +package p.studio.compiler.pbs.semantics; + +public enum PbsEditorialSymbolKind { + KEYWORD, + LOCAL, + PARAMETER, + FIELD, + FUNCTION, + METHOD, + CONSTRUCTOR, + STRUCT, + BUILTIN_TYPE, + SERVICE, + HOST, + CONTRACT, + CALLBACK, + ENUM, + ERROR, + GLOBAL, + CONST +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java index ee33549a..70aeefae 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java @@ -584,5 +584,9 @@ final class PbsFlowSemanticSupport { final var symbol = names.get(name); return symbol != null && symbol.mutable(); } + + Map entries() { + return Map.copyOf(names); + } } } 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 27341fff..6aaa8bc7 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 @@ -133,12 +133,14 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { assembly.parsedSourceFiles(), assembly.moduleTable()); final var flowSemanticsValidator = new PbsFlowSemanticsValidator(); + final Map astByFile = new LinkedHashMap<>(); final Map> supplementalTopDeclsByFile = new LinkedHashMap<>(); final Map> inlineHintsByFile = new LinkedHashMap<>(); for (final var entry : importedSemanticContexts.entrySet()) { supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls()); } for (final var parsedSourceFile : assembly.parsedSourceFiles()) { + astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast()); final var importedSemanticContext = importedSemanticContexts.getOrDefault( parsedSourceFile.fileId(), PbsImportedSemanticContext.empty()); @@ -152,11 +154,13 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } } return new PbsSemanticReadSurface( + Map.copyOf(astByFile), Map.copyOf(supplementalTopDeclsByFile), Map.copyOf(inlineHintsByFile)); } public record PbsSemanticReadSurface( + Map astByFile, Map> supplementalTopDeclsByFile, Map> inlineHintsByFile) { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java new file mode 100644 index 00000000..b9458adb --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java @@ -0,0 +1,215 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +final class PbsEditorialSupportServiceTest { + private final PbsEditorialSupportService service = new PbsEditorialSupportService(); + + @Test + void shouldResolveGeneralCompletionAndHoverAcrossLocalAndImportedSymbols() { + final var source = """ + import { Log } from @sdk:log; + + fn helper(value: int) -> int { + let total = value; + Log.info("hello"); + return total; + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = supplementalTopDecls(""" + declare service Log { + fn info(message: str) -> void { + return; + } + } + """); + + final var completions = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("return total")); + + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("fn") && candidate.kind() == PbsEditorialSymbolKind.KEYWORD)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("value") && candidate.kind() == PbsEditorialSymbolKind.PARAMETER)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("total") && candidate.kind() == PbsEditorialSymbolKind.LOCAL)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("Log") && candidate.kind() == PbsEditorialSymbolKind.SERVICE)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("helper") && candidate.kind() == PbsEditorialSymbolKind.FUNCTION)); + + final var parameterHover = requireHover(source, ast, supplementalTopDecls, "value;"); + assertEquals(PbsEditorialSymbolKind.PARAMETER, parameterHover.kind()); + assertEquals("int", parameterHover.detail()); + + final var importedHover = requireHover(source, ast, supplementalTopDecls, "Log.info"); + assertEquals(PbsEditorialSymbolKind.SERVICE, importedHover.kind()); + assertEquals("@sdk:log", importedHover.origin()); + } + + @Test + void shouldResolveMemberCompletionAndHoverForImportedHostAndBuiltinTypes() { + final var source = """ + import { Input } from @sdk:input; + + fn main() -> void { + let touch: InputTouch = Input.touch(); + Input.touch(); + touch.x_axis(); + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = supplementalTopDecls(""" + declare builtin type InputTouch( + pub x: int + ) { + fn x_axis() -> int; + } + + declare host Input { + fn touch() -> InputTouch; + fn pad() -> InputPad; + } + + declare builtin type InputPad( + pub id: int + ) { + fn strength() -> int; + } + """); + + final var hostMembers = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("touch();")); + + assertTrue(hostMembers.stream().anyMatch(candidate -> + candidate.label().equals("touch") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + assertTrue(hostMembers.stream().anyMatch(candidate -> + candidate.label().equals("pad") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + + final var builtinMembers = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("x_axis();")); + + assertTrue(builtinMembers.stream().anyMatch(candidate -> + candidate.label().equals("x") && candidate.kind() == PbsEditorialSymbolKind.FIELD)); + assertTrue(builtinMembers.stream().anyMatch(candidate -> + candidate.label().equals("x_axis") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + + final var builtinTypeHover = requireHover(source, ast, supplementalTopDecls, "InputTouch"); + assertEquals(PbsEditorialSymbolKind.BUILTIN_TYPE, builtinTypeHover.kind()); + + final var hostMethodHover = requireHover(source, ast, supplementalTopDecls, "touch();"); + assertEquals(PbsEditorialSymbolKind.METHOD, hostMethodHover.kind()); + assertEquals("Input", hostMethodHover.origin()); + } + + @Test + void shouldResolveSignatureHelpForFunctionMethodAndConstructorCalls() { + final var source = """ + declare struct Vec() { + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn mix(left: int, right: int) -> int { return left; } + + fn main(vec: Vec) -> void { + mix(1, 2); + vec.blend(1, 2); + new Vec(); + } + """; + final var ast = parseOrdinary(source); + + final var functionHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.indexOf("2);")); + assertTrue(functionHelp.isPresent()); + assertEquals("mix(left: int, right: int) -> int", functionHelp.orElseThrow().signatures().getFirst().label()); + assertEquals(1, functionHelp.orElseThrow().activeParameter()); + + final var methodHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.lastIndexOf("2);")); + assertTrue(methodHelp.isPresent()); + assertEquals("blend(arg0: int, arg1: int) -> int", methodHelp.orElseThrow().signatures().getFirst().label()); + assertEquals(1, methodHelp.orElseThrow().activeParameter()); + + final var constructorHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.indexOf("Vec();") + 4); + assertTrue(constructorHelp.isPresent()); + assertEquals("Vec()", constructorHelp.orElseThrow().signatures().getFirst().label()); + + final var constructorHover = requireHover(source, ast, ReadOnlyList.empty(), "Vec();"); + assertEquals(PbsEditorialSymbolKind.CONSTRUCTOR, constructorHover.kind()); + assertEquals("Vec()", constructorHover.signatures().getFirst().label()); + } + + private PbsEditorialResolvedSymbol requireHover( + final String source, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final String needle) { + final int offset = source.indexOf(needle); + assertTrue(offset >= 0, "Expected to find needle: " + needle); + return service.hover(source, ast, supplementalTopDecls, offset) + .orElseThrow(); + } + + private ReadOnlyList supplementalTopDecls(final String... interfaceModuleSources) { + final ArrayList topDecls = new ArrayList<>(); + int fileNumber = 100; + for (final var source : interfaceModuleSources) { + topDecls.addAll(parseInterface(source, new FileId(fileNumber)).topDecls().asList()); + fileNumber += 1; + } + return ReadOnlyList.wrap(topDecls); + } + + private PbsAst.File parseOrdinary(final String source) { + return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY); + } + + private PbsAst.File parseInterface( + final String source, + final FileId fileId) { + return parse(source, fileId, PbsParser.ParseMode.INTERFACE_MODULE); + } + + private PbsAst.File parse( + final String source, + final FileId fileId, + final PbsParser.ParseMode parseMode) { + final var diagnostics = DiagnosticSink.empty(); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + final var ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode); + assertFalse(diagnostics.hasErrors(), () -> "Unexpected parse errors: " + diagnostics.stream().toList()); + return ast; + } +}