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, 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 { fn main() -> ExitCode {
match run() { match run() {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
print_diagnostic(&error); eprintln!("{error}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
} }
fn run() -> Result<(), Diagnostic> { fn run() -> Result<(), String> {
let mut args = env::args().skip(1); let mut args = env::args().skip(1);
let first = args.next().unwrap_or_else(|| String::from("--help")); let first = args.next().unwrap_or_else(|| String::from("--help"));
match first.as_str() { match first.as_str() {
@ -51,12 +54,19 @@ fn run() -> Result<(), Diagnostic> {
} }
} }
fn materialize_path(path: &str) -> Result<Data, Diagnostic> { fn materialize_path(path: &str) -> Result<Data, String> {
let root = read_root_source(path)?; let root = read_root_source(path).map_err(format_raw_diagnostic)?;
let root_name = root.name.clone();
let mut engine = Engine::new(FsLoader); let mut engine = Engine::new(FsLoader);
let module = engine.add_root_source(root.key, root.name, &root.source)?; let module = engine
let value = engine.eval_module(module)?; .add_root_source(root.key, root.name, &root.source)
engine.materialize(&value) .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> { fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
@ -139,24 +149,12 @@ fn print_help() {
println!(" decodal - Read DCDL source from stdin"); println!(" decodal - Read DCDL source from stdin");
} }
fn print_diagnostic(error: &Diagnostic) { fn format_diagnostic_with_root(error: &Diagnostic, root_name: &str) -> String {
eprintln!( format_diagnostic_with(error, |source| (source == SourceId(0)).then_some(root_name))
"error[{kind:?}] {source}:{start}..{end}: {message}", }
kind = error.kind,
source = error.span.source.0, fn format_raw_diagnostic(error: Diagnostic) -> String {
start = error.span.start, format_diagnostic_with(&error, |_| None)
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 print_data(data: &Data, indent: usize) { 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()) 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( fn materialize_with_path(
&mut self, &mut self,
value: &RuntimeValue, 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 { fn constraint_primary_span(constraints: &[ConstraintEntry]) -> Span {
constraints constraints
.first() .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 constraints::normalize_constraints;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result}; pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue}; pub use embedding::{HostField, HostValue};
pub use eval::Engine; pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{Lexer, Token, TokenKind}; pub use lexer::{Lexer, Token, TokenKind};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader}; pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
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};

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use decodal_core::{ 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::*; use wasm_bindgen::prelude::*;
@ -24,11 +25,18 @@ fn encode_result(result: Result<String, String>) -> String {
fn evaluate_inner(source: &str) -> Result<String, String> { fn evaluate_inner(source: &str) -> Result<String, String> {
let mut engine = Engine::new(EmptyLoader); let mut engine = Engine::new(EmptyLoader);
let module = engine let module = match engine.add_root_source("playground", "playground", source) {
.add_root_source("playground", "playground", source) Ok(module) => module,
.map_err(format_diagnostic)?; Err(error) => return Err(format_diagnostic_with_root(&error, "playground")),
let value = engine.eval_module(module).map_err(format_diagnostic)?; };
let data = engine.materialize(&value).map_err(format_diagnostic)?; 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)) 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"))?; .ok_or_else(|| format!("entry file `{entry}` was not found"))?;
let mut engine = Engine::new(VirtualLoader { files }); let mut engine = Engine::new(VirtualLoader { files });
let module = engine let module = match engine.add_root_source(entry.clone(), entry.clone(), &source) {
.add_root_source(entry.clone(), entry.clone(), &source) Ok(module) => module,
.map_err(format_diagnostic)?; Err(error) => return Err(format_diagnostic_with_root(&error, &entry)),
let value = engine.eval_module(module).map_err(format_diagnostic)?; };
let data = engine.materialize(&value).map_err(format_diagnostic)?; 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)) Ok(format_data(&data, 0))
} }
@ -124,22 +139,10 @@ fn normalize_path(path: &str) -> Option<String> {
} }
} }
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String { fn format_diagnostic_with_root(diagnostic: &decodal_core::Diagnostic, root_name: &str) -> String {
let mut out = format!( format_diagnostic_with(diagnostic, |source| {
"{:?} at {}:{}..{}: {}", (source == SourceId(0)).then_some(root_name)
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_data(data: &Data, indent: usize) -> String { fn format_data(data: &Data, indent: usize) -> String {
@ -237,4 +240,17 @@ mod tests {
let output = evaluate_project_inner("main.dcdl", files).unwrap(); let output = evaluate_project_inner("main.dcdl", files).unwrap();
assert!(output.contains("\"port\": 9443")); 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` は primary location を示す。
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
`labels` は同じ error に関係する追加 location を示す。 `labels` は同じ error に関係する追加 location を示す。
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。 合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。