Add host-configurable language service
This commit is contained in:
Generated
+7
@@ -58,6 +58,13 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-language-service"
|
||||||
|
version = "0.1.2"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-language-tools"
|
name = "decodal-language-tools"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/decodal-wasm",
|
"crates/decodal-wasm",
|
||||||
"crates/decodal-derive",
|
"crates/decodal-derive",
|
||||||
"crates/decodal-language-tools",
|
"crates/decodal-language-tools",
|
||||||
|
"crates/decodal-language-service",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Decodal is a small deterministic DSL for describing, composing, validating, and
|
|||||||
It is designed around a lightweight Rust library:
|
It is designed around a lightweight Rust library:
|
||||||
|
|
||||||
- host-supplied source and structured-value imports through `ImportLoader`
|
- host-supplied source and structured-value imports through `ImportLoader`
|
||||||
|
- shared host environments for production and semantic editor tooling
|
||||||
- no filesystem access in the library core
|
- no filesystem access in the library core
|
||||||
- concrete and abstract values with constraints and defaults
|
- concrete and abstract values with constraints and defaults
|
||||||
- deterministic expression evaluation
|
- deterministic expression evaluation
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use crate::{Engine, ImportLoader, Result};
|
||||||
|
|
||||||
|
/// Host-owned configuration shared by runtime and language tooling.
|
||||||
|
///
|
||||||
|
/// Implementations create the import loader and install global bindings on a
|
||||||
|
/// fresh engine. Calling [`HostEnvironment::create_engine`] therefore gives
|
||||||
|
/// every consumer the same Decodal execution environment.
|
||||||
|
pub trait HostEnvironment {
|
||||||
|
type Loader: ImportLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader;
|
||||||
|
|
||||||
|
fn configure_engine(&self, _engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_engine(&self) -> Result<Engine<Self::Loader>> {
|
||||||
|
let mut engine = Engine::new(self.create_loader());
|
||||||
|
self.configure_engine(&mut engine)?;
|
||||||
|
Ok(engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HostEnvironment + ?Sized> HostEnvironment for &T {
|
||||||
|
type Loader = T::Loader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
T::create_loader(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||||
|
T::configure_engine(*self, engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ pub mod ast;
|
|||||||
pub mod constraints;
|
pub mod constraints;
|
||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
|
pub mod environment;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
@@ -20,6 +21,7 @@ pub use constraints::normalize_constraints;
|
|||||||
pub use decodal_derive::Decodal;
|
pub use decodal_derive::Decodal;
|
||||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||||
pub use embedding::{HostField, HostValue};
|
pub use embedding::{HostField, HostValue};
|
||||||
|
pub use environment::HostEnvironment;
|
||||||
pub use eval::{Engine, format_diagnostic_with};
|
pub use eval::{Engine, format_diagnostic_with};
|
||||||
pub use module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module};
|
pub use module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module};
|
||||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-language-service"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Host-configurable semantic language service for Decodal."
|
||||||
|
keywords = ["decodal", "lsp", "language-server", "editor"]
|
||||||
|
categories = ["development-tools", "text-editors"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.1.2", path = "../decodal-core" }
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
use decodal::{Data, Diagnostic, HostEnvironment, Result};
|
||||||
|
|
||||||
|
/// Semantic tooling backed by the same host environment as production.
|
||||||
|
pub struct LanguageService<E> {
|
||||||
|
environment: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> LanguageService<E> {
|
||||||
|
pub fn new(environment: E) -> Self {
|
||||||
|
Self { environment }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn environment(&self) -> &E {
|
||||||
|
&self.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn environment_mut(&mut self) -> &mut E {
|
||||||
|
&mut self.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_environment(self) -> E {
|
||||||
|
self.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: HostEnvironment> LanguageService<E> {
|
||||||
|
/// Runs the production parse, evaluation, and materialization pipeline.
|
||||||
|
pub fn evaluate(
|
||||||
|
&self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
source: &str,
|
||||||
|
) -> Result<Data> {
|
||||||
|
let mut engine = self.environment.create_engine()?;
|
||||||
|
let module = engine.add_root_source(key, name, source)?;
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
engine.materialize(&value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a document and exposes failures in an editor-friendly form.
|
||||||
|
///
|
||||||
|
/// The evaluator currently stops at the first failure, so an invalid
|
||||||
|
/// analysis contains one diagnostic. The collection leaves room for a
|
||||||
|
/// future diagnostic-accumulation pass without changing this API.
|
||||||
|
pub fn analyze(
|
||||||
|
&self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
source: &str,
|
||||||
|
) -> SemanticAnalysis {
|
||||||
|
match self.evaluate(key, name, source) {
|
||||||
|
Ok(data) => SemanticAnalysis {
|
||||||
|
data: Some(data),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
},
|
||||||
|
Err(diagnostic) => SemanticAnalysis {
|
||||||
|
data: None,
|
||||||
|
diagnostics: vec![diagnostic],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SemanticAnalysis {
|
||||||
|
pub data: Option<Data>,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemanticAnalysis {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.diagnostics.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use decodal::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ROOT: &str = r#"Post & import "./post.md""#;
|
||||||
|
|
||||||
|
struct ContentEnvironment {
|
||||||
|
draft: HostValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentLoader {
|
||||||
|
draft: HostValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportLoader for ContentLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
_current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedImport> {
|
||||||
|
if specifier != "./post.md" {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"unknown content import",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"content/post.md",
|
||||||
|
HostValue::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([
|
||||||
|
("title", HostValue::string("Hello")),
|
||||||
|
("draft", self.draft.clone()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", HostValue::string("# Hello")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostEnvironment for ContentEnvironment {
|
||||||
|
type Loader = ContentLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
ContentLoader {
|
||||||
|
draft: self.draft.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||||
|
engine.bind_global(
|
||||||
|
"Post",
|
||||||
|
HostValue::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([
|
||||||
|
("title", HostValue::string_type()),
|
||||||
|
("draft", HostValue::bool_type()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", HostValue::string_type()),
|
||||||
|
]),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_direct(environment: &ContentEnvironment) -> decodal::Result<Data> {
|
||||||
|
let mut engine = environment.create_engine()?;
|
||||||
|
let module = engine.add_root_source("main.dcdl", "main.dcdl", ROOT)?;
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
engine.materialize(&value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_environment_matches_direct_evaluation() {
|
||||||
|
let environment = ContentEnvironment {
|
||||||
|
draft: HostValue::bool(false),
|
||||||
|
};
|
||||||
|
let direct = evaluate_direct(&environment).unwrap();
|
||||||
|
let service = LanguageService::new(&environment);
|
||||||
|
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||||
|
|
||||||
|
assert!(analysis.is_valid());
|
||||||
|
assert_eq!(analysis.data, Some(direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_environment_preserves_import_diagnostics() {
|
||||||
|
let service = LanguageService::new(ContentEnvironment {
|
||||||
|
draft: HostValue::string("maybe"),
|
||||||
|
});
|
||||||
|
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||||
|
|
||||||
|
assert!(!analysis.is_valid());
|
||||||
|
assert_eq!(analysis.data, None);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.diagnostics[0].message,
|
||||||
|
"expected Bool, found String"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.diagnostics[0].notes,
|
||||||
|
["imported `content/post.md` at `frontmatter.draft` supplied a value of type String"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,9 +41,21 @@ The WebAssembly package is for execution, not syntax highlighting.
|
|||||||
|
|
||||||
## Language tools
|
## Language tools
|
||||||
|
|
||||||
Source-level tooling lives in the Rust language tools crate.
|
Semantic editor integration lives in the host-configurable language service crate.
|
||||||
|
It depends only on the runtime and accepts the same `HostEnvironment` implementation used by a production application.
|
||||||
|
A host-specific LSP binary can link this crate with its loader and global schema configuration without reimplementing evaluation rules.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-language-service/
|
||||||
|
```
|
||||||
|
|
||||||
|
Source formatting lives in a separate Rust language tools crate.
|
||||||
|
Keeping it separate prevents host-specific semantic services from inheriting the formatter's Tree-sitter and WebAssembly dependencies.
|
||||||
|
|
||||||
This component is responsible for operations that must preserve source text details such as comments and whitespace.
|
This component is responsible for operations that must preserve source text details such as comments and whitespace.
|
||||||
It is an internal crate used by the future Rust LSP and by the CodeMirror package's bundled formatter WebAssembly.
|
It is used by the CodeMirror package's bundled formatter WebAssembly and can also be used by an LSP adapter for formatting.
|
||||||
|
|
||||||
Important paths:
|
Important paths:
|
||||||
|
|
||||||
@@ -52,7 +64,6 @@ crates/decodal-language-tools/
|
|||||||
```
|
```
|
||||||
|
|
||||||
The current language tools crate exposes the formatter.
|
The current language tools crate exposes the formatter.
|
||||||
Future LSP functionality should build on the same source-level tooling layer rather than on the runtime core.
|
|
||||||
|
|
||||||
## Web editor components
|
## Web editor components
|
||||||
|
|
||||||
@@ -112,7 +123,8 @@ Consumers that need syntax information should use the component that matches the
|
|||||||
- Rust execution and embedding: `decodal`
|
- Rust execution and embedding: `decodal`
|
||||||
- Browser execution: `decodal-wasm`
|
- Browser execution: `decodal-wasm`
|
||||||
- Web formatting and editor syntax: Lezer / CodeMirror
|
- Web formatting and editor syntax: Lezer / CodeMirror
|
||||||
- Rust LSP internals: `decodal-language-tools`
|
- Semantic editor analysis: `decodal-language-service`
|
||||||
|
- Rust formatting: `decodal-language-tools`
|
||||||
- General editor syntax: Tree-sitter
|
- General editor syntax: Tree-sitter
|
||||||
|
|
||||||
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
||||||
|
|||||||
@@ -140,6 +140,48 @@ It only generates schema construction and typed decoding code.
|
|||||||
|
|
||||||
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.
|
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.
|
||||||
|
|
||||||
|
## Shared host environment
|
||||||
|
|
||||||
|
An embedded application can implement `HostEnvironment` to keep loader creation and global binding setup in one place.
|
||||||
|
Both production evaluation and semantic editor tooling create their engines from this environment, preventing the editor from drifting onto a separate validation path.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Engine, HostEnvironment};
|
||||||
|
|
||||||
|
struct AppEnvironment;
|
||||||
|
|
||||||
|
impl HostEnvironment for AppEnvironment {
|
||||||
|
type Loader = ContentLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
ContentLoader::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine<Self::Loader>,
|
||||||
|
) -> decodal::Result<()> {
|
||||||
|
engine.bind_global("Site", site_schema())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment = AppEnvironment;
|
||||||
|
let mut engine = environment.create_engine()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
The semantic service accepts the same environment by value:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal_language_service::LanguageService;
|
||||||
|
|
||||||
|
let service = LanguageService::new(&environment);
|
||||||
|
let analysis = service.analyze("site.dcdl", "site.dcdl", source);
|
||||||
|
```
|
||||||
|
|
||||||
|
Each analysis uses a fresh engine and runs the normal parse, evaluate, and materialize pipeline.
|
||||||
|
An environment may create loaders backed by shared filesystem, database, or editor-overlay state when repeated analysis needs a current workspace snapshot.
|
||||||
|
|
||||||
## Structured imports
|
## Structured imports
|
||||||
|
|
||||||
`ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`.
|
`ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`.
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ materialize
|
|||||||
data / diagnostics
|
data / diagnostics
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Production applications and semantic language services can construct this pipeline through the same host-defined `HostEnvironment`.
|
||||||
|
The environment creates the `ImportLoader` and installs host globals before the root module is registered.
|
||||||
|
|
||||||
## lexer / parser
|
## lexer / parser
|
||||||
|
|
||||||
lexer / parser は source を AST に変換する。
|
lexer / parser は source を AST に変換する。
|
||||||
|
|||||||
Reference in New Issue
Block a user