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
14 changes: 14 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -949,3 +949,17 @@ jobs:
if ([BitConverter]::ToUInt16($object, 0) -ne 0xaa64) {
throw "Wave did not emit an ARM64 COFF object"
}

- name: Validate explicit native ARM64 MSVC object output
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$compiler = "target\aarch64-pc-windows-msvc\release\wavec.exe"
$output = Join-Path $env:RUNNER_TEMP "wave-arm64-msvc-object"
& $compiler build tests/cases/windows/arm64/test1.wave `
--target=aarch64-pc-windows-msvc --emit=obj --out-dir $output
if ($LASTEXITCODE -ne 0) { throw "MSVC object compilation failed" }
$object = [System.IO.File]::ReadAllBytes((Join-Path $output "test1.o"))
if ([BitConverter]::ToUInt16($object, 0) -ne 0xaa64) {
throw "MSVC target did not emit an ARM64 COFF object"
}
108 changes: 90 additions & 18 deletions front/error/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ impl WaveErrorKind {
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedDiagnostic {
pub message: String,
pub span: crate::SourceSpan,
}

#[derive(Debug, Clone)]
pub struct WaveError {
pub code: Option<String>,
Expand All @@ -96,6 +102,7 @@ pub struct WaveError {
pub source_code: Option<String>,
pub span_len: usize,
pub span: Option<crate::SourceSpan>,
pub related: Vec<RelatedDiagnostic>,
pub label: Option<String>,
pub context: Option<String>,
pub expected: Vec<String>,
Expand Down Expand Up @@ -133,6 +140,7 @@ impl WaveError {
source_code: None,
span_len: 1,
span: None,
related: Vec::new(),
label: None,
context: None,
expected: Vec::new(),
Expand Down Expand Up @@ -170,6 +178,11 @@ impl WaveError {
self
}

pub fn with_related(mut self, related: impl IntoIterator<Item = RelatedDiagnostic>) -> Self {
self.related.extend(related);
self
}

pub fn with_span_len(mut self, span_len: usize) -> Self {
self.span_len = span_len.max(1);
self
Expand Down Expand Up @@ -244,21 +257,19 @@ impl WaveError {
self.span_len.max(1)
));
out.push_str(",\"span\":");
if let Some(span) = &self.span {
out.push('{');
push_json_field(&mut out, "file", &span.file);
out.push_str(&format!(",\"start\":{},\"end\":{},\"line\":{},\"column\":{},\"end_line\":{},\"end_column\":{}", span.start, span.end, span.line, span.column, span.end_line, span.end_column));
out.push_str(",\"expansion\":[");
for (i, reason) in span.expansion.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&json_string(reason));
push_json_span(&mut out, self.span.as_ref());
out.push_str(",\"related\":[");
for (i, related) in self.related.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str("]}");
} else {
out.push_str("null");
out.push('{');
push_json_field(&mut out, "message", &related.message);
out.push_str(",\"span\":");
push_json_span(&mut out, Some(&related.span));
out.push('}');
}
out.push(']');
out.push(',');
push_json_field(
&mut out,
Expand All @@ -273,6 +284,8 @@ impl WaveError {
out.push(',');
push_json_optional_field(&mut out, "code", self.code.as_deref());
out.push(',');
push_json_optional_field(&mut out, "label", self.label.as_deref());
out.push(',');
push_json_optional_field(&mut out, "context", self.context.as_deref());
out.push(',');
push_json_string_array(&mut out, "expected", &self.expected);
Expand Down Expand Up @@ -374,9 +387,13 @@ impl WaveError {
let col = self.column;

if let Some(source_code) = &self.source_code {
let lines: Vec<&str> = source_code.lines().collect();
if !lines.is_empty() {
let idx = line.saturating_sub(1).min(lines.len().saturating_sub(1));
// `lines()` drops the empty line containing EOF after a final newline.
let lines: Vec<&str> = source_code
.split('\n')
.map(|line| line.strip_suffix('\r').unwrap_or(line))
.collect();
if line <= lines.len() {
let idx = line - 1;
let start = idx.saturating_sub(1);
let end = (idx + 1).min(lines.len().saturating_sub(1));
let width = (end + 1).to_string().len().max(2);
Expand Down Expand Up @@ -405,9 +422,8 @@ impl WaveError {
}
}
}

return;
}
return;
}

if let Some(source_line) = &self.source {
Expand Down Expand Up @@ -473,6 +489,34 @@ impl WaveError {
eprintln!(" {} {}", "-->".color("38,139,235").bold(), self.file);
}
self.display_source_block();
for related in &self.related {
eprintln!(
" {} {}: {}",
"=".color("38,139,235").bold(),
"note".color("0,255,255").bold(),
related.message
);
eprintln!(
" {} {}:{}:{}",
"-->".color("38,139,235").bold(),
related.span.file,
related.span.line,
related.span.column
);
if related.span.file == self.file {
let mut location = Self::new(
self.kind.clone(),
&related.message,
&related.span.file,
related.span.line,
related.span.column,
)
.with_span(Some(&related.span))
.with_severity(ErrorSeverity::Note);
location.source_code = self.source_code.clone();
location.display_source_block();
}
}

if let Some(context) = &self.context {
eprintln!(
Expand Down Expand Up @@ -531,6 +575,13 @@ impl WaveError {

/// Display multiple errors in a batch
pub fn display_batch(errors: &[WaveError]) {
if std::env::var("WAVE_ERROR_FORMAT").as_deref() == Ok("json") {
for error in errors {
eprintln!("{}", error.to_json());
}
return;
}

for (i, error) in errors.iter().enumerate() {
if i > 0 {
eprintln!();
Expand Down Expand Up @@ -619,3 +670,24 @@ fn json_string(value: &str) -> String {
out.push('"');
out
}

fn push_json_span(out: &mut String, span: Option<&crate::SourceSpan>) {
if let Some(span) = span {
out.push('{');
push_json_field(out, "file", &span.file);
out.push_str(&format!(
",\"start\":{},\"end\":{},\"line\":{},\"column\":{},\"end_line\":{},\"end_column\":{}",
span.start, span.end, span.line, span.column, span.end_line, span.end_column
));
out.push_str(",\"expansion\":[");
for (i, reason) in span.expansion.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&json_string(reason));
}
out.push_str("]}");
} else {
out.push_str("null");
}
}
2 changes: 1 addition & 1 deletion front/error/src/span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct SourceSpan {
pub end_column: usize,
/// Empty for physical syntax; generated syntax records its transformation.
pub expansion: Vec<String>,
/// Optional parser-selected name token for declaration diagnostics.
/// Optional parser-selected name token for declaration or member diagnostics.
pub focus: Option<Box<SourceSpan>>,
}

Expand Down
123 changes: 123 additions & 0 deletions front/error/tests/diagnostic_rendering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Exercise the public renderers in isolated processes without changing global test state.
use error::{ErrorSeverity, WaveError, WaveErrorKind};
use std::process::Command;
use utils::json::{self, Json};

fn error() -> WaveError {
WaveError::new(
WaveErrorKind::UnexpectedEndOfFile,
"missing closer",
"input.wave",
2,
1,
)
.with_label("close the block")
}

#[test]
fn renderer_fixture() {
let Ok(case) = std::env::var("WAVE_RENDER_TEST_CASE") else {
return;
};
match case.as_str() {
"batch" => {
WaveError::display_batch(&[error(), error().with_severity(ErrorSeverity::Warning)])
}
"empty-batch" => WaveError::display_batch(&[]),
"single-batch" => WaveError::display_batch(&[error()]),
"lf" => error().with_source_code("fun main() {\n").display(),
"crlf" => error().with_source_code("fun main() {\r\n").display(),
"empty" => WaveError::new(
WaveErrorKind::UnexpectedEndOfFile,
"expected item",
"empty.wave",
1,
1,
)
.with_source_code("")
.display(),
"invalid-line" => error().with_source_code("only one line").display(),
_ => panic!("unknown fixture"),
}
}

fn rendered(case: &str, format: &str) -> String {
let output = Command::new(std::env::current_exe().unwrap())
.args(["--exact", "renderer_fixture", "--nocapture"])
.env("WAVE_RENDER_TEST_CASE", case)
.env("WAVE_ERROR_FORMAT", format)
.env("NO_COLOR", "1")
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
String::from_utf8(output.stderr).unwrap()
}

#[test]
fn json_preserves_labels_and_escapes_them() {
let label = "expected \"value\"\nnext\tcolumn\\";
let value = json::parse(&error().with_label(label).to_json()).unwrap();
assert_eq!(value.get("error").unwrap().get_str("label"), Some(label));
let value = json::parse(
&WaveError::new(WaveErrorKind::UnexpectedEndOfFile, "end", "x", 1, 1).to_json(),
)
.unwrap();
assert!(matches!(
value.get("error").unwrap().get("label"),
Some(Json::Null)
));
}

#[test]
fn json_batches_contain_only_one_json_record_per_diagnostic() {
for (case, count) in [("empty-batch", 0), ("single-batch", 1), ("batch", 2)] {
let output = rendered(case, "json");
let records: Vec<_> = output
.lines()
.map(|line| json::parse(line).expect("each line must be JSON"))
.collect();
assert_eq!(records.len(), count, "{output}");
if count == 2 {
assert_eq!(
records[0].get("error").unwrap().get_str("severity"),
Some("error")
);
assert_eq!(
records[1].get("error").unwrap().get_str("severity"),
Some("warning")
);
}
}
}

#[test]
fn human_batches_keep_summary_counts() {
let output = rendered("batch", "human");
assert!(
output.contains("error: aborting due to 1 previous error"),
"{output}"
);
assert!(output.contains("warning: 1 warning emitted"), "{output}");
}

#[test]
fn eof_carets_render_on_empty_final_source_lines() {
for case in ["lf", "crlf", "empty"] {
let output = rendered(case, "human");
let lines: Vec<_> = output.lines().collect();
let marker = lines
.iter()
.position(|line| line.contains('^'))
.expect("EOF needs a caret");
let source_line = if case == "empty" { " 1 | " } else { " 2 | " };
assert_eq!(lines[marker - 1], source_line, "{output}");
assert!(!output.contains('\r'), "{output}");
}
}

#[test]
fn out_of_range_locations_do_not_show_an_unrelated_source_line() {
let output = rendered("invalid-line", "human");
assert!(output.contains("input.wave:2:1"));
assert!(!output.contains("only one line"), "{output}");
}
12 changes: 12 additions & 0 deletions front/lexer/src/ident.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ impl<'a> Lexer<'a> {

pub(crate) fn keyword_or_ident_token(&self, ident: String) -> Token {
match ident.as_str() {
"async" => Token {
token_type: TokenType::Async,
lexeme: ident,
line: self.line,
span: None,
},
"await" => Token {
token_type: TokenType::Await,
lexeme: ident,
line: self.line,
span: None,
},
"fun" => Token {
token_type: TokenType::Fun,
lexeme: "fun".to_string(),
Expand Down
Loading
Loading