implements PLN-0075
This commit is contained in:
parent
240525286b
commit
7f40541e96
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<PbsEditorialSignature> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package p.studio.compiler.pbs.semantics;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record PbsEditorialSignatureHelp(
|
||||||
|
List<PbsEditorialSignature> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
}
|
||||||
@ -584,5 +584,9 @@ final class PbsFlowSemanticSupport {
|
|||||||
final var symbol = names.get(name);
|
final var symbol = names.get(name);
|
||||||
return symbol != null && symbol.mutable();
|
return symbol != null && symbol.mutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, LocalSymbol> entries() {
|
||||||
|
return Map.copyOf(names);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,12 +133,14 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
|||||||
assembly.parsedSourceFiles(),
|
assembly.parsedSourceFiles(),
|
||||||
assembly.moduleTable());
|
assembly.moduleTable());
|
||||||
final var flowSemanticsValidator = new PbsFlowSemanticsValidator();
|
final var flowSemanticsValidator = new PbsFlowSemanticsValidator();
|
||||||
|
final Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile = new LinkedHashMap<>();
|
||||||
final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
|
final Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile = new LinkedHashMap<>();
|
||||||
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
|
final Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile = new LinkedHashMap<>();
|
||||||
for (final var entry : importedSemanticContexts.entrySet()) {
|
for (final var entry : importedSemanticContexts.entrySet()) {
|
||||||
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
|
supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls());
|
||||||
}
|
}
|
||||||
for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
|
for (final var parsedSourceFile : assembly.parsedSourceFiles()) {
|
||||||
|
astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast());
|
||||||
final var importedSemanticContext = importedSemanticContexts.getOrDefault(
|
final var importedSemanticContext = importedSemanticContexts.getOrDefault(
|
||||||
parsedSourceFile.fileId(),
|
parsedSourceFile.fileId(),
|
||||||
PbsImportedSemanticContext.empty());
|
PbsImportedSemanticContext.empty());
|
||||||
@ -152,11 +154,13 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new PbsSemanticReadSurface(
|
return new PbsSemanticReadSurface(
|
||||||
|
Map.copyOf(astByFile),
|
||||||
Map.copyOf(supplementalTopDeclsByFile),
|
Map.copyOf(supplementalTopDeclsByFile),
|
||||||
Map.copyOf(inlineHintsByFile));
|
Map.copyOf(inlineHintsByFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PbsSemanticReadSurface(
|
public record PbsSemanticReadSurface(
|
||||||
|
Map<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||||
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
|
Map<FileId, ReadOnlyList<PbsInlineHintSurface>> inlineHintsByFile) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<PbsAst.TopDecl> 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<PbsAst.TopDecl> supplementalTopDecls(final String... interfaceModuleSources) {
|
||||||
|
final ArrayList<PbsAst.TopDecl> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user