Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ its frontend, compiler, reconstruction, or Workshop integration internally.

## Status

- Protocol versions: **1.0** and additive **1.1** (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)).
- Protocol versions: **1.0**, additive **1.1** file-entry loading, and additive **1.2** directory targets (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)).
- Repository state: initial published contract and conformance suite.
- Wright is a client/consumer of the protocol; LPP is not a dependency from the
language implementation back into Wright tooling internals.
Expand Down
4 changes: 2 additions & 2 deletions conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LPP v1 Conformance Suite

This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 additive project-loading revision (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)).
This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry and LPP 1.2 directory-target project-loading revisions (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)).

## Layout

Expand All @@ -10,7 +10,7 @@ mock-provider/ Reference provider for the "x-demo-lang" equation DSL (Rus
runner/ Conformance runner that replays fixtures against any provider binary
```

* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains both LPP 1.0 and LPP 1.1 scenarios.
* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, and LPP 1.2 scenarios.
* **Mock provider** (`mock-provider/`): a small Rust binary implementing the full LPP v1 surface for a demonstration language distinct from OPY and OSTW. It runs over stdio so clients (like the Wright LPP client in wrightkit/wright#142) can test against it directly.
* **Runner** (`runner/`): spawns a fresh provider process per scenario, feeds requests over stdin, validates stdout responses against expectations, and checks the process exit code.

Expand Down
28 changes: 28 additions & 0 deletions conformance/fixtures/v1/41-project-loading-directory-target.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "project-loading-directory-target",
"description": "LPP 1.2 lets a client select a directory while the provider owns default-entry selection and source closure loading.",
"scope": "protocol",
"providerArgs": ["--protocol-version", "1.2"],
"projectFiles": {
"entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n",
"support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n"
},
"steps": [
{
"request": {"jsonrpc": "2.0", "id": 1, "method": "lpp/initialize", "params": {"protocolVersion": "1.2"}},
"expectResponse": {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "1.2", "serverInfo": {"name": "lpp-mock-provider", "version": "0.1.0"}, "languages": [{"id": "x-demo-lang", "extensions": ["xdl"]}], "capabilities": {"check": true, "compile": true, "reconstruct": true, "symbols": true, "definition": true, "references": true, "rename": true, "editValidation": true, "projectLoading": true}}}
},
{
"request": {"jsonrpc": "2.0", "id": 2, "method": "lpp/check", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}},
"expectResponse": {"jsonrpc": "2.0", "id": 2, "result": {"documents": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}]}}
},
{
"request": {"jsonrpc": "2.0", "id": 3, "method": "lpp/compile", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}},
"expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}
},
{
"request": {"jsonrpc": "2.0", "id": 4, "method": "lpp/shutdown", "params": {}},
"expectResponse": {"jsonrpc": "2.0", "id": 4, "result": null}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "project-loading-directory-version-gate",
"description": "A directory target is rejected in an LPP 1.1 session instead of receiving LPP 1.2 directory semantics.",
"scope": "protocol",
"providerArgs": ["--protocol-version", "1.1"],
"projectFiles": {
"entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n"
},
"steps": [
{"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}},
{"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"directory project targets require protocol version 1.2","data":{"lpp":{"kind":"invalidEntry","details":{"entryUri":"${PROJECT_URI}","reason":"unsupportedKind"}}}}}},
{"request":{"jsonrpc":"2.0","id":3,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":null}}
],
"expectExitCode": 0
}
137 changes: 115 additions & 22 deletions conformance/mock-provider/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use puzzle::{

const DEFAULT_PROTOCOL_VERSION: &str = "1.0";
const PROTOCOL_VERSION_1_1: &str = "1.1";
const PROTOCOL_VERSION_1_2: &str = "1.2";
const SERVER_NAME: &str = "lpp-mock-provider";
const LANGUAGE_ID: &str = "x-demo-lang";
const LANGUAGE_EXTENSIONS: [&str; 1] = ["xdl"];
Expand Down Expand Up @@ -89,7 +90,10 @@ impl Capabilities {
"rename": self.rename,
"editValidation": self.edit_validation,
});
if protocol_version == PROTOCOL_VERSION_1_1 {
if matches!(
protocol_version,
PROTOCOL_VERSION_1_1 | PROTOCOL_VERSION_1_2
) {
capabilities["projectLoading"] = json!(self.project_loading);
}
capabilities
Expand Down Expand Up @@ -137,6 +141,13 @@ struct ProjectEntry {
uri: String,
language_id: String,
version: i64,
kind: Option<String>,
}

#[derive(Debug, Clone, Copy)]
enum ProjectTargetKind {
File,
Directory,
}

#[allow(dead_code)]
Expand Down Expand Up @@ -276,7 +287,10 @@ fn parse_args() -> (String, Capabilities) {
eprintln!("lpp-mock-provider: --protocol-version requires a version");
std::process::exit(2);
};
if version != DEFAULT_PROTOCOL_VERSION && version != PROTOCOL_VERSION_1_1 {
if version != DEFAULT_PROTOCOL_VERSION
&& version != PROTOCOL_VERSION_1_1
&& version != PROTOCOL_VERSION_1_2
{
eprintln!("lpp-mock-provider: unsupported protocol version '{version}'");
std::process::exit(2);
}
Expand Down Expand Up @@ -838,7 +852,10 @@ fn documents_for_request(
match (documents, entry) {
(Some(documents), None) => Ok((documents, None)),
(None, Some(entry)) => {
if server.protocol_version.as_deref() != Some(PROTOCOL_VERSION_1_1) {
if !matches!(
server.protocol_version.as_deref(),
Some(PROTOCOL_VERSION_1_1) | Some(PROTOCOL_VERSION_1_2)
) {
return Err(HandlerError::Std(-32602, "Invalid params"));
}
if !server.caps.project_loading {
Expand All @@ -855,14 +872,41 @@ fn documents_for_request(
"project entry version must be a non-negative integer".to_string(),
));
}
let (documents, canonical_entry_uri) = load_project(&entry)?;
let target_kind =
project_target_kind(&entry, server.protocol_version.as_deref().unwrap())?;
let (documents, canonical_entry_uri) = load_project(&entry, target_kind)?;
Ok((documents, Some(canonical_entry_uri)))
}
_ => Err(HandlerError::Std(-32602, "Invalid params")),
}
}

fn load_project(entry: &ProjectEntry) -> Result<(HashMap<String, Document>, String), HandlerError> {
fn project_target_kind(
entry: &ProjectEntry,
protocol_version: &str,
) -> Result<ProjectTargetKind, HandlerError> {
match entry.kind.as_deref() {
None | Some("file") => Ok(ProjectTargetKind::File),
Some("directory") if protocol_version == PROTOCOL_VERSION_1_2 => {
Ok(ProjectTargetKind::Directory)
}
Some("directory") => Err(HandlerError::Lpp(
"invalidEntry",
json!({ "entryUri": entry.uri, "reason": "unsupportedKind" }),
"directory project targets require protocol version 1.2".to_string(),
)),
Some(_) => Err(HandlerError::Lpp(
"invalidEntry",
json!({ "entryUri": entry.uri, "reason": "unsupportedKind" }),
"project target kind is not supported".to_string(),
)),
}
}

fn load_project(
entry: &ProjectEntry,
target_kind: ProjectTargetKind,
) -> Result<(HashMap<String, Document>, String), HandlerError> {
if entry.language_id != LANGUAGE_ID {
return Err(HandlerError::Lpp(
"invalidEntry",
Expand All @@ -881,35 +925,84 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap<String, Document>, Stri
));
};
let entry_path = std::fs::canonicalize(&entry_path).map_err(|_| {
let (reason, message) = match target_kind {
ProjectTargetKind::File => ("entryNotFound", "project entry could not be loaded"),
ProjectTargetKind::Directory => {
("targetNotFound", "project target could not be loaded")
}
};
HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "entryNotFound",
"reason": reason,
"uri": entry.uri,
}),
"project entry could not be loaded".to_string(),
message.to_string(),
)
})?;
if !entry_path.is_file() {
return Err(HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "entryNotFile",
"uri": entry.uri,
}),
"project entry is not a file".to_string(),
));
}
let canonical_entry_uri = path_to_file_uri(&entry_path).ok_or_else(|| {
let (project_root, selected_entry) = match target_kind {
ProjectTargetKind::File => {
if !entry_path.is_file() {
return Err(HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "entryNotFile",
"uri": entry.uri,
}),
"project entry is not a file".to_string(),
));
}
let root = entry_path.parent().expect("a file has a parent");
(root.to_path_buf(), entry_path)
}
ProjectTargetKind::Directory => {
if !entry_path.is_dir() {
return Err(HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "targetNotDirectory",
"uri": entry.uri,
}),
"project target is not a directory".to_string(),
));
}
let selected = entry_path.join("entry.xdl");
if !selected.is_file() {
return Err(HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "defaultEntryNotFound",
"uri": entry.uri,
}),
"project directory has no default entry".to_string(),
));
}
let selected = selected.canonicalize().map_err(|error| {
let uri = path_to_file_uri(&selected).unwrap_or_else(|| entry.uri.clone());
HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"reason": "defaultEntryUnreadable",
"uri": uri,
}),
format!("default project entry could not be resolved: {error}"),
)
})?;
(entry_path, selected)
}
};
let canonical_entry_uri = path_to_file_uri(&selected_entry).ok_or_else(|| {
HandlerError::Lpp(
"projectLoadFailed",
json!({ "entryUri": entry.uri, "reason": "sourceIdentityUnavailable" }),
"project entry has no stable URI".to_string(),
)
})?;
let project_root = entry_path.parent().expect("a file has a parent");
let mut paths: Vec<PathBuf> = std::fs::read_dir(project_root)
.map_err(|error| {
HandlerError::Lpp(
Expand All @@ -924,7 +1017,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap<String, Document>, Stri
.filter_map(Result::ok)
.map(|directory_entry| directory_entry.path())
.filter(|path| {
path == &entry_path || path.extension().is_some_and(|extension| extension == "xdl")
path == &selected_entry || path.extension().is_some_and(|extension| extension == "xdl")
})
.collect();
paths.sort();
Expand All @@ -945,7 +1038,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap<String, Document>, Stri
HandlerError::Lpp(
"projectLoadFailed",
json!({
"entryUri": entry.uri,
"entryUri": entry.uri,
"reason": "sourceIdentityUnavailable",
}),
"source file has no stable URI".to_string(),
Expand Down
Loading