Show source names in diagnostics

This commit is contained in:
Keisuke Hirata 2026-06-22 18:05:26 +09:00
parent cc5ab63922
commit a2a6dee025
No known key found for this signature in database
6 changed files with 113 additions and 54 deletions

View File

@ -4,19 +4,22 @@ use std::{
process::ExitCode,
};
use decodal_core::{Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceLoader, Span};
use decodal_core::{
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
format_diagnostic_with,
};
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
print_diagnostic(&error);
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), Diagnostic> {
fn run() -> Result<(), String> {
let mut args = env::args().skip(1);
let first = args.next().unwrap_or_else(|| String::from("--help"));
match first.as_str() {
@ -51,12 +54,19 @@ fn run() -> Result<(), Diagnostic> {
}
}
fn materialize_path(path: &str) -> Result<Data, Diagnostic> {
let root = read_root_source(path)?;
fn materialize_path(path: &str) -> Result<Data, String> {
let root = read_root_source(path).map_err(format_raw_diagnostic)?;
let root_name = root.name.clone();
let mut engine = Engine::new(FsLoader);
let module = engine.add_root_source(root.key, root.name, &root.source)?;
let value = engine.eval_module(module)?;
engine.materialize(&value)
let module = engine
.add_root_source(root.key, root.name, &root.source)
.map_err(|error| format_diagnostic_with_root(&error, &root_name))?;
let value = engine
.eval_module(module)
.map_err(|error| engine.format_diagnostic(&error))?;
engine
.materialize(&value)
.map_err(|error| engine.format_diagnostic(&error))
}
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
@ -139,24 +149,12 @@ fn print_help() {
println!(" decodal - Read DCDL source from stdin");
}
fn print_diagnostic(error: &Diagnostic) {
eprintln!(
"error[{kind:?}] {source}:{start}..{end}: {message}",
kind = error.kind,
source = error.span.source.0,
start = error.span.start,
end = error.span.end,
message = error.message,
);
for label in &error.labels {
eprintln!(
" note {source}:{start}..{end}: {message}",
source = label.span.source.0,
start = label.span.start,
end = label.span.end,
message = label.message,
);
}
fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String {
format_diagnostic_with(error, |source| (source == SourceId(0)).then_some(root_name))
}
fn format_raw_diagnostic(error: Diagnostic) -> String {
format_diagnostic_with(&error, |_| None)
}
fn print_data(data: &Data, indent: usize) {

View File

@ -87,6 +87,17 @@ impl<L: SourceLoader> Engine<L> {
self.materialize_with_path(value, &mut Vec::new())
}
pub fn source_name(&self, source: SourceId) -> Option<&str> {
self.modules
.iter()
.find(|module| module.source == source)
.map(|module| module.name.as_str())
}
pub fn format_diagnostic(&self, diagnostic: &Diagnostic) -> String {
format_diagnostic_with(diagnostic, |source| self.source_name(source))
}
fn materialize_with_path(
&mut self,
value: &RuntimeValue,
@ -1163,6 +1174,39 @@ fn primitive_type(name: &str) -> Option<PrimitiveType> {
}
}
pub fn format_diagnostic_with<'a>(
diagnostic: &Diagnostic,
mut source_name: impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
let mut out = format!(
"{:?} at {}:{}..{}: {}",
diagnostic.kind,
format_source(diagnostic.span.source, &mut source_name),
diagnostic.span.start,
diagnostic.span.end,
diagnostic.message,
);
for label in &diagnostic.labels {
out.push_str(&format!(
"\nnote at {}:{}..{}: {}",
format_source(label.span.source, &mut source_name),
label.span.start,
label.span.end,
label.message,
));
}
out
}
fn format_source<'a>(
source: SourceId,
source_name: &mut impl FnMut(SourceId) -> Option<&'a str>,
) -> String {
source_name(source)
.map(String::from)
.unwrap_or_else(|| format!("source {}", source.0))
}
fn constraint_primary_span(constraints: &[ConstraintEntry]) -> Span {
constraints
.first()

View File

@ -17,7 +17,7 @@ pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use constraints::normalize_constraints;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use eval::Engine;
pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{Lexer, Token, TokenKind};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use decodal_core::{
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceLoader, Span,
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
Span, format_diagnostic_with,
};
use wasm_bindgen::prelude::*;
@ -24,11 +25,18 @@ fn encode_result(result: Result<String, String>) -> String {
fn evaluate_inner(source: &str) -> Result<String, String> {
let mut engine = Engine::new(EmptyLoader);
let module = engine
.add_root_source("playground", "playground", source)
.map_err(format_diagnostic)?;
let value = engine.eval_module(module).map_err(format_diagnostic)?;
let data = engine.materialize(&value).map_err(format_diagnostic)?;
let module = match engine.add_root_source("playground", "playground", source) {
Ok(module) => module,
Err(error) => return Err(format_diagnostic_with_root(&error, "playground")),
};
let value = match engine.eval_module(module) {
Ok(value) => value,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
Ok(format_data(&data, 0))
}
@ -48,11 +56,18 @@ fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, Strin
.ok_or_else(|| format!("entry file `{entry}` was not found"))?;
let mut engine = Engine::new(VirtualLoader { files });
let module = engine
.add_root_source(entry.clone(), entry.clone(), &source)
.map_err(format_diagnostic)?;
let value = engine.eval_module(module).map_err(format_diagnostic)?;
let data = engine.materialize(&value).map_err(format_diagnostic)?;
let module = match engine.add_root_source(entry.clone(), entry.clone(), &source) {
Ok(module) => module,
Err(error) => return Err(format_diagnostic_with_root(&error, &entry)),
};
let value = match engine.eval_module(module) {
Ok(value) => value,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
Ok(format_data(&data, 0))
}
@ -124,22 +139,10 @@ fn normalize_path(path: &str) -> Option<String> {
}
}
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String {
let mut out = format!(
"{:?} at {}:{}..{}: {}",
diagnostic.kind,
diagnostic.span.source.0,
diagnostic.span.start,
diagnostic.span.end,
diagnostic.message,
);
for label in diagnostic.labels {
out.push_str(&format!(
"\nnote at {}:{}..{}: {}",
label.span.source.0, label.span.start, label.span.end, label.message,
));
}
out
fn format_diagnostic_with_root(diagnostic: &decodal_core::Diagnostic, root_name: &str) -> String {
format_diagnostic_with(diagnostic, |source| {
(source == SourceId(0)).then_some(root_name)
})
}
fn format_data(data: &Data, indent: usize) -> String {
@ -237,4 +240,17 @@ mod tests {
let output = evaluate_project_inner("main.dcdl", files).unwrap();
assert!(output.contains("\"port\": 9443"));
}
#[test]
fn project_diagnostics_use_virtual_file_names() {
let files = r#"{
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 80; }",
"schemas/service.dcdl":"Service = { port = Int & > 443 default 8443; }"
}"#;
let error = evaluate_project_inner("main.dcdl", files).unwrap_err();
assert!(error.contains("main.dcdl:"));
assert!(error.contains("schemas/service.dcdl:"));
assert!(!error.contains("source 0:"));
assert!(!error.contains("source 1:"));
}
}

View File

@ -20,6 +20,7 @@ DiagnosticLabel {
```
`span` は primary location を示す。
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
`labels` は同じ error に関係する追加 location を示す。
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。