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);
|
||||
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.moduleTable());
|
||||
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<PbsInlineHintSurface>> 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<FileId, p.studio.compiler.pbs.ast.PbsAst.File> astByFile,
|
||||
Map<FileId, ReadOnlyList<p.studio.compiler.pbs.ast.PbsAst.TopDecl>> supplementalTopDeclsByFile,
|
||||
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