Skip to content

Commit b1904ad

Browse files
committed
Add rustc diagnostic remapping
1 parent f2c3fcd commit b1904ad

6 files changed

Lines changed: 338 additions & 8 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,12 +362,26 @@ review map metadata
362362
Rust source lowering
363363
runtime crate type surface
364364
source map JSON for generated Rust packages
365+
rustc diagnostic remapping through source maps
365366
```
366367

367368
The first milestone is not a production runtime.
368369

369370
The first milestone is a strong review-first checker that can validate the language model against real examples.
370371

372+
Current CLI surface:
373+
374+
```sh
375+
rss check [--json] <file.rss>
376+
rss check --explain <code>
377+
rss fmt <file.rss>
378+
rss review [--json] --diff <old.rss> <new.rss>
379+
rss review [--json] --map <file-or-directory>
380+
rss lower --rust <file.rss>
381+
rss lower --rust <file.rss> --out-dir <directory>
382+
rss remap-rustc [--json] <rsscript-source-map.json> <rustc-json-lines>
383+
```
384+
371385
---
372386

373387
## Roadmap

src/diagnostic.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde::Serialize;
1+
use serde::{Deserialize, Serialize};
22

33
pub mod code {
44
pub const MISSING_FILE_MODE: &str = "RS0001";
@@ -43,6 +43,8 @@ pub mod code {
4343
pub const IMPLICIT_CONVERSION_ATTEMPT: &str = "RS1002";
4444
pub const OWN_STRUCT_ATTEMPT: &str = "RS1003";
4545
pub const SURFACE_REFERENCE_ATTEMPT: &str = "RS1004";
46+
pub const RUSTC_DIAGNOSTIC_MAPPED: &str = "RS1101";
47+
pub const RUSTC_DIAGNOSTIC_UNMAPPABLE: &str = "RS1102";
4648

4749
pub const REVIEW_MODE_CHANGED: &str = "RSR001";
4850
pub const REVIEW_FUNCTION_REMOVED: &str = "RSR002";
@@ -79,7 +81,7 @@ impl Severity {
7981
}
8082
}
8183

82-
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
84+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8385
pub struct Span {
8486
pub file: String,
8587
pub line: usize,
@@ -422,6 +424,16 @@ static DIAGNOSTIC_EXPLANATIONS: &[DiagnosticExplanation] = &[
422424
title: "surface reference attempt",
423425
explanation: "RSScript does not expose `&T` or `&mut T` syntax. Use explicit parameter effects such as `read`, `mut`, and `take`.",
424426
},
427+
DiagnosticExplanation {
428+
code: code::RUSTC_DIAGNOSTIC_MAPPED,
429+
title: "mapped rustc diagnostic",
430+
explanation: "A backend rustc diagnostic was translated through RSScript source-map metadata. Treat this as a compiler, runtime, native binding, or lowering issue unless the mapped RSScript source clearly violates the language rules.",
431+
},
432+
DiagnosticExplanation {
433+
code: code::RUSTC_DIAGNOSTIC_UNMAPPABLE,
434+
title: "unmappable rustc diagnostic",
435+
explanation: "rustc reported a backend diagnostic whose generated Rust location could not be mapped back to RSScript source. The compiler should surface the generated Rust reference as secondary internal detail instead of exposing raw rustc output as the primary diagnostic.",
436+
},
425437
];
426438

427439
pub fn format_diagnostics_json(diagnostics: &[Diagnostic]) -> String {

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ pub use review::{
2121
pub use rust_lower::{
2222
GeneratedRustPackage, LoweredRust, RustSourceMapEntry, lower_program_to_rust,
2323
lower_program_to_rust_with_map, lower_source_to_rust, lower_source_to_rust_package,
24-
lower_source_to_rust_with_map,
24+
lower_source_to_rust_with_map, parse_source_map_json, remap_rustc_diagnostic_json,
25+
remap_rustc_diagnostic_json_lines,
2526
};

src/main.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use rsscript::{
77
analyze_source, explain_diagnostic_code, format_diagnostic_explanation,
88
format_diagnostics_human, format_diagnostics_json, format_review_human, format_review_json,
99
format_review_map_human, format_review_map_json, lower_source_to_rust,
10-
lower_source_to_rust_package, review_map_sources, review_sources,
10+
lower_source_to_rust_package, parse_source_map_json, remap_rustc_diagnostic_json_lines,
11+
review_map_sources, review_sources,
1112
};
1213

1314
fn main() -> ExitCode {
@@ -22,6 +23,7 @@ fn main() -> ExitCode {
2223
"fmt" => run_fmt(&args[2..]),
2324
"review" => run_review(&args[2..]),
2425
"lower" => run_lower(&args[2..]),
26+
"remap-rustc" => run_remap_rustc(&args[2..]),
2527
_ => {
2628
print_usage();
2729
ExitCode::from(2)
@@ -211,6 +213,63 @@ fn run_lower_rust_package(path: &str, source: &str, out_dir: &str) -> ExitCode {
211213
ExitCode::SUCCESS
212214
}
213215

216+
fn run_remap_rustc(args: &[String]) -> ExitCode {
217+
let (json, paths) = parse_multi_path_args(args);
218+
let [source_map_path, rustc_json_path] = paths.as_slice() else {
219+
print_usage();
220+
return ExitCode::from(2);
221+
};
222+
223+
let source_map_json = match fs::read_to_string(source_map_path) {
224+
Ok(source) => source,
225+
Err(error) => {
226+
eprintln!("failed to read {source_map_path}: {error}");
227+
return ExitCode::from(2);
228+
}
229+
};
230+
let rustc_json_lines = match fs::read_to_string(rustc_json_path) {
231+
Ok(source) => source,
232+
Err(error) => {
233+
eprintln!("failed to read {rustc_json_path}: {error}");
234+
return ExitCode::from(2);
235+
}
236+
};
237+
238+
let source_map = match parse_source_map_json(&source_map_json) {
239+
Ok(source_map) => source_map,
240+
Err(error) => {
241+
eprintln!("{error}");
242+
return ExitCode::from(2);
243+
}
244+
};
245+
let remapped = match remap_rustc_diagnostic_json_lines(&source_map, &rustc_json_lines) {
246+
Ok(remapped) => remapped,
247+
Err(error) => {
248+
eprintln!("{error}");
249+
return ExitCode::from(2);
250+
}
251+
};
252+
let diagnostics = remapped
253+
.into_iter()
254+
.map(|remapped| remapped.diagnostic)
255+
.collect::<Vec<_>>();
256+
257+
if json {
258+
println!("{}", format_diagnostics_json(&diagnostics));
259+
} else {
260+
print!("{}", format_diagnostics_human(&diagnostics));
261+
}
262+
263+
if diagnostics
264+
.iter()
265+
.any(|diagnostic| diagnostic.severity.is_error())
266+
{
267+
ExitCode::from(1)
268+
} else {
269+
ExitCode::SUCCESS
270+
}
271+
}
272+
214273
fn parse_explain_args(args: &[String]) -> Option<&str> {
215274
let [flag, code] = args else {
216275
return None;
@@ -233,6 +292,21 @@ fn parse_path_args(args: &[String]) -> (bool, Option<&str>) {
233292
(json, path)
234293
}
235294

295+
fn parse_multi_path_args(args: &[String]) -> (bool, Vec<&str>) {
296+
let mut json = false;
297+
let mut paths = Vec::new();
298+
299+
for arg in args {
300+
if arg == "--json" {
301+
json = true;
302+
} else {
303+
paths.push(arg.as_str());
304+
}
305+
}
306+
307+
(json, paths)
308+
}
309+
236310
struct LowerOptions<'a> {
237311
emit_rust: bool,
238312
path: Option<&'a str>,
@@ -453,6 +527,7 @@ fn print_usage() {
453527
eprintln!(" rsscript fmt <file.rss>");
454528
eprintln!(" rsscript lower --rust <file.rss>");
455529
eprintln!(" rsscript lower --rust <file.rss> --out-dir <directory>");
530+
eprintln!(" rsscript remap-rustc [--json] <rsscript-source-map.json> <rustc-json-lines>");
456531
eprintln!(" rsscript review [--json] --diff <old.rss> <new.rss>");
457532
eprintln!(" rsscript review [--json] --map <file-or-directory>");
458533
}

src/rust_lower.rs

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::{BTreeMap, BTreeSet};
22

33
use crate::analyzer::analyze_source;
4-
use crate::diagnostic::{Diagnostic, Span};
4+
use crate::diagnostic::{Diagnostic, Severity, Span, code};
55
use crate::syntax::ast::{
66
BinaryOp, Block, Callee, DataEffect, EffectDecl, Expr, FieldDecl, FileMode, FunctionDecl,
77
GenericBound, GenericParam, Item, LetKind, Param, Program, Stmt, TypeDecl, TypeKind, TypeRef,
@@ -22,13 +22,19 @@ pub struct LoweredRust {
2222
pub source_map: Vec<RustSourceMapEntry>,
2323
}
2424

25-
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
25+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2626
pub struct RustSourceMapEntry {
2727
pub kind: String,
2828
pub source: Span,
2929
pub generated: Span,
3030
}
3131

32+
#[derive(Debug, Clone, PartialEq, Eq)]
33+
pub struct RemappedRustcDiagnostic {
34+
pub diagnostic: Diagnostic,
35+
pub mapped: bool,
36+
}
37+
3238
pub fn lower_source_to_rust(file: &str, source: &str) -> Result<String, Vec<Diagnostic>> {
3339
lower_source_to_rust_with_map(file, source).map(|lowered| lowered.rust_source)
3440
}
@@ -80,6 +86,103 @@ pub fn lower_program_to_rust_with_map(program: &Program) -> LoweredRust {
8086
RustLowerer::new(program).lower()
8187
}
8288

89+
pub fn parse_source_map_json(source_map_json: &str) -> Result<Vec<RustSourceMapEntry>, String> {
90+
serde_json::from_str(source_map_json)
91+
.map_err(|error| format!("failed to parse RSScript source map JSON: {error}"))
92+
}
93+
94+
pub fn remap_rustc_diagnostic_json(
95+
source_map: &[RustSourceMapEntry],
96+
rustc_json: &str,
97+
) -> Result<Option<RemappedRustcDiagnostic>, String> {
98+
let rustc: RustcJsonDiagnostic = serde_json::from_str(rustc_json)
99+
.map_err(|error| format!("failed to parse rustc JSON diagnostic: {error}"))?;
100+
if !matches!(rustc.level.as_str(), "error" | "warning") {
101+
return Ok(None);
102+
}
103+
104+
let rustc_span = rustc
105+
.spans
106+
.iter()
107+
.find(|span| span.is_primary)
108+
.or_else(|| rustc.spans.first());
109+
let backend_code = rustc
110+
.code
111+
.as_ref()
112+
.map(|code| code.code.as_str())
113+
.unwrap_or("<none>");
114+
115+
if let Some(rustc_span) = rustc_span
116+
&& let Some(entry) = best_source_map_entry(
117+
source_map,
118+
&rustc_span.file_name,
119+
rustc_span.line_start,
120+
rustc_span.column_start,
121+
)
122+
{
123+
let severity = rustc_severity(&rustc.level);
124+
let summary = format!("backend diagnostic mapped to RSScript: {}", rustc.message);
125+
let diagnostic = Diagnostic {
126+
code: code::RUSTC_DIAGNOSTIC_MAPPED.to_string(),
127+
severity,
128+
summary,
129+
span: entry.source.clone(),
130+
label: "backend diagnostic maps to this RSScript construct".to_string(),
131+
causes: vec![
132+
format!("rustc code: {backend_code}"),
133+
format!(
134+
"generated Rust: {}:{}:{}",
135+
rustc_span.file_name, rustc_span.line_start, rustc_span.column_start
136+
),
137+
rustc.message,
138+
],
139+
fixes: Vec::new(),
140+
};
141+
return Ok(Some(RemappedRustcDiagnostic {
142+
diagnostic,
143+
mapped: true,
144+
}));
145+
}
146+
147+
let generated = rustc_span
148+
.map(generated_span_from_rustc)
149+
.unwrap_or_else(|| Span {
150+
file: "<rustc-json>".to_string(),
151+
line: 1,
152+
column: 1,
153+
length: 1,
154+
});
155+
let diagnostic = Diagnostic {
156+
code: code::RUSTC_DIAGNOSTIC_UNMAPPABLE.to_string(),
157+
severity: rustc_severity(&rustc.level),
158+
summary: format!("unmappable backend diagnostic: {}", rustc.message),
159+
span: generated,
160+
label: "generated Rust diagnostic could not be mapped to RSScript source".to_string(),
161+
causes: vec![format!("rustc code: {backend_code}"), rustc.message],
162+
fixes: Vec::new(),
163+
};
164+
Ok(Some(RemappedRustcDiagnostic {
165+
diagnostic,
166+
mapped: false,
167+
}))
168+
}
169+
170+
pub fn remap_rustc_diagnostic_json_lines(
171+
source_map: &[RustSourceMapEntry],
172+
rustc_json_lines: &str,
173+
) -> Result<Vec<RemappedRustcDiagnostic>, String> {
174+
let mut diagnostics = Vec::new();
175+
for line in rustc_json_lines
176+
.lines()
177+
.filter(|line| !line.trim().is_empty())
178+
{
179+
if let Some(diagnostic) = remap_rustc_diagnostic_json(source_map, line)? {
180+
diagnostics.push(diagnostic);
181+
}
182+
}
183+
Ok(diagnostics)
184+
}
185+
83186
struct RustLowerer<'a> {
84187
program: &'a Program,
85188
type_kinds: BTreeMap<String, TypeKind>,
@@ -589,6 +692,64 @@ fn stmt_span(statement: &Stmt) -> &Span {
589692
}
590693
}
591694

695+
fn best_source_map_entry<'a>(
696+
source_map: &'a [RustSourceMapEntry],
697+
file: &str,
698+
line: usize,
699+
column: usize,
700+
) -> Option<&'a RustSourceMapEntry> {
701+
source_map
702+
.iter()
703+
.filter(|entry| entry.generated.file == file)
704+
.filter(|entry| generated_span_starts_before_or_at(&entry.generated, line, column))
705+
.max_by_key(|entry| (entry.generated.line, entry.generated.column))
706+
}
707+
708+
fn generated_span_starts_before_or_at(span: &Span, line: usize, column: usize) -> bool {
709+
span.line < line || (span.line == line && span.column <= column)
710+
}
711+
712+
fn rustc_severity(level: &str) -> Severity {
713+
if level == "warning" {
714+
Severity::Warning
715+
} else {
716+
Severity::Error
717+
}
718+
}
719+
720+
fn generated_span_from_rustc(span: &RustcJsonSpan) -> Span {
721+
Span {
722+
file: span.file_name.clone(),
723+
line: span.line_start,
724+
column: span.column_start,
725+
length: span.column_end.saturating_sub(span.column_start).max(1),
726+
}
727+
}
728+
729+
#[derive(serde::Deserialize)]
730+
struct RustcJsonDiagnostic {
731+
message: String,
732+
level: String,
733+
code: Option<RustcJsonCode>,
734+
#[serde(default)]
735+
spans: Vec<RustcJsonSpan>,
736+
}
737+
738+
#[derive(serde::Deserialize)]
739+
struct RustcJsonCode {
740+
code: String,
741+
}
742+
743+
#[derive(serde::Deserialize)]
744+
struct RustcJsonSpan {
745+
file_name: String,
746+
line_start: usize,
747+
column_start: usize,
748+
column_end: usize,
749+
#[serde(default)]
750+
is_primary: bool,
751+
}
752+
592753
fn push_source_marker(
593754
out: &mut String,
594755
indent: usize,

0 commit comments

Comments
 (0)