From 2ae0a91c57fd76323124c0e4dff552adbfadbe1b Mon Sep 17 00:00:00 2001 From: LunaStev Date: Tue, 8 Sep 2026 22:47:09 +0900 Subject: [PATCH] feat: add async execution and strengthen frontend and platform regressions --- .github/workflows/rust.yml | 14 + front/error/src/error.rs | 108 +- front/error/src/span.rs | 2 +- front/error/tests/diagnostic_rendering.rs | 123 +++ front/lexer/src/ident.rs | 12 + front/lexer/src/literals.rs | 28 +- front/lexer/src/token.rs | 2 + front/lexer/tests/string_escape_spans.rs | 55 + front/parser/src/ast.rs | 39 +- front/parser/src/async_intrinsics.rs | 194 ++++ front/parser/src/async_lower.rs | 949 ++++++++++++++++++ front/parser/src/expr/assign.rs | 15 +- front/parser/src/expr/binary.rs | 64 +- front/parser/src/expr/helpers.rs | 38 +- front/parser/src/expr/mod.rs | 2 + front/parser/src/expr/postfix.rs | 132 +-- front/parser/src/expr/primary.rs | 618 +++++------- front/parser/src/expr/unary.rs | 52 +- front/parser/src/generics.rs | 91 +- front/parser/src/hir.rs | 76 +- front/parser/src/import.rs | 4 +- front/parser/src/lib.rs | 4 + front/parser/src/methods.rs | 289 ++++++ front/parser/src/parser/asm.rs | 266 +++-- front/parser/src/parser/control.rs | 299 +++--- front/parser/src/parser/decl.rs | 376 +++---- front/parser/src/parser/expr.rs | 16 +- front/parser/src/parser/functions.rs | 225 +++-- front/parser/src/parser/io.rs | 98 +- front/parser/src/parser/items.rs | 63 +- front/parser/src/parser/parse.rs | 257 +++-- front/parser/src/parser/stmt.rs | 253 +++-- front/parser/src/parser/types.rs | 36 +- front/parser/src/source.rs | 1 + front/parser/src/verification.rs | 382 +++++-- front/parser/tests/async_frontend.rs | 94 ++ front/parser/tests/control_diagnostics.rs | 136 +++ .../tests/declaration_asm_diagnostics.rs | 111 ++ front/parser/tests/expression_diagnostics.rs | 136 +++ front/parser/tests/generic_methods.rs | 144 +++ front/parser/tests/unclosed_delimiters.rs | 70 ++ llvm/src/backend.rs | 155 ++- llvm/src/codegen/abi_c.rs | 17 +- llvm/src/codegen/address.rs | 33 +- llvm/src/codegen/consts.rs | 10 + llvm/src/codegen/ir.rs | 414 +++++--- llvm/src/codegen/mod.rs | 2 + llvm/src/codegen/plan.rs | 109 +- llvm/src/codegen/target.rs | 44 + llvm/src/codegen/types.rs | 1 + llvm/src/codegen/validation.rs | 293 ++++++ llvm/src/codegen/variants.rs | 10 +- llvm/src/diagnostic.rs | 152 +++ llvm/src/expression/rvalue/asm.rs | 133 +-- llvm/src/expression/rvalue/assign.rs | 3 + llvm/src/expression/rvalue/async_runtime.rs | 198 ++++ llvm/src/expression/rvalue/calls.rs | 255 ++--- llvm/src/expression/rvalue/dispatch.rs | 9 +- llvm/src/expression/rvalue/incdec.rs | 3 + llvm/src/expression/rvalue/mod.rs | 1 + llvm/src/expression/rvalue/structs.rs | 63 +- llvm/src/lib.rs | 2 + llvm/src/statement/control.rs | 18 +- llvm/tests/codegen_errors.rs | 113 +++ src/cli.rs | 123 ++- src/errors.rs | 27 + src/module_resolver.rs | 17 +- src/runner.rs | 444 ++------ std/sys/linux/event.wave | 48 +- std/sys/windows/iocp.wave | 157 +++ std/task.wave | 531 ++++++++++ std/task/net.wave | 201 ++++ tests/cases/cases.toml | 25 +- .../riscv64 => dragonfly/amd64}/test1.wave | 14 +- .../riscv64 => dragonfly/amd64}/test10.wave | 14 +- .../riscv64 => dragonfly/amd64}/test2.wave | 14 +- .../riscv64 => dragonfly/amd64}/test3.wave | 14 +- .../riscv64 => dragonfly/amd64}/test4.wave | 14 +- .../riscv64 => dragonfly/amd64}/test5.wave | 14 +- .../riscv64 => dragonfly/amd64}/test6.wave | 14 +- .../riscv64 => dragonfly/amd64}/test7.wave | 14 +- .../riscv64 => dragonfly/amd64}/test8.wave | 14 +- .../riscv64 => dragonfly/amd64}/test9.wave | 14 +- tests/cases/freebsd/amd64/test6.wave | 222 ++-- tests/cases/ios/riscv64/test1.wave | 167 --- tests/cases/ios/riscv64/test10.wave | 120 --- tests/cases/ios/riscv64/test2.wave | 141 --- tests/cases/ios/riscv64/test3.wave | 155 --- tests/cases/ios/riscv64/test4.wave | 177 ---- tests/cases/ios/riscv64/test5.wave | 172 ---- tests/cases/ios/riscv64/test6.wave | 127 --- tests/cases/ios/riscv64/test7.wave | 140 --- tests/cases/ios/riscv64/test8.wave | 134 --- tests/cases/ios/riscv64/test9.wave | 124 --- tests/cases/linux/amd64/test6.wave | 261 +++-- tests/cases/macos/arm64/test6.wave | 304 ++++-- tests/cases/macos/riscv64/test1.wave | 167 --- tests/cases/macos/riscv64/test10.wave | 120 --- tests/cases/macos/riscv64/test2.wave | 141 --- tests/cases/macos/riscv64/test3.wave | 155 --- tests/cases/macos/riscv64/test4.wave | 177 ---- tests/cases/macos/riscv64/test5.wave | 172 ---- tests/cases/macos/riscv64/test6.wave | 127 --- tests/cases/macos/riscv64/test7.wave | 140 --- tests/cases/macos/riscv64/test8.wave | 134 --- tests/cases/macos/riscv64/test9.wave | 124 --- tests/cases/shared/amd64/test6.wave | 165 +-- tests/cases/shared/arm64/test9.wave | 142 ++- tests/cases/shared/loong64/test5.wave | 223 ++-- tests/cases/shared/riscv64/test7.wave | 187 ++-- tests/cases/shared/test114.wave | 110 ++ tests/cases/shared/test115.wave | 119 +++ tests/cases/windows/amd64/test6.wave | 256 +++-- tests/cases/windows/riscv64/README.md | 5 - tests/codegen_regressions.rs | 234 ++++- tests/fixtures/async/control_flow.wave | 43 + tests/fixtures/async/lazy_nested.wave | 19 + tests/fixtures/async/tcp_roundtrip.wave | 62 ++ tests/fixtures/async/timer_fairness.wave | 9 + tests/frontend_regressions.rs | 467 +++++++++ tools/test_case_manifest.py | 31 +- 121 files changed, 9085 insertions(+), 6313 deletions(-) create mode 100644 front/error/tests/diagnostic_rendering.rs create mode 100644 front/lexer/tests/string_escape_spans.rs create mode 100644 front/parser/src/async_intrinsics.rs create mode 100644 front/parser/src/async_lower.rs create mode 100644 front/parser/src/methods.rs create mode 100644 front/parser/tests/async_frontend.rs create mode 100644 front/parser/tests/control_diagnostics.rs create mode 100644 front/parser/tests/declaration_asm_diagnostics.rs create mode 100644 front/parser/tests/expression_diagnostics.rs create mode 100644 front/parser/tests/generic_methods.rs create mode 100644 front/parser/tests/unclosed_delimiters.rs create mode 100644 llvm/src/codegen/validation.rs create mode 100644 llvm/src/diagnostic.rs create mode 100644 llvm/src/expression/rvalue/async_runtime.rs create mode 100644 llvm/tests/codegen_errors.rs create mode 100644 std/sys/windows/iocp.wave create mode 100644 std/task.wave create mode 100644 std/task/net.wave rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test1.wave (96%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test10.wave (94%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test2.wave (95%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test3.wave (95%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test4.wave (96%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test5.wave (96%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test6.wave (94%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test7.wave (95%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test8.wave (94%) rename tests/cases/{windows/riscv64 => dragonfly/amd64}/test9.wave (94%) delete mode 100644 tests/cases/ios/riscv64/test1.wave delete mode 100644 tests/cases/ios/riscv64/test10.wave delete mode 100644 tests/cases/ios/riscv64/test2.wave delete mode 100644 tests/cases/ios/riscv64/test3.wave delete mode 100644 tests/cases/ios/riscv64/test4.wave delete mode 100644 tests/cases/ios/riscv64/test5.wave delete mode 100644 tests/cases/ios/riscv64/test6.wave delete mode 100644 tests/cases/ios/riscv64/test7.wave delete mode 100644 tests/cases/ios/riscv64/test8.wave delete mode 100644 tests/cases/ios/riscv64/test9.wave delete mode 100644 tests/cases/macos/riscv64/test1.wave delete mode 100644 tests/cases/macos/riscv64/test10.wave delete mode 100644 tests/cases/macos/riscv64/test2.wave delete mode 100644 tests/cases/macos/riscv64/test3.wave delete mode 100644 tests/cases/macos/riscv64/test4.wave delete mode 100644 tests/cases/macos/riscv64/test5.wave delete mode 100644 tests/cases/macos/riscv64/test6.wave delete mode 100644 tests/cases/macos/riscv64/test7.wave delete mode 100644 tests/cases/macos/riscv64/test8.wave delete mode 100644 tests/cases/macos/riscv64/test9.wave create mode 100644 tests/cases/shared/test114.wave create mode 100644 tests/cases/shared/test115.wave delete mode 100644 tests/cases/windows/riscv64/README.md create mode 100644 tests/fixtures/async/control_flow.wave create mode 100644 tests/fixtures/async/lazy_nested.wave create mode 100644 tests/fixtures/async/tcp_roundtrip.wave create mode 100644 tests/fixtures/async/timer_fairness.wave diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8b75b01a..25038dce 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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" + } diff --git a/front/error/src/error.rs b/front/error/src/error.rs index 65cd8e62..cc2c13f1 100644 --- a/front/error/src/error.rs +++ b/front/error/src/error.rs @@ -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, @@ -96,6 +102,7 @@ pub struct WaveError { pub source_code: Option, pub span_len: usize, pub span: Option, + pub related: Vec, pub label: Option, pub context: Option, pub expected: Vec, @@ -133,6 +140,7 @@ impl WaveError { source_code: None, span_len: 1, span: None, + related: Vec::new(), label: None, context: None, expected: Vec::new(), @@ -170,6 +178,11 @@ impl WaveError { self } + pub fn with_related(mut self, related: impl IntoIterator) -> 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 @@ -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, @@ -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); @@ -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); @@ -405,9 +422,8 @@ impl WaveError { } } } - - return; } + return; } if let Some(source_line) = &self.source { @@ -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!( @@ -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!(); @@ -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"); + } +} diff --git a/front/error/src/span.rs b/front/error/src/span.rs index 177ec958..5564ef75 100644 --- a/front/error/src/span.rs +++ b/front/error/src/span.rs @@ -10,7 +10,7 @@ pub struct SourceSpan { pub end_column: usize, /// Empty for physical syntax; generated syntax records its transformation. pub expansion: Vec, - /// Optional parser-selected name token for declaration diagnostics. + /// Optional parser-selected name token for declaration or member diagnostics. pub focus: Option>, } diff --git a/front/error/tests/diagnostic_rendering.rs b/front/error/tests/diagnostic_rendering.rs new file mode 100644 index 00000000..c5087f9b --- /dev/null +++ b/front/error/tests/diagnostic_rendering.rs @@ -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}"); +} diff --git a/front/lexer/src/ident.rs b/front/lexer/src/ident.rs index 3d743f4c..933ac377 100644 --- a/front/lexer/src/ident.rs +++ b/front/lexer/src/ident.rs @@ -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(), diff --git a/front/lexer/src/literals.rs b/front/lexer/src/literals.rs index c59f065d..4208dc99 100644 --- a/front/lexer/src/literals.rs +++ b/front/lexer/src/literals.rs @@ -42,13 +42,19 @@ impl<'a> Lexer<'a> { let c = self.advance(); if c == '\\' { + let escape_line = self.line; + let escape_prefix = &self.source[self.line_start..self.current - 1]; + // Count Unicode columns only on failure, not for every valid escape. + let escape_column = || escape_prefix.chars().count() + 1; if self.is_at_end() { return Err(self - .make_error_here( + .make_error( WaveErrorKind::InvalidString( "dangling escape sequence in string literal".to_string(), ), "invalid escape sequence: trailing `\\` at end of string", + escape_line, + escape_column(), ) .with_code("E1004") .with_label("escape sequence is incomplete") @@ -65,25 +71,29 @@ impl<'a> Lexer<'a> { '\\' => string_literal.push('\\'), '"' => string_literal.push('"'), 'x' => { - if self.is_at_end() { + if self.is_at_end() || matches!(self.peek(), '"' | '\n' | '\r') { return Err(self - .make_error_here( + .make_error( WaveErrorKind::InvalidString( "incomplete hex escape sequence".to_string(), ), "invalid escape sequence: expected two hex digits after `\\x`", + escape_line, + escape_column(), ) .with_code("E1004") .with_help("example: `\\x41` for `A`")); } let h1 = self.advance(); - if self.is_at_end() { + if self.is_at_end() || matches!(self.peek(), '"' | '\n' | '\r') { return Err(self - .make_error_here( + .make_error( WaveErrorKind::InvalidString( "incomplete hex escape sequence".to_string(), ), "invalid escape sequence: expected two hex digits after `\\x`", + escape_line, + escape_column(), ) .with_code("E1004") .with_help("example: `\\x41` for `A`")); @@ -95,7 +105,7 @@ impl<'a> Lexer<'a> { Ok(v) => v, Err(_) => { return Err(self - .make_error_here( + .make_error( WaveErrorKind::InvalidString(format!( "invalid hex escape: \\x{}", hex @@ -104,6 +114,8 @@ impl<'a> Lexer<'a> { "invalid hex escape sequence `\\x{}` in string literal", hex ), + escape_line, + escape_column(), ) .with_code("E1004") .with_label( @@ -117,12 +129,14 @@ impl<'a> Lexer<'a> { } _ => { return Err(self - .make_error_here( + .make_error( WaveErrorKind::InvalidString(format!( "unknown escape sequence: \\{}", next )), format!("unknown escape sequence `\\{}` in string literal", next), + escape_line, + escape_column(), ) .with_code("E1004") .with_label("unsupported escape sequence") diff --git a/front/lexer/src/token.rs b/front/lexer/src/token.rs index a3691641..c3d29c0b 100644 --- a/front/lexer/src/token.rs +++ b/front/lexer/src/token.rs @@ -87,6 +87,8 @@ impl fmt::Display for UnsignedIntegerType { #[derive(Debug, PartialEq, Clone)] pub enum TokenType { Fun, + Async, + Await, Extern, Export, Pub, diff --git a/front/lexer/tests/string_escape_spans.rs b/front/lexer/tests/string_escape_spans.rs new file mode 100644 index 00000000..bbd966ff --- /dev/null +++ b/front/lexer/tests/string_escape_spans.rs @@ -0,0 +1,55 @@ +//! Invalid string escapes identify the consumed escape, including its backslash. +use lexer::{token::TokenType, Lexer}; + +#[test] +fn invalid_string_escapes_preserve_exact_utf8_ranges() { + for (escape, suffix) in [ + ("\\q", "\"; }"), + ("\\한", "\"; }"), + ("\\xGG", "\"; }"), + ("\\x", "\"; }"), + ("\\xA", "\"; }"), + ("\\xG", "\"; }"), + ("\\x", "\r\nnext line"), + ("\\xA", ""), + ("\\x", ""), + ("\\", ""), + ] { + for prefix in ["\"", "// 한글\r\nfun main() { \"앞"] { + let source = format!("{prefix}{escape}{suffix}"); + let error = Lexer::new_with_file(&source, "escape.wave") + .tokenize() + .unwrap_err(); + assert_eq!(error.code.as_deref(), Some("E1004"), "{error:?}"); + let span = error.span.as_ref().unwrap(); + assert_eq!(span.file, "escape.wave"); + assert_eq!(span.start, prefix.len(), "{source:?}: {error:?}"); + assert_eq!( + span.end, + prefix.len() + escape.len(), + "{source:?}: {error:?}" + ); + assert_eq!(&source[span.start..span.end], escape); + assert_eq!( + span.line, + prefix.bytes().filter(|b| *b == b'\n').count() + 1 + ); + assert_eq!( + span.column, + prefix.rsplit('\n').next().unwrap().chars().count() + 1 + ); + assert_eq!(error.column, span.column); + assert_eq!(error.span_len, escape.chars().count()); + } + } +} + +#[test] +fn supported_string_escapes_keep_their_values() { + let source = r#""앞\n\t\r\\\"\x41""#; + let tokens = Lexer::new(source).tokenize().unwrap(); + assert_eq!( + tokens[0].token_type, + TokenType::String("앞\n\t\r\\\"A".into()) + ); +} diff --git a/front/parser/src/ast.rs b/front/parser/src/ast.rs index 2ba4c309..28d08702 100644 --- a/front/parser/src/ast.rs +++ b/front/parser/src/ast.rs @@ -17,8 +17,6 @@ //! node forms must be handled by both semantic passes and that rewrite where //! they may contain types or expressions. -use std::collections::HashMap; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum WaveType { /// Target-sized integers remain symbolic until the target-resolution pass. @@ -32,6 +30,8 @@ pub enum WaveType { Byte, String, Pointer(Box), + /// A lazy asynchronous computation with this completion type. + Future(Box), Array(Box, u32), Void, /// A function that cannot return to its caller (return position only). @@ -105,6 +105,7 @@ pub struct VariantCaseNode { #[derive(Debug, Clone)] pub struct FunctionNode { + pub is_async: bool, pub span: Option, pub name: String, pub generic_params: Vec, @@ -195,8 +196,10 @@ pub enum Expression { MethodCall { object: Box, name: String, + type_args: Vec, args: Vec, }, + Await(Box), Null, Literal(Literal), Variable(String), @@ -416,38 +419,6 @@ impl Expression { _ => None, } } - - pub fn get_wave_type(&self, variables: &HashMap) -> WaveType { - match self.unspanned() { - Expression::Variable(name) => variables - .get(name) - .unwrap_or_else(|| panic!("Variable '{}' not found", name)) - .ty - .clone(), - Expression::Literal(Literal::Int(_)) => { - panic!("integer literal type is context-dependent and must be resolved by type checking") - } - Expression::Literal(Literal::Float(_)) => WaveType::Float(32), - Expression::Literal(Literal::String(_)) => WaveType::String, - Expression::MethodCall { .. } => { - panic!("nested method call type inference not supported yet") - } - Expression::Unary { operator, expr } => { - let t = expr.get_wave_type(variables); - match operator { - Operator::Neg => match &t { - WaveType::Int(_) | WaveType::Uint(_) | WaveType::Float(_) => t, - _ => panic!("unary '-' not allowed for type {:?}", t), - }, - Operator::Not | Operator::LogicalNot => WaveType::Bool, - Operator::BitwiseNot => t, - _ => panic!("unary op type inference not supported: {:?}", operator), - } - } - Expression::Cast { target_type, .. } => target_type.clone(), - _ => panic!("get_wave_type not implemented for {:?}", self), - } - } } impl ASTNode { diff --git a/front/parser/src/async_intrinsics.rs b/front/parser/src/async_intrinsics.rs new file mode 100644 index 00000000..dd2439f2 --- /dev/null +++ b/front/parser/src/async_intrinsics.rs @@ -0,0 +1,194 @@ +//! Compiler/runtime operations used by async lowering and std::task. +use crate::ast::WaveType; + +pub fn is_intrinsic(name: &str) -> bool { + matches!( + name, + "__wave_async_alloc" + | "__wave_async_create" + | "__wave_async_complete" + | "__wave_async_wait" + | "__wave_async_ready" + | "__wave_async_take" + | "__wave_async_block_on" + | "__wave_async_spawn" + | "__wave_async_cancel" + | "__wave_async_yield" + | "__wave_async_shutdown" + | "__wave_async_invoke" + | "__wave_async_free_slot" + | "__wave_async_interest" + | "__wave_async_sleep" + | "__wave_async_close_fd" + | "__wave_async_cancel_join" + | "__wave_async_io" + | "__wave_async_windows_notify_address" + ) +} +/// The caller validates argument expressions before checking this signature. +pub fn signature( + name: &str, + types: &[WaveType], + arguments: &[WaveType], +) -> Result<(Vec, WaveType), String> { + use WaveType::*; + let ptr = |t| Pointer(Box::new(t)); + let future = |t| Future(Box::new(t)); + let scalar = || { + arguments + .last() + .and_then(|t| { + if let Future(t) = t { + Some(*t.clone()) + } else { + None + } + }) + .ok_or_else(|| format!("{name} requires a Future")) + }; + let no_types = || { + if types.is_empty() { + Ok(()) + } else { + Err(format!("{name} does not accept explicit type arguments")) + } + }; + Ok(match name { + "__wave_async_alloc" if types.len() == 1 => (vec![], ptr(types[0].clone())), + "__wave_async_create" if types.len() == 2 => ( + vec![ + ptr(types[0].clone()), + ptr(if types[1] == Void { + Byte + } else { + types[1].clone() + }), + String, + ], + future(types[1].clone()), + ), + "__wave_async_complete" => { + no_types()?; + (vec![Int(64)], Void) + } + "__wave_async_wait" => { + no_types()?; + let t = scalar()?; + (vec![Int(64), future(t)], Void) + } + "__wave_async_ready" | "__wave_async_cancel" => { + no_types()?; + let t = scalar()?; + (vec![future(t)], Bool) + } + "__wave_async_take" | "__wave_async_block_on" => { + no_types()?; + let t = scalar()?; + (vec![future(t.clone())], t) + } + "__wave_async_spawn" => { + no_types()?; + let t = scalar()?; + (vec![future(t.clone())], future(t)) + } + "__wave_async_yield" => { + no_types()?; + (vec![], future(Void)) + } + "__wave_async_shutdown" => { + no_types()?; + (vec![], Void) + } + "__wave_async_free_slot" if arguments.len() == 1 && matches!(&arguments[0], Pointer(_)) => { + no_types()?; + (arguments.to_vec(), Void) + } + "__wave_async_interest" => { + no_types()?; + (vec![Int(64), Int(32), Int(64)], future(Int(32))) + } + "__wave_async_sleep" => { + no_types()?; + (vec![Int(64)], future(Void)) + } + "__wave_async_cancel_join" => { + no_types()?; + let t = scalar()?; + (vec![future(t)], future(Void)) + } + "__wave_async_io" => { + no_types()?; + ( + vec![Int(64), ptr(Uint(8)), Int(64), Int(32), Int(64)], + future(Int(64)), + ) + } + "__wave_async_windows_notify_address" => { + no_types()?; + (vec![], ptr(Uint(8))) + } + "__wave_async_close_fd" => { + no_types()?; + (vec![Int(64)], Void) + } + "__wave_async_invoke" => { + no_types()?; + (vec![ptr(Uint(8)), ptr(Uint(8)), Int(64)], Bool) + } + _ => return Err(format!("invalid async intrinsic {name}")), + }) +} + +/// Runtime entry points needed by an intrinsic after async frame lowering. +pub fn runtime_symbols(name: &str) -> &'static [&'static str] { + match name { + "__wave_async_alloc" => &["__wave_task_alloc"], + "__wave_async_free_slot" => &["__wave_task_free"], + "__wave_async_create" => &["__wave_task_new"], + "__wave_async_take" => &["__wave_task_result", "__wave_task_release"], + "__wave_async_block_on" => &[ + "__wave_task_drive", + "__wave_task_result", + "__wave_task_release", + ], + "__wave_async_ready" => &["__wave_task_ready"], + "__wave_async_wait" => &["__wave_task_wait"], + "__wave_async_complete" => &["__wave_task_complete"], + "__wave_async_spawn" => &["__wave_task_spawn"], + "__wave_async_cancel" => &["__wave_task_cancel"], + "__wave_async_cancel_join" => &["__wave_task_cancel_join"], + "__wave_async_yield" => &["__wave_task_yield"], + "__wave_async_shutdown" => &["__wave_task_shutdown"], + "__wave_async_interest" | "__wave_async_sleep" => &["__wave_task_interest"], + "__wave_async_close_fd" => &["__wave_task_close_fd"], + "__wave_async_io" => &["__wave_task_io"], + "__wave_async_windows_notify_address" => &["__wave_task_windows_notify"], + _ => &[], + } +} + +/// Scalar and pointer boundary shared by generated frames and the Wave executor. +pub fn runtime_signature(symbol: &str) -> Option<(Vec, WaveType, &'static str)> { + use WaveType::*; + let pointer = || Pointer(Box::new(Uint(8))); + let (args, result) = match symbol { + "__wave_task_alloc" => (vec![Int(64)], pointer()), + "__wave_task_free" => (vec![pointer(), Int(64)], Void), + "__wave_task_new" => (vec![pointer(), Int(64), pointer(), pointer()], Int(64)), + "__wave_task_result" => (vec![Int(64)], pointer()), + "__wave_task_release" + | "__wave_task_drive" + | "__wave_task_complete" + | "__wave_task_close_fd" => (vec![Int(64)], Void), + "__wave_task_ready" | "__wave_task_cancel" => (vec![Int(64)], Int(32)), + "__wave_task_spawn" | "__wave_task_cancel_join" => (vec![Int(64)], Int(64)), + "__wave_task_wait" => (vec![Int(64), Int(64)], Void), + "__wave_task_yield" => (vec![], Int(64)), + "__wave_task_shutdown" => (vec![], Void), + "__wave_task_interest" => (vec![Int(64), Int(32), Int(64)], Int(64)), + "__wave_task_io" => (vec![Int(64), pointer(), Int(64), Int(32), Int(64)], Int(64)), + "__wave_task_windows_notify" => return Some((vec![pointer(), Uint(8)], Void, "system")), + _ => return None, + }; + Some((args, result, "c")) +} diff --git a/front/parser/src/async_lower.rs b/front/parser/src/async_lower.rs new file mode 100644 index 00000000..cbe3d10e --- /dev/null +++ b/front/parser/src/async_lower.rs @@ -0,0 +1,949 @@ +//! Backend-neutral async frames and explicit suspension control flow. +//! +//! Source expressions are evaluated in order into frame slots. A pending await +//! returns from the resume function; re-entry selects only its saved state. +use crate::ast::*; +use crate::hir::{HirExpressionType, TypedProgram}; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct AsyncLoweringError { + pub message: String, + pub span: Option, +} +#[derive(Debug, Clone)] +pub struct FrameSlot { + pub name: String, + pub ty: WaveType, +} +#[derive(Debug, Clone)] +pub enum Transition { + Jump(usize), + Branch(Expression, usize, usize), + Match(Expression, Vec<(MatchPattern, Vec, usize)>), + Await { + future: Expression, + output: String, + result: WaveType, + next: usize, + }, + Complete(Option), +} +#[derive(Debug, Clone)] +pub struct AsyncState { + pub operations: Vec, + pub transition: Transition, +} +#[derive(Debug, Clone)] +pub struct AsyncFrame { + pub function: String, + pub result: WaveType, + pub slots: Vec, + pub parameters: Vec, + pub states: Vec, +} + +pub fn plan( + program: &TypedProgram, + function: &FunctionNode, +) -> Result { + let mut lower = Lower { + program, + frame: AsyncFrame { + function: function.name.clone(), + result: function.return_type.clone().unwrap_or(WaveType::Void), + slots: vec![], + parameters: vec![], + states: vec![empty_state()], + }, + current: 0, + bindings: HashMap::new(), + loops: vec![], + }; + for p in &function.parameters { + let name = lower.new_slot(p.param_type.clone()); + lower.bindings.insert(p.name.clone(), name.clone()); + lower.frame.parameters.push(name); + } + lower.block(&function.body)?; + Ok(lower.frame) +} +fn empty_state() -> AsyncState { + AsyncState { + operations: vec![], + transition: Transition::Complete(None), + } +} +struct Lower<'a> { + program: &'a TypedProgram, + frame: AsyncFrame, + current: usize, + bindings: HashMap, + loops: Vec<(usize, usize)>, +} +fn var(name: impl Into) -> Expression { + Expression::Variable(name.into()) +} +fn field(name: impl Into) -> Expression { + Expression::FieldAccess { + object: Box::new(var("$async$frame")), + field: name.into(), + } +} +fn store(target: Expression, value: Expression) -> ASTNode { + ASTNode::Statement(StatementNode::Expression(Expression::Assignment { + target: Box::new(target), + value: Box::new(value), + })) +} +impl Lower<'_> { + fn error(&self, e: &Expression, message: impl Into) -> AsyncLoweringError { + AsyncLoweringError { + message: message.into(), + span: self + .program + .expression_id(e) + .and_then(|id| self.program.expression_span(id)) + .cloned(), + } + } + fn ty( + &self, + e: &Expression, + expected: Option<&WaveType>, + ) -> Result { + match self.program.type_of(e) { + Some(HirExpressionType::Resolved(t)) => Ok(t.clone()), + Some(HirExpressionType::IntegerLiteral) => { + Ok(expected.cloned().unwrap_or(WaveType::Int(32))) + } + Some(HirExpressionType::FloatLiteral) => { + Ok(expected.cloned().unwrap_or(WaveType::Float(32))) + } + _ => expected + .cloned() + .ok_or_else(|| self.error(e, "async temporary needs a concrete type")), + } + } + fn new_slot(&mut self, ty: WaveType) -> String { + let name = format!("v{}", self.frame.slots.len()); + self.frame.slots.push(FrameSlot { + name: name.clone(), + ty, + }); + name + } + fn state(&mut self) -> usize { + let id = self.frame.states.len(); + self.frame.states.push(empty_state()); + id + } + fn emit(&mut self, n: ASTNode) { + self.frame.states[self.current].operations.push(n); + } + fn end(&mut self, t: Transition) { + self.frame.states[self.current].transition = t; + } + fn snapshot(&mut self, e: Expression, ty: WaveType) -> Expression { + if ty == WaveType::Void { + self.emit(ASTNode::Statement(StatementNode::Expression(e))); + return Expression::Null; + } + let name = self.new_slot(ty); + self.emit(store(field(&name), e)); + field(name) + } + fn place(&mut self, e: &Expression) -> Result { + Ok(match e.unspanned() { + Expression::Variable(n) => self.bindings.get(n).map_or_else(|| var(n), |v| field(v)), + Expression::Grouped(inner) => self.place(inner)?, + Expression::Deref(inner) => { + if matches!( + inner.unspanned(), + Expression::FieldAccess { .. } | Expression::IndexAccess { .. } + ) { + self.place(inner)? + } else { + let t = self.ty(inner, None)?; + Expression::Deref(Box::new(Expression::Cast { + expr: Box::new(self.expr(inner, None)?), + target_type: t, + })) + } + } + Expression::FieldAccess { + object, + field: name, + } => { + let obj = if matches!(self.ty(object, None)?, WaveType::Pointer(_)) { + self.expr(object, None)? + } else { + self.place(object)? + }; + Expression::FieldAccess { + object: Box::new(obj), + field: name.clone(), + } + } + Expression::IndexAccess { target, index } => { + let obj = if matches!(self.ty(target, None)?, WaveType::Array(..)) { + self.place(target)? + } else { + self.expr(target, None)? + }; + let index = self.expr(index, None)?; + Expression::IndexAccess { + target: Box::new(obj), + index: Box::new(index), + } + } + _ => return Err(self.error(e, "unsupported address across async suspension")), + }) + } + fn expr( + &mut self, + e: &Expression, + expected: Option<&WaveType>, + ) -> Result { + let ty = self.ty(e, expected)?; + let value = match e.unspanned() { + Expression::Await(inner) => { + let future = self.expr(inner, None)?; + let output = self.new_slot(if ty == WaveType::Void { + WaveType::Byte + } else { + ty.clone() + }); + let wait = self.state(); + let next = self.state(); + self.end(Transition::Jump(wait)); + self.current = wait; + self.end(Transition::Await { + future, + output: output.clone(), + result: ty, + next, + }); + self.current = next; + return Ok(field(output)); + } + Expression::Variable(n) => self.bindings.get(n).map_or_else(|| var(n), |v| field(v)), + Expression::Literal(_) | Expression::Null => e.clone(), + Expression::Grouped(inner) => return self.expr(inner, expected), + Expression::AddressOf(inner) => Expression::AddressOf(Box::new(self.place(inner)?)), + Expression::Deref(inner) => { + if matches!( + inner.unspanned(), + Expression::FieldAccess { .. } | Expression::IndexAccess { .. } + ) { + self.place(inner)? + } else { + let t = self.ty(inner, None)?; + Expression::Deref(Box::new(Expression::Cast { + expr: Box::new(self.expr(inner, None)?), + target_type: t, + })) + } + } + Expression::Unary { operator, expr } => Expression::Unary { + operator: operator.clone(), + expr: Box::new(self.expr(expr, Some(&ty))?), + }, + Expression::Cast { expr, target_type } => { + // A contextual literal must not be truncated into a default i32 + // temporary before its explicit widening cast is performed. + let source_context = match self.program.type_of(expr) { + Some(HirExpressionType::IntegerLiteral) => match target_type { + WaveType::Int(_) | WaveType::Uint(_) | WaveType::Byte | WaveType::Char => { + Some(target_type.clone()) + } + WaveType::Pointer(_) | WaveType::Float(_) => Some(WaveType::Int(64)), + _ => None, + }, + Some(HirExpressionType::FloatLiteral) => match target_type { + WaveType::Float(_) => Some(target_type.clone()), + _ => Some(WaveType::Float(64)), + }, + Some(HirExpressionType::Null) + if matches!(target_type, WaveType::Pointer(_)) => + { + Some(target_type.clone()) + } + _ => None, + }; + Expression::Cast { + expr: Box::new(self.expr(expr, source_context.as_ref())?), + target_type: target_type.clone(), + } + } + Expression::BinaryExpression { + left, + operator, + right, + } if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) => { + let value = self.expr(left, Some(&WaveType::Bool))?; + let output = self.new_slot(WaveType::Bool); + self.emit(store(field(&output), value.clone())); + let rhs = self.state(); + let done = self.state(); + let (yes, no) = if matches!(operator, Operator::LogicalAnd) { + (rhs, done) + } else { + (done, rhs) + }; + self.end(Transition::Branch(value, yes, no)); + self.current = rhs; + let value = self.expr(right, Some(&WaveType::Bool))?; + self.emit(store(field(&output), value)); + self.end(Transition::Jump(done)); + self.current = done; + return Ok(field(output)); + } + Expression::BinaryExpression { + left, + operator, + right, + } => { + let lt = self.ty( + left, + if ty == WaveType::Bool { + match self.program.type_of(right) { + Some(HirExpressionType::Resolved(right_ty)) => Some(right_ty), + _ => None, + } + } else { + Some(&ty) + }, + )?; + let rt = self.ty(right, Some(<))?; + let left = self.expr(left, Some(&rt))?; + let right = self.expr(right, Some(<))?; + Expression::BinaryExpression { + left: Box::new(left), + operator: operator.clone(), + right: Box::new(right), + } + } + Expression::FunctionCall { + name, + type_args, + args, + } => { + let parameters = self + .program + .syntax() + .iter() + .find_map(|n| match n { + ASTNode::Function(f) if &f.name == name => Some( + f.parameters + .iter() + .map(|p| p.param_type.clone()) + .collect::>(), + ), + ASTNode::ExternFunction(f) if &f.name == name => { + Some(f.params.iter().map(|p| p.1.clone()).collect()) + } + ASTNode::Struct(s) => s + .methods + .iter() + .find(|f| format!("{}_{}", s.name, f.name) == *name) + .map(|f| f.parameters.iter().map(|p| p.param_type.clone()).collect()), + ASTNode::ProtoImpl(p) => p + .methods + .iter() + .find(|f| format!("{}_{}", p.target, f.name) == *name) + .map(|f| f.parameters.iter().map(|p| p.param_type.clone()).collect()), + _ => None, + }) + .unwrap_or_default(); + let args = args + .iter() + .enumerate() + .map(|(i, a)| self.expr(a, parameters.get(i))) + .collect::>()?; + Expression::FunctionCall { + name: name.clone(), + type_args: type_args.clone(), + args, + } + } + Expression::MethodCall { + object, + name, + type_args, + args, + } => { + let object = self.expr(object, None)?; + let args = args + .iter() + .map(|a| self.expr(a, None)) + .collect::>()?; + Expression::MethodCall { + object: Box::new(object), + name: name.clone(), + type_args: type_args.clone(), + args, + } + } + Expression::StructLiteral { name, fields } => { + let types = self + .program + .syntax() + .iter() + .find_map(|n| match n { + ASTNode::Struct(s) if &s.name == name => Some(s.fields.clone()), + _ => None, + }) + .unwrap_or_default(); + Expression::StructLiteral { + name: name.clone(), + fields: fields + .iter() + .map(|(n, e)| { + Ok(( + n.clone(), + self.expr(e, types.iter().find(|(f, _)| f == n).map(|(_, t)| t))?, + )) + }) + .collect::>()?, + } + } + Expression::ArrayLiteral(values) => { + let inner = if let WaveType::Array(t, _) = &ty { + Some(t.as_ref()) + } else { + None + }; + Expression::ArrayLiteral( + values + .iter() + .map(|v| self.expr(v, inner)) + .collect::>()?, + ) + } + Expression::IndexAccess { .. } => self.place(e)?, + Expression::FieldAccess { + object, + field: name, + } => Expression::FieldAccess { + object: Box::new(self.expr(object, None)?), + field: name.clone(), + }, + Expression::Assignment { target, value } + | Expression::AssignOperation { target, value, .. } => { + let target_ty = self.ty(target, None)?; + let place = self.place(target)?; + let pointer = self.snapshot( + Expression::AddressOf(Box::new(place)), + WaveType::Pointer(Box::new(target_ty.clone())), + ); + let target = Box::new(Expression::Deref(Box::new(Expression::Cast { + expr: Box::new(pointer), + target_type: WaveType::Pointer(Box::new(target_ty.clone())), + }))); + let value = Box::new(self.expr(value, Some(&target_ty))?); + if let Expression::AssignOperation { operator, .. } = e.unspanned() { + Expression::AssignOperation { + target, + operator: operator.clone(), + value, + } + } else { + Expression::Assignment { target, value } + } + } + Expression::IncDec { kind, target } => Expression::IncDec { + kind: kind.clone(), + target: Box::new(self.place(target)?), + }, + Expression::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + } => Expression::AsmBlock { + instructions: instructions.clone(), + inputs: inputs + .iter() + .map(|(n, e)| Ok((n.clone(), self.expr(e, None)?))) + .collect::>()?, + outputs: outputs + .iter() + .map(|(n, e)| Ok((n.clone(), self.place(e)?))) + .collect::>()?, + clobbers: clobbers.clone(), + }, + Expression::Located { .. } => unreachable!(), + }; + Ok(self.snapshot(value, ty)) + } + fn block(&mut self, nodes: &[ASTNode]) -> Result<(), AsyncLoweringError> { + let bindings = self.bindings.clone(); + for n in nodes { + self.node(n)?; + } + self.bindings = bindings; + Ok(()) + } + fn node(&mut self, n: &ASTNode) -> Result<(), AsyncLoweringError> { + match n.unspanned() { + ASTNode::Variable(v) => { + let init = v + .initial_value + .as_ref() + .map(|e| self.expr(e, Some(&v.type_name))) + .transpose()?; + let slot = self.new_slot(v.type_name.clone()); + self.bindings.insert(v.name.clone(), slot.clone()); + if let Some(init) = init { + self.emit(store(field(slot), init)); + } + } + ASTNode::Statement(s) => match s { + StatementNode::Return(e) => { + let ty = self.frame.result.clone(); + let value = e.as_ref().map(|e| self.expr(e, Some(&ty))).transpose()?; + self.end(Transition::Complete(value)); + self.current = self.state(); + } + StatementNode::Expression(e) => { + self.expr(e, None)?; + } + StatementNode::Assign { variable, value } => { + let target = self + .bindings + .get(variable) + .map_or_else(|| var(variable), |s| field(s)); + let value = self.expr(value, None)?; + self.emit(store(target, value)); + } + StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + } => { + let done = self.state(); + let mut branches = vec![(condition, body.as_slice())]; + if let Some(others) = else_if_blocks { + branches.extend(others.iter().map(|(c, b)| (c, b.as_slice()))); + } + for (condition, body) in branches { + let condition = self.expr(condition, Some(&WaveType::Bool))?; + let yes = self.state(); + let no = self.state(); + self.end(Transition::Branch(condition, yes, no)); + self.current = yes; + self.block(body)?; + self.end(Transition::Jump(done)); + self.current = no; + } + if let Some(body) = else_block { + self.block(body)?; + } + self.end(Transition::Jump(done)); + self.current = done; + } + StatementNode::While { condition, body } => { + self.loop_body(None, condition, None, body)? + } + StatementNode::For { + initialization, + condition, + increment, + body, + } => self.loop_body(Some(initialization), condition, Some(increment), body)?, + StatementNode::Break | StatementNode::Continue => { + let (exit, next) = *self.loops.last().expect("validated loop"); + self.end(Transition::Jump(if matches!(s, StatementNode::Break) { + exit + } else { + next + })); + self.current = self.state(); + } + StatementNode::Match { value, arms } => { + let value_ty = self.ty(value, None)?; + let value = self.expr(value, None)?; + let done = self.state(); + let start = self.current; + let mut branches = vec![]; + for arm in arms { + let state = self.state(); + self.current = state; + let old = self.bindings.clone(); + self.bind_pattern(&arm.pattern, &value_ty)?; + let binding_stores = + std::mem::take(&mut self.frame.states[state].operations); + branches.push((arm.pattern.clone(), binding_stores, state)); + for n in &arm.body { + self.node(n)?; + } + self.bindings = old; + self.end(Transition::Jump(done)); + } + self.current = start; + self.end(Transition::Match(value, branches)); + self.current = done; + } + StatementNode::Input { format, args } => { + let args = args + .iter() + .map(|e| self.place(e)) + .collect::>()?; + self.emit(ASTNode::Statement(StatementNode::Input { + format: format.clone(), + args, + })); + } + StatementNode::PrintFormat { format, args } + | StatementNode::PrintlnFormat { format, args } => { + let args = args + .iter() + .map(|e| self.expr(e, None)) + .collect::>()?; + let s = match s { + StatementNode::PrintFormat { .. } => StatementNode::PrintFormat { + format: format.clone(), + args, + }, + _ => StatementNode::PrintlnFormat { + format: format.clone(), + args, + }, + }; + self.emit(ASTNode::Statement(s)); + } + StatementNode::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + } => { + let inputs = inputs + .iter() + .map(|(n, e)| Ok((n.clone(), self.expr(e, None)?))) + .collect::>()?; + let outputs = outputs + .iter() + .map(|(n, e)| Ok((n.clone(), self.place(e)?))) + .collect::>()?; + self.emit(ASTNode::Statement(StatementNode::AsmBlock { + instructions: instructions.clone(), + inputs, + outputs, + clobbers: clobbers.clone(), + })); + } + _ => self.emit(n.clone()), + }, + ASTNode::Expression(e) => { + self.expr(e, None)?; + } + _ => { + return Err(AsyncLoweringError { + message: "unsupported declaration inside async function".into(), + span: n.span().cloned(), + }) + } + } + Ok(()) + } + fn bind_pattern(&mut self, p: &MatchPattern, ty: &WaveType) -> Result<(), AsyncLoweringError> { + match p.unspanned() { + MatchPattern::Binding(name) => { + let slot = self.new_slot(ty.clone()); + self.bindings.insert(name.clone(), slot.clone()); + self.emit(store(field(slot), var(name))); + } + MatchPattern::Variant { payloads, .. } => { + let info = self + .program + .variant_pattern_of(p) + .expect("typed variant pattern"); + let types = info.payload_types.clone(); + for (p, t) in payloads.iter().zip(types) { + self.bind_pattern(p, &t)?; + } + } + _ => {} + } + Ok(()) + } + fn loop_body( + &mut self, + init: Option<&ASTNode>, + condition: &Expression, + increment: Option<&Expression>, + body: &[ASTNode], + ) -> Result<(), AsyncLoweringError> { + let old = self.bindings.clone(); + if let Some(init) = init { + self.node(init)?; + } + let check = self.state(); + let run = self.state(); + let step = self.state(); + let done = self.state(); + self.end(Transition::Jump(check)); + self.current = check; + let condition = self.expr(condition, Some(&WaveType::Bool))?; + self.end(Transition::Branch(condition, run, done)); + self.current = run; + self.loops.push((done, step)); + self.block(body)?; + self.loops.pop(); + self.end(Transition::Jump(step)); + self.current = step; + if let Some(e) = increment { + self.expr(e, None)?; + } + self.end(Transition::Jump(check)); + self.current = done; + self.bindings = old; + Ok(()) + } +} + +fn integer(n: usize) -> Expression { + Expression::Literal(Literal::Int(n.to_string())) +} +fn call(name: &str, type_args: Vec, args: Vec) -> Expression { + Expression::FunctionCall { + name: name.into(), + type_args, + args, + } +} +fn statement(e: Expression) -> ASTNode { + ASTNode::Statement(StatementNode::Expression(e)) +} +fn ret(e: Expression) -> ASTNode { + ASTNode::Statement(StatementNode::Return(Some(e))) +} +fn parameter(name: &str, ty: WaveType) -> ParameterNode { + ParameterNode { + span: None, + name: name.into(), + param_type: ty, + initial_value: None, + } +} +fn function( + name: String, + parameters: Vec, + result: WaveType, + body: Vec, +) -> FunctionNode { + FunctionNode { + is_async: false, + span: None, + name, + generic_params: vec![], + parameters, + return_type: Some(result), + return_type_span: None, + body, + export: None, + visibility: Visibility::Private, + } +} +fn jump(next: usize) -> Vec { + vec![ + store(field("state"), integer(next)), + ASTNode::Statement(StatementNode::Continue), + ] +} +fn emit_frame( + original: &FunctionNode, + frame: AsyncFrame, + id: usize, +) -> (FunctionNode, Vec) { + let frame_name = format!("$async$frame${id}"); + let poll_name = format!("$async$poll${id}"); + let frame_ty = WaveType::Struct(frame_name.clone()); + let mut fields = frame + .slots + .iter() + .map(|s| (s.name.clone(), s.ty.clone())) + .collect::>(); + fields.push(("state".into(), WaveType::Int(32))); + fields.push(( + "result".into(), + if frame.result == WaveType::Void { + WaveType::Byte + } else { + frame.result.clone() + }, + )); + let structure = ASTNode::Struct(StructNode { + name: frame_name, + generic_params: vec![], + field_spans: vec![None; fields.len()], + fields, + methods: vec![], + visibility: Visibility::Private, + }); + let mut arms = vec![]; + for (index, state) in frame.states.into_iter().enumerate() { + let mut body = state.operations; + match state.transition { + Transition::Jump(next) => body.extend(jump(next)), + Transition::Branch(condition, yes, no) => { + body.push(ASTNode::Statement(StatementNode::If { + condition, + body: jump(yes), + else_if_blocks: None, + else_block: Some(Box::new(jump(no))), + })) + } + Transition::Match(value, branches) => { + body.push(ASTNode::Statement(StatementNode::Match { + value, + arms: branches + .into_iter() + .map(|(pattern, mut body, next)| { + body.extend(jump(next)); + MatchArm { + span: None, + pattern, + body, + } + }) + .collect(), + })) + } + Transition::Await { + future, + output, + result, + next, + } => { + let take = call("__wave_async_take", vec![], vec![future.clone()]); + let mut ready = vec![if result == WaveType::Void { + statement(take) + } else { + store(field(output), take) + }]; + ready.extend(jump(next)); + body.push(ASTNode::Statement(StatementNode::If { + condition: call("__wave_async_ready", vec![], vec![future.clone()]), + body: ready, + else_if_blocks: None, + else_block: None, + })); + body.push(statement(call( + "__wave_async_wait", + vec![], + vec![var("$async$id"), future], + ))); + body.push(ret(Expression::Literal(Literal::Bool(false)))); + } + Transition::Complete(value) => { + if frame.result != WaveType::Void { + if let Some(value) = value { + body.push(store(field("result"), value)); + } + } + body.push(statement(call( + "__wave_async_complete", + vec![], + vec![var("$async$id")], + ))); + body.push(ret(Expression::Literal(Literal::Bool(true)))); + } + } + arms.push(MatchArm { + span: None, + pattern: MatchPattern::Int(index.to_string()), + body, + }); + } + arms.push(MatchArm { + span: None, + pattern: MatchPattern::Wildcard, + body: vec![ret(Expression::Literal(Literal::Bool(false)))], + }); + let resume = function( + poll_name.clone(), + vec![ + parameter( + "$async$frame", + WaveType::Pointer(Box::new(frame_ty.clone())), + ), + parameter("$async$id", WaveType::Int(64)), + ], + WaveType::Bool, + vec![ASTNode::Statement(StatementNode::While { + condition: Expression::Literal(Literal::Bool(true)), + body: vec![ASTNode::Statement(StatementNode::Match { + value: field("state"), + arms, + })], + })], + ); + let mut constructor = original.clone(); + constructor.is_async = false; + constructor.return_type = Some(WaveType::Future(Box::new(frame.result.clone()))); + constructor.body = vec![ASTNode::Variable(VariableNode { + name: "$async$frame".into(), + type_name: WaveType::Pointer(Box::new(frame_ty.clone())), + initial_value: Some(call("__wave_async_alloc", vec![frame_ty.clone()], vec![])), + mutability: Mutability::Var, + visibility: Visibility::Private, + })]; + for (p, slot) in original.parameters.iter().zip(frame.parameters) { + constructor.body.push(store(field(slot), var(&p.name))); + } + constructor.body.push(ret(call( + "__wave_async_create", + vec![frame_ty, frame.result], + vec![ + var("$async$frame"), + Expression::AddressOf(Box::new(field("result"))), + Expression::Literal(Literal::String(poll_name)), + ], + ))); + (constructor, vec![structure, ASTNode::Function(resume)]) +} +/// Emit ordinary typed operations from the resumable plan. No LLVM details occur here. +pub fn lower_program(program: &TypedProgram) -> Result, AsyncLoweringError> { + let mut output = program.syntax().to_vec(); + let mut generated = vec![]; + let mut next = 0; + for (source, target) in program.syntax().iter().zip(output.iter_mut()) { + match (source, target) { + (ASTNode::Function(f), ASTNode::Function(out)) + if f.is_async && f.generic_params.is_empty() => + { + let (ctor, extra) = emit_frame(f, plan(program, f)?, next); + next += 1; + *out = ctor; + generated.extend(extra); + } + (ASTNode::Struct(s), ASTNode::Struct(out)) if s.generic_params.is_empty() => { + for (f, target) in s.methods.iter().zip(&mut out.methods) { + if f.is_async && f.generic_params.is_empty() { + let (ctor, extra) = emit_frame(f, plan(program, f)?, next); + next += 1; + *target = ctor; + generated.extend(extra); + } + } + } + (ASTNode::ProtoImpl(s), ASTNode::ProtoImpl(out)) => { + for (f, target) in s.methods.iter().zip(&mut out.methods) { + if f.is_async && f.generic_params.is_empty() { + let (ctor, extra) = emit_frame(f, plan(program, f)?, next); + next += 1; + *target = ctor; + generated.extend(extra); + } + } + } + _ => {} + } + } + output.extend(generated); + Ok(output) +} diff --git a/front/parser/src/expr/assign.rs b/front/parser/src/expr/assign.rs index 5fc097ad..23049dd7 100644 --- a/front/parser/src/expr/assign.rs +++ b/front/parser/src/expr/assign.rs @@ -17,17 +17,22 @@ use crate::ast::{AssignOperator, Expression}; use crate::expr::binary::parse_logical_or_expression; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; -pub fn parse_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { parse_assignment_expression(tokens) } -pub fn parse_assignment_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_assignment_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -43,20 +48,20 @@ where TokenType::StarEq => AssignOperator::MulAssign, TokenType::DivEq => AssignOperator::DivAssign, TokenType::RemainderEq => AssignOperator::RemAssign, - _ => return Some(left), + _ => return Ok(left), }; tokens.next(); // consume op let right = parse_assignment_expression(tokens)?; - return Some(Expression::AssignOperation { + return Ok(Expression::AssignOperation { target: Box::new(left), operator: op, value: Box::new(right), }); } - Some(left) + Ok(left) })(); result.map(|value: Expression| value.with_span(lexer::consumed_span(before, tokens))) } diff --git a/front/parser/src/expr/binary.rs b/front/parser/src/expr/binary.rs index 4062fca3..4411823a 100644 --- a/front/parser/src/expr/binary.rs +++ b/front/parser/src/expr/binary.rs @@ -19,11 +19,14 @@ use crate::ast::{Expression, Operator}; use crate::expr::unary::parse_unary_expression; +use crate::parser::ParseError; use crate::types::parse_type_from_stream; use lexer::token::TokenType; use lexer::Token; -pub fn parse_logical_or_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_logical_or_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -38,12 +41,12 @@ where left = Expression::binary(left, Operator::LogicalOr, right); } - Some(left) + Ok(left) } pub fn parse_logical_and_expression<'a, T>( tokens: &mut std::iter::Peekable, -) -> Option +) -> Result where T: Iterator + Clone, { @@ -58,10 +61,12 @@ where left = Expression::binary(left, Operator::LogicalAnd, right); } - Some(left) + Ok(left) } -pub fn parse_bitwise_or_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_bitwise_or_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -76,12 +81,12 @@ where left = Expression::binary(left, Operator::BitwiseOr, right); } - Some(left) + Ok(left) } pub fn parse_bitwise_xor_expression<'a, T>( tokens: &mut std::iter::Peekable, -) -> Option +) -> Result where T: Iterator + Clone, { @@ -93,12 +98,12 @@ where left = Expression::binary(left, Operator::BitwiseXor, right); } - Some(left) + Ok(left) } pub fn parse_bitwise_and_expression<'a, T>( tokens: &mut std::iter::Peekable, -) -> Option +) -> Result where T: Iterator + Clone, { @@ -113,10 +118,12 @@ where left = Expression::binary(left, Operator::BitwiseAnd, right); } - Some(left) + Ok(left) } -pub fn parse_equality_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_equality_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -133,10 +140,12 @@ where left = Expression::binary(left, op, right); } - Some(left) + Ok(left) } -pub fn parse_relational_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_relational_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -155,10 +164,12 @@ where left = Expression::binary(left, op, right); } - Some(left) + Ok(left) } -pub fn parse_shift_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_shift_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -176,10 +187,12 @@ where left = Expression::binary(left, op, right); } - Some(left) + Ok(left) } -pub fn parse_additive_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_additive_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -196,12 +209,12 @@ where left = Expression::binary(left, op, right); } - Some(left) + Ok(left) } pub fn parse_multiplicative_expression<'a, T>( tokens: &mut std::iter::Peekable, -) -> Option +) -> Result where T: Iterator + Clone, { @@ -219,10 +232,12 @@ where left = Expression::binary(left, op, right); } - Some(left) + Ok(left) } -fn parse_cast_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +fn parse_cast_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -232,7 +247,10 @@ where let before = tokens.clone(); let first = expr.span().cloned(); tokens.next(); // consume `as` - let target_type = parse_type_from_stream(tokens)?; + let anchor = tokens.peek().copied(); + let target_type = parse_type_from_stream(tokens).ok_or_else(|| { + ParseError::expected_at(tokens.peek().copied(), anchor, "type", "cast expression") + })?; expr = Expression::Cast { expr: Box::new(expr), target_type, @@ -245,5 +263,5 @@ where ); } - Some(expr) + Ok(expr) } diff --git a/front/parser/src/expr/helpers.rs b/front/parser/src/expr/helpers.rs index 50680d33..46901ddd 100644 --- a/front/parser/src/expr/helpers.rs +++ b/front/parser/src/expr/helpers.rs @@ -16,9 +16,11 @@ //! indices may form storage targets. Mutability and type legality are checked by //! semantic validation. +use super::primary::{expect_token, identifier}; use crate::ast::Expression; use crate::expr::parse_expression; use crate::expr::unary::parse_unary_expression; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; @@ -40,22 +42,13 @@ pub fn is_assignable(expr: &Expression) -> bool { fn parse_lvalue_tail( mut base: Expression, tokens: &mut Peekable>, -) -> Option { +) -> Result { loop { match tokens.peek().map(|t| &t.token_type) { // a.b Some(TokenType::Dot) => { - tokens.next(); // '.' - let field = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(s), - .. - }) => s.clone(), - _ => { - println!("Error: Expected identifier after '.'"); - return None; - } - }; + let dot = tokens.next(); + let field = identifier(tokens, dot, "member access")?; base = Expression::FieldAccess { object: Box::new(base), @@ -65,13 +58,9 @@ fn parse_lvalue_tail( // a[b] Some(TokenType::Lbrack) => { - tokens.next(); // '[' + let opener = tokens.next(); let idx = parse_expression(tokens)?; - if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Rbrack) { - println!("Error: Expected ']' after index expression"); - return None; - } - tokens.next(); // ']' + expect_token(tokens, opener, TokenType::Rbrack, "']'", "index expression")?; base = Expression::IndexAccess { target: Box::new(base), @@ -83,13 +72,13 @@ fn parse_lvalue_tail( } } - Some(base) + Ok(base) } pub fn parse_expression_from_token( first_token: &Token, tokens: &mut Peekable>, -) -> Option { +) -> Result { match &first_token.token_type { TokenType::Identifier(name) => { let base = Expression::Variable(name.clone()); @@ -98,9 +87,14 @@ pub fn parse_expression_from_token( TokenType::Deref => { let inner = parse_unary_expression(tokens)?; - Some(Expression::Deref(Box::new(inner))) + Ok(Expression::Deref(Box::new(inner))) } - _ => None, + _ => Err(ParseError::expected_at( + Some(first_token), + Some(first_token), + "lvalue", + "assignment target", + )), } } diff --git a/front/parser/src/expr/mod.rs b/front/parser/src/expr/mod.rs index 92c1f24e..88c4f589 100644 --- a/front/parser/src/expr/mod.rs +++ b/front/parser/src/expr/mod.rs @@ -25,3 +25,5 @@ mod unary; pub use assign::parse_expression; pub use helpers::*; + +pub(crate) use primary::{expect_token, identifier}; diff --git a/front/parser/src/expr/postfix.rs b/front/parser/src/expr/postfix.rs index 47941f14..cdc1315a 100644 --- a/front/parser/src/expr/postfix.rs +++ b/front/parser/src/expr/postfix.rs @@ -21,76 +21,54 @@ use std::iter::Peekable; use lexer::token::TokenType; use lexer::Token; +use super::primary::{argument_list, expect_token, identifier, peek_is_generic_call}; use crate::ast::{Expression, IncDecKind}; use crate::expr::{is_assignable, parse_expression}; +use crate::parser::ParseError; pub fn parse_postfix_expression<'a, T>( tokens: &mut Peekable, mut expr: Expression, -) -> Option +) -> Result where T: Iterator + Clone, { let first = expr.span().cloned(); let before = tokens.clone(); loop { + let mut focus = None; match tokens.peek().map(|t| &t.token_type) { Some(TokenType::Dot) => { - tokens.next(); // consume '.' - - let name = if let Some(Token { - token_type: TokenType::Identifier(name), - .. - }) = tokens.next() - { - name.clone() - } else { - println!("Error: Expected identifier after '.'"); - return None; - }; - - if let Some(Token { - token_type: TokenType::Lparen, - .. - }) = tokens.peek() - { - // ----- MethodCall ----- - tokens.next(); // consume '(' - - let mut args = Vec::new(); - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rparen) - { - loop { - let arg = parse_expression(tokens)?; - args.push(arg); - - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); // consume ',' - } else { - break; - } - } - } - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' after method call arguments"); - return None; + let dot = tokens.next(); + focus = tokens + .peek() + .and_then(|token| token.span.clone()) + .map(Box::new); + let name = identifier(tokens, dot, "member access")?; + let mut type_args = Vec::new(); + if peek_is_generic_call(tokens) { + tokens.next(); + let inner = crate::decl::collect_generic_inner(tokens) + .expect("generic call lookahead validated the suffix"); + for arg in crate::types::split_top_level_generic_args(&inner).unwrap() { + type_args.push( + crate::types::token_type_to_wave_type( + &crate::types::parse_type(&arg).unwrap(), + ) + .unwrap(), + ); } - tokens.next(); // consume ')' - + } + if tokens + .peek() + .is_some_and(|token| token.token_type == TokenType::Lparen) + { + let args = argument_list(tokens, TokenType::Rparen, "')'", "method call")?; let base_expr = expr; expr = Expression::MethodCall { object: Box::new(base_expr), name, + type_args, args, }; } else { @@ -104,17 +82,9 @@ where } Some(TokenType::Lbrack) => { - tokens.next(); // consume '[' - + let opener = tokens.next(); let index_expr = parse_expression(tokens)?; - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rbrack) - { - println!("Error: Expected ']' after index"); - return None; - } - tokens.next(); // consume ']' + expect_token(tokens, opener, TokenType::Rbrack, "']'", "index expression")?; let base_expr = expr; expr = Expression::IndexAccess { @@ -124,15 +94,15 @@ where } Some(TokenType::Increment) => { - let line = tokens.peek().unwrap().line; - tokens.next(); // consume '++' + let operator = tokens.next(); // consume '++' if !is_assignable(&expr) { - println!( - "Error: postfix ++ target must be assignable (line {})", - line - ); - return None; + return Err(ParseError::expected_at( + operator, + operator, + "assignable expression", + "postfix mutation", + )); } let base = expr; @@ -141,19 +111,19 @@ where target: Box::new(base), }; - return Some(expr); + return Ok(expr); } Some(TokenType::Decrement) => { - let line = tokens.peek().unwrap().line; - tokens.next(); // consume '--' + let operator = tokens.next(); // consume '--' if !is_assignable(&expr) { - println!( - "Error: postfix -- target must be assignable (line {})", - line - ); - return None; + return Err(ParseError::expected_at( + operator, + operator, + "assignable expression", + "postfix mutation", + )); } let base = expr; @@ -162,7 +132,7 @@ where target: Box::new(base), }; - return Some(expr); + return Ok(expr); } _ => break, @@ -170,9 +140,13 @@ where let span = first .as_ref() .zip(lexer::consumed_span(before.clone(), tokens)) - .map(|(first, last)| first.through(&last)); + .map(|(first, last)| { + let mut span = first.through(&last); + span.focus = focus; + span + }); expr = expr.with_span(span); } - Some(expr) + Ok(expr) } diff --git a/front/parser/src/expr/primary.rs b/front/parser/src/expr/primary.rs index 719be847..9cb3dfa7 100644 --- a/front/parser/src/expr/primary.rs +++ b/front/parser/src/expr/primary.rs @@ -20,11 +20,12 @@ use std::iter::Peekable; use lexer::token::TokenType; use lexer::Token; -use crate::asm::{parse_asm_clobber_clause, parse_asm_inout_clause}; +use crate::asm::parse_asm_body; use crate::ast::{Expression, Literal}; use crate::decl::collect_generic_inner; use crate::expr::parse_expression; use crate::expr::postfix::parse_postfix_expression; +use crate::parser::ParseError; use crate::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; fn skip_ws<'a, T>(tokens: &mut Peekable) @@ -39,7 +40,7 @@ where } } -fn peek_is_generic_call<'a, T>(tokens: &Peekable) -> bool +pub(super) fn peek_is_generic_call<'a, T>(tokens: &Peekable) -> bool where T: Iterator + Clone, { @@ -82,409 +83,272 @@ where probe.peek().is_some_and(|token| token.token_type == suffix) } -fn parse_struct_literal_fields<'a, T>(tokens: &mut Peekable) -> Option> +pub(crate) fn expect_token<'a, T>( + tokens: &mut Peekable, + anchor: Option<&Token>, + kind: TokenType, + expected: &str, + context: &str, +) -> Result<(), ParseError> where T: Iterator + Clone, { - let mut fields = vec![]; + if tokens.peek().is_some_and(|token| token.token_type == kind) { + tokens.next(); + Ok(()) + } else { + Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + expected, + context, + )) + } +} - while tokens - .peek() - .is_some_and(|token| token.token_type != TokenType::Rbrace) - { - let field_name = if let Some(Token { +pub(crate) fn identifier<'a, T>( + tokens: &mut Peekable, + anchor: Option<&Token>, + context: &str, +) -> Result +where + T: Iterator + Clone, +{ + match tokens.peek().copied() { + Some(Token { token_type: TokenType::Identifier(name), .. - }) = tokens.next() - { - name.clone() - } else { - println!("Error: Expected field name in struct literal."); - return None; - }; + }) => { + let name = name.clone(); + tokens.next(); + Ok(name) + } + found => Err(ParseError::expected_at( + found, + anchor, + "identifier", + context, + )), + } +} +pub(super) fn argument_list<'a, T>( + tokens: &mut Peekable, + close: TokenType, + spelling: &str, + context: &str, +) -> Result, ParseError> +where + T: Iterator + Clone, +{ + let opener = tokens.next(); + let mut args = Vec::new(); + if tokens + .peek() + .is_none_or(|token| token.token_type == TokenType::Eof) + { + return Err(ParseError::expected_at( + tokens.peek().copied(), + opener, + spelling, + context, + )); + } + if tokens.peek().is_some_and(|token| token.token_type == close) { + tokens.next(); + return Ok(args); + } + loop { + args.push(parse_expression(tokens)?); if tokens .peek() - .is_none_or(|token| token.token_type != TokenType::Colon) + .is_some_and(|token| token.token_type == TokenType::Comma) { - println!("Error: Expected ':' after field name '{}'", field_name); - return None; - } - tokens.next(); - - let value = parse_expression(tokens)?; - fields.push((field_name, value)); - - if matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::Comma) - ) { tokens.next(); } else { - break; + expect_token(tokens, opener, close, spelling, context)?; + return Ok(args); } } +} - if tokens +fn parse_struct_literal_fields<'a, T>( + tokens: &mut Peekable, +) -> Result, ParseError> +where + T: Iterator + Clone, +{ + let opener = tokens.next(); + let mut fields = Vec::new(); + while !tokens .peek() - .is_none_or(|token| token.token_type != TokenType::Rbrace) + .is_some_and(|token| token.token_type == TokenType::Rbrace) { - println!("Error: Expected '}}' to close struct literal"); - return None; + if tokens + .peek() + .is_none_or(|token| token.token_type == TokenType::Eof) + { + return Err(ParseError::expected_at( + tokens.peek().copied(), + opener, + "'}'", + "struct literal", + )); + } + let name = identifier(tokens, opener, "struct literal field")?; + expect_token( + tokens, + opener, + TokenType::Colon, + "':'", + "struct literal field", + )?; + fields.push((name, parse_expression(tokens)?)); + match tokens.peek().map(|token| &token.token_type) { + Some(TokenType::Comma) => { + tokens.next(); + } + Some(TokenType::Rbrace) => break, + _ => { + return Err(ParseError::expected_at( + tokens.peek().copied(), + opener, + "',' or '}'", + "struct literal", + )) + } + } } - tokens.next(); - - Some(fields) + expect_token(tokens, opener, TokenType::Rbrace, "'}'", "struct literal")?; + Ok(fields) } -pub fn parse_primary_expression<'a, T>(tokens: &mut Peekable) -> Option +pub fn parse_primary_expression<'a, T>(tokens: &mut Peekable) -> Result where T: Iterator + Clone, { let before = tokens.clone(); - let result = (|| { - let token = (*tokens.peek()?).clone(); - - let expr = match &token.token_type { - TokenType::IntLiteral(s) => { - tokens.next(); - Some(Expression::Literal(Literal::Int(s.clone()))) - } - TokenType::Float(value) => { - tokens.next(); - Some(Expression::Literal(Literal::Float(*value))) - } - TokenType::CharLiteral(c) => { - tokens.next(); - Some(Expression::Literal(Literal::Char(*c))) - } - TokenType::BoolLiteral(b) => { - tokens.next(); - Some(Expression::Literal(Literal::Bool(*b))) - } - TokenType::Null => { - tokens.next(); - Some(Expression::Null) + let token = tokens + .peek() + .copied() + .ok_or_else(|| ParseError::expected_at(None, None, "expression", "primary expression"))? + .clone(); + let expr = match &token.token_type { + TokenType::IntLiteral(s) => { + tokens.next(); + Ok(Expression::Literal(Literal::Int(s.clone()))) + } + TokenType::Float(value) => { + tokens.next(); + Ok(Expression::Literal(Literal::Float(*value))) + } + TokenType::CharLiteral(c) => { + tokens.next(); + Ok(Expression::Literal(Literal::Char(*c))) + } + TokenType::BoolLiteral(b) => { + tokens.next(); + Ok(Expression::Literal(Literal::Bool(*b))) + } + TokenType::String(s) => { + tokens.next(); + Ok(Expression::Literal(Literal::String(s.clone()))) + } + TokenType::Null => { + tokens.next(); + Ok(Expression::Null) + } + TokenType::Identifier(name) => { + let mut name = name.clone(); + tokens.next(); + while tokens + .peek() + .is_some_and(|token| token.token_type == TokenType::DoubleColon) + { + let separator = tokens.next(); + let segment = identifier(tokens, separator, "qualified name")?; + name.push_str("::"); + name.push_str(&segment); } - TokenType::Identifier(name) => { - let mut name = name.clone(); - tokens.next(); - - while matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::DoubleColon) - ) { - tokens.next(); - let segment = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(segment), - .. - }) => segment, - _ => { - println!("Error: Expected identifier after '::'"); - return None; - } - }; - name.push_str("::"); - name.push_str(segment); - } - - let expr = if let Some(peeked_token) = tokens.peek() { - match &peeked_token.token_type { - TokenType::Lchevr if peek_is_generic_call(tokens) => { - tokens.next(); // consume '<' - let inner = collect_generic_inner(tokens)?; - let arg_strs = split_top_level_generic_args(&inner)?; - - let mut type_args = Vec::with_capacity(arg_strs.len()); - for arg in arg_strs { - let tt = parse_type(&arg)?; - let wt = token_type_to_wave_type(&tt)?; - type_args.push(wt); - } - - skip_ws(tokens); - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Lparen) - { - println!( - "Error: Expected '(' after generic function type arguments" - ); - return None; - } - tokens.next(); // consume '(' - - let mut args = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rparen) - { - loop { - let arg = parse_expression(tokens)?; - args.push(arg); - - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; - } - } - } - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' after function call arguments"); - return None; - } - tokens.next(); - - Expression::FunctionCall { - name, - type_args, - args, - } - } - TokenType::Lchevr if peek_is_generic_struct_literal(tokens) => { - tokens.next(); // consume '<' - let inner = collect_generic_inner(tokens)?; - let arg_strs = split_top_level_generic_args(&inner)?; - - // Validate the application here so malformed type arguments - // fail as syntax instead of becoming an opaque struct name. - for arg in &arg_strs { - let token_type = parse_type(arg)?; - token_type_to_wave_type(&token_type)?; - } - - skip_ws(tokens); - if tokens - .peek() - .is_none_or(|token| token.token_type != TokenType::Lbrace) - { - println!( - "Error: Expected '{{' after generic struct type arguments" - ); - return None; - } - tokens.next(); // consume '{' - - name.push('<'); - name.push_str(&arg_strs.join(",")); - name.push('>'); - - Expression::StructLiteral { - name, - fields: parse_struct_literal_fields(tokens)?, - } - } - TokenType::Lparen => { - tokens.next(); - - let mut args = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rparen) - { - loop { - let arg = parse_expression(tokens)?; - args.push(arg); - - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; - } - } - } - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' after function call arguments"); - return None; - } - tokens.next(); - - Expression::FunctionCall { - name, - type_args: Vec::new(), - args, - } - } - TokenType::Lbrace => { - tokens.next(); - Expression::StructLiteral { - name, - fields: parse_struct_literal_fields(tokens)?, - } - } - _ => Expression::Variable(name), - } - } else { - Expression::Variable(name) + let generic_call = peek_is_generic_call(tokens); + let generic_struct = !generic_call && peek_is_generic_struct_literal(tokens); + let mut type_args = Vec::new(); + if generic_call || generic_struct { + let opener = tokens.next(); + let invalid = |found| { + ParseError::expected_at(found, opener, "type argument", "generic expression") }; - - Some(expr) - } - TokenType::Lparen => { - tokens.next(); - let inner_expr = parse_expression(tokens)?; - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' to close grouped expression"); - return None; - } - tokens.next(); - Some(Expression::Grouped(Box::new(inner_expr))) - } - TokenType::String(value) => { - tokens.next(); - Some(Expression::Literal(Literal::String(value.clone()))) - } - TokenType::Lbrack => { - tokens.next(); - let mut elements = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rbrack) - { - loop { - elements.push(parse_expression(tokens)?); - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; - } - } + let inner = + collect_generic_inner(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; + let arg_strs = split_top_level_generic_args(&inner) + .ok_or_else(|| invalid(tokens.peek().copied()))?; + for arg in &arg_strs { + let ty = parse_type(arg) + .and_then(|ty| token_type_to_wave_type(&ty)) + .ok_or_else(|| invalid(tokens.peek().copied()))?; + type_args.push(ty); } - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rbrack) - { - println!("Error: Expected ']' to close array literal"); - return None; + if generic_struct { + name.push('<'); + name.push_str(&arg_strs.join(",")); + name.push('>'); } - tokens.next(); - Some(Expression::ArrayLiteral(elements)) + skip_ws(tokens); } - TokenType::Asm => { - tokens.next(); - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Expected '{{' after 'asm'"); - return None; - } - tokens.next(); - - let mut instructions: Vec = vec![]; - let mut inputs: Vec<(String, Expression)> = vec![]; - let mut outputs: Vec<(String, Expression)> = vec![]; - let mut clobbers: Vec = vec![]; - - let mut closed = false; - while let Some(token) = tokens.peek() { - match &token.token_type { - TokenType::Rbrace => { - tokens.next(); - closed = true; - break; - } - - TokenType::In => { - tokens.next(); - parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; - } - - TokenType::Out => { - tokens.next(); - parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; - } - - TokenType::Clobber => { - tokens.next(); - parse_asm_clobber_clause(tokens, &mut clobbers)?; - } - - TokenType::Identifier(s) if s == "in" => { - tokens.next(); - parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; - } - - TokenType::Identifier(s) if s == "out" => { - tokens.next(); - parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; - } - - TokenType::Identifier(s) if s == "clobber" => { - tokens.next(); - parse_asm_clobber_clause(tokens, &mut clobbers)?; - } - - TokenType::String(s) => { - instructions.push(s.clone()); - tokens.next(); - } - - TokenType::SemiColon | TokenType::Comma => { - tokens.next(); - } - other => { - println!("Unexpected token in asm expression: {:?}", other); - return None; - } - } - } - - if !closed { - return None; - } - Some(Expression::AsmBlock { - instructions, - inputs, - outputs, - clobbers, - }) + match tokens.peek().map(|token| &token.token_type) { + Some(TokenType::Lparen) => Ok(Expression::FunctionCall { + name, + type_args, + args: argument_list(tokens, TokenType::Rparen, "')'", "function call")?, + }), + Some(TokenType::Lbrace) => Ok(Expression::StructLiteral { + name, + fields: parse_struct_literal_fields(tokens)?, + }), + _ => Ok(Expression::Variable(name)), } - _ => match token.token_type { - TokenType::Continue - | TokenType::Break - | TokenType::Return - | TokenType::SemiColon => None, - _ => { - println!( - "Error: Expected primary expression, found {:?}", - token.token_type - ); - println!( - "Error: Expected primary expression, found {:?}", - token.lexeme - ); - println!("Error: Expected primary expression, found {:?}", token.line); - None - } - }, - }; - - let base = expr?.with_span(lexer::consumed_span(before.clone(), tokens)); - - parse_postfix_expression(tokens, base) - })(); - result.map(|value: Expression| value.with_span(lexer::consumed_span(before, tokens))) + } + TokenType::Lparen => { + let opener = tokens.next(); + let inner = parse_expression(tokens)?; + expect_token( + tokens, + opener, + TokenType::Rparen, + "')'", + "grouped expression", + )?; + Ok(Expression::Grouped(Box::new(inner))) + } + TokenType::Lbrack => Ok(Expression::ArrayLiteral(argument_list( + tokens, + TokenType::Rbrack, + "']'", + "array literal", + )?)), + TokenType::Asm => { + tokens.next(); + let (instructions, inputs, outputs, clobbers) = parse_asm_body(tokens)?; + Ok(Expression::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + }) + } + _ => Err(ParseError::expected_at( + Some(&token), + Some(&token), + "expression", + "primary expression", + )), + }?; + let base = expr.with_span(lexer::consumed_span(before.clone(), tokens)); + parse_postfix_expression(tokens, base).map(|value| { + let mut span = lexer::consumed_span(before, tokens); + if let Some(span) = &mut span { + span.focus = value.span().and_then(|span| span.focus.clone()); + } + value.with_span(span) + }) } diff --git a/front/parser/src/expr/unary.rs b/front/parser/src/expr/unary.rs index a1bbdc27..4ece5fa4 100644 --- a/front/parser/src/expr/unary.rs +++ b/front/parser/src/expr/unary.rs @@ -18,10 +18,13 @@ use crate::ast::{Expression, IncDecKind, Literal, Operator}; use crate::expr::is_assignable; use crate::expr::primary::parse_primary_expression; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; -pub fn parse_unary_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Option +pub fn parse_unary_expression<'a, T>( + tokens: &mut std::iter::Peekable, +) -> Result where T: Iterator + Clone, { @@ -29,10 +32,15 @@ where let result = (|| { if let Some(token) = tokens.peek() { match token.token_type { + TokenType::Await => { + tokens.next(); + let inner = parse_unary_expression(tokens)?; + return Ok(Expression::Await(Box::new(inner))); + } TokenType::Not => { tokens.next(); let inner = parse_unary_expression(tokens)?; - return Some(Expression::Unary { + return Ok(Expression::Unary { operator: Operator::Not, expr: Box::new(inner), }); @@ -40,7 +48,7 @@ where TokenType::BitwiseNot => { tokens.next(); let inner = parse_unary_expression(tokens)?; - return Some(Expression::Unary { + return Ok(Expression::Unary { operator: Operator::BitwiseNot, expr: Box::new(inner), }); @@ -48,55 +56,63 @@ where TokenType::AddressOf => { tokens.next(); let inner = parse_unary_expression(tokens)?; - return Some(Expression::AddressOf(Box::new(inner))); + return Ok(Expression::AddressOf(Box::new(inner))); } TokenType::Deref => { tokens.next(); let inner = parse_unary_expression(tokens)?; - return Some(Expression::Deref(Box::new(inner))); + return Ok(Expression::Deref(Box::new(inner))); } TokenType::Increment => { - let tok = tokens.next()?; // '++' + let tok = tokens.next().expect("operator was just peeked"); // '++' let inner = parse_unary_expression(tokens)?; if !is_assignable(&inner) { - println!("Error: ++ target must be assignable (line {})", tok.line); - return None; + return Err(ParseError::expected_at( + Some(tok), + Some(tok), + "assignable expression", + "prefix mutation", + )); } - return Some(Expression::IncDec { + return Ok(Expression::IncDec { kind: IncDecKind::PreInc, target: Box::new(inner), }); } TokenType::Decrement => { - let tok = tokens.next()?; // '--' + let tok = tokens.next().expect("operator was just peeked"); // '--' let inner = parse_unary_expression(tokens)?; if !is_assignable(&inner) { - println!("Error: -- target must be assignable (line {})", tok.line); - return None; + return Err(ParseError::expected_at( + Some(tok), + Some(tok), + "assignable expression", + "prefix mutation", + )); } - return Some(Expression::IncDec { + return Ok(Expression::IncDec { kind: IncDecKind::PreDec, target: Box::new(inner), }); } TokenType::Minus => { - let _tok = tokens.next()?; // '-' + let _tok = tokens.next().expect("operator was just peeked"); // '-' let inner = parse_unary_expression(tokens)?; match inner.into_unspanned() { Expression::Literal(Literal::Int(s)) => { - return Some(Expression::Literal(Literal::Int( + return Ok(Expression::Literal(Literal::Int( s.strip_prefix('-') .map(str::to_string) .unwrap_or_else(|| format!("-{s}")), ))); } Expression::Literal(Literal::Float(f)) => { - return Some(Expression::Literal(Literal::Float(-f))); + return Ok(Expression::Literal(Literal::Float(-f))); } other => { - return Some(Expression::Unary { + return Ok(Expression::Unary { operator: Operator::Neg, expr: Box::new(other), }) @@ -107,7 +123,7 @@ where TokenType::Plus => { tokens.next(); // consume '+' let inner = parse_unary_expression(tokens)?; - return Some(inner); + return Ok(inner); } _ => {} } diff --git a/front/parser/src/generics.rs b/front/parser/src/generics.rs index 5cd41195..48e8442e 100644 --- a/front/parser/src/generics.rs +++ b/front/parser/src/generics.rs @@ -24,7 +24,14 @@ use crate::ast::{ VariantNode, WaveType, }; use crate::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; + +struct PendingFunction { + name: String, + function: FunctionNode, + substitution: HashMap, + depth: usize, +} #[derive(Default)] struct GenericEnv { @@ -42,6 +49,8 @@ struct GenericEnv { variant_instances: BTreeMap, function_in_progress: HashSet, + pending_functions: VecDeque, + function_depth: usize, struct_in_progress: HashSet, variant_in_progress: HashSet, } @@ -53,6 +62,7 @@ struct GenericEnv { /// but later phases do not accept unresolved generic parameters in emitted /// function definitions or backend-lowered aggregate definitions. pub fn monomorphize_generics(ast: Vec) -> Result, String> { + let ast = crate::methods::lower_generic_methods(ast)?; let mut env = GenericEnv::default(); // Pass one records every callable signature and generic template before any @@ -215,6 +225,15 @@ pub fn monomorphize_generics(ast: Vec) -> Result, String> } } + // Materialize function bodies iteratively. Deep, finite specialization + // chains do not consume one compiler stack frame per function instance. + while let Some(pending) = env.pending_functions.pop_front() { + env.function_depth = pending.depth; + let function = rewrite_function(pending.function, &pending.substitution, &mut env)?; + env.function_in_progress.remove(&pending.name); + env.function_instances.insert(pending.name, function); + } + for (_, variant) in env.variant_instances { let span = env.origin_spans.get(&variant.name).cloned(); out.push(ASTNode::Variant(variant).with_span(span)); @@ -298,15 +317,7 @@ fn rewrite_struct( s.methods = s .methods .into_iter() - .map(|m| { - if !m.generic_params.is_empty() { - return Err(format!( - "generic methods are not supported yet: '{}::{}'", - s.name, m.name - )); - } - rewrite_function(m, subst, env) - }) + .map(|m| rewrite_function(m, subst, env)) .collect::, _>>()?; Ok(s) @@ -335,15 +346,7 @@ fn rewrite_proto( p.methods = p .methods .into_iter() - .map(|m| { - if !m.generic_params.is_empty() { - return Err(format!( - "generic methods are not supported yet: 'proto {}::{}'", - p.target, m.name - )); - } - rewrite_function(m, subst, env) - }) + .map(|m| rewrite_function(m, subst, env)) .collect::, _>>()?; Ok(p) } @@ -592,6 +595,13 @@ fn rewrite_expression( .map(|t| rewrite_wave_type(t, subst, env)) .collect::, _>>()?; + if crate::async_intrinsics::is_intrinsic(&name) { + return Ok(Expression::FunctionCall { + name, + type_args: concrete_args, + args, + }); + } if !env.function_templates.contains_key(&name) { return Err(format!( "type arguments provided for non-generic function '{}'", @@ -606,9 +616,18 @@ fn rewrite_expression( args, }) } - Expression::MethodCall { object, name, args } => Ok(Expression::MethodCall { + Expression::MethodCall { + object, + name, + args, + type_args, + } => Ok(Expression::MethodCall { object: Box::new(rewrite_expression(*object, subst, env)?), name, + type_args: type_args + .iter() + .map(|t| rewrite_wave_type(t, subst, env)) + .collect::>()?, args: rewrite_expr_list(args, subst, env)?, }), Expression::StructLiteral { name, fields } => { @@ -644,6 +663,9 @@ fn rewrite_expression( Expression::ArrayLiteral(items) => Ok(Expression::ArrayLiteral(rewrite_expr_list( items, subst, env, )?)), + Expression::Await(inner) => Ok(Expression::Await(Box::new(rewrite_expression( + *inner, subst, env, + )?))), Expression::Grouped(inner) => Ok(Expression::Grouped(Box::new(rewrite_expression( *inner, subst, env, )?))), @@ -736,6 +758,9 @@ fn rewrite_wave_type( env: &mut GenericEnv, ) -> Result { match ty { + WaveType::Future(inner) => Ok(WaveType::Future(Box::new(rewrite_wave_type( + inner, subst, env, + )?))), WaveType::Pointer(inner) => Ok(WaveType::Pointer(Box::new(rewrite_wave_type( inner, subst, env, )?))), @@ -918,6 +943,11 @@ fn ensure_struct_instance( map.insert(k.clone(), v.clone()); } + if env.struct_in_progress.len() + env.variant_in_progress.len() >= 16 { + return Err( + "generic instantiation depth exceeded (possible expanding recursion)".to_string(), + ); + } env.struct_in_progress.insert(inst_name.clone()); let mut instantiated = template; @@ -974,18 +1004,21 @@ fn ensure_function_instance( map.insert(k.clone(), v.clone()); } + if env.function_depth >= 128 { + return Err( + "generic instantiation depth exceeded (possible expanding recursion)".to_string(), + ); + } env.function_in_progress.insert(inst_name.clone()); - let mut instantiated = template; instantiated.name = inst_name.clone(); instantiated.generic_params.clear(); - instantiated = rewrite_function(instantiated, &map, env)?; - - env.function_in_progress.remove(&inst_name); - env.function_instances - .insert(inst_name.clone(), instantiated) - .map(|_| ()) - .unwrap_or(()); + env.pending_functions.push_back(PendingFunction { + name: inst_name.clone(), + function: instantiated, + substitution: map, + depth: env.function_depth + 1, + }); Ok(inst_name) } @@ -1049,6 +1082,7 @@ fn mangle_type(ty: &WaveType) -> String { WaveType::String => "str".to_string(), WaveType::Void => "void".to_string(), WaveType::Never => "!".to_string(), + WaveType::Future(inner) => format!("future_{}", mangle_type(inner)), WaveType::Pointer(inner) => format!("p_{}", mangle_type(inner)), WaveType::Array(inner, n) => format!("a{}_{}", n, mangle_type(inner)), WaveType::Struct(name) => sanitize_ident(name), @@ -1067,6 +1101,7 @@ fn display_type_for_application(ty: &WaveType) -> String { WaveType::Char => "char".to_string(), WaveType::Byte => "byte".to_string(), WaveType::String => "str".to_string(), + WaveType::Future(inner) => format!("Future<{}>", display_type_for_application(inner)), WaveType::Pointer(inner) => format!("ptr<{}>", display_type_for_application(inner)), WaveType::Array(inner, size) => { format!("array<{},{}>", display_type_for_application(inner), size) diff --git a/front/parser/src/hir.rs b/front/parser/src/hir.rs index fb270703..1e718c3a 100644 --- a/front/parser/src/hir.rs +++ b/front/parser/src/hir.rs @@ -209,6 +209,50 @@ impl TypedProgram { }) } + /// Whether this program needs the task executor. + pub fn uses_async_runtime(&self) -> bool { + let mut found = false; + walk_nodes(self.syntax(), &mut |e| { + if matches!(e,Expression::FunctionCall{name,..} if crate::async_intrinsics::is_intrinsic(name)) + { + found = true; + } + }); + found + } + + /// Runtime symbols referenced by intrinsic calls, with source locations for diagnostics. + pub fn async_runtime_requirements(&self) -> Vec<(&'static str, Option)> { + let mut requirements = std::collections::BTreeMap::new(); + walk_nodes(self.syntax(), &mut |expression| { + if let Expression::FunctionCall { name, .. } = expression { + for &symbol in crate::async_intrinsics::runtime_symbols(name) { + requirements.entry(symbol).or_insert_with(|| { + self.expression_id(expression) + .and_then(|id| self.expression_span(id)) + .cloned() + }); + } + } + }); + requirements.into_iter().collect() + } + + /// Stable await identities and completion types, independent of a backend. + pub fn await_sites(&self) -> Vec<(ExpressionId, WaveType)> { + let mut sites = Vec::new(); + walk_nodes(self.syntax(), &mut |expression| { + if matches!(expression, Expression::Await(_)) { + if let (Some(id), Some(HirExpressionType::Resolved(ty))) = + (self.expression_id(expression), self.type_of(expression)) + { + sites.push((id, ty.clone())); + } + } + }); + sites + } + pub fn node_id(&self, node: &ASTNode) -> Option { self.node_ids.get(&(node as *const _ as usize)).copied() } @@ -342,6 +386,9 @@ fn canonical_type( match ty { WaveType::Isz => named.get("isz").cloned().unwrap_or(WaveType::Isz), WaveType::Usz => named.get("usz").cloned().unwrap_or(WaveType::Usz), + WaveType::Future(inner) => { + WaveType::Future(Box::new(canonical_type(inner, named, visiting))) + } WaveType::Pointer(inner) => { WaveType::Pointer(Box::new(canonical_type(inner, named, visiting))) } @@ -414,6 +461,7 @@ fn display_wave_type(ty: &WaveType) -> String { WaveType::Char => "char".to_string(), WaveType::Byte => "byte".to_string(), WaveType::String => "str".to_string(), + WaveType::Future(inner) => format!("Future<{}>", display_wave_type(inner)), WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), WaveType::Array(inner, length) => { format!("array<{},{}>", display_wave_type(inner), length) @@ -592,7 +640,15 @@ fn canonicalize_expression_types(expression: &mut Expression, named: &HashMap { + Expression::MethodCall { + object, + args, + type_args, + .. + } => { + for type_argument in type_args { + canonicalize_type(type_argument, named); + } canonicalize_expression_types(object, named); for argument in args { canonicalize_expression_types(argument, named); @@ -600,6 +656,7 @@ fn canonicalize_expression_types(expression: &mut Expression, named: &HashMap { for method in &structure.methods { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, visit); + } + } walk_nodes(&method.body, visit); } } ASTNode::ProtoImpl(implementation) => { for method in &implementation.methods { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, visit); + } + } walk_nodes(&method.body, visit); } } @@ -755,6 +822,10 @@ fn walk_statement(statement: &StatementNode, visit: &mut impl FnMut(&Expression) } fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) { + if let Expression::Located { value, .. } = expression { + walk_expression(value, visit); + return; + } visit(expression); match expression { Expression::Located { value, .. } => walk_expression(value, visit), @@ -776,6 +847,7 @@ fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) } Expression::Deref(inner) | Expression::AddressOf(inner) + | Expression::Await(inner) | Expression::Grouped(inner) | Expression::Unary { expr: inner, .. } | Expression::Cast { expr: inner, .. } diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index 35150eb2..4b8c6193 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.rs @@ -870,7 +870,9 @@ fn parse_wave_file( .with_code(code) .with_source_code(content.clone()); - we = we.with_span(e.span()); + we = we + .with_span(e.span()) + .with_related(e.related().iter().cloned()); if let Some(ctx) = e.context() { we = we.with_context(ctx.to_string()); } diff --git a/front/parser/src/lib.rs b/front/parser/src/lib.rs index 26eed97a..0addbf5b 100644 --- a/front/parser/src/lib.rs +++ b/front/parser/src/lib.rs @@ -42,6 +42,8 @@ macro_rules! println { pub mod arch; pub mod ast; +pub mod async_intrinsics; +pub mod async_lower; pub mod expr; pub mod format; pub mod generics; @@ -55,3 +57,5 @@ pub mod verification; pub use parser::*; pub mod source; + +mod methods; diff --git a/front/parser/src/methods.rs b/front/parser/src/methods.rs new file mode 100644 index 00000000..71a9a883 --- /dev/null +++ b/front/parser/src/methods.rs @@ -0,0 +1,289 @@ +//! Generic methods reuse function specialization after semantic receiver resolution. +//! +//! Explicit arguments bind method parameters in declaration order. Otherwise, +//! receiver and argument types must infer every parameter consistently; plain +//! integer/float literals use the frontend's i32/f32 defaults. Enclosing type +//! parameters are substituted independently and cannot be shadowed by a method. +//! Bodies are then specialized by the shared work queue, preserving source spans. +use crate::ast::{ASTNode, Expression, StatementNode, WaveType}; + +#[derive(Clone, Debug)] +pub(crate) struct GenericMethodCall { + pub function: String, + pub type_args: Vec, +} + +pub(crate) fn method_symbol(owner: &str, name: &str) -> String { + // '$' is unavailable in source identifiers, preventing user symbol collisions. + format!("$method${owner}${name}") +} + +pub(crate) fn lower_generic_methods(mut ast: Vec) -> Result, String> { + let has_templates = ast.iter().any(|node| match node.unspanned() { + ASTNode::Function(f) => !f.generic_params.is_empty(), + ASTNode::Struct(s) => s.methods.iter().any(|m| !m.generic_params.is_empty()), + ASTNode::ProtoImpl(p) => p.methods.iter().any(|m| !m.generic_params.is_empty()), + _ => false, + }); + // Ordinary programs keep the existing monomorphization path. + if !has_templates { + return Ok(ast); + } + let mut snapshot = ast.clone(); + let sources = crate::source::SourceMap::detach(&mut snapshot); + let mut calls = crate::verification::analyze_generic_method_calls(&snapshot, &sources) + .map_err(|e| e.to_string())?; + let mut ordered = Vec::new(); + crate::hir::walk_nodes(&snapshot, &mut |expression| { + ordered.push(calls.remove(&(expression as *const _ as usize))) + }); + let mut ordered = ordered.into_iter(); + walk_nodes(&mut ast, &mut |expression| { + let Some(call) = ordered + .next() + .expect("source and semantic traversal must agree") + else { + return; + }; + let arguments = match std::mem::replace(expression, Expression::Null) { + Expression::MethodCall { object, args, .. } => { + let mut arguments = vec![*object]; + arguments.extend(args); + arguments + } + Expression::FunctionCall { args, .. } => args, + _ => unreachable!("semantic generic call resolution"), + }; + *expression = Expression::FunctionCall { + name: call.function, + type_args: call.type_args, + args: arguments, + }; + }); + assert!( + ordered.next().is_none(), + "source and semantic traversal must agree" + ); + let mut lifted = Vec::new(); + for node in &mut ast { + lift(node, &mut lifted); + } + ast.extend(lifted); + Ok(ast) +} + +fn lift(node: &mut ASTNode, lifted: &mut Vec) { + let span = node.span().cloned(); + let (owner, parameters, methods) = match node { + ASTNode::Located { value, .. } => { + let start = lifted.len(); + lift(value, lifted); + for node in &mut lifted[start..] { + *node = node.clone().with_span(span.clone()); + } + return; + } + ASTNode::Struct(s) => (&s.name, s.generic_params.clone(), &mut s.methods), + ASTNode::ProtoImpl(p) => (&p.target, Vec::new(), &mut p.methods), + _ => return, + }; + let mut retained = Vec::new(); + for mut method in std::mem::take(methods) { + if method.generic_params.is_empty() { + retained.push(method); + continue; + } + method.name = method_symbol(owner, &method.name); + let mut all = parameters.clone(); + all.extend(method.generic_params); + method.generic_params = all; + lifted.push(ASTNode::Function(method).with_span(span.clone())); + } + *methods = retained; +} + +pub(crate) fn walk_nodes(nodes: &mut [ASTNode], visit: &mut impl FnMut(&mut Expression)) { + for node in nodes { + walk_node(node, visit); + } +} + +fn walk_node(node: &mut ASTNode, visit: &mut impl FnMut(&mut Expression)) { + match node { + ASTNode::Located { value, .. } => walk_node(value, visit), + ASTNode::Function(function) => { + for parameter in &mut function.parameters { + if let Some(default) = &mut parameter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&mut function.body, visit); + } + ASTNode::Struct(structure) => { + for method in &mut structure.methods { + for parameter in &mut method.parameters { + if let Some(default) = &mut parameter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&mut method.body, visit); + } + } + ASTNode::ProtoImpl(implementation) => { + for method in &mut implementation.methods { + for parameter in &mut method.parameters { + if let Some(default) = &mut parameter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&mut method.body, visit); + } + } + ASTNode::Statement(statement) => walk_statement(statement, visit), + ASTNode::Variable(variable) => { + if let Some(initializer) = &mut variable.initial_value { + walk_expression(initializer, visit); + } + } + ASTNode::Expression(expression) => walk_expression(expression, visit), + ASTNode::ExternFunction(_) + | ASTNode::Program(_) + | ASTNode::TypeAlias(_) + | ASTNode::Enum(_) + | ASTNode::Variant(_) => {} + } +} + +fn walk_statement(statement: &mut StatementNode, visit: &mut impl FnMut(&mut Expression)) { + match statement { + StatementNode::PrintFormat { args, .. } + | StatementNode::PrintlnFormat { args, .. } + | StatementNode::Input { args, .. } => { + for argument in args { + walk_expression(argument, visit); + } + } + StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + } => { + walk_expression(condition, visit); + walk_nodes(body, visit); + if let Some(blocks) = else_if_blocks { + for (condition, body) in blocks.iter_mut() { + walk_expression(condition, visit); + walk_nodes(body, visit); + } + } + if let Some(body) = else_block { + walk_nodes(body, visit); + } + } + StatementNode::For { + initialization, + condition, + increment, + body, + } => { + walk_node(initialization, visit); + walk_expression(condition, visit); + walk_expression(increment, visit); + walk_nodes(body, visit); + } + StatementNode::While { condition, body } => { + walk_expression(condition, visit); + walk_nodes(body, visit); + } + StatementNode::Match { value, arms } => { + walk_expression(value, visit); + for arm in arms { + walk_nodes(&mut arm.body, visit); + } + } + StatementNode::Assign { value, .. } => walk_expression(value, visit), + StatementNode::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { + walk_expression(expression, visit); + } + } + StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { + walk_expression(expression, visit) + } + StatementNode::Print(_) + | StatementNode::Println(_) + | StatementNode::Variable(_) + | StatementNode::Import(_) + | StatementNode::Break + | StatementNode::Continue + | StatementNode::Return(None) => {} + } +} + +fn walk_expression(expression: &mut Expression, visit: &mut impl FnMut(&mut Expression)) { + if let Expression::Located { value, .. } = expression { + walk_expression(value, visit); + return; + } + visit(expression); + match expression { + Expression::Located { value, .. } => walk_expression(value, visit), + Expression::StructLiteral { fields, .. } => { + for (_, value) in fields { + walk_expression(value, visit); + } + } + Expression::FunctionCall { args, .. } => { + for argument in args { + walk_expression(argument, visit); + } + } + Expression::MethodCall { object, args, .. } => { + walk_expression(object, visit); + for argument in args { + walk_expression(argument, visit); + } + } + Expression::Deref(inner) + | Expression::AddressOf(inner) + | Expression::Await(inner) + | Expression::Grouped(inner) + | Expression::Unary { expr: inner, .. } + | Expression::Cast { expr: inner, .. } + | Expression::FieldAccess { object: inner, .. } + | Expression::IncDec { target: inner, .. } => walk_expression(inner, visit), + Expression::BinaryExpression { left, right, .. } + | Expression::IndexAccess { + target: left, + index: right, + } + | Expression::AssignOperation { + target: left, + value: right, + .. + } + | Expression::Assignment { + target: left, + value: right, + } => { + walk_expression(left, visit); + walk_expression(right, visit); + } + Expression::ArrayLiteral(values) => { + for value in values { + walk_expression(value, visit); + } + } + Expression::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { + walk_expression(expression, visit); + } + } + Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {} + } +} diff --git a/front/parser/src/parser/asm.rs b/front/parser/src/parser/asm.rs index 773879b4..68029812 100644 --- a/front/parser/src/parser/asm.rs +++ b/front/parser/src/parser/asm.rs @@ -18,153 +18,139 @@ use crate::ast::{ASTNode, Expression, StatementNode}; use crate::expr::is_assignable; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; use std::slice::Iter; -pub fn parse_asm_block(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Expected '{{' after 'asm'"); - return None; - } - tokens.next(); // consume '{' - - let mut instructions = vec![]; - let mut inputs: Vec<(String, Expression)> = vec![]; - let mut outputs: Vec<(String, Expression)> = vec![]; - let mut clobbers: Vec = vec![]; +type AsmBody = ( + Vec, + Vec<(String, Expression)>, + Vec<(String, Expression)>, + Vec, +); - let mut closed = false; +pub fn parse_asm_block(tokens: &mut Peekable>) -> Result { + let (instructions, inputs, outputs, clobbers) = parse_asm_body(tokens)?; + Ok(ASTNode::Statement(StatementNode::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + })) +} - while let Some(tok) = tokens.peek() { - match &tok.token_type { - TokenType::Rbrace => { - tokens.next(); // consume '}' - closed = true; +pub(crate) fn parse_asm_body<'a, T>(tokens: &mut Peekable) -> Result +where + T: Iterator + Clone, +{ + let anchor = tokens.peek().copied(); + crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, "'{'", "asm block")?; + let (mut instructions, mut inputs, mut outputs, mut clobbers) = + (vec![], vec![], vec![], vec![]); + loop { + match tokens.peek().map(|t| &t.token_type) { + Some(TokenType::Rbrace) => { + tokens.next(); break; } - - TokenType::SemiColon | TokenType::Comma => { + Some(TokenType::SemiColon | TokenType::Comma) => { tokens.next(); } - - TokenType::String(s) => { + Some(TokenType::String(s)) => { instructions.push(s.clone()); tokens.next(); } - - TokenType::In => { - tokens.next(); // consume 'in' - parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; - } - - TokenType::Out => { - tokens.next(); // consume 'out' - parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; - } - - TokenType::Clobber => { - tokens.next(); - parse_asm_clobber_clause(tokens, &mut clobbers)?; - } - - TokenType::Identifier(s) if s == "in" => { + Some(TokenType::In) => { tokens.next(); parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; } - TokenType::Identifier(s) if s == "out" => { + Some(TokenType::Out) => { tokens.next(); parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; } - TokenType::Identifier(s) if s == "clobber" => { + Some(TokenType::Clobber) => { tokens.next(); parse_asm_clobber_clause(tokens, &mut clobbers)?; } - - other => { - println!("Unexpected token in asm block: {:?}", other); - return None; + Some(TokenType::Identifier(s)) if s == "in" || s == "out" || s == "clobber" => { + let clause = s.clone(); + tokens.next(); + if clause == "clobber" { + parse_asm_clobber_clause(tokens, &mut clobbers)?; + } else { + parse_asm_inout_clause(tokens, clause == "in", &mut inputs, &mut outputs)?; + } + } + found => { + return Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + if found.is_none() || found == Some(&TokenType::Eof) { + "'}'" + } else { + "instruction string, in, out, clobber, or '}'" + }, + "asm block", + )) } } } + Ok((instructions, inputs, outputs, clobbers)) +} - if !closed { - println!("Expected '}}' to close asm block"); - return None; +fn register<'a, T>(tokens: &mut Peekable, context: &str) -> Result +where + T: Iterator + Clone, +{ + match tokens.peek().copied() { + Some(Token { + token_type: TokenType::String(s) | TokenType::Identifier(s), + .. + }) => { + let name = s.clone(); + tokens.next(); + Ok(name) + } + found => Err(ParseError::expected_at( + found, + found, + "register string or identifier", + context, + )), } - - Some(ASTNode::Statement(StatementNode::AsmBlock { - instructions, - inputs, - outputs, - clobbers, - })) } -/// clobber("rax, "rcx, "memory") pub fn parse_asm_clobber_clause<'a, T>( tokens: &mut Peekable, clobbers: &mut Vec, -) -> Option<()> +) -> Result<(), ParseError> where T: Iterator + Clone, { - // expect '(' - if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Lparen) { - println!("Expected '(' after 'clobber'"); - return None; - } - tokens.next(); // '(' - - // empty: clobber() - if tokens.peek().map(|t| &t.token_type) == Some(&TokenType::Rparen) { - tokens.next(); // ')' - return Some(()); + let anchor = tokens.peek().copied(); + let context = "asm clobber clause"; + crate::expr::expect_token(tokens, anchor, TokenType::Lparen, "'('", context)?; + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rparen) + { + tokens.next(); + return Ok(()); } - loop { - let item = match tokens.next() { - Some(Token { - token_type: TokenType::String(s), - .. - }) => s.clone(), - Some(Token { - token_type: TokenType::Identifier(s), - .. - }) => s.clone(), - Some(other) => { - println!( - "Expected clobber item (string/identifier), got {:?}", - other.token_type - ); - return None; - } - None => { - println!("Unexpected EOF while parsing clobber(...)"); - return None; - } - }; - - clobbers.push(item); - - match tokens.peek().map(|t| &t.token_type) { - Some(TokenType::Comma) => { - tokens.next(); // ',' - continue; - } - Some(TokenType::Rparen) => { - tokens.next(); // ')' - break; - } - other => { - println!("Expected ',' or ')' in clobber(...), got {:?}", other); - return None; - } + clobbers.push(register(tokens, context)?); + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Comma) + { + tokens.next(); + } else { + crate::expr::expect_token(tokens, anchor, TokenType::Rparen, "',' or ')'", context)?; + return Ok(()); } } - - Some(()) } pub fn parse_asm_inout_clause<'a, T>( @@ -172,60 +158,38 @@ pub fn parse_asm_inout_clause<'a, T>( is_input: bool, inputs: &mut Vec<(String, Expression)>, outputs: &mut Vec<(String, Expression)>, -) -> Option<()> +) -> Result<(), ParseError> where T: Iterator + Clone, { - if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Lparen) { - println!("Expected '(' after in/out"); - return None; - } - tokens.next(); // '(' - - let reg = match tokens.next() { - Some(Token { - token_type: TokenType::String(s), - .. - }) => s.clone(), - Some(Token { - token_type: TokenType::Identifier(s), - .. - }) => s.clone(), - Some(other) => { - println!( - "Expected register string or identifier, got {:?}", - other.token_type - ); - return None; - } - None => { - println!("Expected register in in/out(...)"); - return None; - } + let anchor = tokens.peek().copied(); + let context = if is_input { + "asm input clause" + } else { + "asm output clause" }; - - if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Rparen) { - println!("Expected ')' after in/out(...)"); - return None; - } - tokens.next(); // ')' - - let value_expr = parse_asm_operand(tokens)?; - + crate::expr::expect_token(tokens, anchor, TokenType::Lparen, "'('", context)?; + let reg = register(tokens, context)?; + crate::expr::expect_token(tokens, anchor, TokenType::Rparen, "')'", context)?; + let operand = tokens.peek().copied(); + let value = parse_asm_operand(tokens)?; if is_input { - inputs.push((reg, value_expr)); + inputs.push((reg, value)); } else { - if !is_assignable(&value_expr) { - println!("Error: out(...) target must be assignable"); - return None; + if !is_assignable(&value) { + return Err(ParseError::expected_at( + operand, + anchor, + "assignable expression", + context, + )); } - outputs.push((reg, value_expr)); + outputs.push((reg, value)); } - - Some(()) + Ok(()) } -pub(crate) fn parse_asm_operand<'a, T>(tokens: &mut Peekable) -> Option +pub(crate) fn parse_asm_operand<'a, T>(tokens: &mut Peekable) -> Result where T: Iterator + Clone, { diff --git a/front/parser/src/parser/control.rs b/front/parser/src/parser/control.rs index cda6992b..b4c2bf65 100644 --- a/front/parser/src/parser/control.rs +++ b/front/parser/src/parser/control.rs @@ -23,6 +23,7 @@ use crate::ast::{ use crate::expr::parse_expression; use crate::parser::stmt::parse_block; use crate::parser::types::parse_type_from_stream; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; @@ -168,78 +169,73 @@ fn parse_match_pattern( result.map(|value: MatchPattern| value.with_span(lexer::consumed_span(before, tokens))) } -pub fn parse_if(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { - println!("Error: Expected '(' after 'if'"); - return None; +fn expect_header_token( + tokens: &mut Peekable>, + anchor: Option<&Token>, + kind: TokenType, + spelling: &str, + context: &str, +) -> Result<(), ParseError> { + if tokens.peek().is_some_and(|token| token.token_type == kind) { + tokens.next(); + Ok(()) + } else { + Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + spelling, + context, + )) } - tokens.next(); // Consume '(' - - let condition = parse_expression(tokens)?; +} - if tokens.peek()?.token_type != TokenType::Rparen { - println!("Error: Expected ')' after 'if' condition"); - return None; - } - tokens.next(); // Consume ')' +fn header_expression( + tokens: &mut Peekable>, + context: &str, +) -> Result { + parse_expression(tokens).map_err(|error| { + if error.context() == Some("primary expression") { + error.with_context(context) + } else { + error + } + }) +} - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after 'if' condition"); - return None; - } - tokens.next(); // Consume '{' - let body = parse_block(tokens)?; +fn conditional_body( + tokens: &mut Peekable>, + context: &str, +) -> Result<(Expression, Vec), ParseError> { + let anchor = tokens.peek().copied(); + expect_header_token(tokens, anchor, TokenType::Lparen, "'('", context)?; + let condition = header_expression(tokens, context)?; + expect_header_token(tokens, anchor, TokenType::Rparen, "')'", context)?; + expect_header_token(tokens, anchor, TokenType::Lbrace, "'{'", context)?; + Ok((condition, parse_block(tokens)?)) +} - let mut else_if_blocks: Vec<(Expression, Vec)> = Vec::new(); // Changed to store conditions and bodies +pub fn parse_if(tokens: &mut Peekable>) -> Result { + let (condition, body) = conditional_body(tokens, "if header")?; + let mut else_if_blocks = Vec::new(); let mut else_block = None; - - while let Some(token) = tokens.peek() { - if token.token_type != TokenType::Else { - break; - } - tokens.next(); // consume 'else' - - if let Some(Token { - token_type: TokenType::If, - .. - }) = tokens.peek() + while tokens + .peek() + .is_some_and(|token| token.token_type == TokenType::Else) + { + let anchor = tokens.next(); + if tokens + .peek() + .is_some_and(|token| token.token_type == TokenType::If) { - tokens.next(); // consume 'if' - - if tokens.peek()?.token_type != TokenType::Lparen { - println!("Error: Expected '(' after 'else if'"); - return None; - } tokens.next(); - let else_if_condition = parse_expression(tokens)?; - - if tokens.peek()?.token_type != TokenType::Rparen { - println!("Error: Expected ')' after 'else if' condition"); - return None; - } - tokens.next(); - - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after 'else if'"); - return None; - } - tokens.next(); - let else_if_body = parse_block(tokens)?; - - // Store condition and body directly instead of nested If node - else_if_blocks.push((else_if_condition, else_if_body)); + else_if_blocks.push(conditional_body(tokens, "else if header")?); } else { - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after 'else'"); - return None; - } - tokens.next(); + expect_header_token(tokens, anchor, TokenType::Lbrace, "'{'", "else header")?; else_block = Some(Box::new(parse_block(tokens)?)); break; } } - - Some(ASTNode::Statement(StatementNode::If { + Ok(ASTNode::Statement(StatementNode::If { condition, body, else_if_blocks: if else_if_blocks.is_empty() { @@ -262,43 +258,38 @@ fn is_typed_for_initializer(tokens: &Peekable>) -> bool { fn parse_typed_for_initializer( tokens: &mut Peekable>, mutability: Mutability, -) -> Option { - let name = match tokens.next() { +) -> Result { + let anchor = tokens.peek().copied(); + let name = match tokens.peek().copied() { Some(Token { token_type: TokenType::Identifier(name), .. }) => name.clone(), - _ => { - println!("Error: Expected identifier in for-loop initializer"); - return None; + other => { + return Err(ParseError::expected_at( + other, + anchor, + "identifier", + "for initializer", + )) } }; - - if tokens.peek()?.token_type != TokenType::Colon { - println!( - "Error: Expected ':' after '{}' in for-loop initializer", - name - ); - return None; - } - tokens.next(); // consume ':' - - let type_name = match parse_type_from_stream(tokens) { - Some(ty) => ty, - None => { - println!("Error: Expected type in for-loop initializer"); - return None; - } - }; - - let initial_value = if tokens.peek()?.token_type == TokenType::Equal { - tokens.next(); // consume '=' - Some(parse_expression(tokens)?) + tokens.next(); + expect_header_token(tokens, anchor, TokenType::Colon, "':'", "for initializer")?; + let type_name = parse_type_from_stream(tokens).ok_or_else(|| { + ParseError::expected_at(tokens.peek().copied(), anchor, "type", "for initializer") + })?; + let initial_value = if tokens + .peek() + .is_some_and(|token| token.token_type == TokenType::Equal) + { + tokens.next(); + Some(header_expression(tokens, "for initializer")?) } else { None }; - Some(ASTNode::Variable(VariableNode { + Ok(ASTNode::Variable(VariableNode { name, type_name, initial_value, @@ -307,7 +298,8 @@ fn parse_typed_for_initializer( })) } -fn parse_for_initializer(tokens: &mut Peekable>) -> Option { +fn parse_for_initializer(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); let before = tokens.clone(); let result = (|| { match tokens.peek().map(|t| &t.token_type) { @@ -315,20 +307,18 @@ fn parse_for_initializer(tokens: &mut Peekable>) -> Option tokens.next(); // consume `var` parse_typed_for_initializer(tokens, Mutability::Var) } - Some(TokenType::Const) => { - println!("Error: `const` is not allowed in local for-loop initializer"); - None - } - Some(TokenType::Static) => { - println!("Error: `static` is not allowed in local for-loop initializer"); - None - } + Some(TokenType::Const | TokenType::Static) => Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + "var declaration or expression", + "for initializer", + )), _ if is_typed_for_initializer(tokens) => { parse_typed_for_initializer(tokens, Mutability::Var) } _ => { - let expr = parse_expression(tokens)?; - Some(ASTNode::Statement(StatementNode::Expression(expr))) + let expr = header_expression(tokens, "for initializer")?; + Ok(ASTNode::Statement(StatementNode::Expression(expr))) } } })(); @@ -338,47 +328,24 @@ fn parse_for_initializer(tokens: &mut Peekable>) -> Option }) } -// FOR parsing -pub fn parse_for(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { - println!("Error: Expected '(' after 'for'"); - return None; - } - tokens.next(); // Consume '(' - +pub fn parse_for(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + expect_header_token(tokens, anchor, TokenType::Lparen, "'('", "for header")?; let initialization = parse_for_initializer(tokens)?; - - if tokens.peek()?.token_type != TokenType::SemiColon { - println!("Error: Expected ';' after for-loop initializer"); - return None; - } - tokens.next(); // Consume ';' - - let condition = parse_expression(tokens)?; - - if tokens.peek()?.token_type != TokenType::SemiColon { - println!("Error: Expected ';' after for-loop condition"); - return None; - } - tokens.next(); // Consume ';' - - let increment = parse_expression(tokens)?; - - if tokens.peek()?.token_type != TokenType::Rparen { - println!("Error: Expected ')' after for-loop increment"); - return None; - } - tokens.next(); // Consume ')' - - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after 'for' header"); - return None; - } - tokens.next(); // Consume '{' - + expect_header_token( + tokens, + anchor, + TokenType::SemiColon, + "';'", + "for initializer", + )?; + let condition = header_expression(tokens, "for condition")?; + expect_header_token(tokens, anchor, TokenType::SemiColon, "';'", "for condition")?; + let increment = header_expression(tokens, "for increment")?; + expect_header_token(tokens, anchor, TokenType::Rparen, "')'", "for increment")?; + expect_header_token(tokens, anchor, TokenType::Lbrace, "'{'", "for header")?; let body = parse_block(tokens)?; - - Some(ASTNode::Statement(StatementNode::For { + Ok(ASTNode::Statement(StatementNode::For { initialization: Box::new(initialization), condition, increment, @@ -386,37 +353,18 @@ pub fn parse_for(tokens: &mut Peekable>) -> Option { })) } -// WHILE parsing -pub fn parse_while(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { - println!("Error: Expected '(' after 'while'"); - return None; - } - tokens.next(); // Consume '(' - - let condition = parse_expression(tokens)?; - - if tokens.peek()?.token_type != TokenType::Rparen { - println!("Error: Expected ')' after 'while' condition"); - return None; - } - tokens.next(); // Consume ')' - - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after 'while'"); - return None; - } - tokens.next(); // Consume '{' - - let body = parse_block(tokens)?; - - Some(ASTNode::Statement(StatementNode::While { condition, body })) +pub fn parse_while(tokens: &mut Peekable>) -> Result { + let (condition, body) = conditional_body(tokens, "while header")?; + Ok(ASTNode::Statement(StatementNode::While { condition, body })) } -pub fn parse_match(tokens: &mut Peekable>) -> Option { +pub fn parse_match(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = + |token| ParseError::expected_at(token, anchor, "match value and arms", "match statement"); skip_ws_and_newlines(tokens); - let parenthesized = tokens.peek()?.token_type == TokenType::Lparen; + let parenthesized = tokens.peek().ok_or_else(|| invalid(None))?.token_type == TokenType::Lparen; if parenthesized { tokens.next(); } @@ -441,23 +389,23 @@ pub fn parse_match(tokens: &mut Peekable>) -> Option { } if expression_iter.peek().is_some() { println!("Error: Unexpected token after match value"); - return None; + return Err(invalid(tokens.peek().copied())); } value }; if parenthesized { skip_ws_and_newlines(tokens); - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected ')' after match value"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); } skip_ws_and_newlines(tokens); - if tokens.peek()?.token_type != TokenType::Lbrace { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lbrace { println!("Error: Expected '{{' after match header"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume '{' @@ -476,23 +424,24 @@ pub fn parse_match(tokens: &mut Peekable>) -> Option { } let before = tokens.clone(); - let pattern = parse_match_pattern(tokens, false)?; + let pattern = + parse_match_pattern(tokens, false).ok_or_else(|| invalid(tokens.peek().copied()))?; if matches!(pattern.unspanned(), MatchPattern::Wildcard) { if saw_wildcard { println!("Error: Duplicate wildcard arm `_` in match"); - return None; + return Err(invalid(tokens.peek().copied())); } saw_wildcard = true; } if !expect_fat_arrow(tokens) { - return None; + return Err(invalid(tokens.peek().copied())); } skip_ws_and_newlines(tokens); - if tokens.peek()?.token_type != TokenType::Lbrace { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lbrace { println!("Error: Expected '{{' to start match arm body"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume '{' @@ -512,5 +461,5 @@ pub fn parse_match(tokens: &mut Peekable>) -> Option { } } - Some(ASTNode::Statement(StatementNode::Match { value, arms })) + Ok(ASTNode::Statement(StatementNode::Match { value, arms })) } diff --git a/front/parser/src/parser/decl.rs b/front/parser/src/parser/decl.rs index 18d2c25f..10c2fe7d 100644 --- a/front/parser/src/parser/decl.rs +++ b/front/parser/src/parser/decl.rs @@ -22,6 +22,7 @@ use crate::ast::{ }; use crate::expr::parse_expression; use crate::parser::functions::parse_generic_param_names; +use crate::parser::ParseError; use crate::types::parse_type_from_stream; use lexer::token::TokenType; use lexer::Token; @@ -79,11 +80,12 @@ where } } - println!("Unclosed generic type: missing '>'"); None } -pub fn parse_const_decl(tokens: &mut Peekable>) -> Option { +pub fn parse_const_decl(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid const decl", "const decl"); let mutability = Mutability::Const; skip_ws(tokens); let name = match tokens.next() { @@ -93,21 +95,21 @@ pub fn parse_const_decl(tokens: &mut Peekable>) -> Option name.clone(), _ => { println!("Expected identifier after `const`"); - return None; + return Err(invalid(tokens.peek().copied())); } }; skip_ws(tokens); if !matches!(tokens.next().map(|t| &t.token_type), Some(TokenType::Colon)) { println!("Expected ':' after identifier"); - return None; + return Err(invalid(tokens.peek().copied())); } let wave_type = match parse_type_from_stream(tokens) { Some(wave_type) => wave_type, None => { println!("Expected a valid type after ':'"); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -126,7 +128,7 @@ pub fn parse_const_decl(tokens: &mut Peekable>) -> Option>) -> Option>) -> Option>) -> Option { +pub fn parse_const(tokens: &mut Peekable>) -> Result { parse_const_decl(tokens) } -pub fn parse_static(tokens: &mut Peekable>) -> Option { +pub fn parse_static(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid static", "static"); let node = parse_var(tokens)?; let ASTNode::Variable(mut v) = node else { - return None; + return Err(invalid(tokens.peek().copied())); }; v.mutability = Mutability::Static; - Some(ASTNode::Variable(v)) + Ok(ASTNode::Variable(v)) } // VAR parsing -pub fn parse_var(tokens: &mut Peekable>) -> Option { +pub fn parse_var(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid var", "var"); let mutability = Mutability::Var; skip_ws(tokens); @@ -178,7 +184,7 @@ pub fn parse_var(tokens: &mut Peekable>) -> Option { }) => name.clone(), _ => { println!("Expected identifier"); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -188,14 +194,14 @@ pub fn parse_var(tokens: &mut Peekable>) -> Option { "Wave variable declarations require an explicit type: `var {}: Type = value;`", name ); - return None; + return Err(invalid(tokens.peek().copied())); } let wave_type = match parse_type_from_stream(tokens) { Some(wave_type) => wave_type, None => { println!("Expected a valid type after ':'"); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -214,7 +220,7 @@ pub fn parse_var(tokens: &mut Peekable>) -> Option { if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -228,11 +234,11 @@ pub fn parse_var(tokens: &mut Peekable>) -> Option { expected_len, elements.len() ); - return None; + return Err(invalid(tokens.peek().copied())); } } - Some(ASTNode::Variable(VariableNode { + Ok(ASTNode::Variable(VariableNode { name, type_name: wave_type, initial_value, @@ -637,176 +643,75 @@ pub fn parse_extern(tokens: &mut Peekable>) -> Option>) -> Option { - // type = ; - let name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(n), - .. - }) => n.clone(), - other => { - println!("Error: Expected identifier after 'type', found {:?}", other); - return None; - } - }; - - match tokens.next() { - Some(Token { - token_type: TokenType::Equal, - .. - }) => {} - other => { - println!("Error: Expected '=' in type alias, found {:?}", other); - return None; - } - } - - let target = match parse_type_from_stream(tokens) { - Some(t) => t, - None => { - println!("Error: Expected type after '=' in type alias '{}'", name); - return None; - } - }; - - match tokens.next() { - Some(Token { - token_type: TokenType::SemiColon, - .. - }) => {} - other => { - println!("Error: Expected ';' after type alias, found {:?}", other); - return None; - } - } - - Some(ASTNode::TypeAlias(TypeAliasNode { +pub fn parse_type_alias(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let context = "type alias"; + let name = crate::expr::identifier(tokens, anchor, context)?; + crate::expr::expect_token(tokens, anchor, TokenType::Equal, "'='", context)?; + let target = crate::types::parse_type_checked(tokens, context)?; + crate::expr::expect_token(tokens, anchor, TokenType::SemiColon, "';'", context)?; + Ok(ASTNode::TypeAlias(TypeAliasNode { name, target, visibility: Visibility::Private, })) } -pub fn parse_enum(tokens: &mut Peekable>) -> Option { - // enum -> { (=)? (, ...)* } - let name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(n), - .. - }) => n.clone(), - other => { - println!("Error: Expected enum name after 'enum', found {:?}", other); - return None; - } - }; - - match tokens.next() { - Some(Token { - token_type: TokenType::Arrow, - .. - }) => {} - other => { - println!("Error: Expected '->' after enum name, found {:?}", other); - return None; - } - } - - let repr_type = match parse_type_from_stream(tokens) { - Some(t) => t, - None => { - println!("Error: Expected repr type after '->' in enum '{}'", name); - return None; - } - }; - - match tokens.next() { - Some(Token { - token_type: TokenType::Lbrace, - .. - }) => {} - other => { - println!("Error: Expected '{{' to start enum body, found {:?}", other); - return None; - } - } - - let mut variants: Vec = Vec::new(); - - loop { - let next_ty = match tokens.peek() { - Some(t) => t.token_type.clone(), - None => { - println!("Error: Unexpected end of file inside enum '{}'", name); - return None; - } +pub fn parse_enum(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let context = "enum declaration"; + let name = crate::expr::identifier(tokens, anchor, context)?; + crate::expr::expect_token(tokens, anchor, TokenType::Arrow, "'->'", context)?; + let repr_type = crate::types::parse_type_checked(tokens, "enum representation type")?; + crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, "'{'", context)?; + let mut variants = vec![]; + while !tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rbrace) + { + let before = tokens.clone(); + let vname = crate::expr::identifier(tokens, anchor, "enum case")?; + let explicit_value = if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Equal) + { + tokens.next(); + let at = tokens.peek().copied(); + let value = parse_expression(tokens)?; + let Expression::Literal(Literal::Int(raw)) = value.unspanned() else { + return Err(ParseError::expected_at( + at, + anchor, + "integer literal", + "enum case value", + )); + }; + Some(raw.clone()) + } else { + None }; - - match next_ty { - TokenType::Rbrace => { - tokens.next(); // consume '}' - break; - } - TokenType::Identifier(_) => { - let before = tokens.clone(); - // variant name - let vname = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(n), - .. - }) => n.clone(), - _ => unreachable!(), - }; - - // optional '= ' - let mut explicit_value: Option = None; - if matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::Equal)) { - tokens.next(); // consume '=' - - let value = parse_expression(tokens)?; - let Expression::Literal(Literal::Int(raw)) = value.unspanned() else { - return None; - }; - let raw = raw.clone(); - - explicit_value = Some(raw); - } - - variants.push(EnumVariantNode { - span: lexer::consumed_span(before, tokens), - name: vname, - explicit_value, - }); - - // after variant: ',' or '}' - match tokens.peek().map(|t| t.token_type.clone()) { - Some(TokenType::Comma) => { - tokens.next(); // consume ',' - - continue; - } - Some(TokenType::Rbrace) => { - continue; - } - other => { - println!( - "Error: Expected ',' or '}}' after enum variant in '{}', found {:?}", - name, other - ); - return None; - } - } + variants.push(EnumVariantNode { + span: lexer::consumed_span(before, tokens), + name: vname, + explicit_value, + }); + match tokens.peek().map(|t| &t.token_type) { + Some(TokenType::Comma) => { + tokens.next(); } - other => { - println!( - "Error: Expected enum variant name or '}}' in '{}', found {:?}", - name, other - ); - return None; + Some(TokenType::Rbrace) => {} + _ => { + return Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + "',' or '}'", + context, + )) } } } - - Some(ASTNode::Enum(EnumNode { + tokens.next(); + Ok(ASTNode::Enum(EnumNode { name, repr_type, variants, @@ -814,109 +719,86 @@ pub fn parse_enum(tokens: &mut Peekable>) -> Option { })) } -pub fn parse_variant(tokens: &mut Peekable>) -> Option { +pub fn parse_variant(tokens: &mut Peekable>) -> Result { skip_ws(tokens); - let name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => name.clone(), - _ => { - println!("Error: Expected variant name after 'variant'"); - return None; - } - }; + let anchor = tokens.peek().copied(); + let context = "variant declaration"; + let name = crate::expr::identifier(tokens, anchor, context)?; let generic_params = parse_generic_param_names(tokens)?; - skip_ws(tokens); - if !matches!( - tokens.next().map(|token| &token.token_type), - Some(TokenType::Lbrace) - ) { - println!("Error: Expected '{{' to start variant '{}'", name); - return None; - } - - let mut cases = Vec::new(); + crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, "'{'", context)?; + let mut cases = vec![]; loop { skip_ws(tokens); - if matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::Rbrace) - ) { + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rbrace) + { tokens.next(); break; } - let before = tokens.clone(); - let case_name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(case_name), - .. - }) => case_name.clone(), - _ => { - println!("Error: Expected case name in variant '{}'", name); - return None; - } - }; - + let case_name = crate::expr::identifier(tokens, anchor, "variant case")?; skip_ws(tokens); - let mut payload_types = Vec::new(); - if matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::Lparen) - ) { + let mut payload_types = vec![]; + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Lparen) + { tokens.next(); loop { skip_ws(tokens); - if matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::Rparen) - ) { + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rparen) + { tokens.next(); break; } - payload_types.push(parse_type_from_stream(tokens)?); + payload_types.push(crate::types::parse_type_checked( + tokens, + "variant payload type", + )?); skip_ws(tokens); - match tokens.peek().map(|token| &token.token_type) { - Some(TokenType::Comma) => { - tokens.next(); - } - Some(TokenType::Rparen) => { - tokens.next(); - break; - } - _ => { - println!( - "Error: Expected ',' or ')' after payload type in '{}::{}'", - name, case_name - ); - return None; - } + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Comma) + { + tokens.next(); + } else { + crate::expr::expect_token( + tokens, + anchor, + TokenType::Rparen, + "',' or ')'", + "variant payload", + )?; + break; } } } - cases.push(VariantCaseNode { span: lexer::consumed_span(before, tokens), name: case_name, payload_types, }); - skip_ws(tokens); - match tokens.peek().map(|token| &token.token_type) { + match tokens.peek().map(|t| &t.token_type) { Some(TokenType::Comma) => { tokens.next(); } Some(TokenType::Rbrace) => {} _ => { - println!("Error: Expected ',' or '}}' after variant case"); - return None; + return Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + "',' or '}'", + context, + )) } } } - - Some(ASTNode::Variant(VariantNode { + Ok(ASTNode::Variant(VariantNode { name, generic_params, cases, diff --git a/front/parser/src/parser/expr.rs b/front/parser/src/parser/expr.rs index 6819c1ce..07d738cb 100644 --- a/front/parser/src/parser/expr.rs +++ b/front/parser/src/parser/expr.rs @@ -18,6 +18,7 @@ use crate::ast::Expression; use crate::expr::parse_expression; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; @@ -26,12 +27,15 @@ use std::slice::Iter; pub fn parse_function_call( name: Option, tokens: &mut Peekable>, -) -> Option { - let name = name?; +) -> Result { + let anchor = tokens.peek().copied(); + let invalid = + |token| ParseError::expected_at(token, anchor, "valid function call", "function call"); + let name = name.ok_or_else(|| invalid(tokens.peek().copied()))?; - if tokens.peek()?.token_type != TokenType::Lparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lparen { println!("❌ Expected '(' after function name '{}'", name); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume '(' @@ -56,12 +60,12 @@ pub fn parse_function_call( "❌ Unexpected token in function arguments: {:?}", tokens.peek() ); - return None; + return Err(invalid(tokens.peek().copied())); } } } - Some(Expression::FunctionCall { + Ok(Expression::FunctionCall { name, type_args: Vec::new(), args, diff --git a/front/parser/src/parser/functions.rs b/front/parser/src/parser/functions.rs index 0e501107..6b17fbf3 100644 --- a/front/parser/src/parser/functions.rs +++ b/front/parser/src/parser/functions.rs @@ -19,6 +19,7 @@ use crate::ast::{ASTNode, ExportAttribute, Expression, FunctionNode, ParameterNode, Visibility}; use crate::parser::decl::parse_ffi_header; use crate::parser::types::parse_type_from_stream; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::collections::HashSet; @@ -34,70 +35,66 @@ fn skip_ws(tokens: &mut Peekable>) { } } -pub fn parse_generic_param_names(tokens: &mut Peekable>) -> Option> { +pub fn parse_generic_param_names( + tokens: &mut Peekable>, +) -> Result, ParseError> { skip_ws(tokens); - if !matches!( - tokens.peek().map(|t| &t.token_type), - Some(TokenType::Lchevr) - ) { - return Some(Vec::new()); + if !tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Lchevr) + { + return Ok(vec![]); } - - tokens.next(); // consume '<' - let mut params: Vec = Vec::new(); - let mut seen: HashSet = HashSet::new(); - + let anchor = tokens.next(); + let context = "generic parameters"; + let mut params = Vec::new(); + let mut seen = HashSet::new(); loop { skip_ws(tokens); - - if matches!( - tokens.peek().map(|t| &t.token_type), - Some(TokenType::Rchevr) - ) { - if params.is_empty() { - return None; - } - tokens.next(); // consume '>' + if !params.is_empty() + && tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rchevr) + { + tokens.next(); break; } - - let ident = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => name.clone(), - _ => { - println!("Error: Expected generic parameter name inside '<...>'"); - return None; - } - }; - - if !seen.insert(ident.clone()) { - println!("Error: Duplicate generic parameter '{}'", ident); - return None; + let at = tokens.peek().copied(); + let name = crate::expr::identifier(tokens, anchor, context)?; + if !seen.insert(name.clone()) { + return Err( + ParseError::syntax_at(at, format!("duplicate generic parameter '{name}'")) + .with_context(context) + .with_found_token(at), + ); } - params.push(ident); - + params.push(name); skip_ws(tokens); - match tokens.peek().map(|t| &t.token_type) { - Some(TokenType::Comma) => { - tokens.next(); - } - Some(TokenType::Rchevr) => { - tokens.next(); // consume '>' - break; - } - _ => { - println!("Error: Expected ',' or '>' in generic parameter list"); - return None; - } + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Comma) + { + tokens.next(); + } else { + crate::expr::expect_token(tokens, anchor, TokenType::Rchevr, "',' or '>'", context)?; + break; } } - - Some(params) + Ok(params) } -pub fn parse_parameters(tokens: &mut Peekable>) -> Option> { +pub fn parse_parameters( + tokens: &mut Peekable>, +) -> Result, ParseError> { + let anchor = tokens.peek().copied(); + let invalid = |token| { + ParseError::expected_at( + token, + anchor, + "valid function parameters", + "function parameters", + ) + }; let mut params = vec![]; loop { skip_ws(tokens); @@ -113,12 +110,14 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Option>) -> Option>) -> Option pt, None => { println!("Error: Failed to parse type for parameter '{}'", name); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -146,7 +145,7 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Option>) -> Option { println!("Error: use `,` instead of `;` to separate parameters"); - return None; + return Err(invalid(tokens.peek().copied())); } Some(TokenType::Rparen) => { // loop end } _ => { println!("Error: Expected ',' or ')' after parameter"); - return None; + return Err(invalid(tokens.peek().copied())); } } } @@ -184,24 +183,50 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Option>) -> Option { +pub fn parse_function(tokens: &mut Peekable>) -> Result { parse_function_with_export(tokens, None) } pub fn parse_function_with_export( tokens: &mut Peekable>, export: Option, -) -> Option { +) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| { + ParseError::syntax_at(anchor, "failed to parse function declaration") + .with_context("top-level function") + .with_expected_many([ + "fun name(params) { ... }", + "fun name(params) -> return_type { ... }", + ]) + .with_found_token(token) + .with_help("check parameter syntax, return type arrow, and function body braces") + }; let before = tokens.clone(); - tokens.next(); + let is_async = tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Async); + if is_async { + tokens.next(); + skip_ws(tokens); + crate::expr::expect_token(tokens, anchor, TokenType::Fun, "'fun'", "async function")?; + } else { + crate::expr::expect_token( + tokens, + anchor, + TokenType::Fun, + "'fun'", + "function declaration", + )?; + } skip_ws(tokens); @@ -210,14 +235,25 @@ pub fn parse_function_with_export( token_type: TokenType::Identifier(name), .. }) => name.clone(), - _ => return None, + _ => return Err(invalid(tokens.peek().copied())), }; + if is_async && (name == "main" || export.is_some()) { + return Err(ParseError::syntax_at( + anchor, + if name == "main" { + "entry function `main` must be synchronous; start the executor with task::block_on" + } else { + "async functions cannot be exported through an FFI ABI" + }, + ) + .with_context("async function")); + } let generic_params = parse_generic_param_names(tokens)?; skip_ws(tokens); - if tokens.peek()?.token_type != TokenType::Lparen { - return None; + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lparen { + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume '(' @@ -230,7 +266,7 @@ pub fn parse_function_with_export( "Error: Parameter '{}' is declared multiple times", param.name ); - return None; + return Err(invalid(tokens.peek().copied())); } } @@ -243,7 +279,7 @@ pub fn parse_function_with_export( { tokens.next(); // consume '->' let before_type = tokens.clone(); - let ty = parse_type_from_stream(tokens)?; + let ty = parse_type_from_stream(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; return_type_span = lexer::consumed_span(before_type, tokens); Some(ty) } else { @@ -252,7 +288,8 @@ pub fn parse_function_with_export( skip_ws(tokens); let body = extract_body(tokens)?; - Some(ASTNode::Function(FunctionNode { + Ok(ASTNode::Function(FunctionNode { + is_async, span: lexer::consumed_span(before, tokens), name, generic_params, @@ -265,8 +302,21 @@ pub fn parse_function_with_export( })) } -pub fn parse_export(tokens: &mut Peekable>) -> Option> { - let (abi, global_symbol) = parse_ffi_header(tokens, "export")?; +pub fn parse_export(tokens: &mut Peekable>) -> Result, ParseError> { + let anchor = tokens.peek().copied(); + let invalid = |token| { + ParseError::syntax_at(anchor, "failed to parse export declaration") + .with_context("top-level export block/declaration") + .with_expected_many([ + "export(c) fun name(...) { ... }", + "export(c, \"symbol\") fun name(...) { ... }", + "export(c) { fun a(...) { ... } fun b(...) { ... } }", + ]) + .with_found_token(token) + .with_help("exports require a concrete non-generic function body") + }; + let (abi, global_symbol) = + parse_ffi_header(tokens, "export").ok_or_else(|| invalid(tokens.peek().copied()))?; let export = ExportAttribute { abi, symbol: global_symbol, @@ -277,7 +327,7 @@ pub fn parse_export(tokens: &mut Peekable>) -> Option> if tokens.peek().map(|t| t.token_type.clone()) == Some(TokenType::Lbrace) { if export.symbol.is_some() { println!("Error: export block cannot use a single symbol alias"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -291,12 +341,12 @@ pub fn parse_export(tokens: &mut Peekable>) -> Option> tokens.next(); break; } - Some(TokenType::Fun) => { + Some(TokenType::Fun | TokenType::Async) => { let node = parse_function_with_export(tokens, Some(export.clone()))?; if let ASTNode::Function(func) = &node { if !func.generic_params.is_empty() { println!("Error: exported functions cannot be generic"); - return None; + return Err(invalid(tokens.peek().copied())); } } nodes.push(node); @@ -306,7 +356,7 @@ pub fn parse_export(tokens: &mut Peekable>) -> Option> } other => { println!("Error: Unexpected token in export block: {:?}", other); - return None; + return Err(invalid(tokens.peek().copied())); } } } @@ -316,25 +366,30 @@ pub fn parse_export(tokens: &mut Peekable>) -> Option> tokens.next(); } - Some(nodes) - } else if tokens.peek().map(|t| t.token_type.clone()) == Some(TokenType::Fun) { + Ok(nodes) + } else if matches!( + tokens.peek().map(|t| &t.token_type), + Some(TokenType::Fun | TokenType::Async) + ) { let node = parse_function_with_export(tokens, Some(export))?; if let ASTNode::Function(func) = &node { if !func.generic_params.is_empty() { println!("Error: exported functions cannot be generic"); - return None; + return Err(invalid(tokens.peek().copied())); } } - Some(vec![node]) + Ok(vec![node]) } else { println!("Error: Expected 'fun' or '{{' after export(...)"); - None + Err(invalid(tokens.peek().copied())) } } -pub fn extract_body(tokens: &mut Peekable>) -> Option> { - if tokens.peek()?.token_type != TokenType::Lbrace { - return None; +pub fn extract_body(tokens: &mut Peekable>) -> Result, ParseError> { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "'{'", "function body"); + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lbrace { + return Err(invalid(tokens.peek().copied())); } tokens.next(); crate::parser::stmt::parse_block(tokens) diff --git a/front/parser/src/parser/io.rs b/front/parser/src/parser/io.rs index 5a45cf97..7aa35c54 100644 --- a/front/parser/src/parser/io.rs +++ b/front/parser/src/parser/io.rs @@ -18,16 +18,19 @@ use crate::ast::{ASTNode, StatementNode}; use crate::expr::parse_expression; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; use std::slice::Iter; use utils::formatx::*; -pub fn parse_println(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { +pub fn parse_println(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid println", "println"); + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lparen { println!("Error: Expected '(' after 'println'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume '(' @@ -39,25 +42,25 @@ pub fn parse_println(tokens: &mut Peekable>) -> Option { content.clone() } else { println!("Error: Expected string literal in 'println'"); - return None; + return Err(invalid(tokens.peek().copied())); }; let placeholder_count = count_placeholders(&content); if placeholder_count == 0 { - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected closing ')'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume ')' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); - return Some(ASTNode::Statement(StatementNode::Println(format!( + return Ok(ASTNode::Statement(StatementNode::Println(format!( "{}\n", content )))); @@ -70,23 +73,18 @@ pub fn parse_println(tokens: &mut Peekable>) -> Option { }) = tokens.peek() { tokens.next(); // Consume ',' - if let Some(expr) = parse_expression(tokens) { - args.push(expr); - } else { - println!("Error: Failed to parse expression in 'println'"); - return None; - } + args.push(parse_expression(tokens)?); } - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected closing ')'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume ')' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -96,20 +94,22 @@ pub fn parse_println(tokens: &mut Peekable>) -> Option { placeholder_count, args.len() ); - return None; + return Err(invalid(tokens.peek().copied())); } - Some(ASTNode::Statement(StatementNode::PrintlnFormat { + Ok(ASTNode::Statement(StatementNode::PrintlnFormat { format: format!("{}\n", content), args, })) } // PRINT parsing -pub fn parse_print(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { +pub fn parse_print(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid print", "print"); + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lparen { println!("Error: Expected '(' after 'println'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume '(' @@ -121,26 +121,26 @@ pub fn parse_print(tokens: &mut Peekable>) -> Option { content.clone() // Need clone() because it is String } else { println!("Error: Expected string literal in 'println'"); - return None; + return Err(invalid(tokens.peek().copied())); }; let placeholder_count = count_placeholders(&content); if placeholder_count == 0 { // No format → Print just a string - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected closing ')'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume ')' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); - return Some(ASTNode::Statement(StatementNode::Print(format!( + return Ok(ASTNode::Statement(StatementNode::Print(format!( "{}", content )))); @@ -153,23 +153,18 @@ pub fn parse_print(tokens: &mut Peekable>) -> Option { }) = tokens.peek() { tokens.next(); // Consume ',' - if let Some(expr) = parse_expression(tokens) { - args.push(expr); - } else { - println!("Error: Failed to parse expression in 'println'"); - return None; - } + args.push(parse_expression(tokens)?); } - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected closing ')'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume ')' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -179,19 +174,21 @@ pub fn parse_print(tokens: &mut Peekable>) -> Option { placeholder_count, args.len() ); - return None; + return Err(invalid(tokens.peek().copied())); } - Some(ASTNode::Statement(StatementNode::PrintFormat { + Ok(ASTNode::Statement(StatementNode::PrintFormat { format: content, args, })) } -pub fn parse_input(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { +pub fn parse_input(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| ParseError::expected_at(token, anchor, "valid input", "input"); + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lparen { println!("Error: Expected '(' after 'println'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume '(' @@ -203,7 +200,7 @@ pub fn parse_input(tokens: &mut Peekable>) -> Option { content.clone() // Need clone() because it is String } else { println!("Error: Expected string literal in 'input'"); - return None; + return Err(invalid(tokens.peek().copied())); }; let placeholder_count = count_placeholders(&content); @@ -215,23 +212,18 @@ pub fn parse_input(tokens: &mut Peekable>) -> Option { }) = tokens.peek() { tokens.next(); // Consume ',' - if let Some(expr) = parse_expression(tokens) { - args.push(expr); - } else { - println!("Error: Failed to parse expression in 'println'"); - return None; - } + args.push(parse_expression(tokens)?); } - if tokens.peek()?.token_type != TokenType::Rparen { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Rparen { println!("Error: Expected closing ')'"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // Consume ')' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::SemiColon) { println!("Expected ';' after expression"); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -241,10 +233,10 @@ pub fn parse_input(tokens: &mut Peekable>) -> Option { placeholder_count, args.len() ); - return None; + return Err(invalid(tokens.peek().copied())); } - Some(ASTNode::Statement(StatementNode::Input { + Ok(ASTNode::Statement(StatementNode::Input { format: content, args, })) diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index be84e3e8..f5748f68 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -20,6 +20,7 @@ use crate::ast::{ ASTNode, ImportNode, ProtoImplNode, StatementNode, StructNode, Visibility, WaveType, }; use crate::parser::functions::{parse_function, parse_generic_param_names}; +use crate::parser::ParseError; use crate::types::parse_type_from_stream; use lexer::token::TokenType; use lexer::Token; @@ -147,7 +148,15 @@ pub fn parse_import(tokens: &mut Peekable>) -> Option { }))) } -pub fn parse_proto(tokens: &mut Peekable>) -> Option { +pub fn parse_proto(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| { + ParseError::syntax_at(anchor, "failed to parse proto implementation") + .with_context("top-level proto block") + .with_expected("proto Type { fun method(...); }") + .with_found_token(token) + .with_help("check braces and method declarations inside proto") + }; let target_struct = match tokens.next() { Some(Token { token_type: TokenType::Identifier(name), @@ -158,16 +167,16 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Option { "Error: Expected struct name after 'proto', found {:?}", other ); - return None; + return Err(invalid(tokens.peek().copied())); } }; - if tokens.peek()?.token_type != TokenType::Lbrace { + if tokens.peek().ok_or_else(|| invalid(None))?.token_type != TokenType::Lbrace { println!( "Error: Expected '{{' after proto target '{}'", target_struct ); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume '{' @@ -181,7 +190,7 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Option { "Error: Unexpected end of file inside proto '{}' definition.", target_struct ); - return None; + return Err(invalid(tokens.peek().copied())); }; match token_type { @@ -190,8 +199,8 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Option { break; } - TokenType::Fun => { - if let Some(ASTNode::Function(mut func_node)) = parse_function(tokens) { + TokenType::Fun | TokenType::Async => { + if let ASTNode::Function(mut func_node) = parse_function(tokens)? { if func_node.return_type.is_none() { func_node.return_type = Some(WaveType::Void); } @@ -201,7 +210,7 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Option { "Error: Failed to parse method inside proto '{}'.", target_struct ); - return None; + return Err(invalid(tokens.peek().copied())); } } @@ -211,18 +220,26 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Option { other => { println!("Error: Unexpected token inside proto body: {:?}", other); - return None; + return Err(invalid(tokens.peek().copied())); } } } - Some(ASTNode::ProtoImpl(ProtoImplNode { + Ok(ASTNode::ProtoImpl(ProtoImplNode { target: target_struct, methods, })) } -pub fn parse_struct(tokens: &mut Peekable>) -> Option { +pub fn parse_struct(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = |token| { + ParseError::syntax_at(anchor, "failed to parse struct declaration") + .with_context("top-level struct declaration") + .with_expected("struct Name { field: type; fun method(...) { ... } }") + .with_found_token(token) + .with_help("check field separators (`;`) and method bodies") + }; let name = match tokens.next() { Some(Token { token_type: TokenType::Identifier(name), @@ -230,7 +247,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { }) => name.clone(), _ => { println!("Error: Expected struct name after 'struct' keyword."); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -241,7 +258,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { .map_or(true, |t| t.token_type != TokenType::Lbrace) { println!("Error: Expected '{{' after struct name '{}'.", name); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); @@ -259,7 +276,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Unexpected end of file inside struct '{}' definition.", name ); - return None; + return Err(invalid(tokens.peek().copied())); }; match token_type { @@ -272,8 +289,8 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { tokens.next(); } - TokenType::Fun => { - if let Some(ASTNode::Function(func_node)) = parse_function(tokens) { + TokenType::Fun | TokenType::Async => { + if let ASTNode::Function(func_node) = parse_function(tokens)? { if func_node.return_type.is_none() { let mut func_node_with_return = func_node.clone(); func_node_with_return.return_type = Some(WaveType::Void); @@ -283,7 +300,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { } } else { println!("Error: Failed to parse method inside struct '{}'.", name); - return None; + return Err(invalid(tokens.peek().copied())); } } TokenType::Identifier(_) => { @@ -324,7 +341,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Expected ':' after field '{}' in struct '{}'.", field_name, name ); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume ':' @@ -337,7 +354,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Invalid type for field '{}' in struct '{}'.", field_name, name ); - return None; + return Err(invalid(tokens.peek().copied())); } }; @@ -351,7 +368,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Expected ';' after field declaration in struct '{}'.", name ); - return None; + return Err(invalid(tokens.peek().copied())); } tokens.next(); // consume ';' @@ -368,7 +385,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Unexpected identifier '{}' in struct '{}' body. Expected field or method.", id_str, name ); - return None; + return Err(invalid(tokens.peek().copied())); } } @@ -377,12 +394,12 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { "Error: Unexpected token inside struct body: {:?}", other_token ); - return None; + return Err(invalid(tokens.peek().copied())); } } } - Some(ASTNode::Struct(StructNode { + Ok(ASTNode::Struct(StructNode { name, generic_params, fields, diff --git a/front/parser/src/parser/parse.rs b/front/parser/src/parser/parse.rs index 7493087e..78ea6d87 100644 --- a/front/parser/src/parser/parse.rs +++ b/front/parser/src/parser/parse.rs @@ -30,6 +30,8 @@ pub struct ParseDiagnostic { pub line: usize, pub column: usize, pub span: Option, + pub related: Vec, + at_eof: bool, pub expected: Vec, pub found: Option, pub context: Option, @@ -50,6 +52,8 @@ impl ParseError { line: 0, column: 0, span: None, + related: Vec::new(), + at_eof: false, expected: Vec::new(), found: None, context: None, @@ -67,12 +71,30 @@ impl ParseError { err } + pub(crate) fn expected_at( + token: Option<&Token>, + anchor: Option<&Token>, + expected: &str, + context: &str, + ) -> Self { + Self::syntax_at( + token.or(anchor), + format!("expected {expected} in {context}"), + ) + .with_expected(expected) + .with_context(context) + .with_found("end of file") + .with_found_token(token) + } + pub fn semantic(message: impl Into) -> Self { Self::Semantic(ParseDiagnostic { message: message.into(), line: 0, column: 0, span: None, + related: Vec::new(), + at_eof: false, expected: Vec::new(), found: None, context: None, @@ -128,9 +150,13 @@ impl ParseError { d.column = tok.span.as_ref().map_or(0, |s| s.column); d.span = tok.span.clone(); d.found = Some(Self::token_desc(tok)); + d.at_eof = tok.token_type == TokenType::Eof; if let Some(spelling) = tok.token_type.reserved_spelling() { d.message = format!("reserved syntax `{spelling}` is not implemented in Alpha"); } + } else { + self.diag_mut().at_eof = true; + self.diag_mut().found = Some("end of file".into()); } self } @@ -162,6 +188,63 @@ impl ParseError { } } + pub fn related(&self) -> &[error::RelatedDiagnostic] { + match self { + Self::Syntax(d) | Self::Semantic(d) => &d.related, + } + } + + fn with_unclosed_delimiter(mut self, tokens: &[Token]) -> Self { + if !matches!(&self, Self::Syntax(d) if d.at_eof) { + return self; + } + // Legacy leaf parsers may already have consumed the EOF sentinel. + if let Some(eof) = tokens + .last() + .filter(|token| token.token_type == TokenType::Eof) + { + self = self.with_found_token(Some(eof)); + } + let mut openers: Vec<&Token> = Vec::new(); + for token in tokens { + match token.token_type { + TokenType::Lparen | TokenType::Lbrack | TokenType::Lbrace => openers.push(token), + TokenType::Rparen | TokenType::Rbrack | TokenType::Rbrace => { + let Some(opener) = openers.pop() else { + return self; + }; + if !matches!( + (&opener.token_type, &token.token_type), + (TokenType::Lparen, TokenType::Rparen) + | (TokenType::Lbrack, TokenType::Rbrack) + | (TokenType::Lbrace, TokenType::Rbrace) + ) { + return self; + } + } + _ => {} + } + } + if let Some(opener) = openers.last() { + let (open, close) = match opener.token_type { + TokenType::Lparen => ("(", ")"), + TokenType::Lbrack => ("[", "]"), + TokenType::Lbrace => ("{", "}"), + _ => unreachable!(), + }; + let message = format!("unclosed '{open}' opened here; expected '{close}'"); + if let Some(span) = &opener.span { + self.diag_mut().related.push(error::RelatedDiagnostic { + message, + span: span.clone(), + }); + } else { + self = self.with_note(format!("{message} (line {})", opener.line)); + } + } + self + } + pub fn line(&self) -> usize { match self { ParseError::Syntax(d) | ParseError::Semantic(d) => d.line, @@ -216,6 +299,10 @@ pub fn parse_syntax_only(tokens: &[Token]) -> Result, ParseError> { /// Parse physical syntax with byte ranges preserved through frontend rewrites. pub fn parse_syntax_with_spans(tokens: &[Token]) -> Result, ParseError> { + parse_syntax_impl(tokens).map_err(|error| error.with_unclosed_delimiter(tokens)) +} + +fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { validate_explicit_variable_types(tokens)?; let mut iter = tokens.iter().peekable(); @@ -262,38 +349,37 @@ pub fn parse_syntax_with_spans(tokens: &[Token]) -> Result, ParseEr } Some(TokenType::Export) => { iter.next(); - parse_export(&mut iter).and_then(|mut declarations| { - if declarations.len() == 1 { - declarations.pop() - } else { - None - } - }) + let mut declarations = parse_export(&mut iter)?; + if declarations.len() == 1 { + declarations.pop() + } else { + None + } } - Some(TokenType::Fun) => parse_function(&mut iter), + Some(TokenType::Fun | TokenType::Async) => Some(parse_function(&mut iter)?), Some(TokenType::Struct) => { iter.next(); - parse_struct(&mut iter) + Some(parse_struct(&mut iter)?) } Some(TokenType::Type) => { iter.next(); - parse_type_alias(&mut iter) + Some(parse_type_alias(&mut iter)?) } Some(TokenType::Enum) => { iter.next(); - parse_enum(&mut iter) + Some(parse_enum(&mut iter)?) } Some(TokenType::Variant) => { iter.next(); - parse_variant(&mut iter) + Some(parse_variant(&mut iter)?) } Some(TokenType::Const) => { iter.next(); - parse_const(&mut iter) + Some(parse_const(&mut iter)?) } Some(TokenType::Static) => { iter.next(); - parse_static(&mut iter) + Some(parse_static(&mut iter)?) } _ => None, }; @@ -373,155 +459,42 @@ pub fn parse_syntax_with_spans(tokens: &[Token]) -> Result, ParseEr } } TokenType::Export => { - let anchor = (*token).clone(); iter.next(); - if let Some(export_nodes) = parse_export(&mut iter) { - nodes.extend(export_nodes); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse export declaration", - ) - .with_context("top-level export block/declaration") - .with_expected_many([ - "export(c) fun name(...) { ... }", - "export(c, \"symbol\") fun name(...) { ... }", - "export(c) { fun a(...) { ... } fun b(...) { ... } }", - ]) - .with_found_token(iter.peek().copied()) - .with_help("exports require a concrete non-generic function body")); - } + let export_nodes = parse_export(&mut iter)?; + nodes.extend(export_nodes); } TokenType::Const => { - let anchor = (*token).clone(); iter.next(); - if let Some(var) = parse_const(&mut iter) { - nodes.push(var); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse const declaration", - ) - .with_context("top-level constant declaration") - .with_expected("const name: type = value;") - .with_found_token(iter.peek().copied()) - .with_help("const declarations require explicit type and initializer")); - } + nodes.push(parse_const(&mut iter)?); } TokenType::Static => { - let anchor = (*token).clone(); iter.next(); - if let Some(var) = parse_static(&mut iter) { - nodes.push(var); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse static declaration", - ) - .with_context("top-level static declaration") - .with_expected("static name: type = value;") - .with_found_token(iter.peek().copied()) - .with_help("static declarations require an explicit type")); - } + nodes.push(parse_static(&mut iter)?); } TokenType::Proto => { - let anchor = (*token).clone(); iter.next(); - if let Some(proto_impl) = parse_proto(&mut iter) { - nodes.push(proto_impl); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse proto implementation", - ) - .with_context("top-level proto block") - .with_expected("proto Type { fun method(...); }") - .with_found_token(iter.peek().copied()) - .with_help("check braces and method declarations inside proto")); - } + nodes.push(parse_proto(&mut iter)?); } TokenType::Type => { - let anchor = (*token).clone(); - iter.next(); // consume 'type' - if let Some(node) = parse_type_alias(&mut iter) { - nodes.push(node); - } else { - return Err( - ParseError::syntax_at(Some(&anchor), "failed to parse type alias") - .with_context("top-level type alias") - .with_expected("type Name = ExistingType;") - .with_found_token(iter.peek().copied()) - .with_help("type aliases must include `=` and end with ';'"), - ); - } + iter.next(); + nodes.push(parse_type_alias(&mut iter)?); } TokenType::Enum => { - let anchor = (*token).clone(); - iter.next(); // consume 'enum' - if let Some(node) = parse_enum(&mut iter) { - nodes.push(node); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse enum declaration", - ) - .with_context("top-level enum declaration") - .with_expected("enum Name -> i32 { A = 0, B = 1 }") - .with_found_token(iter.peek().copied()) - .with_help("check enum repr type, braces, and variant values")); - } + iter.next(); + nodes.push(parse_enum(&mut iter)?); } TokenType::Variant => { - let anchor = (*token).clone(); iter.next(); - if let Some(node) = parse_variant(&mut iter) { - nodes.push(node); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse variant declaration", - ) - .with_context("top-level variant declaration") - .with_expected("variant Result { Ok(T), Err(E) }") - .with_found_token(iter.peek().copied()) - .with_help("check generic parameters, payload types, commas, and braces")); - } + nodes.push(parse_variant(&mut iter)?); } TokenType::Struct => { - let anchor = (*token).clone(); iter.next(); - if let Some(struct_node) = parse_struct(&mut iter) { - nodes.push(struct_node); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse struct declaration", - ) - .with_context("top-level struct declaration") - .with_expected("struct Name { field: type; fun method(...) { ... } }") - .with_found_token(iter.peek().copied()) - .with_help("check field separators (`;`) and method bodies")); - } + let struct_node = parse_struct(&mut iter)?; + nodes.push(struct_node); } - TokenType::Fun => { - let anchor = (*token).clone(); - if let Some(func) = parse_function(&mut iter) { - nodes.push(func); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse function declaration", - ) - .with_context("top-level function") - .with_expected_many([ - "fun name(params) { ... }", - "fun name(params) -> return_type { ... }", - ]) - .with_found_token(iter.peek().copied()) - .with_help( - "check parameter syntax, return type arrow, and function body braces", - )); - } + TokenType::Fun | TokenType::Async => { + let func = parse_function(&mut iter)?; + nodes.push(func); } TokenType::Eof => break, _ => { diff --git a/front/parser/src/parser/stmt.rs b/front/parser/src/parser/stmt.rs index b89fcf77..85625557 100644 --- a/front/parser/src/parser/stmt.rs +++ b/front/parser/src/parser/stmt.rs @@ -22,6 +22,7 @@ use crate::parser::control::{parse_for, parse_if, parse_match, parse_while}; use crate::parser::decl::parse_var; use crate::parser::io::*; use crate::parser::types::is_expression_start; +use crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; @@ -35,148 +36,142 @@ fn semicolon(tokens: &mut Peekable>) -> Option<()> { Some(()) } -pub fn parse_block(tokens: &mut Peekable>) -> Option> { +pub fn parse_block(tokens: &mut Peekable>) -> Result, ParseError> { + let anchor = tokens.peek().copied(); let mut body = vec![]; - - while let Some(token) = tokens.peek() { - if token.token_type == TokenType::Rbrace { - break; - } - - if let Some(node) = parse_statement(tokens) { - body.push(node); - } else { - println!("Error: Failed to parse statement inside block."); - return None; + loop { + match tokens.peek().map(|token| &token.token_type) { + Some(TokenType::Rbrace) => { + tokens.next(); + return Ok(body); + } + None | Some(TokenType::Eof) => { + return Err(ParseError::expected_at( + tokens.peek().copied(), + anchor, + "'}'", + "block", + )); + } + _ => body.push(parse_statement(tokens)?), } } - - if let Some(token) = tokens.next() { - if token.token_type != TokenType::Rbrace { - println!( - "Error: Expected '}}' to close the block, but found {:?}", - token.token_type - ); - return None; - } - } else { - println!("Error: Unexpected end of file, expected '}}'"); - return None; - } - - Some(body) } -pub fn parse_statement(tokens: &mut Peekable>) -> Option { +pub fn parse_statement(tokens: &mut Peekable>) -> Result { let before = tokens.clone(); - let result = (|| { - let token = match tokens.peek() { - Some(t) => (*t).clone(), - None => return None, - }; + let anchor = tokens.peek().copied(); + let result = match anchor.map(|token| &token.token_type) { + Some(TokenType::If) => { + tokens.next(); + parse_if(tokens) + } + Some(TokenType::For) => { + tokens.next(); + parse_for(tokens) + } + Some(TokenType::While) => { + tokens.next(); + parse_while(tokens) + } + Some(TokenType::Match) => { + tokens.next(); + parse_match(tokens) + } + _ => parse_simple_statement(tokens), + }; + result.map(|value: ASTNode| { + let span = crate::source::node_span(before, tokens, &value); + value.with_span(span) + }) +} - let node = match token.token_type { - TokenType::Var => { - tokens.next(); - parse_var(tokens) - } - TokenType::Let | TokenType::Mut => { - println!("Error: `let` and `let mut` declarations were removed; use `var`"); - None - } - TokenType::Const => { - println!("Error: `const` is only allowed at top level"); - None - } - TokenType::Static => { - println!("Error: `static` is only allowed at top level"); - None - } - TokenType::Println => { - tokens.next(); - parse_println(tokens) - } - TokenType::Print => { - tokens.next(); - parse_print(tokens) - } - TokenType::Input => { - tokens.next(); - parse_input(tokens) - } - TokenType::If => { - tokens.next(); - parse_if(tokens) - } - TokenType::For => { - tokens.next(); - parse_for(tokens) - } - TokenType::While => { - tokens.next(); - parse_while(tokens) - } - TokenType::Match => { - tokens.next(); - parse_match(tokens) - } - TokenType::Continue | TokenType::Break => { - tokens.next(); - semicolon(tokens)?; - Some(ASTNode::Statement( - if token.token_type == TokenType::Continue { - StatementNode::Continue - } else { - StatementNode::Break - }, - )) - } - TokenType::Return => { - tokens.next(); - let expr = if tokens.peek()?.token_type == TokenType::SemiColon { +// Legacy statement forms still return Option; the caller supplies a structured fallback. +fn parse_simple_statement(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let invalid = + |token| ParseError::expected_at(token, anchor, "valid block statement", "block statement"); + let token = match tokens.peek() { + Some(t) => (*t).clone(), + None => return Err(invalid(None)), + }; + + match token.token_type { + TokenType::Var => { + tokens.next(); + parse_var(tokens) + } + TokenType::Let | TokenType::Mut => { + println!("Error: `let` and `let mut` declarations were removed; use `var`"); + Err(invalid(tokens.peek().copied())) + } + TokenType::Const => { + println!("Error: `const` is only allowed at top level"); + Err(invalid(tokens.peek().copied())) + } + TokenType::Static => { + println!("Error: `static` is only allowed at top level"); + Err(invalid(tokens.peek().copied())) + } + TokenType::Println => { + tokens.next(); + parse_println(tokens) + } + TokenType::Print => { + tokens.next(); + parse_print(tokens) + } + TokenType::Input => { + tokens.next(); + parse_input(tokens) + } + TokenType::Continue | TokenType::Break => { + tokens.next(); + semicolon(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; + Ok(ASTNode::Statement( + if token.token_type == TokenType::Continue { + StatementNode::Continue + } else { + StatementNode::Break + }, + )) + } + TokenType::Return => { + tokens.next(); + let expr = + if tokens.peek().ok_or_else(|| invalid(None))?.token_type == TokenType::SemiColon { None } else { Some(parse_expression(tokens)?) }; - semicolon(tokens)?; - Some(ASTNode::Statement(StatementNode::Return(expr))) - } - TokenType::Asm => { + semicolon(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; + Ok(ASTNode::Statement(StatementNode::Return(expr))) + } + TokenType::Asm => { + tokens.next(); + let node = crate::parser::asm::parse_asm_block(tokens)?; + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::SemiColon) + { tokens.next(); - let node = crate::parser::asm::parse_asm_block(tokens)?; - if tokens - .peek() - .is_some_and(|t| t.token_type == TokenType::SemiColon) - { - tokens.next(); - } - Some(node) } - TokenType::Rbrace => None, + Ok(node) + } + TokenType::Rbrace => Err(invalid(tokens.peek().copied())), - _ => { - if is_expression_start(&token.token_type) { - if let Some(expr) = parse_expression(tokens) { - semicolon(tokens)?; - Some(ASTNode::Statement(StatementNode::Expression(expr))) - } else { - println!("Error: Failed to parse expression statement."); - None - } - } else { - println!( - "Error: Unexpected token, cannot start a statement with: {:?}", - token.token_type - ); - None - } + _ => { + if is_expression_start(&token.token_type) { + let expr = parse_expression(tokens)?; + semicolon(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; + Ok(ASTNode::Statement(StatementNode::Expression(expr))) + } else { + println!( + "Error: Unexpected token, cannot start a statement with: {:?}", + token.token_type + ); + Err(invalid(tokens.peek().copied())) } - }; - - node - })(); - result.map(|value: ASTNode| { - let span = crate::source::node_span(before, tokens, &value); - value.with_span(span) - }) + } + } } diff --git a/front/parser/src/parser/types.rs b/front/parser/src/parser/types.rs index 5cfa5d16..6ac41267 100644 --- a/front/parser/src/parser/types.rs +++ b/front/parser/src/parser/types.rs @@ -103,6 +103,16 @@ pub fn token_type_to_wave_type(token_type: &TokenType) -> Option { TokenType::TypeArray(inner, size) => { token_type_to_wave_type(inner).map(|t| WaveType::Array(Box::new(t), *size)) } + TokenType::TypeCustom(name) if name.starts_with("Future<") => { + let inner = name.strip_prefix("Future<")?.strip_suffix('>')?; + let args = split_top_level_generic_args(inner)?; + if args.len() != 1 { + return None; + } + Some(WaveType::Future(Box::new(token_type_to_wave_type( + &parse_type(&args[0])?, + )?))) + } TokenType::TypeCustom(name) => Some(WaveType::Struct(name.clone())), _ => None, } @@ -111,7 +121,8 @@ pub fn token_type_to_wave_type(token_type: &TokenType) -> Option { pub fn is_expression_start(token_type: &TokenType) -> bool { matches!( token_type, - TokenType::Identifier(_) + TokenType::Await + | TokenType::Identifier(_) | TokenType::IntLiteral(_) | TokenType::Float(_) | TokenType::Lparen @@ -320,3 +331,26 @@ where token_type_to_wave_type(&type_token.token_type) } + +/// Parses a declaration type without losing its starting location on failure. +/// The legacy optional parser is transactional here: callers never observe a +/// partially consumed malformed type or a consumed following declaration. +pub(crate) fn parse_type_checked<'a, T>( + tokens: &mut Peekable, + context: &str, +) -> Result +where + T: Iterator + Clone, +{ + let anchor = tokens.peek().copied(); + let mut probe = tokens.clone(); + match parse_type_from_stream(&mut probe) { + Some(ty) => { + *tokens = probe; + Ok(ty) + } + None => Err(crate::parser::ParseError::expected_at( + anchor, anchor, "type", context, + )), + } +} diff --git a/front/parser/src/source.rs b/front/parser/src/source.rs index 148bd0a9..b01da4d5 100644 --- a/front/parser/src/source.rs +++ b/front/parser/src/source.rs @@ -182,6 +182,7 @@ impl SourceMap { } Expression::Deref(e) | Expression::AddressOf(e) + | Expression::Await(e) | Expression::Grouped(e) | Expression::Unary { expr: e, .. } | Expression::Cast { expr: e, .. } diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index 70600c15..6af28e55 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -468,6 +468,7 @@ impl ProgramTypes { WaveType::Isz | WaveType::Usz => Err(format!("{context}: target-sized integer requires target resolution before semantic analysis")), WaveType::Never if !allow_void => Err(format!("{context} cannot use the return-only `!` type")), WaveType::Void if !allow_void => Err(format!("{} cannot use the `void` type", context)), + WaveType::Future(inner) => self.validate_type(inner, generic_params, true, context), WaveType::Pointer(inner) | WaveType::Array(inner, _) => { self.validate_type(inner, generic_params, false, context) } @@ -541,6 +542,9 @@ impl ProgramTypes { seen.remove(name); resolved } + WaveType::Future(inner) => { + WaveType::Future(Box::new(self.canonical_type_inner(inner, seen))) + } WaveType::Pointer(inner) => { WaveType::Pointer(Box::new(self.canonical_type_inner(inner, seen))) } @@ -605,7 +609,13 @@ fn function_type(function: &FunctionNode) -> FunctionType { .iter() .filter(|parameter| parameter.initial_value.is_none()) .count(), - return_type: function.return_type.clone().unwrap_or(WaveType::Void), + return_type: if function.is_async { + WaveType::Future(Box::new( + function.return_type.clone().unwrap_or(WaveType::Void), + )) + } else { + function.return_type.clone().unwrap_or(WaveType::Void) + }, generic_params: function.generic_params.clone(), variadic: false, } @@ -643,6 +653,9 @@ fn substitute_wave_type(ty: &WaveType, substitutions: &HashMap ty.clone() } } + WaveType::Future(inner) => { + WaveType::Future(Box::new(substitute_wave_type(inner, substitutions))) + } WaveType::Pointer(inner) => { WaveType::Pointer(Box::new(substitute_wave_type(inner, substitutions))) } @@ -712,7 +725,7 @@ fn infer_variant_type_pair( if let Some(previous) = substitutions.get(name) { if program.canonical_type(previous) != program.canonical_type(actual) { return Err(format!( - "conflicting inferred types `{}` and `{}` for variant generic `{}`", + "conflicting inferred types `{}` and `{}` for generic parameter `{}`", display_wave_type(previous), display_wave_type(actual), name @@ -726,7 +739,8 @@ fn infer_variant_type_pair( } match (template, actual) { - (WaveType::Pointer(template), WaveType::Pointer(actual)) + (WaveType::Future(template), WaveType::Future(actual)) + | (WaveType::Pointer(template), WaveType::Pointer(actual)) | (WaveType::Array(template, _), WaveType::Array(actual, _)) => { infer_variant_type_pair(program, template, actual, generic_params, substitutions) } @@ -764,6 +778,7 @@ struct Validator<'a> { scopes: Vec>, current_function: Option, current_return_type: Option, + current_async: bool, current_type_params: HashSet, loop_depth: usize, top_level_index: usize, @@ -776,6 +791,7 @@ struct Validator<'a> { hir_expression_types: HashMap, hir_variant_constructions: HashMap, hir_variant_patterns: HashMap, + generic_method_calls: HashMap, } impl<'a> Validator<'a> { @@ -785,6 +801,7 @@ impl<'a> Validator<'a> { scopes: vec![HashMap::new()], current_function: None, current_return_type: None, + current_async: false, current_type_params: HashSet::new(), loop_depth: 0, top_level_index: 0, @@ -797,6 +814,7 @@ impl<'a> Validator<'a> { hir_expression_types: HashMap::new(), hir_variant_constructions: HashMap::new(), hir_variant_patterns: HashMap::new(), + generic_method_calls: HashMap::new(), } } @@ -870,6 +888,7 @@ impl<'a> Validator<'a> { )); } } + let previous_async = std::mem::replace(&mut self.current_async, function.is_async); let previous_function = self.current_function.replace(display_name.to_string()); let previous_return = self .current_return_type @@ -950,6 +969,7 @@ impl<'a> Validator<'a> { Ok(()) }); + self.current_async = previous_async; self.current_function = previous_function; self.current_return_type = previous_return; self.loop_depth = previous_loop_depth; @@ -1590,10 +1610,16 @@ impl<'a> Validator<'a> { "never-returning function `{function}` cannot contain a return statement" )), (WaveType::Void, None) => Ok(()), - (WaveType::Void, Some(_)) => Err(format!( - "void function `{}` cannot return a value", - function - )), + (WaveType::Void, Some(expression)) => { + if matches!( + self.validate_expr(expression)?, + ExpressionType::Known(WaveType::Void) + ) { + Ok(()) + } else { + Err(format!("void function `{function}` cannot return a value")) + } + } (expected, None) => Err(format!( "non-void function `{}` must return `{}`", function, @@ -1856,6 +1882,20 @@ impl<'a> Validator<'a> { let right_type = self.validate_expr(right)?; infer_binary_type(self.program, operator, left_type, right_type) } + Expression::Await(inner) => { + if !self.current_async { + return Err("await is only valid inside an async function".into()); + } + match self.validate_expr(inner)? { + ExpressionType::Known(WaveType::Future(result)) => { + Ok(ExpressionType::Known(*result)) + } + other => Err(format!( + "await requires a Future, found `{}`", + display_expression_type(&other) + )), + } + } Expression::Unary { operator, expr } => { let ty = self.validate_expr(expr)?; self.validate_unary(operator, ty) @@ -1875,13 +1915,15 @@ impl<'a> Validator<'a> { } self.validate_variant_constructor(expression, name, args, expected) } else { - self.validate_function_call(name, type_args, args) + self.validate_function_call(expression, name, type_args, args) } } - Expression::MethodCall { object, name, args } => { - self.mark_span(SemanticSpanKind::Identifier, name.clone()); - self.validate_method_call(object, name, args) - } + Expression::MethodCall { + object, + name, + args, + type_args, + } => self.validate_method_call(expression, object, name, type_args, args), Expression::StructLiteral { name, fields } => { self.mark_span(SemanticSpanKind::Identifier, name.clone()); let known_fields = self @@ -2122,10 +2164,100 @@ impl<'a> Validator<'a> { fn validate_function_call( &mut self, + expression: &Expression, name: &str, type_args: &[WaveType], args: &[Expression], ) -> Result { + if crate::async_intrinsics::is_intrinsic(name) { + for (index, ty) in type_args.iter().enumerate() { + self.program.validate_type( + ty, + &self.current_type_params, + name == "__wave_async_create" && index == 1, + "async intrinsic type argument", + )?; + } + if name == "__wave_async_create" { + let Some(Expression::Literal(Literal::String(symbol))) = + args.get(2).map(Expression::unspanned) + else { + return Err("async frame creation requires a generated resume symbol".into()); + }; + if !symbol.starts_with("$async$poll$") + || !self.program.functions.contains_key(symbol) + { + return Err("async frame creation requires a generated resume function".into()); + } + } + let actual = args + .iter() + .map(|a| self.validate_expr(a)) + .collect::, _>>()?; + let concrete = actual + .iter() + .map(|a| match a { + ExpressionType::Known(t) => t.clone(), + ExpressionType::IntLiteral(_) => WaveType::Int(64), + _ => WaveType::Void, + }) + .collect::>(); + let (params, result) = crate::async_intrinsics::signature(name, type_args, &concrete)?; + if actual.len() != params.len() { + return Err(format!( + "{name} expects {} arguments, found {}", + params.len(), + actual.len() + )); + } + for (actual, expected) in actual.iter().zip(params.iter()) { + self.require_assignable(actual, expected, name)?; + } + return Ok(ExpressionType::Known(result)); + } + let mut inferred = Vec::new(); + if let Some(signature) = self.program.functions.get(name).cloned() { + if type_args.is_empty() + && !signature.generic_params.is_empty() + && signature + .params + .iter() + .any(|t| matches!(t, WaveType::Future(_))) + { + let mut subst = HashMap::new(); + for (template, arg) in signature.params.iter().zip(args) { + let actual = self.validate_expr(arg)?; + infer_variant_substitution( + self.program, + template, + &actual, + &signature.generic_params, + &mut subst, + )?; + } + inferred = signature + .generic_params + .iter() + .map(|p| { + subst.get(p).cloned().ok_or_else(|| { + format!("cannot infer generic parameter `{p}` in `{name}`") + }) + }) + .collect::>()?; + self.generic_method_calls.insert( + expression as *const _ as usize, + crate::methods::GenericMethodCall { + function: name.into(), + type_args: inferred.clone(), + }, + ); + } + } + let type_args = if inferred.is_empty() { + type_args + } else { + &inferred + }; let signature = self.program.functions.get(name).map(|signature| { let substitutions: HashMap = signature .generic_params @@ -2273,72 +2405,127 @@ impl<'a> Validator<'a> { fn validate_method_call( &mut self, + expression: &Expression, object: &Expression, name: &str, + type_args: &[WaveType], args: &[Expression], ) -> Result { let object_type = self.validate_expr(object)?; - let structure = match &object_type { - ExpressionType::Known(WaveType::Struct(name)) => Some(name.clone()), - ExpressionType::Known(WaveType::Pointer(inner)) => match inner.as_ref() { - WaveType::Struct(name) => Some(name.clone()), + self.source_span = self + .source_map + .expressions + .get(&(expression as *const _ as usize)) + .cloned(); + self.mark_span(SemanticSpanKind::Identifier, name); + let structure = match canonical_expression_type(self.program, &object_type) { + Some(WaveType::Struct(name)) => Some(name), + Some(WaveType::Pointer(inner)) => match *inner { + WaveType::Struct(name) => Some(name), _ => None, }, _ => None, }; - - if let Some(ref structure) = structure { - if let Some(signature) = self.program.method_type(structure, name) { - if let Some(expected_self) = signature.params.first() { - self.require_assignable( - &object_type, - expected_self, - &format!("receiver of method `{}.{}`", structure, name), - )?; - } - let params = signature.params.get(1..).unwrap_or(&[]); - self.validate_call_arguments( - "method", - name, - args, - params, - signature.required_params.saturating_sub(1), - false, - )?; - return Ok(ExpressionType::Known(signature.return_type)); - } - } - - if let Some(signature) = self.program.functions.get(name).cloned() { - if let Some(expected_self) = signature.params.first() { - self.require_assignable( + let method = structure + .as_ref() + .and_then(|owner| self.program.method_type(owner, name)); + let inherent = method.is_some(); + let Some(mut signature) = method.or_else(|| self.program.functions.get(name).cloned()) + else { + return Err(match structure { + Some(owner) => format!("struct `{owner}` has no method `{name}`"), + None => format!( + "method call `{name}` requires a struct receiver, found `{}`", + display_expression_type(&object_type) + ), + }); + }; + let generic_args = if type_args.is_empty() && !signature.generic_params.is_empty() { + let mut substitutions = HashMap::new(); + if let Some(receiver) = signature.params.first() { + infer_variant_substitution( + self.program, + receiver, &object_type, - expected_self, - &format!("receiver of method-style call `{}`", name), + &signature.generic_params, + &mut substitutions, )?; - self.validate_call_arguments( - "method", - name, - args, - &signature.params[1..], - signature.required_params.saturating_sub(1), - false, + } + for (parameter, argument) in signature.params.iter().skip(1).zip(args) { + let actual = self.validate_expr(argument)?; + infer_variant_substitution( + self.program, + parameter, + &actual, + &signature.generic_params, + &mut substitutions, )?; - return Ok(ExpressionType::Known(signature.return_type)); } + signature.generic_params.iter().map(|parameter| substitutions.get(parameter).cloned() + .ok_or_else(|| format!("cannot infer generic parameter `{parameter}` for method `{name}`; provide explicit type arguments"))) + .collect::, _>>()? + } else { + type_args.to_vec() + }; + if generic_args.len() != signature.generic_params.len() { + return Err(format!( + "method `{name}` expects {} generic argument(s), found {}", + signature.generic_params.len(), + generic_args.len() + )); } - - for argument in args { - self.validate_expr(argument)?; + for ty in &generic_args { + self.program.validate_type( + ty, + &self.current_type_params, + false, + "method type argument", + )?; } - match structure { - Some(structure) => Err(format!("struct `{}` has no method `{}`", structure, name)), - None => Err(format!( - "method call `{}` requires a struct receiver, found `{}`", - name, - display_expression_type(&object_type) - )), + if !signature.generic_params.is_empty() { + let substitutions = signature + .generic_params + .iter() + .cloned() + .zip(generic_args.iter().cloned()) + .collect(); + signature = substitute_function_type(&signature, &substitutions); + let (function, mut all_args) = if inherent { + let owner = structure.as_ref().unwrap(); + let base = self.program.named_type_base(owner); + let owner_args = parse_named_type_application(owner) + .map(|(_, args)| args) + .unwrap_or_default(); + (crate::methods::method_symbol(base, name), owner_args) + } else { + (name.to_string(), Vec::new()) + }; + all_args.extend(generic_args); + self.generic_method_calls.insert( + expression as *const _ as usize, + crate::methods::GenericMethodCall { + function, + type_args: all_args, + }, + ); } + let Some(receiver) = signature.params.first() else { + return Err(format!("method `{name}` requires a receiver parameter")); + }; + self.require_assignable( + &object_type, + receiver, + &format!("receiver of method `{name}`"), + )?; + self.validate_call_arguments( + "method", + name, + args, + &signature.params[1..], + signature.required_params.saturating_sub(1), + false, + )?; + Ok(ExpressionType::Known(signature.return_type)) } fn validate_unary( @@ -3066,6 +3253,7 @@ fn condition_mutation(expression: &Expression) -> Option { } Expression::Deref(inner) | Expression::AddressOf(inner) + | Expression::Await(inner) | Expression::Grouped(inner) | Expression::Unary { expr: inner, .. } | Expression::Cast { expr: inner, .. } @@ -3294,6 +3482,7 @@ fn display_wave_type(ty: &WaveType) -> String { WaveType::Char => "char".to_string(), WaveType::Byte => "byte".to_string(), WaveType::String => "str".to_string(), + WaveType::Future(inner) => format!("Future<{}>", display_wave_type(inner)), WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), WaveType::Array(inner, size) => format!("array<{}, {}>", display_wave_type(inner), size), WaveType::Void => "void".to_string(), @@ -3363,11 +3552,19 @@ pub(crate) fn analyze_hir_expression_types( }) } +pub(crate) fn analyze_generic_method_calls( + nodes: &[ASTNode], + sources: &crate::source::SourceMap, +) -> Result, SemanticDiagnostic> { + analyze_program_types(nodes, sources).map(|analysis| analysis.generic_method_calls) +} + struct ProgramAnalysis { expression_types: HashMap, hir_expression_types: HashMap, hir_variant_constructions: HashMap, hir_variant_patterns: HashMap, + generic_method_calls: HashMap, } fn is_supported_foreign_abi(abi: &str) -> bool { @@ -3470,6 +3667,7 @@ fn analyze_program_types( hir_expression_types: validator.hir_expression_types, hir_variant_constructions: validator.hir_variant_constructions, hir_variant_patterns: validator.hir_variant_patterns, + generic_method_calls: validator.generic_method_calls, }) } @@ -3524,6 +3722,32 @@ fn validate_declaration_types( let mut checked_aliases = HashSet::new(); for (index, node) in nodes.iter().enumerate() { + let ffi_types = match node { + ASTNode::Function(f) if f.export.is_some() => f + .parameters + .iter() + .map(|p| &p.param_type) + .chain(f.return_type.iter()) + .collect::>(), + ASTNode::ExternFunction(f) => f + .params + .iter() + .map(|(_, t)| t) + .chain(std::iter::once(&f.return_type)) + .collect(), + _ => Vec::new(), + }; + if ffi_types + .iter() + .any(|t| contains_future(program, t, &mut HashSet::new())) + { + return Err(semantic_diagnostic_for_top_level( + nodes, + index, + "Future cannot cross an FFI ABI".into(), + None, + )); + } let result = match node { ASTNode::Function(function) => { let mut result = @@ -3830,7 +4054,7 @@ fn validate_alias_type_cycle( WaveType::Struct(name) if program.aliases.contains_key(name) => { validate_alias_cycle(name, program, active, checked) } - WaveType::Pointer(inner) | WaveType::Array(inner, _) => { + WaveType::Future(inner) | WaveType::Pointer(inner) | WaveType::Array(inner, _) => { validate_alias_type_cycle(inner, program, active, checked) } _ => Ok(()), @@ -3849,3 +4073,35 @@ fn validate_unique_generic_params(params: &[String], owner: &str) -> Result<(), } Ok(()) } + +fn contains_future(program: &ProgramTypes, ty: &WaveType, seen: &mut HashSet) -> bool { + match ty { + WaveType::Future(_) => true, + WaveType::Pointer(inner) | WaveType::Array(inner, _) => { + contains_future(program, inner, seen) + } + WaveType::Struct(name) | WaveType::Variant(name) if seen.insert(name.clone()) => { + if let Some(alias) = program.aliases.get(name) { + if contains_future(program, alias, seen) { + return true; + } + } + if let Some(fields) = program.structs.get(name) { + if fields.values().any(|t| contains_future(program, t, seen)) { + return true; + } + } + if let Some(v) = program.variants.get(name) { + if v.cases + .iter() + .flat_map(|(_, p)| p) + .any(|t| contains_future(program, t, seen)) + { + return true; + } + } + false + } + _ => false, + } +} diff --git a/front/parser/tests/async_frontend.rs b/front/parser/tests/async_frontend.rs new file mode 100644 index 00000000..a077095f --- /dev/null +++ b/front/parser/tests/async_frontend.rs @@ -0,0 +1,94 @@ +use lexer::Lexer; +use parser::{ + ast::{ASTNode, WaveType}, + generics::monomorphize_generics, + hir::TypedProgram, + parse_syntax_with_spans, + verification::validate_program, +}; +fn syntax(s: &str) -> Vec { + parse_syntax_with_spans(&Lexer::new_with_file(s, "async.wave").tokenize().unwrap()).unwrap() +} +fn typed(s: &str) -> TypedProgram { + TypedProgram::lower(monomorphize_generics(syntax(s)).unwrap()).unwrap() +} +#[test] +fn async_and_await_preserve_types_spans_and_specialization() { + let source="pub async fun value(x:T) -> T { return x; }\nasync fun use_value() -> i64 { return await value(42); }\nfun main() {}"; + let program = typed(source); + let sites = program.await_sites(); + assert_eq!(sites.len(), 1); + assert_eq!(sites[0].1, WaveType::Int(64)); + let span = program.expression_span(sites[0].0).unwrap(); + assert_eq!(&source[span.start..span.start + 5], "await"); + let function = program + .syntax() + .iter() + .find_map(|n| { + if let ASTNode::Function(f) = n { + if f.name == "use_value" { + Some(f) + } else { + None + } + } else { + None + } + }) + .unwrap(); + let plan = parser::async_lower::plan(&program, function).unwrap(); + assert!(plan + .states + .iter() + .any(|s| matches!(s.transition, parser::async_lower::Transition::Await { .. }))); + // The ordinary output is itself type checked before any backend consumes it. + TypedProgram::lower(parser::async_lower::lower_program(&program).unwrap()).unwrap(); +} +#[test] +fn rejects_invalid_async_declarations_and_operands() { + for source in [ + "async main() {}", + "async fun main() {}", + "pub async fun main() {}", + "export(c) async fun work() {}", + "async fun f() { await ; }", + ] { + let tokens = Lexer::new_with_file(source, "bad.wave").tokenize().unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert!(error.span().is_some(), "{source}: {error:?}"); + } + for (source, needle) in [ + ("fun f() { await 1; }", "only valid inside"), + ("async fun f() { await 1; }", "requires a Future"), + ( + "async fun f() -> i64 { return 1; } fun main(){var v:i64=f();}", + "Future", + ), + ("extern(c) fun send(f:Future); fun main(){}", "FFI"), + ("type Bad = Future; fun main(){}", "cyclic"), + ] { + let error = validate_program(&syntax(source)).unwrap_err(); + assert!(error.contains(needle), "{source}: {error}"); + } +} +#[test] +fn await_is_not_an_lvalue_and_short_circuit_has_distinct_states() { + let program=typed("async fun flag()->bool{return true;} async fun f()->bool{return false && await flag();} fun main(){}"); + let f = program + .syntax() + .iter() + .find_map(|n| match n { + ASTNode::Function(f) if f.name == "f" => Some(f), + _ => None, + }) + .unwrap(); + let plan = parser::async_lower::plan(&program, f).unwrap(); + assert!(plan + .states + .iter() + .any(|s| matches!(s.transition, parser::async_lower::Transition::Branch(..)))); + let bad = syntax( + "async fun f()->i32{return 1;} async fun g(){var p:ptr = &(await f());} fun main(){}", + ); + assert!(validate_program(&bad).unwrap_err().contains("non-lvalue")); +} diff --git a/front/parser/tests/control_diagnostics.rs b/front/parser/tests/control_diagnostics.rs new file mode 100644 index 00000000..31e3e66c --- /dev/null +++ b/front/parser/tests/control_diagnostics.rs @@ -0,0 +1,136 @@ +//! Control-flow failures retain their cause through every enclosing declaration. +use lexer::Lexer; +use parser::{parse_syntax_only, parse_syntax_with_spans, ParseError}; + +fn diagnostic(source: &str) -> ParseError { + let tokens = Lexer::new_with_file(source, "control.wave") + .tokenize() + .unwrap(); + parse_syntax_with_spans(&tokens).unwrap_err() +} + +#[test] +fn missing_header_delimiters_identify_the_unexpected_token() { + let cases = [ + ("if @true) {}", "'('", "if header"), + ("if (true @{ }", "')'", "if header"), + ("if (true) @return;", "'{'", "if header"), + ("if (true) {} else if @false) {}", "'('", "else if header"), + ("if (true) {} else if (false @{ }", "')'", "else if header"), + ( + "if (true) {} else if (false) @return;", + "'{'", + "else if header", + ), + ("if (true) {} else @return;", "'{'", "else header"), + ("while @true) {}", "'('", "while header"), + ("while (true @{ }", "')'", "while header"), + ("while (true) @return;", "'{'", "while header"), + ("for @i = 0; i < 2; i = i + 1) {}", "'('", "for header"), + ("for (i = 0 @i < 2; i = i + 1) {}", "';'", "for initializer"), + ( + "for (var i: i32 = 0 @i < 2; i = i + 1) {}", + "';'", + "for initializer", + ), + ( + "for (i: i32 = 0 @i < 2; i = i + 1) {}", + "';'", + "for initializer", + ), + ("for (i = 0; i < 2 @i = i + 1) {}", "';'", "for condition"), + ("for (i = 0; i < 2; i = i + 1 @{ }", "')'", "for increment"), + ( + "for (i = 0; i < 2; i = i + 1) @return;", + "'{'", + "for header", + ), + ]; + for (body, expected, context) in cases { + // Unicode and CRLF ensure byte offsets and source columns are not conflated. + let marked = format!("// 이름\r\nfun main() {{\r\n {body}\r\n}}"); + let start = marked.find('@').unwrap(); + let source = marked.replace('@', ""); + let error = diagnostic(&source); + assert_eq!(error.expected(), [expected], "{source}: {error:?}"); + assert_eq!(error.context(), Some(context), "{source}: {error:?}"); + assert_eq!(error.span().unwrap().start, start, "{source}: {error:?}"); + assert_eq!(error.line(), 3); + assert_eq!( + error.column(), + source[..start].rsplit('\n').next().unwrap().chars().count() + 1 + ); + assert!(error + .found() + .unwrap() + .contains(&source[start..error.span().unwrap().end])); + let tokens = Lexer::new(&source).tokenize().unwrap(); + let without_spans = parse_syntax_only(&tokens).unwrap_err(); + assert_eq!(without_spans.message(), error.message()); + assert_eq!(without_spans.expected(), error.expected()); + assert_eq!(without_spans.line(), error.line()); + } +} + +#[test] +fn header_errors_survive_nested_blocks_exports_and_methods() { + for source in [ + "fun f() { if (true) { while (true) { for (i = 0 i < 2; i = i + 1) {} } } }", + "fun f() { match (1) { _ => { for (i = 0 i < 2; i = i + 1) {} } } }", + "pub fun f() { for (i = 0 i < 2; i = i + 1) {} }", + "export(c) fun f() { for (i = 0 i < 2; i = i + 1) {} }", + "pub export(c) fun f() { for (i = 0 i < 2; i = i + 1) {} }", + "export(c) { fun good() {} fun f() { for (i = 0 i < 2; i = i + 1) {} } }", + "struct S { fun f() { for (i = 0 i < 2; i = i + 1) {} } }", + "pub struct S { fun f() { for (i = 0 i < 2; i = i + 1) {} } }", + "proto S { fun f() { for (i = 0 i < 2; i = i + 1) {} } }", + ] { + let error = diagnostic(source); + assert_eq!( + error.context(), + Some("for initializer"), + "{source}: {error:?}" + ); + assert_eq!(error.expected(), ["';'"]); + assert_eq!(error.span().unwrap().start, source.find("i < 2").unwrap()); + } +} + +#[test] +fn truncated_headers_report_eof_at_the_end_of_the_source() { + for (body, expected, context) in [ + ("if", "'('", "if header"), + ("if (1", "')'", "if header"), + ("if (1)", "'{'", "if header"), + ("if (1) {} else", "'{'", "else header"), + ("if (1) {} else if (1", "')'", "else if header"), + ("while (1", "')'", "while header"), + ("for (i = 0", "';'", "for initializer"), + ("for (i = 0; i < 2", "';'", "for condition"), + ("for (i = 0; i < 2; i = i + 1", "')'", "for increment"), + ] { + let source = format!("fun f() {{\n {body}"); + let error = diagnostic(&source); + assert_eq!(error.expected(), [expected], "{source}: {error:?}"); + assert_eq!(error.context(), Some(context)); + assert_eq!(error.found(), Some("Eof")); + assert_eq!(error.span().unwrap().start, source.len()); + assert_eq!(error.span().unwrap().end, source.len()); + } +} + +#[test] +fn valid_headers_keep_expression_and_initializer_forms() { + let source = r#" +fun f() { + var i: i32 = 0; + if (1) { while (i < 2) { i = i + 1; } } + else if (0) {} else if (i) {} else {} + for (i = 0; i < 2; i = i + 1) {} + for (var j: i32 = 0; j < 2; j = j + 1) {} + for (k: i32 = 0; k < 2; k = k + 1) {} +} +"#; + let tokens = Lexer::new(source).tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap(); +} diff --git a/front/parser/tests/declaration_asm_diagnostics.rs b/front/parser/tests/declaration_asm_diagnostics.rs new file mode 100644 index 00000000..8dc82b55 --- /dev/null +++ b/front/parser/tests/declaration_asm_diagnostics.rs @@ -0,0 +1,111 @@ +use lexer::Lexer; +use parser::parse_syntax_with_spans; + +fn failure(marked: &str, expected: &str, context: &str) { + let marked = format!("// 한글\r\n{marked}"); + let start = marked.find('@').unwrap(); + let source = marked.replace('@', ""); + let tokens = Lexer::new_with_file(&source, "diagnostics.wave") + .tokenize() + .unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert_eq!(error.span().unwrap().start, start, "{source}: {error:?}"); + assert_eq!(error.expected(), [expected], "{source}: {error:?}"); + assert_eq!(error.context(), Some(context), "{source}: {error:?}"); +} + +#[test] +fn declaration_errors_keep_the_failing_token_for_private_and_public_forms() { + for (source, expected, context) in [ + ("type @= i32;", "identifier", "type alias"), + ("type T @i32;", "'='", "type alias"), + ("type T = @;", "type", "type alias"), + ("type T = i32 @type U = i64;", "';'", "type alias"), + ("type T = @ptr<,>;", "type", "type alias"), + ("enum @-> i32 {}", "identifier", "enum declaration"), + ("enum E @i32 {}", "'->'", "enum declaration"), + ("enum E -> @{}", "type", "enum representation type"), + ("enum E -> i32 @X", "'{'", "enum declaration"), + ("enum E -> i32 { @1 }", "identifier", "enum case"), + ( + "enum E -> i32 { A = @true }", + "integer literal", + "enum case value", + ), + ("enum E -> i32 { A @B }", "',' or '}'", "enum declaration"), + ("variant @{ A }", "identifier", "variant declaration"), + ("variant V @A", "'{'", "variant declaration"), + ("variant V<@> { A }", "identifier", "generic parameters"), + ("variant V { A }", "',' or '>'", "generic parameters"), + ("variant V { @1 }", "identifier", "variant case"), + ("variant V { A(@,) }", "type", "variant payload type"), + ("variant V { A(i32 @i64) }", "',' or ')'", "variant payload"), + ("variant V { A @B }", "',' or '}'", "variant declaration"), + ("variant V { A(i32@", "',' or ')'", "variant payload"), + ("enum E -> i32 { A@", "',' or '}'", "enum declaration"), + ] { + failure(source, expected, context); + failure(&format!("pub {source}"), expected, context); + } +} + +#[test] +fn both_asm_forms_share_precise_clause_errors() { + for (body, expected, context) in [ + ("@;", "'{'", "asm block"), + ( + "{ @123 }", + "instruction string, in, out, clobber, or '}'", + "asm block", + ), + ("{ in @rax }", "'('", "asm input clause"), + ( + "{ in(@123) value }", + "register string or identifier", + "asm input clause", + ), + ("{ in(rax @value }", "')'", "asm input clause"), + ( + "{ out(rax) @123 }", + "assignable expression", + "asm output clause", + ), + ("{ clobber @rax }", "'('", "asm clobber clause"), + ( + "{ clobber(@1) }", + "register string or identifier", + "asm clobber clause", + ), + ("{ clobber(rax @rcx) }", "',' or ')'", "asm clobber clause"), + ( + "{ clobber(rax, @) }", + "register string or identifier", + "asm clobber clause", + ), + ] { + failure(&format!("fun f() {{ asm {body}; }}"), expected, context); + failure( + &format!("fun f() {{ var x: i32 = asm {body}; }}"), + expected, + context, + ); + } + failure("fun f() { asm { @", "'}'", "asm block"); + failure("fun f() { var x: i32 = asm { @", "'}'", "asm block"); +} + +#[test] +fn valid_declarations_and_asm_keep_their_existing_syntax() { + let source = r#" + pub type P = ptr>; + enum E -> i32 { A = -1, B = 0x10, C, } + pub variant V { Empty, Pair(T, ptr,), Unit(), } + fun f() { + var x: i64 = 0; + asm { "nop"; in(rax) x + 1, out("rax") x clobber() } + var y: i64 = asm { "nop" in("rax") x out(rax) x clobber("memory", rcx) }; + } + "#; + let tokens = Lexer::new(source).tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap(); +} diff --git a/front/parser/tests/expression_diagnostics.rs b/front/parser/tests/expression_diagnostics.rs new file mode 100644 index 00000000..64b86bf6 --- /dev/null +++ b/front/parser/tests/expression_diagnostics.rs @@ -0,0 +1,136 @@ +//! Expression failures retain the offending token through each surrounding syntax form. +use lexer::Lexer; +use parser::{parse_syntax_with_spans, ParseError}; + +fn failure(marked: &str, expected: &str, context: &str) -> ParseError { + let start = marked.find('@').expect("mark the offending token"); + let source = marked.replace('@', ""); + let tokens = Lexer::new_with_file(&source, "expressions.wave") + .tokenize() + .unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert_eq!(error.expected(), [expected], "{source}: {error:?}"); + assert_eq!(error.context(), Some(context), "{source}: {error:?}"); + let span = error.span().unwrap(); + assert_eq!(span.start, start, "{source}: {error:?}"); + assert_eq!(span.file, "expressions.wave"); + assert_eq!( + error.line(), + source[..start].bytes().filter(|b| *b == b'\n').count() + 1 + ); + assert_eq!( + error.column(), + source[..start].rsplit('\n').next().unwrap().chars().count() + 1 + ); + if span.start < source.len() { + assert!( + error + .found() + .unwrap() + .contains(&source[span.start..span.end]), + "{error:?}" + ); + } else { + assert_eq!(error.found(), Some("Eof")); + } + error +} + +#[test] +fn malformed_postfix_and_aggregate_forms_keep_their_construct_context() { + for (expr, expected, context) in [ + ("object.@;", "identifier", "member access"), + ("object.@)", "identifier", "member access"), + ("pkg::@;", "identifier", "qualified name"), + ("pkg::Type::@)", "identifier", "qualified name"), + ("call(1 @;", "')'", "function call"), + ("call(1 @;", "')'", "function call"), + ("object.method(1 @;", "')'", "method call"), + ("array[1 @;", "']'", "index expression"), + ("(1 + 2 @;", "')'", "grouped expression"), + ("[1, 2 @;", "']'", "array literal"), + ("Point { @1: 2 }", "identifier", "struct literal field"), + ("Point { x @1 }", "':'", "struct literal field"), + ("Point { x: 1 @y: 2 }", "',' or '}'", "struct literal"), + ("Point { x: 1 @y: 2 }", "',' or '}'", "struct literal"), + ("call(1, @)", "expression", "primary expression"), + ("[1, @]", "expression", "primary expression"), + ( + "call(object.method([Point { x: item.@; }]))", + "identifier", + "member access", + ), + ("call>(item.@;)", "identifier", "member access"), + ] { + failure( + &format!("// 한글\r\nfun f() {{\r\n {expr}\r\n}}"), + expected, + context, + ); + } +} + +#[test] +fn expression_errors_propagate_through_declarations_statements_and_asm() { + for source in [ + "fun f() { var x: i32 = item.@; }", + "const x: i32 = item.@;", + "static x: i32 = item.@;", + "fun f(x: i32 = item.@;) {}", + "enum E -> i32 { X = item.@; }", + "fun f() { return item.@; }", + "fun f() { if (item.@;) {} }", + "fun f() { while (item.@;) {} }", + "fun f() { for (i: i32 = item.@; i < 2; i += 1) {} }", + "fun f() { match (item.@;) { _ => {} } }", + "fun f() { println(\"{}\", item.@;); }", + "fun f() { print(\"{}\", item.@;); }", + "fun f() { input(\"{}\", item.@;); }", + "fun f() { asm { in(\"rax\") item.@; } }", + "fun f() { var x: i32 = asm { in(\"rax\") item.@; }; }", + "pub const x: i32 = item.@;", + "pub static x: i32 = item.@;", + "pub enum E -> i32 { X = item.@; }", + "export(c) fun f() { return item.@; }", + "struct S { fun f() { return item.@; } }", + ] { + failure(source, "identifier", "member access"); + } +} + +#[test] +fn truncated_expression_delimiters_have_explicit_expectations() { + for (expr, expected, context) in [ + ("call(@", "')'", "function call"), + ("call(1@", "')'", "function call"), + ("object.method(@", "')'", "method call"), + ("array[1@", "']'", "index expression"), + ("(1@", "')'", "grouped expression"), + ("[@", "']'", "array literal"), + ("[1@", "']'", "array literal"), + ("Point {@", "'}'", "struct literal"), + ("pkg::@", "identifier", "qualified name"), + ("object.@", "identifier", "member access"), + ] { + failure(&format!("fun f() {{ {expr}"), expected, context); + } +} + +#[test] +fn nested_postfix_and_aggregate_forms_preserve_valid_syntax() { + for expr in [ + "call().field[1].method(other(2), [3, 4])[0]", + "generic>(Point { x: 1, }).method()", + "((a + b) * c)[index()]", + "[[1, 2], [3, 4]][0][1]", + "pkg::Type::call().field", + "Point { x: 1, y: other().field, }", + "array[1]++", + "++array[1]", + "a = b = call()", + ] { + let source = format!("fun f() {{ {expr}; }}"); + let tokens = Lexer::new(&source).tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap_or_else(|error| panic!("{source}: {error:?}")); + } +} diff --git a/front/parser/tests/generic_methods.rs b/front/parser/tests/generic_methods.rs new file mode 100644 index 00000000..2b8a3b71 --- /dev/null +++ b/front/parser/tests/generic_methods.rs @@ -0,0 +1,144 @@ +use lexer::Lexer; +use parser::generics::monomorphize_generics; +use parser::hir::TypedProgram; +use parser::{ast::ASTNode, parse_syntax_with_spans}; + +fn specialize(source: &str) -> Result, String> { + let tokens = Lexer::new_with_file(source, "methods.wave") + .tokenize() + .unwrap(); + monomorphize_generics(parse_syntax_with_spans(&tokens).unwrap()) +} + +#[test] +fn specializes_struct_and_proto_methods_with_enclosing_and_inferred_types() { + let ast = specialize( + r#" + struct Box { + value: T; + fun choose(self: ptr>, value: U) -> U { return value; } + } + struct Plain { value: i32; } + proto Plain { fun choose(self: ptr, value: U) -> U { return value; } } + fun main() -> i32 { + var box: Box = Box { value: 2 }; + var plain: Plain = Plain { value: 1 }; + var a: i64 = (&box).choose(4); + var b: i64 = (&box).choose(5); + var c: i32 = (&plain).choose(6); + return c; + } + "#, + ) + .unwrap(); + let methods = ast.iter().filter(|n| matches!(n.unspanned(), ASTNode::Function(f) if f.name.starts_with("$method$") && f.generic_params.is_empty())).count(); + assert_eq!(methods, 2, "identical calls share one instance"); + TypedProgram::lower(ast).unwrap(); +} + +#[test] +fn generic_method_failures_are_bounded_and_descriptive() { + for (source, message) in [ + ("struct S { fun f(self: ptr>, x: T) -> T { return x; } }", "duplicate generic parameter"), + ("struct S { fun f(self: ptr) -> ptr { return null; } } fun main() { var s: S = S {}; (&s).f(); }", "cannot infer generic parameter"), + ("struct S { fun f(self: ptr, a: T, b: T) {} } fun main() { var s: S = S {}; var x: i64 = 1; (&s).f(1, x); }", "conflicting inferred types"), + ("struct S { fun f(self: ptr, x: T) -> T { return x; } } fun main() { var s: S = S {}; (&s).f(1); }", "expects 1 generic argument"), + ("struct S { fun f(self: ptr, x: T) -> T { return x; } } fun main() { var s: S = S {}; (&s).f(\"bad\"); }", "type mismatch"), + ] { + let error = specialize(source).unwrap_err(); + assert!(error.contains(message), "{error}"); + } +} + +#[test] +fn recursive_generic_methods_reuse_instances_and_expanding_recursion_is_rejected() { + let ast = specialize( + r#" + struct S { fun recur(self: ptr, x: T, count: i32) -> T { + if (count == 0) { return x; } + return self.recur(x, count - 1); + } } + fun main() -> i32 { var s: S = S {}; return (&s).recur(7, 3); } + "#, + ) + .unwrap(); + TypedProgram::lower(ast).unwrap(); + let error = specialize( + r#" + struct S { fun grow(self: ptr) { self.grow>(); } } + fun main() { var s: S = S {}; (&s).grow(); } + "#, + ) + .unwrap_err(); + assert!(error.contains("instantiation depth"), "{error}"); +} + +#[test] +fn nested_method_receivers_have_resolved_hir_types() { + use parser::ast::{Expression, WaveType}; + use parser::hir::HirExpressionType; + let ast = specialize( + r#" + struct S { + value: i32; + fun identity(self: ptr) -> ptr { return self; } + fun read(self: ptr) -> i32 { return self.value; } + } + fun main() -> i32 { + var s: S = S { value: 4 }; + var result: i32 = (&s).identity().identity().read(); + return result; + } + "#, + ) + .unwrap(); + let program = TypedProgram::lower(ast).unwrap(); + let ASTNode::Function(main) = &program.syntax()[1] else { + panic!("main"); + }; + let ASTNode::Variable(result) = &main.body[1] else { + panic!("result"); + }; + let expression = result.initial_value.as_ref().unwrap(); + assert_eq!( + program.type_of(expression), + Some(&HirExpressionType::Resolved(WaveType::Int(32))) + ); + let Expression::MethodCall { object, .. } = expression else { + panic!("read call"); + }; + let pointer = + HirExpressionType::Resolved(WaveType::Pointer(Box::new(WaveType::Struct("S".into())))); + assert_eq!(program.type_of(object), Some(&pointer)); + let Expression::MethodCall { object, .. } = object.as_ref() else { + panic!("identity call"); + }; + assert_eq!(program.type_of(object), Some(&pointer)); +} + +#[test] +fn invalid_chains_point_at_the_first_failing_member() { + let source = "struct S {} fun main() { var s: S = S {}; (&s).missing().later(); }"; + let ast = specialize(source).unwrap(); + let error = TypedProgram::lower(ast).unwrap_err(); + let diagnostic = error.diagnostic(); + assert!(diagnostic.message.contains("missing"), "{diagnostic:?}"); + let span = diagnostic.span.as_ref().unwrap(); + let focus = span.focus.as_deref().unwrap_or(span); + assert_eq!(&source[focus.start..focus.end], "missing"); +} + +#[test] +fn finite_deep_generic_method_chains_do_not_use_the_compiler_call_stack() { + let mut source = String::from("struct S {"); + for index in 0..40 { + source.push_str(&format!("fun step{index}(self: ptr, x: T) -> T {{ ")); + if index == 39 { + source.push_str("return x; }"); + } else { + source.push_str(&format!("return self.step{}(x); }}", index + 1)); + } + } + source.push_str("} fun main() -> i32 { var s: S = S {}; return (&s).step0(7); }"); + TypedProgram::lower(specialize(&source).unwrap()).unwrap(); +} diff --git a/front/parser/tests/unclosed_delimiters.rs b/front/parser/tests/unclosed_delimiters.rs new file mode 100644 index 00000000..db664b72 --- /dev/null +++ b/front/parser/tests/unclosed_delimiters.rs @@ -0,0 +1,70 @@ +//! EOF keeps its primary position and links to the innermost unmatched opener. +use lexer::Lexer; +use parser::{parse_syntax_only, parse_syntax_with_spans}; + +#[test] +fn unclosed_delimiters_retain_their_opening_span_at_every_depth() { + for (marked, closer) in [ + ("fun f() @{", "}"), + ("fun f() {\n if (1) @{", "}"), + ("fun f() {\n while (1) @{\n", "}"), + ("fun f@(", ")"), + ("fun f@(a: i32", ")"), + ("fun f@(a:", ")"), + ("fun f@(a: i32,", ")"), + ("fun f() { call@(", ")"), + ("fun f() { call@(1", ")"), + ("fun f() { object.method@(", ")"), + ("fun f() { @(1 + 2", ")"), + ("fun f() { var xs: array = @[", "]"), + ("fun f() { call([1, @[2, 3", "]"), + ("fun f() { Point @{ x: 1", "}"), + ("fun f() { Point { x: call@(", ")"), + ("fun f() { \"({[\"; // ([{\n call@(", ")"), + ("fun f() { /* ([{ */ call@(", ")"), + ] { + let marked = format!("// 한글\r\n{marked}"); + let start = marked.find('@').unwrap(); + let source = marked.replace('@', ""); + let tokens = Lexer::new_with_file(&source, "unclosed.wave") + .tokenize() + .unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert_eq!(error.found(), Some("Eof"), "{source}: {error:?}"); + assert_eq!( + error.span().unwrap().start, + source.len(), + "{source}: {error:?}" + ); + assert_eq!(error.related().len(), 1, "{source}: {error:?}"); + let related = &error.related()[0]; + assert_eq!(related.span.start, start, "{source}: {error:?}"); + assert_eq!(related.span.end, start + 1); + assert_eq!(related.span.file, "unclosed.wave"); + assert!( + related.message.contains(&format!("expected '{closer}'")), + "{error:?}" + ); + let no_spans = parse_syntax_only(&tokens).unwrap_err(); + assert!(no_spans + .note() + .unwrap() + .contains(&format!("expected '{closer}'"))); + } +} + +#[test] +fn earlier_errors_do_not_acquire_unrelated_unclosed_delimiters() { + for source in ["fun f() { object.;", "fun f() { call(1 ];"] { + let tokens = Lexer::new(source).tokenize().unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert!(error.related().is_empty(), "{source}: {error:?}"); + } +} + +#[test] +fn balanced_nested_delimiters_parse_without_extra_validation_rules() { + let source = "fun f(a: array) { call(Box { value: (a[0] + a[1]) }); }"; + let tokens = Lexer::new(source).tokenize().unwrap(); + parse_syntax_with_spans(&tokens).unwrap(); +} diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index 52e3e340..a236d424 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -17,6 +17,7 @@ //! supplies platform startup/default-library arguments. use crate::codegen::target::{llvm_triple_for_abi, target_spec_for_triple, CodegenTarget}; +use crate::diagnostic::{CodegenError, CodegenPhase, PendingOutput}; use std::env; use std::path::PathBuf; use std::process::Command; @@ -43,6 +44,76 @@ fn is_windows_gnu_target(target: Option<&str>) -> bool { .is_some_and(|spec| spec.os == "windows" && spec.env == "gnu") } +pub fn is_windows_msvc_target(target: &str) -> bool { + target_spec_for_triple(target).is_some_and(|spec| spec.os == "windows" && spec.env == "msvc") +} + +/// Native COFF flags for an explicitly selected MSVC output target. SDK and +/// VC library discovery follows link.exe/lld-link's LIB environment or -L. +pub fn msvc_link_args( + target: &str, + objects: &[String], + output: &str, + libs: &[String], + paths: &[String], + no_default_libs: bool, + static_crt: bool, + shared: bool, + entry: Option<&str>, + extra: &[String], +) -> Vec { + let machine = if target.starts_with("aarch64-") { + "ARM64" + } else { + "X64" + }; + let mut args = vec![ + "/NOLOGO".into(), + format!("/MACHINE:{machine}"), + format!("/OUT:{output}"), + ]; + if shared { + args.push("/DLL".into()); + } else { + args.push("/SUBSYSTEM:CONSOLE".into()); + } + if let Some(entry) = entry { + args.push(format!("/ENTRY:{entry}")); + } + args.extend(objects.iter().cloned()); + args.extend(paths.iter().map(|path| format!("/LIBPATH:{path}"))); + args.extend(libs.iter().map(|lib| { + if lib.to_ascii_lowercase().ends_with(".lib") { + lib.clone() + } else { + format!("{lib}.lib") + } + })); + if no_default_libs { + args.push("/NODEFAULTLIB".into()); + } else { + let crt = if static_crt { + ["libcmt", "libvcruntime", "libucrt"] + } else { + ["msvcrt", "vcruntime", "ucrt"] + }; + args.extend( + crt.into_iter() + .chain([ + "legacy_stdio_definitions", + "kernel32", + "user32", + "advapi32", + "shell32", + "ws2_32", + ]) + .map(|lib| format!("/DEFAULTLIB:{lib}.lib")), + ); + } + args.extend(extra.iter().cloned()); + args +} + fn is_wasm_target(target: Option<&str>) -> bool { target .and_then(target_spec_for_triple) @@ -63,8 +134,9 @@ pub fn compile_ir_to_object( file_stem: &str, opt_flag: &str, backend: &BackendOptions, -) -> String { +) -> Result { let object_path = format!("{}.o", file_stem); + let pending = PendingOutput::new(std::path::Path::new(&object_path))?; let normalized_opt = normalize_llvm_opt_flag(opt_flag); let llc = resolve_bundled_tool("llc"); @@ -108,26 +180,35 @@ pub fn compile_ir_to_object( .arg("--filetype=obj") .arg("-") .arg("-o") - .arg(&object_path) + .arg(pending.path()) .stdin(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) .spawn() - .expect("Failed to execute llc"); + .map_err(|e| CodegenError::tool_launch(CodegenPhase::Tool, "llc", e))?; use std::io::Write; - child - .stdin - .as_mut() - .unwrap() - .write_all(ir.as_bytes()) - .unwrap(); - - let output = child.wait_with_output().unwrap(); + let mut stdin = child.stdin.take().expect("piped child stdin"); + let (written, output) = std::thread::scope(|scope| { + let writer = scope.spawn(move || stdin.write_all(ir.as_bytes())); + let output = child.wait_with_output(); + (writer.join(), output) + }); + let output = output.map_err(|e| CodegenError::new(CodegenPhase::Tool, "wait for llc", e))?; if !output.status.success() { - eprintln!("llc failed: {}", String::from_utf8_lossy(&output.stderr)); - return String::new(); + return Err(CodegenError::new( + CodegenPhase::Tool, + "llc", + String::from_utf8_lossy(&output.stderr), + )); } - - object_path + written + .map_err(|_| { + CodegenError::new(CodegenPhase::Tool, "write llc input", "input writer failed") + })? + .map_err(|e| CodegenError::new(CodegenPhase::Tool, "write llc input", e))?; + pending.commit()?; + Ok(object_path) } pub fn link_objects( @@ -136,7 +217,8 @@ pub fn link_objects( libs: &[String], lib_paths: &[String], backend: &BackendOptions, -) { +) -> Result<(), CodegenError> { + let pending = PendingOutput::new(std::path::Path::new(output))?; let target = backend.target.as_deref().unwrap_or(""); let linker_bin = backend .linker @@ -145,6 +227,31 @@ pub fn link_objects( let mut cmd = Command::new(&linker_bin); configure_bundled_llvm_tool_env(&mut cmd, &linker_bin); + if is_windows_msvc_target(target) { + cmd.args(msvc_link_args( + target, + objects, + &pending.path().to_string_lossy(), + libs, + lib_paths, + backend.no_default_libs, + false, + false, + None, + &backend.link_args, + )); + let result = cmd + .output() + .map_err(|e| CodegenError::tool_launch(CodegenPhase::Link, "lld-link", e))?; + if !result.status.success() { + return Err(CodegenError::new( + CodegenPhase::Link, + "MSVC link (use matching Windows SDK/UCRT/VC libraries via LIB or -L)", + String::from_utf8_lossy(&result.stderr), + )); + } + return pending.commit(); + } if is_wasm_target(Some(target)) { cmd.arg("--no-entry") .arg("--allow-undefined") @@ -172,7 +279,7 @@ pub fn link_objects( cmd.arg(arg); } - cmd.arg("-o").arg(output); + cmd.arg("-o").arg(pending.path()); if !backend.no_default_libs && !is_wasm_target(Some(target)) { if is_darwin_target(target) { @@ -182,13 +289,23 @@ pub fn link_objects( } } - let output = cmd.output().expect("Failed to link"); + let output = cmd + .output() + .map_err(|e| CodegenError::tool_launch(CodegenPhase::Link, &linker_bin, e))?; if !output.status.success() { - eprintln!("link failed: {}", String::from_utf8_lossy(&output.stderr)); + return Err(CodegenError::new( + CodegenPhase::Link, + linker_bin, + String::from_utf8_lossy(&output.stderr), + )); } + pending.commit() } fn default_lld_for_target(target: &str) -> String { + if is_windows_msvc_target(target) { + return resolve_bundled_tool("lld-link"); + } if is_wasm_target(Some(target)) { resolve_bundled_tool("wasm-ld") } else if is_darwin_target(target) { diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index 7c9dc597..44ef0558 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -120,7 +120,9 @@ fn integer_extension_for_target(target: CodegenTarget, ty: &WaveType) -> Option< | CodegenTarget::LinuxArm64 | CodegenTarget::FreestandingArm64 | CodegenTarget::WindowsX86_64Gnu - | CodegenTarget::WindowsArm64Gnu => None, + | CodegenTarget::WindowsX86_64Msvc + | CodegenTarget::WindowsArm64Gnu + | CodegenTarget::WindowsArm64Msvc => None, CodegenTarget::Wasm32Unknown | CodegenTarget::Wasm32WasiP1 | CodegenTarget::Wasm64Unknown => narrow_extension(), @@ -470,9 +472,8 @@ fn classify_param_x86_64_windows<'ctx>( .custom_width_int_type((size * 8) as u32) .as_basic_type_enum(), ), - _ => ParamLowering::ByVal { + _ => ParamLowering::Indirect { ty: t.as_any_type_enum(), - align: td.get_abi_alignment(&t) as u32, }, }, _ => ParamLowering::Direct(t), @@ -977,11 +978,14 @@ fn classify_param<'ctx>( | CodegenTarget::DarwinX86_64 | CodegenTarget::FreeBsdX86_64 | CodegenTarget::FreestandingX86_64 => classify_param_x86_64_sysv(context, td, t), - CodegenTarget::WindowsX86_64Gnu => classify_param_x86_64_windows(context, td, t), + CodegenTarget::WindowsX86_64Gnu | CodegenTarget::WindowsX86_64Msvc => { + classify_param_x86_64_windows(context, td, t) + } CodegenTarget::FreeBsdArm64 | CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::WindowsArm64Gnu + | CodegenTarget::WindowsArm64Msvc | CodegenTarget::FreestandingArm64 => classify_param_arm64(context, td, t), CodegenTarget::FreeBsdRISCV64 | CodegenTarget::LinuxRISCV64 @@ -1007,11 +1011,14 @@ fn classify_ret<'ctx>( | CodegenTarget::DarwinX86_64 | CodegenTarget::FreeBsdX86_64 | CodegenTarget::FreestandingX86_64 => classify_ret_x86_64_sysv(context, td, t), - CodegenTarget::WindowsX86_64Gnu => classify_ret_x86_64_windows(context, td, t), + CodegenTarget::WindowsX86_64Gnu | CodegenTarget::WindowsX86_64Msvc => { + classify_ret_x86_64_windows(context, td, t) + } CodegenTarget::FreeBsdArm64 | CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 | CodegenTarget::WindowsArm64Gnu + | CodegenTarget::WindowsArm64Msvc | CodegenTarget::FreestandingArm64 => classify_ret_arm64(context, td, t), CodegenTarget::FreeBsdRISCV64 | CodegenTarget::LinuxRISCV64 diff --git a/llvm/src/codegen/address.rs b/llvm/src/codegen/address.rs index 27376a23..17c2291a 100644 --- a/llvm/src/codegen/address.rs +++ b/llvm/src/codegen/address.rs @@ -103,12 +103,18 @@ fn load_ptr_from_slot<'ctx>( fn pointee_ty_of_ptr_expr<'ctx>( context: &'ctx Context, expr: &Expression, + program: &TypedProgram, variables: &HashMap>, struct_types: &HashMap>, ) -> BasicTypeEnum<'ctx> { + if let Some(parser::hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = + program.type_of(expr) + { + return wave_type_to_llvm_type(context, inner, struct_types, TypeFlavor::AbiC); + } match expr { Expression::Grouped(inner) => { - pointee_ty_of_ptr_expr(context, inner, variables, struct_types) + pointee_ty_of_ptr_expr(context, inner, program, variables, struct_types) } Expression::Variable(name) => { @@ -136,12 +142,20 @@ fn pointee_ty_of_ptr_expr<'ctx>( fn struct_ty_of_ptr_expr<'ctx>( context: &'ctx Context, expr: &Expression, + program: &TypedProgram, variables: &HashMap>, struct_types: &HashMap>, ) -> StructType<'ctx> { + if let Some(parser::hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = + program.type_of(expr) + { + if let WaveType::Struct(name) = inner.as_ref() { + return struct_types[name]; + } + } match expr { Expression::Grouped(inner) => { - struct_ty_of_ptr_expr(context, inner, variables, struct_types) + struct_ty_of_ptr_expr(context, inner, program, variables, struct_types) } Expression::Variable(name) => { @@ -187,7 +201,11 @@ fn addr_and_ty<'ctx>( struct_field_indices: &HashMap>, ) -> (PointerValue<'ctx>, BasicTypeEnum<'ctx>) { match expr { - Expression::Grouped(inner) => addr_and_ty( + Expression::Cast { + expr: inner, + target_type: WaveType::Pointer(_), + } + | Expression::Grouped(inner) => addr_and_ty( context, builder, program, @@ -246,7 +264,8 @@ fn addr_and_ty<'ctx>( let pv = load_ptr_from_slot(context, builder, slot_ptr, "deref_target"); - let pointee_ty = pointee_ty_of_ptr_expr(context, inner, variables, struct_types); + let pointee_ty = + pointee_ty_of_ptr_expr(context, inner, program, variables, struct_types); (pv, pointee_ty) } @@ -268,7 +287,8 @@ fn addr_and_ty<'ctx>( BasicTypeEnum::StructType(st) => (obj_addr, st), BasicTypeEnum::PointerType(_) => { let p = load_ptr_from_slot(context, builder, obj_addr, "obj_load"); - let st = struct_ty_of_ptr_expr(context, object, variables, struct_types); + let st = + struct_ty_of_ptr_expr(context, object, program, variables, struct_types); (p, st) } other => panic!("FieldAccess on non-struct object type: {:?}", other), @@ -334,7 +354,8 @@ fn addr_and_ty<'ctx>( BasicTypeEnum::PointerType(_) => { let base_ptr = load_ptr_from_slot(context, builder, t_addr, "idx_base_load"); - let pointee = pointee_ty_of_ptr_expr(context, target, variables, struct_types); + let pointee = + pointee_ty_of_ptr_expr(context, target, program, variables, struct_types); // ptr-to-array: gep [0, idx] if let BasicTypeEnum::ArrayType(at) = pointee { diff --git a/llvm/src/codegen/consts.rs b/llvm/src/codegen/consts.rs index c94d685a..a2860123 100644 --- a/llvm/src/codegen/consts.rs +++ b/llvm/src/codegen/consts.rs @@ -334,6 +334,16 @@ fn const_from_expected<'ctx>( } } + Expression::Literal(Literal::Bool(value)) => match expected { + BasicTypeEnum::IntType(ty) => { + Ok(ty.const_int(u64::from(*value), false).as_basic_value_enum()) + } + _ => Err(ConstEvalError::TypeMismatch { + expected: type_name(expected), + got: "bool".into(), + note: "boolean constant requires integer storage".into(), + }), + }, // --- ints --- Expression::Literal(Literal::Int(s)) => match expected { BasicTypeEnum::FloatType(float_ty) => { diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index ae7e7d77..83163b25 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -17,6 +17,7 @@ //! initialization is process-wide, while each compilation receives its own LLVM //! context and module. +use crate::diagnostic::{CodegenError, CodegenPhase, PendingOutput}; use inkwell::attributes::{Attribute, AttributeLoc}; use inkwell::context::Context; use inkwell::module::{FlagBehavior, Linkage, Module}; @@ -39,9 +40,7 @@ use std::collections::HashMap; use std::sync::Once; use crate::backend::BackendOptions; -use crate::codegen::target::{ - llvm_triple_for_abi, require_supported_target_from_triple, CodegenTarget, -}; +use crate::codegen::target::{llvm_triple_for_abi, target_spec_for_triple, CodegenTarget}; use crate::statement::generate_statement_ir; use super::consts::{create_llvm_const_value, ConstEvalError}; @@ -407,7 +406,10 @@ fn is_implicit_i32_main(name: &str, return_type: &Option) -> bool { fn is_supported_extern_abi(abi: &str, target: CodegenTarget) -> bool { match target { - CodegenTarget::WindowsX86_64Gnu | CodegenTarget::WindowsArm64Gnu => { + CodegenTarget::WindowsX86_64Gnu + | CodegenTarget::WindowsX86_64Msvc + | CodegenTarget::WindowsArm64Gnu + | CodegenTarget::WindowsArm64Msvc => { abi.eq_ignore_ascii_case("c") || abi.eq_ignore_ascii_case("system") } _ => abi.eq_ignore_ascii_case("c"), @@ -416,8 +418,11 @@ fn is_supported_extern_abi(abi: &str, target: CodegenTarget) -> bool { fn supported_extern_abi_description(target: CodegenTarget) -> &'static str { match target { - CodegenTarget::WindowsX86_64Gnu | CodegenTarget::WindowsArm64Gnu => "'c' and 'system'", - _ => "'c'; Windows 'system' is accepted only on Windows GNU targets", + CodegenTarget::WindowsX86_64Gnu + | CodegenTarget::WindowsX86_64Msvc + | CodegenTarget::WindowsArm64Gnu + | CodegenTarget::WindowsArm64Msvc => "'c' and 'system'", + _ => "'c'; Windows 'system' is accepted only on Windows targets", } } @@ -429,58 +434,78 @@ fn normalize_opt_flag_for_passes(opt_flag: &str) -> &str { } } -fn target_opt_level_from_flag(opt_flag: &str) -> OptimizationLevel { - match normalize_opt_flag_for_passes(opt_flag) { +fn target_opt_level_from_flag(opt_flag: &str) -> Result { + Ok(match normalize_opt_flag_for_passes(opt_flag) { "" | "-O0" => OptimizationLevel::None, "-O1" => OptimizationLevel::Less, "-O2" | "-Os" | "-Oz" => OptimizationLevel::Default, "-O3" => OptimizationLevel::Aggressive, - other => panic!("unknown opt flag for target machine: {}", other), - } + other => { + return Err(CodegenError::new( + CodegenPhase::Target, + "optimization level", + other, + )) + } + }) } -fn code_model_from_backend(backend: &BackendOptions, target: CodegenTarget) -> CodeModel { +fn code_model_from_backend( + backend: &BackendOptions, + target: CodegenTarget, +) -> Result { if let Some(model) = backend.code_model.as_deref() { - return match model { + return Ok(match model { "default" => CodeModel::Default, "jitdefault" | "jit-default" => CodeModel::JITDefault, "small" => CodeModel::Small, "kernel" => CodeModel::Kernel, "medium" => CodeModel::Medium, "large" => CodeModel::Large, - other => panic!("unsupported -C code-model={}", other), - }; + other => return Err(CodegenError::new(CodegenPhase::Target, "code-model", other)), + }); } - match target { + Ok(match target { CodegenTarget::FreestandingX86_64 => CodeModel::Kernel, _ => CodeModel::Default, - } + }) } -fn reloc_mode_from_backend(backend: &BackendOptions, target: CodegenTarget) -> RelocMode { +fn reloc_mode_from_backend( + backend: &BackendOptions, + target: CodegenTarget, +) -> Result { if let Some(model) = backend.relocation_model.as_deref() { - return match model { + return Ok(match model { "default" => RelocMode::Default, "static" => RelocMode::Static, "pic" | "pie" => RelocMode::PIC, "dynamic-no-pic" | "dynamic_no_pic" => RelocMode::DynamicNoPic, - other => panic!("unsupported -C relocation-model={}", other), - }; + other => { + return Err(CodegenError::new( + CodegenPhase::Target, + "relocation-model", + other, + )) + } + }); } - if backend.freestanding - || matches!( - target, - CodegenTarget::FreestandingX86_64 - | CodegenTarget::FreestandingArm64 - | CodegenTarget::FreestandingRISCV64 - ) - { - RelocMode::Static - } else { - RelocMode::Default - } + Ok( + if backend.freestanding + || matches!( + target, + CodegenTarget::FreestandingX86_64 + | CodegenTarget::FreestandingArm64 + | CodegenTarget::FreestandingRISCV64 + ) + { + RelocMode::Static + } else { + RelocMode::Default + }, + ) } static INIT_LLVM_TARGETS: Once = Once::new(); @@ -622,20 +647,28 @@ fn build_wasi_start_wrapper<'ctx>( builder: &inkwell::builder::Builder<'ctx>, module: &Module<'ctx>, target: CodegenTarget, -) { +) -> Result<(), CodegenError> { if target != CodegenTarget::Wasm32WasiP1 { - return; + return Ok(()); } let Some(main) = module.get_function("main") else { - return; + return Ok(()); }; if main.count_params() != 0 || main.get_type().get_return_type() != Some(context.i32_type().into()) { - panic!("wasm32-wasip1 requires 'main' to take no parameters and return i32 or omit its return type"); + return Err(CodegenError::new( + CodegenPhase::Validation, + "WASI entry", + "main must take no parameters and return i32 or omit its return type", + )); } if module.get_function("_start").is_some() { - panic!("wasm32-wasip1 reserves '_start' for its command entry point"); + return Err(CodegenError::new( + CodegenPhase::Validation, + "WASI entry", + "_start is reserved for the command entry point", + )); } let exit_type = context @@ -648,13 +681,14 @@ fn build_wasi_start_wrapper<'ctx>( apply_wasm_export_attr(context, start, target, "_start"); let block = context.append_basic_block(start, "entry"); builder.position_at_end(block); - let call = builder.build_call(main, &[], "main_status").unwrap(); + let call = builder.build_call(main, &[], "main_status")?; let status = match call.try_as_basic_value() { ValueKind::Basic(value) => value.into_int_value(), ValueKind::Instruction(_) => unreachable!("validated WASI main returned void"), }; - builder.build_call(proc_exit, &[status.into()], "").unwrap(); - builder.build_unreachable().unwrap(); + builder.build_call(proc_exit, &[status.into()], "")?; + builder.build_unreachable()?; + Ok(()) } /// Builds an LLVM module and returns its textual representation. @@ -668,9 +702,9 @@ pub unsafe fn generate_ir( program: &TypedProgram, opt_flag: &str, backend: &BackendOptions, -) -> String { - let generated = build_module(program, opt_flag, backend); - generated.module.print_to_string().to_string() +) -> Result { + let generated = build_module(program, opt_flag, backend)?; + Ok(generated.module.print_to_string().to_string()) } /// Builds a module and emits one target-machine output file. @@ -686,44 +720,118 @@ pub unsafe fn emit_codegen_file( backend: &BackendOptions, output: &std::path::Path, kind: CodegenFileKind, -) { - let generated = build_module(program, opt_flag, backend); - +) -> Result<(), CodegenError> { + let generated = build_module(program, opt_flag, backend)?; + let pending = PendingOutput::new(output)?; match kind { CodegenFileKind::Bitcode => { - if !generated.module.write_bitcode_to_path(output) { - panic!("failed to write LLVM bitcode to '{}'", output.display()); + if !generated.module.write_bitcode_to_path(pending.path()) { + return Err(CodegenError::new( + CodegenPhase::Emission, + "write bitcode", + output.display(), + )); } } - CodegenFileKind::Assembly => generated - .target_machine - .write_to_file(generated.module, FileType::Assembly, output) - .unwrap_or_else(|e| { - panic!( - "failed to emit LLVM assembly to '{}': {}", - output.display(), - e.to_string() - ) - }), - CodegenFileKind::Object => generated - .target_machine - .write_to_file(generated.module, FileType::Object, output) - .unwrap_or_else(|e| { - panic!( - "failed to emit object file to '{}': {}", - output.display(), - e.to_string() - ) - }), + CodegenFileKind::Assembly | CodegenFileKind::Object => { + let file_type = if matches!(kind, CodegenFileKind::Assembly) { + FileType::Assembly + } else { + FileType::Object + }; + generated + .target_machine + .write_to_file(generated.module, file_type, pending.path()) + .map_err(|e| { + CodegenError::new( + CodegenPhase::Emission, + format!("write {}", output.display()), + e, + ) + })?; + } } + pending.commit() } fn build_module( program: &TypedProgram, opt_flag: &str, backend: &BackendOptions, -) -> GeneratedModule { +) -> Result { + let lowered; + let has_async = program.syntax().iter().any(|node| match node { + ASTNode::Function(f) => f.is_async, + ASTNode::Struct(s) => s.methods.iter().any(|f| f.is_async), + ASTNode::ProtoImpl(p) => p.methods.iter().any(|f| f.is_async), + _ => false, + }); + let program = if has_async { + let ast = parser::async_lower::lower_program(program).map_err(|e| { + CodegenError::new(CodegenPhase::Lowering, "async state machine", e.message) + .with_span(e.span) + })?; + lowered = parser::hir::TypedProgram::lower(ast).map_err(|e| { + CodegenError::new( + CodegenPhase::Lowering, + "validate async state machine", + e.to_string(), + ) + })?; + &lowered + } else { + program + }; let ast_nodes = program.syntax(); + let uses_tasks = program.uses_async_runtime(); + for (symbol, span) in program.async_runtime_requirements() { + let declaration = ast_nodes.iter().find_map(|node| match node { + ASTNode::Function(function) + if function + .export + .as_ref() + .and_then(|export| export.symbol.as_deref()) + == Some(symbol) => + { + Some(function) + } + _ => None, + }); + let Some(function) = declaration else { + return Err(CodegenError::new( + CodegenPhase::Validation, "async runtime", + format!("missing task runtime entry point '{symbol}'; import std::task and use an executor supported by the selected target"), + ).with_span(span)); + }; + if let Some((parameters, result, abi)) = parser::async_intrinsics::runtime_signature(symbol) + { + let actual = function + .parameters + .iter() + .map(|parameter| parameter.param_type.clone()) + .collect::>(); + if actual != parameters + || function.return_type.as_ref().unwrap_or(&WaveType::Void) != &result + || function.export.as_ref().map(|export| export.abi.as_str()) != Some(abi) + || !function.generic_params.is_empty() + || function.is_async + { + return Err(CodegenError::new( + CodegenPhase::Validation, + "async runtime", + format!("task runtime entry point '{symbol}' has an incompatible signature"), + ) + .with_span(span)); + } + } + } + if uses_tasks && backend.freestanding { + return Err(CodegenError::new( + CodegenPhase::Validation, + "async runtime", + "the standard task executor requires a hosted target", + )); + } codegen_trace("initialize targets"); initialize_llvm_targets(); @@ -745,24 +853,41 @@ fn build_module( } else { TargetMachine::get_default_triple() }; - let abi_target = require_supported_target_from_triple(&requested_triple); + let raw_target = requested_triple.as_str().to_string_lossy(); + let spec = target_spec_for_triple(&raw_target).ok_or_else(|| { + CodegenError::new( + CodegenPhase::Target, + "select target", + format!("unsupported target or disabled LLVM backend: {raw_target}"), + ) + })?; + let abi_target = spec.codegen; + crate::codegen::target::resolve_target_options( + spec, + backend.cpu.as_deref(), + backend.features.as_deref(), + backend.abi.as_deref(), + ) + .map_err(|e| CodegenError::new(CodegenPhase::Target, "resolve target options", e))?; + crate::codegen::validation::validate(program, abi_target)?; let triple = TargetTriple::create(&llvm_triple_for_abi( requested_triple.as_str().to_str().unwrap_or_default(), backend.abi.as_deref(), )); let disable_red_zone = should_disable_red_zone(backend, abi_target); codegen_trace("lookup target"); - let target = Target::from_triple(&triple).unwrap(); + let target = Target::from_triple(&triple) + .map_err(|e| CodegenError::new(CodegenPhase::Target, "lookup LLVM target", e))?; let cpu = backend.cpu.as_deref().unwrap_or("generic"); let features = backend.features.as_deref().unwrap_or(""); - let reloc_mode = reloc_mode_from_backend(backend, abi_target); - let code_model = code_model_from_backend(backend, abi_target); + let reloc_mode = reloc_mode_from_backend(backend, abi_target)?; + let code_model = code_model_from_backend(backend, abi_target)?; codegen_trace("create target machine"); let mut target_options = TargetMachineOptions::new() .set_cpu(cpu) .set_features(features) - .set_level(target_opt_level_from_flag(opt_flag)) + .set_level(target_opt_level_from_flag(opt_flag)?) .set_reloc_mode(reloc_mode) .set_code_model(code_model); if abi_target.architecture() != super::arch::Architecture::LoongArch64 { @@ -772,7 +897,13 @@ fn build_module( } let tm = target .create_target_machine_from_options(&triple, target_options) - .unwrap(); + .ok_or_else(|| { + CodegenError::new( + CodegenPhase::Target, + "create target machine", + raw_target.as_ref(), + ) + })?; codegen_trace("set target metadata"); module.set_triple(&triple); @@ -889,7 +1020,11 @@ fn build_module( next_pending.push(v); } Err(e) => { - panic!("const '{}' evaluation failed: {}", v.name, e); + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!("const '{}' evaluation failed: {}", v.name, e), + )); } } } @@ -899,10 +1034,14 @@ fn build_module( } if !progressed { let names: Vec = next_pending.iter().map(|v| v.name.clone()).collect(); - panic!( - "unresolved const cycle or missing symbols after {} rounds: {:?}", - round, names - ); + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!( + "unresolved const cycle or missing symbols after {} rounds: {:?}", + round, names + ), + )); } pending = next_pending; @@ -930,7 +1069,19 @@ fn build_module( &global_consts, Some(program), ) - .unwrap_or_else(|e| panic!("static '{}' initialization failed: {}", v.name, e)) + .map_err(|e| { + CodegenError::new( + CodegenPhase::Lowering, + format!("initialize static {}", v.name), + e, + ) + .with_span( + program + .expression_id(expr) + .and_then(|id| program.expression_span(id)) + .cloned(), + ) + })? } else { llvm_ty.const_zero().as_basic_value_enum() }; @@ -966,6 +1117,14 @@ fn build_module( }); } } + ASTNode::Struct(structure) if structure.generic_params.is_empty() => { + for method in &structure.methods { + function_nodes.push(FunctionCodegenEntry { + symbol: format!("{}_{}", structure.name, method.name), + node: method, + }); + } + } _ => {} } } @@ -992,13 +1151,17 @@ fn build_module( let symbol = &entry.symbol; if let Some(export) = export { if !is_supported_extern_abi(&export.abi, abi_target) { - panic!( + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!( "unsupported export ABI '{}' for function '{}' on {}: supported ABIs are {}", export.abi, name, abi_target.desc(), supported_extern_abi_description(abi_target) - ); + ), + )); } } @@ -1118,13 +1281,17 @@ fn build_module( for ext in &extern_functions { if !is_supported_extern_abi(&ext.abi, abi_target) { - panic!( - "unsupported extern ABI '{}' for function '{}' on {}: supported ABIs are {}", - ext.abi, - ext.name, - abi_target.desc(), - supported_extern_abi_description(abi_target) - ); + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!( + "unsupported extern ABI '{}' for function '{}' on {}: supported ABIs are {}", + ext.abi, + ext.name, + abi_target.desc(), + supported_extern_abi_description(abi_target) + ), + )); } let lowered = lower_extern_c( @@ -1148,7 +1315,9 @@ fn build_module( for entry in &function_nodes { let func_node = entry.node; - let function = *functions.get(&entry.symbol).unwrap(); + let function = *functions + .get(&entry.symbol) + .expect("validated function lowering invariant"); let entry_block = context.append_basic_block(function, "entry"); builder.position_at_end(entry_block); @@ -1160,9 +1329,11 @@ fn build_module( for (i, param) in func_node.parameters.iter().enumerate() { let llvm_type = wave_type_to_llvm_type(context, ¶m.param_type, &struct_types, TypeFlavor::AbiC); - let alloca = builder.build_alloca(llvm_type, ¶m.name).unwrap(); - let param_val = function.get_nth_param(i as u32).unwrap(); - builder.build_store(alloca, param_val).unwrap(); + let alloca = builder.build_alloca(llvm_type, ¶m.name)?; + let param_val = function + .get_nth_param(i as u32) + .expect("validated function lowering invariant"); + builder.build_store(alloca, param_val)?; variables.insert( param.name.clone(), @@ -1201,11 +1372,17 @@ fn build_module( program, ); } else { - panic!("Unsupported node inside function '{}'", func_node.name); + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!("Unsupported node inside function '{}'", func_node.name), + )); } } - let current_block = builder.get_insert_block().unwrap(); + let current_block = builder + .get_insert_block() + .expect("validated function lowering invariant"); if current_block.get_terminator().is_none() { let implicit_i32_main = is_implicit_i32_main(&func_node.name, &func_node.return_type); let is_void_like = match &func_node.return_type { @@ -1216,16 +1393,20 @@ fn build_module( if implicit_i32_main { let zero = context.i32_type().const_zero(); - builder.build_return(Some(&zero)).unwrap(); + builder.build_return(Some(&zero))?; } else if func_node.return_type == Some(WaveType::Never) { - builder.build_unreachable().unwrap(); + builder.build_unreachable()?; } else if is_void_like { - builder.build_return(None).unwrap(); + builder.build_return(None)?; } else { - panic!( - "Non-void function '{}' is missing a return statement", - func_node.name - ); + return Err(CodegenError::new( + CodegenPhase::Lowering, + "lower program", + format!( + "Non-void function '{}' is missing a return statement", + func_node.name + ), + )); } } } @@ -1234,37 +1415,46 @@ fn build_module( build_export_c_wrapper(context, builder, td, export); } - build_wasi_start_wrapper(context, builder, module, abi_target); + build_wasi_start_wrapper(context, builder, module, abi_target)?; + module + .verify() + .map_err(|e| CodegenError::new(CodegenPhase::Lowering, "verify LLVM module", e))?; if should_run_llvm_pass_pipeline() { let pbo = PassBuilderOptions::create(); - let pipeline = pipeline_from_opt_flag(opt_flag); + let pipeline = pipeline_from_opt_flag(opt_flag)?; codegen_trace("run optimization passes"); module .run_passes(pipeline, &tm, pbo) - .expect("failed to run optimization passes"); + .map_err(|e| CodegenError::new(CodegenPhase::Optimization, "run LLVM passes", e))?; } else { codegen_trace("skip optimization passes"); } codegen_trace("finish module"); - GeneratedModule { + Ok(GeneratedModule { module, target_machine: tm, - } + }) } -fn pipeline_from_opt_flag(opt_flag: &str) -> &'static str { - match normalize_opt_flag_for_passes(opt_flag) { +fn pipeline_from_opt_flag(opt_flag: &str) -> Result<&'static str, CodegenError> { + Ok(match normalize_opt_flag_for_passes(opt_flag) { "" | "-O0" => "default", "-O1" => "default", "-O2" => "default", "-O3" => "default", "-Os" => "default", "-Oz" => "default", - other => panic!("unknown opt flag for LLVM passes: {}", other), - } + other => { + return Err(CodegenError::new( + CodegenPhase::Optimization, + "pass pipeline", + other, + )) + } + }) } fn parse_int_literal(raw: &str) -> Option { diff --git a/llvm/src/codegen/mod.rs b/llvm/src/codegen/mod.rs index 982bdec2..623ba5d8 100644 --- a/llvm/src/codegen/mod.rs +++ b/llvm/src/codegen/mod.rs @@ -36,3 +36,5 @@ pub use types::{wave_type_to_llvm_type, VariableInfo}; pub use legacy::{create_alloc, get_llvm_type}; pub(crate) mod number; + +mod validation; diff --git a/llvm/src/codegen/plan.rs b/llvm/src/codegen/plan.rs index 699e32c6..0da5acdc 100644 --- a/llvm/src/codegen/plan.rs +++ b/llvm/src/codegen/plan.rs @@ -214,48 +214,48 @@ fn is_noreturn_pseudo_clobber(token: &str) -> bool { ) } -fn normalize_clobber_item(target: CodegenTarget, s: &str) -> String { +fn normalize_clobber_item(target: CodegenTarget, s: &str) -> Result { let t = s.trim(); if let Some(inner) = t.strip_prefix("~{").and_then(|x| x.strip_suffix('}')) { let n = normalize_token(inner); if let Some(special) = normalize_special_clobber(target, &n) { - return special; + return Ok(special); } if let Some(pg) = arch::register_group(target.architecture(), &n) { - return format!("~{{{}}}", pg); + return Ok(format!("~{{{}}}", pg)); } - panic!("Invalid clobber token: '{}'", inner); + return Err(format!("Invalid clobber token: '{}'", inner)); } if let Some(inner) = t.strip_prefix('{').and_then(|x| x.strip_suffix('}')) { let n = normalize_token(inner); if let Some(special) = normalize_special_clobber(target, &n) { - return special; + return Ok(special); } if let Some(pg) = arch::register_group(target.architecture(), &n) { - return format!("~{{{}}}", pg); + return Ok(format!("~{{{}}}", pg)); } - panic!("Invalid clobber token: '{}'", inner); + return Err(format!("Invalid clobber token: '{}'", inner)); } // specials (plain) let lower = t.to_ascii_lowercase(); if let Some(special) = normalize_special_clobber(target, &lower) { - return special; + return Ok(special); } if let Some(pg) = arch::register_group(target.architecture(), &normalize_token(t)) { - return format!("~{{{}}}", pg); + return Ok(format!("~{{{}}}", pg)); } - panic!("Invalid clobber token: '{}'", t); + return Err(format!("Invalid clobber token: '{}'", t)); } fn merge_clobbers( @@ -263,7 +263,7 @@ fn merge_clobbers( mut base: Vec, user: &[String], used_phys: &HashSet, -) -> Vec { +) -> Result, String> { let mut seen: HashSet = base.iter().cloned().collect(); for raw in user { @@ -274,15 +274,15 @@ fn merge_clobbers( continue; } - let c = normalize_clobber_item(target, raw); + let c = normalize_clobber_item(target, raw)?; if let Some(inner) = c.strip_prefix("~{").and_then(|x| x.strip_suffix('}')) { let inner_norm = normalize_token(inner); if used_phys.contains(&inner_norm) { - panic!( + return Err(format!( "clobber '{}' conflicts with an input/output operand register", raw - ); + )); } } @@ -291,7 +291,7 @@ fn merge_clobbers( } } - base + Ok(base) } #[derive(Debug, Clone, Copy)] @@ -301,7 +301,7 @@ struct StackContract { noreturn_declared: bool, } -fn stack_contract_from_user_clobbers(user: &[String]) -> StackContract { +fn stack_contract_from_user_clobbers(user: &[String]) -> Result { let mut stack_declared = false; let mut nostack_declared = false; let mut noreturn_declared = false; @@ -319,14 +319,16 @@ fn stack_contract_from_user_clobbers(user: &[String]) -> StackContract { } if stack_declared && nostack_declared { - panic!("asm cannot declare both clobber(\"stack\") and clobber(\"nostack\")"); + return Err( + "asm cannot declare both clobber(\"stack\") and clobber(\"nostack\")".to_string(), + ); } - StackContract { + Ok(StackContract { stack_declared, nostack_declared, noreturn_declared, - } + }) } fn asm_stack_analysis(target: CodegenTarget, instructions: &[String]) -> arch::StackAnalysis { @@ -348,58 +350,69 @@ fn validate_stack_contract( target: CodegenTarget, instructions: &[String], contract: StackContract, -) { +) -> Result<(), String> { let analysis = asm_stack_analysis(target, instructions); if analysis.touches_stack && !contract.stack_declared { - panic!( - "asm touches the stack or performs a call/return; declare clobber(\"stack\") to make the stack contract explicit" - ); + return Err("asm touches the stack or performs a call/return; declare clobber(\"stack\") to make the stack contract explicit".to_string()); } if analysis.touches_stack && contract.nostack_declared { - panic!("asm declares clobber(\"nostack\") but touches the stack or performs a call/return"); + return Err( + "asm declares clobber(\"nostack\") but touches the stack or performs a call/return" + .to_string(), + ); } if analysis.nonreturning_branch && !contract.noreturn_declared { - panic!( - "asm contains a non-returning branch; declare clobber(\"noreturn\") so codegen can terminate the block explicitly" - ); + return Err("asm contains a non-returning branch; declare clobber(\"noreturn\") so codegen can terminate the block explicitly".to_string()); } if analysis.unknown_stack_write && !contract.noreturn_declared { - panic!( - "asm writes the stack pointer in a way codegen cannot prove balanced; restore the original stack pointer or declare clobber(\"noreturn\")" - ); + return Err("asm writes the stack pointer in a way codegen cannot prove balanced; restore the original stack pointer or declare clobber(\"noreturn\")".to_string()); } if analysis.unbalanced_delta != 0 && !contract.noreturn_declared { - panic!( + return Err(format!( "asm stack delta is not balanced ({} bytes); restore the stack pointer or declare clobber(\"noreturn\")", analysis.unbalanced_delta - ); + )); } + Ok(()) } impl<'a> AsmPlan<'a> { + /// Used only after the public emission boundary validates every asm block. pub fn build( + target: CodegenTarget, + instructions: &'a [String], + inputs: &'a [(String, Expression)], + outputs: &'a [(String, Expression)], + clobbers: &'a [String], + mode: AsmSafetyMode, + ) -> Self { + Self::try_build(target, instructions, inputs, outputs, clobbers, mode) + .expect("asm contract was validated before LLVM lowering") + } + + pub fn try_build( target: CodegenTarget, instructions: &'a [String], inputs_raw: &'a [(String, Expression)], outputs_raw: &'a [(String, Expression)], user_clobbers_raw: &'a [String], mode: AsmSafetyMode, - ) -> Self { + ) -> Result { if matches!( target, CodegenTarget::Wasm32Unknown | CodegenTarget::Wasm32WasiP1 | CodegenTarget::Wasm64Unknown ) { - panic!( + return Err(format!( "inline assembly is not supported for {}; use a WebAssembly host import instead", target.desc() - ); + )); } let asm_code = instructions.join("\n"); @@ -413,8 +426,8 @@ impl<'a> AsmPlan<'a> { asm_code }; let asm_code = gcc_percent_to_llvm_dollar(&asm_code); - let stack_contract = stack_contract_from_user_clobbers(user_clobbers_raw); - validate_stack_contract(target, instructions, stack_contract); + let stack_contract = stack_contract_from_user_clobbers(user_clobbers_raw)?; + validate_stack_contract(target, instructions, stack_contract)?; // outputs let mut used_out_phys: HashSet = HashSet::new(); @@ -425,19 +438,19 @@ impl<'a> AsmPlan<'a> { let t = parse_token(target, reg); if t.phys_group.is_none() && !is_valid_constraint_class(&t.raw_norm) { - panic!( + return Err(format!( "asm output register/constraint '{}' is not valid for target {:?}", reg, target - ); + )); } // real reg outputs: disallow duplicates by physical group if let Some(pg) = &t.phys_group { if !used_out_phys.insert(pg.clone()) { - panic!( + return Err(format!( "Register '{}' duplicated in asm outputs (same phys group '{}')", reg, pg - ); + )); } // enable tied input only when exact same token used (ex: out("rax") + in("rax")) out_index_by_exact_reg.insert(t.raw_norm.clone(), outputs.len()); @@ -461,19 +474,19 @@ impl<'a> AsmPlan<'a> { let t = parse_token(target, reg); if t.phys_group.is_none() && !is_valid_constraint_class(&t.raw_norm) { - panic!( + return Err(format!( "asm input register/constraint '{}' is not valid for target {:?}", reg, target - ); + )); } // real reg inputs: disallow duplicates by physical group if let Some(pg) = &t.phys_group { if !used_in_phys.insert(pg.clone()) { - panic!( + return Err(format!( "Register '{}' duplicated in asm inputs (same phys group '{}')", reg, pg - ); + )); } // tied only when exact same reg token matches a real-reg output token @@ -515,9 +528,9 @@ impl<'a> AsmPlan<'a> { } let default_clobbers = build_default_clobbers(target, mode, inputs_raw, outputs_raw); - let clobbers = merge_clobbers(target, default_clobbers, user_clobbers_raw, &used_phys); + let clobbers = merge_clobbers(target, default_clobbers, user_clobbers_raw, &used_phys)?; - Self { + Ok(Self { asm_code, outputs, inputs, @@ -525,7 +538,7 @@ impl<'a> AsmPlan<'a> { has_side_effects: true, align_stack: stack_contract.stack_declared, noreturn: stack_contract.noreturn_declared, - } + }) } pub fn constraints_string(&self) -> String { diff --git a/llvm/src/codegen/target.rs b/llvm/src/codegen/target.rs index f161457b..fa566690 100644 --- a/llvm/src/codegen/target.rs +++ b/llvm/src/codegen/target.rs @@ -33,6 +33,8 @@ pub enum CodegenTarget { DarwinArm64, WindowsX86_64Gnu, WindowsArm64Gnu, + WindowsX86_64Msvc, + WindowsArm64Msvc, FreeBsdX86_64, FreeBsdArm64, FreeBsdRISCV64, @@ -404,6 +406,24 @@ const WINDOWS_PC_X86_64_GNU: TargetSpec = TargetSpec { default_abi: None, }; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] +const WINDOWS_PC_X86_64_MSVC: TargetSpec = TargetSpec { + triple: "x86_64-pc-windows-msvc", + codegen: CodegenTarget::WindowsX86_64Msvc, + architecture: Architecture::X86_64, + vendor: "pc", + os: "windows", + env: "msvc", + object_format: "coff", + hosted: true, + cpus: arch::x86_64::CPUS, + features: arch::x86_64::FEATURES, + abis: &[], + default_cpu: arch::x86_64::DEFAULT_CPU, + default_features: arch::x86_64::DEFAULT_FEATURES, + default_abi: None, +}; + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] const FREEBSD_X86_64: TargetSpec = TargetSpec { triple: "x86_64-unknown-freebsd", @@ -530,6 +550,24 @@ const WINDOWS_PC_AARCH64_GNU: TargetSpec = TargetSpec { default_abi: None, }; +#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] +const WINDOWS_PC_AARCH64_MSVC: TargetSpec = TargetSpec { + triple: "aarch64-pc-windows-msvc", + codegen: CodegenTarget::WindowsArm64Msvc, + architecture: Architecture::Aarch64, + vendor: "pc", + os: "windows", + env: "msvc", + object_format: "coff", + hosted: true, + cpus: arch::aarch64::CPUS, + features: arch::aarch64::FEATURES, + abis: &[], + default_cpu: arch::aarch64::DEFAULT_CPU, + default_features: arch::aarch64::DEFAULT_FEATURES, + default_abi: None, +}; + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] const FREESTANDING_AARCH64: TargetSpec = TargetSpec { triple: "aarch64-unknown-none-elf", @@ -684,6 +722,7 @@ pub fn supported_target_specs() -> Vec<&'static TargetSpec> { &DARWIN_X86_64, &WINDOWS_W64_X86_64_GNU, &WINDOWS_PC_X86_64_GNU, + &WINDOWS_PC_X86_64_MSVC, &FREEBSD_X86_64, &FREESTANDING_X86_64, ]); @@ -695,6 +734,7 @@ pub fn supported_target_specs() -> Vec<&'static TargetSpec> { &FREEBSD_AARCH64, &WINDOWS_AARCH64_GNU, &WINDOWS_PC_AARCH64_GNU, + &WINDOWS_PC_AARCH64_MSVC, &FREESTANDING_AARCH64, ]); @@ -743,12 +783,14 @@ impl CodegenTarget { Self::LinuxX86_64 | Self::DarwinX86_64 | Self::WindowsX86_64Gnu + | Self::WindowsX86_64Msvc | Self::FreeBsdX86_64 | Self::FreestandingX86_64 => Architecture::X86_64, Self::LinuxArm64 | Self::FreeBsdArm64 | Self::DarwinArm64 | Self::WindowsArm64Gnu + | Self::WindowsArm64Msvc | Self::FreestandingArm64 => Architecture::Aarch64, Self::LinuxRISCV64 | Self::FreeBsdRISCV64 | Self::FreestandingRISCV64 => { Architecture::Riscv64 @@ -779,7 +821,9 @@ impl CodegenTarget { Self::LinuxArm64 => "linux arm64", Self::DarwinX86_64 => "darwin x86_64", Self::DarwinArm64 => "darwin arm64", + Self::WindowsX86_64Msvc => "windows x86_64 msvc", Self::WindowsX86_64Gnu => "windows x86_64 gnu", + Self::WindowsArm64Msvc => "windows arm64 msvc", Self::WindowsArm64Gnu => "windows arm64 gnu", Self::FreeBsdX86_64 => "freebsd x86_64", Self::FreeBsdArm64 => "freebsd arm64", diff --git a/llvm/src/codegen/types.rs b/llvm/src/codegen/types.rs index c24b58ed..c21cf6d9 100644 --- a/llvm/src/codegen/types.rs +++ b/llvm/src/codegen/types.rs @@ -81,6 +81,7 @@ pub fn wave_type_to_llvm_type<'ctx>( WaveType::Void => context.i8_type().as_basic_type_enum(), + WaveType::Future(_) => context.i64_type().as_basic_type_enum(), WaveType::Pointer(_inner) => context .ptr_type(AddressSpace::default()) .as_basic_type_enum(), diff --git a/llvm/src/codegen/validation.rs b/llvm/src/codegen/validation.rs new file mode 100644 index 00000000..72f22392 --- /dev/null +++ b/llvm/src/codegen/validation.rs @@ -0,0 +1,293 @@ +//! Target-dependent user errors are rejected before LLVM lowering starts. +use super::{ + plan::{AsmPlan, AsmSafetyMode}, + target::CodegenTarget, +}; +use crate::diagnostic::{CodegenError, CodegenPhase}; +use parser::ast::{ASTNode, Expression, StatementNode}; +use parser::hir::TypedProgram; + +pub(crate) fn validate(program: &TypedProgram, target: CodegenTarget) -> Result<(), CodegenError> { + let mut state = State { + program, + target, + error: None, + }; + walk_nodes(program.syntax(), &mut state); + state.error.map_or(Ok(()), Err) +} +struct State<'a> { + program: &'a TypedProgram, + target: CodegenTarget, + error: Option, +} +impl State<'_> { + fn block( + &mut self, + instructions: &[String], + inputs: &[(String, Expression)], + outputs: &[(String, Expression)], + clobbers: &[String], + expression: bool, + span: Option, + ) { + let result = AsmPlan::try_build( + self.target, + instructions, + inputs, + outputs, + clobbers, + AsmSafetyMode::ConservativeKernel, + ) + .and_then(|plan| { + if expression && plan.noreturn { + return Err("asm expression cannot declare clobber(\"noreturn\")".to_string()); + } + if expression && plan.outputs.len() > 1 { + return Err("asm expression supports at most one output".to_string()); + } + Ok(()) + }); + if let Err(message) = result { + self.error = Some( + CodegenError::new(CodegenPhase::Validation, "inline assembly", message) + .invalid_assembly() + .with_span(span), + ); + } + } + fn node(&mut self, node: &ASTNode) { + if self.error.is_some() { + return; + } + if let ASTNode::Statement(StatementNode::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + }) = node + { + let span = self + .program + .node_id(node) + .and_then(|id| self.program.node_span(id)) + .cloned(); + self.block(instructions, inputs, outputs, clobbers, false, span); + } + } + fn expression(&mut self, expression: &Expression) { + if self.error.is_some() { + return; + } + if let Expression::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + } = expression + { + let span = self + .program + .expression_id(expression) + .and_then(|id| self.program.expression_span(id)) + .cloned(); + self.block(instructions, inputs, outputs, clobbers, true, span); + } + } +} + +fn walk_nodes(nodes: &[ASTNode], state: &mut State<'_>) { + for node in nodes { + walk_node(node, state); + } +} + +fn walk_node(node: &ASTNode, state: &mut State<'_>) { + state.node(node); + if state.error.is_some() { + return; + } + match node { + ASTNode::Located { value, .. } => walk_node(value, state), + ASTNode::Function(function) => { + for parameter in &function.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, state); + } + } + walk_nodes(&function.body, state); + } + ASTNode::Struct(structure) => { + if !structure.generic_params.is_empty() { + return; + } + for method in &structure.methods { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, state); + } + } + walk_nodes(&method.body, state); + } + } + ASTNode::ProtoImpl(implementation) => { + for method in &implementation.methods { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, state); + } + } + walk_nodes(&method.body, state); + } + } + ASTNode::Statement(statement) => walk_statement(statement, state), + ASTNode::Variable(variable) => { + if let Some(initializer) = &variable.initial_value { + walk_expression(initializer, state); + } + } + ASTNode::Expression(expression) => walk_expression(expression, state), + ASTNode::ExternFunction(_) + | ASTNode::Program(_) + | ASTNode::TypeAlias(_) + | ASTNode::Enum(_) + | ASTNode::Variant(_) => {} + } +} + +fn walk_statement(statement: &StatementNode, state: &mut State<'_>) { + match statement { + StatementNode::PrintFormat { args, .. } + | StatementNode::PrintlnFormat { args, .. } + | StatementNode::Input { args, .. } => { + for argument in args { + walk_expression(argument, state); + } + } + StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + } => { + walk_expression(condition, state); + walk_nodes(body, state); + if let Some(blocks) = else_if_blocks { + for (condition, body) in blocks.iter() { + walk_expression(condition, state); + walk_nodes(body, state); + } + } + if let Some(body) = else_block { + walk_nodes(body, state); + } + } + StatementNode::For { + initialization, + condition, + increment, + body, + } => { + walk_node(initialization, state); + walk_expression(condition, state); + walk_expression(increment, state); + walk_nodes(body, state); + } + StatementNode::While { condition, body } => { + walk_expression(condition, state); + walk_nodes(body, state); + } + StatementNode::Match { value, arms } => { + walk_expression(value, state); + for arm in arms { + walk_nodes(&arm.body, state); + } + } + StatementNode::Assign { value, .. } => walk_expression(value, state), + StatementNode::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + walk_expression(expression, state); + } + } + StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { + walk_expression(expression, state) + } + StatementNode::Print(_) + | StatementNode::Println(_) + | StatementNode::Variable(_) + | StatementNode::Import(_) + | StatementNode::Break + | StatementNode::Continue + | StatementNode::Return(None) => {} + } +} + +fn walk_expression(expression: &Expression, state: &mut State<'_>) { + if let Expression::Located { value, .. } = expression { + walk_expression(value, state); + return; + } + state.expression(expression); + if state.error.is_some() { + return; + } + match expression { + Expression::Located { value, .. } => walk_expression(value, state), + Expression::StructLiteral { fields, .. } => { + for (_, value) in fields { + walk_expression(value, state); + } + } + Expression::FunctionCall { args, .. } => { + for argument in args { + walk_expression(argument, state); + } + } + Expression::MethodCall { object, args, .. } => { + walk_expression(object, state); + for argument in args { + walk_expression(argument, state); + } + } + Expression::Deref(inner) + | Expression::AddressOf(inner) + | Expression::Await(inner) + | Expression::Grouped(inner) + | Expression::Unary { expr: inner, .. } + | Expression::Cast { expr: inner, .. } + | Expression::FieldAccess { object: inner, .. } + | Expression::IncDec { target: inner, .. } => walk_expression(inner, state), + Expression::BinaryExpression { left, right, .. } + | Expression::IndexAccess { + target: left, + index: right, + } + | Expression::AssignOperation { + target: left, + value: right, + .. + } + | Expression::Assignment { + target: left, + value: right, + } => { + walk_expression(left, state); + walk_expression(right, state); + } + Expression::ArrayLiteral(values) => { + for value in values { + walk_expression(value, state); + } + } + Expression::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + walk_expression(expression, state); + } + } + Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {} + } +} diff --git a/llvm/src/codegen/variants.rs b/llvm/src/codegen/variants.rs index 44a5f48e..b652c536 100644 --- a/llvm/src/codegen/variants.rs +++ b/llvm/src/codegen/variants.rs @@ -183,6 +183,11 @@ fn resolve_payload_type( templates: &HashMap, ) -> WaveType { match ty { + WaveType::Future(inner) => WaveType::Future(Box::new(resolve_payload_type( + inner, + substitutions, + templates, + ))), WaveType::Pointer(inner) => WaveType::Pointer(Box::new(resolve_payload_type( inner, substitutions, @@ -247,6 +252,7 @@ fn display_wave_type(ty: &WaveType) -> String { WaveType::Char => "char".to_string(), WaveType::Byte => "byte".to_string(), WaveType::String => "str".to_string(), + WaveType::Future(inner) => format!("Future<{}>", display_wave_type(inner)), WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), WaveType::Array(inner, length) => format!("array<{},{}>", display_wave_type(inner), length), WaveType::Void => "void".to_string(), @@ -260,7 +266,9 @@ fn collect_type_variants(ty: &WaveType, names: &mut BTreeSet) { WaveType::Variant(name) => { names.insert(name.clone()); } - WaveType::Pointer(inner) | WaveType::Array(inner, _) => collect_type_variants(inner, names), + WaveType::Future(inner) | WaveType::Pointer(inner) | WaveType::Array(inner, _) => { + collect_type_variants(inner, names) + } _ => {} } } diff --git a/llvm/src/diagnostic.rs b/llvm/src/diagnostic.rs new file mode 100644 index 00000000..34508ae2 --- /dev/null +++ b/llvm/src/diagnostic.rs @@ -0,0 +1,152 @@ +//! Recoverable backend failures. Panics remain reserved for compiler invariants. +use std::fmt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CodegenPhase { + Target, + Validation, + Lowering, + Optimization, + Emission, + Tool, + Link, +} +impl fmt::Display for CodegenPhase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Target => "target-configuration", + Self::Validation => "lowering-validation", + Self::Lowering => "ir-generation", + Self::Optimization => "optimization", + Self::Emission => "file-emission", + Self::Tool => "external-tool", + Self::Link => "linking", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CodegenErrorKind { + BackendFailure, + InvalidAssembly, + MissingTool, +} + +#[derive(Debug, Clone)] +pub struct CodegenError { + pub phase: CodegenPhase, + pub kind: CodegenErrorKind, + pub operation: String, + pub message: String, + pub span: Option>, +} +impl CodegenError { + pub fn new(phase: CodegenPhase, operation: impl Into, message: impl ToString) -> Self { + Self { + phase, + kind: CodegenErrorKind::BackendFailure, + operation: operation.into(), + message: message.to_string(), + span: None, + } + } + pub fn tool_launch(phase: CodegenPhase, tool: &str, error: std::io::Error) -> Self { + let missing = error.kind() == std::io::ErrorKind::NotFound; + let mut diagnostic = Self::new(phase, format!("launch {tool}"), error); + if missing { + diagnostic.kind = CodegenErrorKind::MissingTool; + } + diagnostic + } + pub fn invalid_assembly(mut self) -> Self { + self.kind = CodegenErrorKind::InvalidAssembly; + self + } + pub fn with_span(mut self, span: Option) -> Self { + self.span = span.map(Box::new); + self + } +} +impl fmt::Display for CodegenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}: {}", self.phase, self.operation, self.message) + } +} +impl std::error::Error for CodegenError {} +impl From for CodegenError { + fn from(error: inkwell::builder::BuilderError) -> Self { + Self::new(CodegenPhase::Lowering, "LLVM builder", error) + } +} + +/// Stage outputs alongside the destination. A failed producer cannot truncate +/// a previously successful artifact or leave a partial artifact at its path. +pub struct PendingOutput { + path: PathBuf, + destination: PathBuf, +} +impl PendingOutput { + pub fn new(destination: &Path) -> Result { + static NEXT: AtomicU64 = AtomicU64::new(0); + let parent = destination + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + for _ in 0..128 { + let path = parent.join(format!( + ".wave-output-{}-{}.tmp", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + { + Ok(_) => { + return Ok(Self { + path, + destination: destination.to_owned(), + }) + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => { + return Err(CodegenError::new( + CodegenPhase::Emission, + "create output", + e, + )) + } + } + } + Err(CodegenError::new( + CodegenPhase::Emission, + "create output", + "temporary output name collision", + )) + } + pub fn path(&self) -> &Path { + &self.path + } + pub fn commit(self) -> Result<(), CodegenError> { + let length = std::fs::metadata(&self.path) + .map_err(|e| CodegenError::new(CodegenPhase::Emission, "inspect output", e))? + .len(); + if length == 0 { + return Err(CodegenError::new( + CodegenPhase::Emission, + "inspect output", + "producer emitted an empty output", + )); + } + std::fs::rename(&self.path, &self.destination) + .map_err(|e| CodegenError::new(CodegenPhase::Emission, "publish output", e)) + } +} +impl Drop for PendingOutput { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.path); + } +} diff --git a/llvm/src/expression/rvalue/asm.rs b/llvm/src/expression/rvalue/asm.rs index 9dcd2ebc..d7536fd8 100644 --- a/llvm/src/expression/rvalue/asm.rs +++ b/llvm/src/expression/rvalue/asm.rs @@ -21,11 +21,11 @@ use crate::codegen::arch; use crate::codegen::plan::*; use crate::codegen::target::require_supported_target_from_module; use crate::codegen::types::{wave_type_to_llvm_type, TypeFlavor}; -use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum, StringRadix}; +use inkwell::types::{BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; use inkwell::values::{ AsValueRef, BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; -use parser::ast::{Expression, Literal, WaveType}; +use parser::ast::{Expression, WaveType}; pub(crate) fn gen<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, @@ -134,129 +134,22 @@ fn resolve_expr_out_type<'ctx, 'a>( env: &ExprGenEnv<'ctx, 'a>, target: &Expression, ) -> BasicTypeEnum<'ctx> { - match target { - Expression::Variable(name) => { - let info = env - .variables - .get(name) - .unwrap_or_else(|| panic!("Output var '{}' not found", name)); - llvm_type_of_wave(env, &info.ty) - } - - Expression::Deref(inner) => match inner.as_ref() { - Expression::Variable(name) => { - let info = env - .variables - .get(name) - .unwrap_or_else(|| panic!("Pointer var '{}' not found", name)); - - match &info.ty { - WaveType::Pointer(inner_ty) => llvm_type_of_wave(env, inner_ty), - WaveType::String => env.context.i8_type().as_basic_type_enum(), - other => panic!( - "asm expr out(*{}) requires pointer/string, got {:?}", - name, other - ), - } - } - other => panic!("Unsupported expr deref output: {:?}", other), - }, - - other => panic!( - "asm expr out(...) target must be variable or deref(var) for now: {:?}", - other - ), - } + llvm_type_of_wave( + env, + &env.wave_type(target) + .expect("asm output has a validated HIR type"), + ) } fn eval_asm_in_expr<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, - e: &Expression, + expression: &Expression, ) -> BasicValueEnum<'ctx> { - match e { - Expression::Literal(Literal::Int(n)) => { - let s = n.as_str(); - let (neg, digits) = if let Some(rest) = s.strip_prefix('-') { - (true, rest) - } else { - (false, s) - }; - - let ty = env.context.i64_type(); - let mut iv = ty - .const_int_from_string(digits, StringRadix::Decimal) - .unwrap_or_else(|| panic!("invalid int literal: {}", s)); - - if neg { - iv = iv.const_neg(); - } - iv.as_basic_value_enum() - } - - Expression::Variable(name) => { - let info = env - .variables - .get(name) - .unwrap_or_else(|| panic!("Input variable '{}' not found", name)); - - let ty = llvm_type_of_wave(env, &info.ty); - env.builder - .build_load(ty, info.ptr, &format!("asm_in_load_{}", name)) - .unwrap() - .as_basic_value_enum() - } - - Expression::AddressOf(inner) => match inner.as_ref() { - Expression::Variable(name) => { - let info = env - .variables - .get(name) - .unwrap_or_else(|| panic!("Input variable '{}' not found", name)); - info.ptr.as_basic_value_enum() - } - _ => panic!("Unsupported address-of input: {:?}", inner), - }, - - Expression::Deref(inner) => match inner.as_ref() { - Expression::Variable(name) => { - let info = env - .variables - .get(name) - .unwrap_or_else(|| panic!("Input pointer '{}' not found", name)); - - // 1) load pointer value from the variable slot (typed load) - let ptr_ty = match &info.ty { - WaveType::Pointer(_) | WaveType::String => llvm_type_of_wave(env, &info.ty), - other => panic!("deref input '{}' is not a pointer type: {:?}", name, other), - }; - - let pv_val = env - .builder - .build_load(ptr_ty, info.ptr, "asm_in_ptr") - .unwrap(); - - let pv = match pv_val { - BasicValueEnum::PointerValue(p) => p, - _ => panic!("deref input '{}' loaded value is not a pointer", name), - }; - - // 2) load pointee value (must be typed, opaque pointer safe) - let pointee_ty = match &info.ty { - WaveType::Pointer(inner_ty) => llvm_type_of_wave(env, inner_ty), - WaveType::String => env.context.i8_type().as_basic_type_enum(), - _ => unreachable!(), - }; - - env.builder - .build_load(pointee_ty, pv, "asm_in_deref") - .unwrap() - .as_basic_value_enum() - } - _ => panic!("Unsupported deref input: {:?}", inner), - }, - - other => panic!("Unsupported asm input expr: {:?}", other), - } + let expected = env + .wave_type(expression) + .map(|ty| llvm_type_of_wave(env, &ty)) + .or_else(|| Some(env.context.i64_type().into())); + env.gen(expression, expected) } fn meta_val_type<'ctx>(v: &BasicMetadataValueEnum<'ctx>) -> BasicMetadataTypeEnum<'ctx> { diff --git a/llvm/src/expression/rvalue/assign.rs b/llvm/src/expression/rvalue/assign.rs index 36f0bdb0..8829cffd 100644 --- a/llvm/src/expression/rvalue/assign.rs +++ b/llvm/src/expression/rvalue/assign.rs @@ -79,6 +79,9 @@ fn basic_to_wave<'ctx, 'a>( } fn wave_type_of_lvalue<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> Option { + if let Some(ty) = env.wave_type(e) { + return Some(ty); + } match e { Expression::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()), Expression::Grouped(inner) => wave_type_of_lvalue(env, inner), diff --git a/llvm/src/expression/rvalue/async_runtime.rs b/llvm/src/expression/rvalue/async_runtime.rs new file mode 100644 index 00000000..d3f29638 --- /dev/null +++ b/llvm/src/expression/rvalue/async_runtime.rs @@ -0,0 +1,198 @@ +//! Private task runtime calls. Frame allocation sizes use the selected LLVM layout. +use super::ExprGenEnv; +use crate::codegen::types::{wave_type_to_llvm_type, TypeFlavor}; +use inkwell::{ + values::{BasicMetadataValueEnum, BasicValueEnum}, + IntPredicate, +}; +use parser::ast::{Expression, Literal, WaveType}; +fn runtime<'ctx>( + env: &ExprGenEnv<'ctx, '_>, + name: &str, + args: &[BasicMetadataValueEnum<'ctx>], +) -> BasicValueEnum<'ctx> { + let function = env + .module + .get_function(name) + .expect("async runtime validated before emission"); + env.builder + .build_call(function, args, "task") + .unwrap() + .try_as_basic_value() + .basic() + .unwrap_or_else(|| env.context.i8_type().const_zero().into()) +} +pub(crate) fn gen<'ctx>( + env: &mut ExprGenEnv<'ctx, '_>, + name: &str, + types: &[WaveType], + args: &[Expression], +) -> BasicValueEnum<'ctx> { + let i64t = env.context.i64_type(); + match name { + "__wave_async_alloc" => { + let ty = + wave_type_to_llvm_type(env.context, &types[0], env.struct_types, TypeFlavor::Value); + runtime( + env, + "__wave_task_alloc", + &[i64t + .const_int(env.target_data.get_abi_size(&ty), false) + .into()], + ) + } + "__wave_async_create" => { + let frame = env.gen(&args[0], None); + let result = env.gen(&args[1], None); + let Expression::Literal(Literal::String(name)) = args[2].unspanned() else { + unreachable!("generated poll symbol") + }; + let poll = env + .module + .get_function(name) + .expect("declared async resume function") + .as_global_value() + .as_pointer_value(); + let ty = + wave_type_to_llvm_type(env.context, &types[0], env.struct_types, TypeFlavor::Value); + runtime( + env, + "__wave_task_new", + &[ + frame.into(), + i64t.const_int(env.target_data.get_abi_size(&ty), false) + .into(), + result.into(), + poll.into(), + ], + ) + } + "__wave_async_take" | "__wave_async_block_on" => { + let Some(WaveType::Future(result)) = env.wave_type(&args[0]) else { + unreachable!("typed future operand") + }; + let id = env.gen(&args[0], Some(i64t.into())); + if name == "__wave_async_block_on" { + runtime(env, "__wave_task_drive", &[id.into()]); + } + let pointer = runtime(env, "__wave_task_result", &[id.into()]).into_pointer_value(); + let result = if *result == WaveType::Void { + env.context.i8_type().const_zero().into() + } else { + let ty = wave_type_to_llvm_type( + env.context, + &result, + env.struct_types, + TypeFlavor::Value, + ); + env.builder.build_load(ty, pointer, "task_result").unwrap() + }; + runtime(env, "__wave_task_release", &[id.into()]); + result + } + "__wave_async_interest" | "__wave_async_sleep" => { + let values = if name == "__wave_async_sleep" { + let ms = env.gen(&args[0], Some(i64t.into())); + vec![ + i64t.const_all_ones().into(), + env.context.i32_type().const_zero().into(), + ms.into(), + ] + } else { + let fd = env.gen(&args[0], Some(i64t.into())); + let flags = env.gen(&args[1], Some(env.context.i32_type().into())); + let ms = env.gen(&args[2], Some(i64t.into())); + vec![fd.into(), flags.into(), ms.into()] + }; + runtime(env, "__wave_task_interest", &values) + } + "__wave_async_windows_notify_address" => env + .module + .get_function("__wave_task_windows_notify") + .expect("Windows wait callback declared by std::task") + .as_global_value() + .as_pointer_value() + .into(), + "__wave_async_io" => { + let fd = env.gen(&args[0], Some(i64t.into())); + let buffer = env.gen(&args[1], None); + let length = env.gen(&args[2], Some(i64t.into())); + let writing = env.gen(&args[3], Some(env.context.i32_type().into())); + let timeout = env.gen(&args[4], Some(i64t.into())); + runtime( + env, + "__wave_task_io", + &[ + fd.into(), + buffer.into(), + length.into(), + writing.into(), + timeout.into(), + ], + ) + } + "__wave_async_invoke" => { + let callback = env.gen(&args[0], None).into_pointer_value(); + let frame = env.gen(&args[1], None); + let id = env.gen(&args[2], Some(i64t.into())); + let ty = env.context.bool_type().fn_type( + &[env.context.ptr_type(Default::default()).into(), i64t.into()], + false, + ); + env.builder + .build_indirect_call(ty, callback, &[frame.into(), id.into()], "resume") + .unwrap() + .try_as_basic_value() + .basic() + .unwrap() + } + "__wave_async_free_slot" => { + let Some(WaveType::Pointer(t)) = env.wave_type(&args[0]) else { + unreachable!() + }; + let pointer = env.gen(&args[0], None); + let ty = wave_type_to_llvm_type(env.context, &t, env.struct_types, TypeFlavor::Value); + runtime( + env, + "__wave_task_free", + &[ + pointer.into(), + i64t.const_int(env.target_data.get_abi_size(&ty), false) + .into(), + ], + ) + } + _ => { + let target = match name { + "__wave_async_ready" => "__wave_task_ready", + "__wave_async_wait" => "__wave_task_wait", + "__wave_async_complete" => "__wave_task_complete", + "__wave_async_spawn" => "__wave_task_spawn", + "__wave_async_cancel" => "__wave_task_cancel", + "__wave_async_cancel_join" => "__wave_task_cancel_join", + "__wave_async_yield" => "__wave_task_yield", + "__wave_async_shutdown" => "__wave_task_shutdown", + "__wave_async_close_fd" => "__wave_task_close_fd", + _ => unreachable!("known async intrinsic"), + }; + let values = args + .iter() + .map(|a| env.gen(a, Some(i64t.into())).into()) + .collect::>(); + let result = runtime(env, target, &values); + if matches!(name, "__wave_async_ready" | "__wave_async_cancel") { + env.builder + .build_int_compare( + IntPredicate::NE, + result.into_int_value(), + env.context.i32_type().const_zero(), + "task_flag", + ) + .unwrap() + .into() + } else { + result + } + } + } +} diff --git a/llvm/src/expression/rvalue/calls.rs b/llvm/src/expression/rvalue/calls.rs index 46f3ebce..fdfe72f9 100644 --- a/llvm/src/expression/rvalue/calls.rs +++ b/llvm/src/expression/rvalue/calls.rs @@ -23,7 +23,7 @@ use crate::codegen::abi_c::{ RetLowering, }; use crate::statement::variable::{coerce_basic_value, wave_type_is_unsigned, CoercionMode}; -use inkwell::types::{AnyTypeEnum, AsTypeRef, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; +use inkwell::types::{AnyTypeEnum, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; use inkwell::values::{ BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; @@ -138,30 +138,6 @@ fn unpack_int_to_agg<'ctx, 'a>( .as_basic_value_enum() } -fn normalize_struct_name(raw: &str) -> &str { - raw.strip_prefix("struct.") - .unwrap_or(raw) - .trim_start_matches('%') -} - -fn resolve_struct_key<'ctx>( - st: inkwell::types::StructType<'ctx>, - struct_types: &std::collections::HashMap>, -) -> String { - if let Some(raw) = st.get_name().and_then(|n| n.to_str().ok()) { - return normalize_struct_name(raw).to_string(); - } - - let st_ref = st.as_type_ref(); - for (name, ty) in struct_types { - if ty.as_type_ref() == st_ref { - return name.clone(); - } - } - - panic!("LLVM struct type has no name and cannot be matched to struct_types"); -} - fn lower_c_variadic_argument<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, expression: &Expression, @@ -204,192 +180,56 @@ fn lower_c_variadic_argument<'ctx, 'a>( } } -fn infer_struct_name_for_method<'ctx, 'a>( - env: &ExprGenEnv<'ctx, 'a>, - object: &Expression, - obj_preview: BasicValueEnum<'ctx>, -) -> Option { - match obj_preview.get_type() { - BasicTypeEnum::StructType(st) => return Some(resolve_struct_key(st, env.struct_types)), - _ => {} - } - - let wt = env.wave_type(object)?; - match wt { - WaveType::Struct(name) => Some(name), - WaveType::Pointer(inner) => match *inner { - WaveType::Struct(name) => Some(name), - _ => None, - }, - _ => None, - } -} - pub(crate) fn gen_method_call<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, object: &Expression, name: &str, args: &[Expression], ) -> BasicValueEnum<'ctx> { - // struct method sugar: obj.method(...) - if let Expression::Variable(var_name) = object { - if let Some(var_info) = env.variables.get(var_name) { - if let WaveType::Struct(struct_name) = &var_info.ty { - let fn_name = format!("{}_{}", struct_name, name); - - let function = env - .module - .get_function(&fn_name) - .unwrap_or_else(|| panic!("Function '{}' not found", fn_name)); - - let fn_type = function.get_type(); - let param_types = fn_type.get_param_types(); - let expected_self = opt_meta_to_opt_basic(param_types.get(0).cloned()); - - let obj_val = env.gen(object, expected_self); - - let mut call_args: Vec = Vec::new(); - call_args.push(obj_val.into()); - - for (i, arg_expr) in args.iter().enumerate() { - let expected_ty = opt_meta_to_opt_basic(param_types.get(i + 1).cloned()); - let mut arg_val = env.gen(arg_expr, expected_ty); - if let Some(et) = expected_ty { - arg_val = coerce_basic_value( - env.context, - env.builder, - arg_val, - et, - &format!("arg{}_cast", i), - CoercionMode::Implicit, - wave_type_is_unsigned(env.wave_type(arg_expr).as_ref()), - ); - } - call_args.push(arg_val.into()); - } - - let call_site = env - .builder - .build_call(function, &call_args, &format!("call_{}", fn_name)) - .unwrap(); - - if function.get_type().get_return_type().is_some() { - return callsite_to_ret(call_site, true, "struct method").unwrap(); - } else { - return env.context.i32_type().const_zero().as_basic_value_enum(); - } - } - } - } - - // Attempt "Struct_Method" dispatch by looking at object type (WaveType or LLVM struct value) - { - let obj_preview = env.gen(object, None); - - if let Some(struct_name) = infer_struct_name_for_method(env, object, obj_preview) { - let fn_name = format!("{}_{}", struct_name, name); - - if let Some(function) = env.module.get_function(&fn_name) { - let fn_type = function.get_type(); - let param_types = fn_type.get_param_types(); - let expected_self = opt_meta_to_opt_basic(param_types.get(0).cloned()); - - let mut obj_val = obj_preview; - if let Some(et) = expected_self { - obj_val = coerce_basic_value( - env.context, - env.builder, - obj_val, - et, - "self_cast", - CoercionMode::Implicit, - wave_type_is_unsigned(env.wave_type(object).as_ref()), - ); - } - - let mut call_args: Vec = Vec::new(); - call_args.push(obj_val.into()); - - for (i, arg_expr) in args.iter().enumerate() { - let expected_ty = opt_meta_to_opt_basic(param_types.get(i + 1).cloned()); - let mut arg_val = env.gen(arg_expr, expected_ty); - if let Some(et) = expected_ty { - arg_val = coerce_basic_value( - env.context, - env.builder, - arg_val, - et, - &format!("arg{}_cast", i), - CoercionMode::Implicit, - wave_type_is_unsigned(env.wave_type(arg_expr).as_ref()), - ); - } - call_args.push(arg_val.into()); - } - - let call_site = env - .builder - .build_call(function, &call_args, &format!("call_{}", fn_name)) - .unwrap(); - - if function.get_type().get_return_type().is_some() { - return callsite_to_ret(call_site, true, "method dispatch").unwrap(); - } else { - return env.context.i32_type().const_zero().as_basic_value_enum(); - } - } - } - } - - // method-style call: fn(self, ...) - let function = env - .module - .get_function(name) - .unwrap_or_else(|| panic!("Function '{}' not found for method-style call", name)); - - let fn_type = function.get_type(); - let param_types = fn_type.get_param_types(); - - if param_types.is_empty() { - panic!( - "Method-style call {}() requires at least 1 parameter (self)", - name - ); - } - - let expected_self = opt_meta_to_opt_basic(param_types.get(0).cloned()); - let obj_val = env.gen(object, expected_self); - - let mut call_args: Vec = Vec::new(); - call_args.push(obj_val.into()); - - for (i, arg_expr) in args.iter().enumerate() { - let expected_ty = opt_meta_to_opt_basic(param_types.get(i + 1).cloned()); - let mut arg_val = env.gen(arg_expr, expected_ty); - if let Some(et) = expected_ty { - arg_val = coerce_basic_value( + // Resolve dispatch from semantic facts before evaluating the receiver. + // Preview codegen would execute side effects twice on free-function fallback. + let receiver_type = env.wave_type(object); + let owner = match receiver_type.as_ref() { + Some(WaveType::Struct(name)) => Some(name.as_str()), + Some(WaveType::Pointer(inner)) => match inner.as_ref() { + WaveType::Struct(name) => Some(name.as_str()), + _ => None, + }, + _ => None, + }; + let method = owner.and_then(|owner| env.module.get_function(&format!("{owner}_{name}"))); + let function = method + .or_else(|| env.module.get_function(name)) + .expect("validated method must have a lowered function"); + let params = function.get_type().get_param_types(); + let mut call_args = Vec::with_capacity(args.len() + 1); + for (index, expression) in std::iter::once(object).chain(args.iter()).enumerate() { + let expected = opt_meta_to_opt_basic(params.get(index).copied()); + let value = env.gen(expression, expected); + let value = match expected { + Some(ty) => coerce_basic_value( env.context, env.builder, - arg_val, - et, - &format!("arg{}_cast", i), + value, + ty, + &format!("method_arg{index}"), CoercionMode::Implicit, - wave_type_is_unsigned(env.wave_type(arg_expr).as_ref()), - ); - } - call_args.push(arg_val.into()); + wave_type_is_unsigned(env.wave_type(expression).as_ref()), + ), + None => value, + }; + call_args.push(value.into()); } - - let call_site = env + let call = env .builder - .build_call(function, &call_args, &format!("call_{}", name)) + .build_call(function, &call_args, "method_call") .unwrap(); - - if function.get_type().get_return_type().is_some() { - callsite_to_ret(call_site, true, "method-style call").unwrap() - } else { - env.context.i32_type().const_zero().as_basic_value_enum() - } + callsite_to_ret( + call, + function.get_type().get_return_type().is_some(), + "method call", + ) + .unwrap_or_else(|| env.context.i32_type().const_zero().as_basic_value_enum()) } pub(crate) fn gen_function_call<'ctx, 'a>( @@ -399,6 +239,9 @@ pub(crate) fn gen_function_call<'ctx, 'a>( args: &[Expression], expected_type: Option>, ) -> BasicValueEnum<'ctx> { + if parser::async_intrinsics::is_intrinsic(name) { + return super::async_runtime::gen(env, name, type_args, args); + } if !type_args.is_empty() { panic!( "generic call '{}<...>(...)' reached codegen without monomorphization", @@ -471,6 +314,20 @@ pub(crate) fn gen_function_call<'ctx, 'a>( .builder .build_alloca(agg, &format!("{}_byval_tmp_{}", name, i)) .unwrap(); + // Windows x64 passes odd-sized aggregates through a caller + // temporary aligned to at least 16 bytes, without byval. + if matches!( + crate::codegen::target::CodegenTarget::from_module(env.module), + Some( + crate::codegen::target::CodegenTarget::WindowsX86_64Msvc + | crate::codegen::target::CodegenTarget::WindowsX86_64Gnu + ) + ) { + tmp.as_instruction_value() + .unwrap() + .set_alignment(16.max(env.target_data.get_abi_alignment(&agg))) + .unwrap(); + } env.builder.build_store(tmp, v).unwrap(); let expected_ptr = meta_into_ptr(llvm_param_types[llvm_pi]); diff --git a/llvm/src/expression/rvalue/dispatch.rs b/llvm/src/expression/rvalue/dispatch.rs index ec14a5bf..9787d075 100644 --- a/llvm/src/expression/rvalue/dispatch.rs +++ b/llvm/src/expression/rvalue/dispatch.rs @@ -41,9 +41,9 @@ pub(crate) fn gen_expr<'ctx, 'a>( Expression::Deref(inner) => pointers::gen_deref(env, inner), Expression::AddressOf(inner) => pointers::gen_addressof(env, inner, expected_type), - Expression::MethodCall { object, name, args } => { - calls::gen_method_call(env, object, name, args) - } + Expression::MethodCall { + object, name, args, .. + } => calls::gen_method_call(env, object, name, args), Expression::FunctionCall { name, type_args, @@ -87,6 +87,9 @@ pub(crate) fn gen_expr<'ctx, 'a>( Expression::Unary { operator, expr } => unary::gen(env, operator, expr, expected_type), Expression::IncDec { kind, target } => incdec::gen(env, kind, target), + Expression::Await(_) => { + unreachable!("async lowering must remove await before LLVM emission") + } Expression::Grouped(inner) => env.gen(inner, expected_type), Expression::ArrayLiteral(elements) => { arrays::gen_array_literal(env, elements, expected_type) diff --git a/llvm/src/expression/rvalue/incdec.rs b/llvm/src/expression/rvalue/incdec.rs index 2b3c56e4..c39753f4 100644 --- a/llvm/src/expression/rvalue/incdec.rs +++ b/llvm/src/expression/rvalue/incdec.rs @@ -52,6 +52,9 @@ fn wave_to_basic<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, wt: &WaveType) -> BasicTy } fn wave_type_of_lvalue<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> Option { + if let Some(ty) = env.wave_type(e) { + return Some(ty); + } match e { Expression::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()), Expression::Grouped(inner) => wave_type_of_lvalue(env, inner), diff --git a/llvm/src/expression/rvalue/mod.rs b/llvm/src/expression/rvalue/mod.rs index 85c8acd1..de0a43c9 100644 --- a/llvm/src/expression/rvalue/mod.rs +++ b/llvm/src/expression/rvalue/mod.rs @@ -35,6 +35,7 @@ pub mod utils; pub mod arrays; pub mod asm; pub mod assign; +mod async_runtime; pub mod binary; pub mod calls; pub mod cast; diff --git a/llvm/src/expression/rvalue/structs.rs b/llvm/src/expression/rvalue/structs.rs index fdfcaa9f..2a281a66 100644 --- a/llvm/src/expression/rvalue/structs.rs +++ b/llvm/src/expression/rvalue/structs.rs @@ -17,11 +17,10 @@ //! loaded as values. use super::ExprGenEnv; -use crate::codegen::generate_address_and_type_ir; use crate::statement::variable::{coerce_basic_value, wave_type_is_unsigned, CoercionMode}; use inkwell::types::{BasicType, BasicTypeEnum}; use inkwell::values::{BasicValue, BasicValueEnum}; -use parser::ast::Expression; +use parser::ast::{Expression, WaveType}; pub(crate) fn gen_struct_literal<'ctx, 'a>( env: &mut ExprGenEnv<'ctx, 'a>, @@ -95,24 +94,46 @@ pub(crate) fn gen_field_access<'ctx, 'a>( return value; } - let full = Expression::FieldAccess { - object: Box::new(object.clone()), - field: field.to_string(), + // Keep the original receiver identity so typed HIR remains authoritative. + // A returned struct is a value; reconstructing an lvalue would require an + // address that function/method call results do not have. + let receiver_type = env + .wave_type(object) + .expect("field receiver has a resolved HIR type"); + let struct_name = match &receiver_type { + WaveType::Struct(name) => name, + WaveType::Pointer(inner) => match inner.as_ref() { + WaveType::Struct(name) => name, + other => panic!("field receiver points to non-struct type: {other:?}"), + }, + other => panic!("field receiver is not a struct: {other:?}"), }; - - let (ptr, field_ty) = generate_address_and_type_ir( - env.context, - env.builder, - env.program, - &full, - env.variables, - env.module, - env.struct_types, - env.struct_field_indices, - ); - - env.builder - .build_load(field_ty, ptr, &format!("load_field_{}", field)) - .unwrap() - .as_basic_value_enum() + let index = *env + .struct_field_indices + .get(struct_name) + .and_then(|fields| fields.get(field)) + .expect("field index was established by semantic validation"); + match env.gen(object, None) { + BasicValueEnum::StructValue(value) => env + .builder + .build_extract_value(value, index, &format!("field_{field}")) + .expect("field index matches the resolved struct"), + BasicValueEnum::PointerValue(pointer) => { + let struct_type = *env + .struct_types + .get(struct_name) + .expect("resolved struct has an LLVM type"); + let field_type = struct_type + .get_field_type_at_index(index) + .expect("resolved field has an LLVM type"); + let pointer = env + .builder + .build_struct_gep(struct_type, pointer, index, &format!("field_ptr_{field}")) + .expect("typed pointer receiver supports field projection"); + env.builder + .build_load(field_type, pointer, &format!("load_field_{field}")) + .expect("field pointer has the resolved field type") + } + other => panic!("resolved struct receiver lowered to an invalid LLVM value: {other:?}"), + } } diff --git a/llvm/src/lib.rs b/llvm/src/lib.rs index 97cb83d4..b46a462c 100644 --- a/llvm/src/lib.rs +++ b/llvm/src/lib.rs @@ -53,3 +53,5 @@ pub fn backend() -> Option { } Some(format!("LLVM {}.{}.{}", major, minor, patch)) } + +pub mod diagnostic; diff --git a/llvm/src/statement/control.rs b/llvm/src/statement/control.rs index d697b3d6..701e5fd1 100644 --- a/llvm/src/statement/control.rs +++ b/llvm/src/statement/control.rs @@ -1043,8 +1043,22 @@ pub(super) fn gen_return_ir<'ctx>( builder.build_return(None).unwrap(); } - (None, Some(_)) => { - panic!("Void function cannot return a value"); + (None, Some(expression)) => { + generate_expression_ir( + program, + context, + builder, + expression, + variables, + module, + None, + global_consts, + struct_types, + struct_field_indices, + target_data, + extern_c_info, + ); + builder.build_return(None).unwrap(); } (Some(ret_ty), None) => { diff --git a/llvm/tests/codegen_errors.rs b/llvm/tests/codegen_errors.rs new file mode 100644 index 00000000..5186f071 --- /dev/null +++ b/llvm/tests/codegen_errors.rs @@ -0,0 +1,113 @@ +#![cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))] +use llvm::backend::{link_objects, BackendOptions}; +use llvm::codegen::ir::{emit_codegen_file, generate_ir, CodegenFileKind}; +use llvm::diagnostic::CodegenPhase; +use parser::hir::TypedProgram; +use std::sync::atomic::{AtomicU64, Ordering}; + +fn program(source: &str) -> TypedProgram { + let tokens = lexer::Lexer::new_with_file(source, "backend-errors.wave") + .tokenize() + .unwrap(); + TypedProgram::lower(parser::parse_syntax_with_spans(&tokens).unwrap()).unwrap() +} +fn options() -> BackendOptions { + BackendOptions { + target: Some("x86_64-unknown-linux-gnu".into()), + ..Default::default() + } +} +fn directory() -> std::path::PathBuf { + static NEXT: AtomicU64 = AtomicU64::new(0); + let dir = std::env::temp_dir().join(format!( + "wave-backend-errors-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + +#[test] +fn target_and_asm_errors_return_without_unwinding() { + let valid = program("fun main() -> i32 { return 0; }"); + let mut invalid = options(); + invalid.target = Some("invalid-target".into()); + assert_eq!( + unsafe { generate_ir(&valid, "", &invalid) } + .unwrap_err() + .phase, + CodegenPhase::Target + ); + invalid = options(); + invalid.code_model = Some("invalid-model".into()); + assert_eq!( + unsafe { generate_ir(&valid, "", &invalid) } + .unwrap_err() + .phase, + CodegenPhase::Target + ); + let invalid_asm = program("fun main() { asm { in(\"not_a_register\") 1 } }"); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| unsafe { + generate_ir(&invalid_asm, "", &options()) + })); + let error = result + .expect("user-triggered failure must not unwind") + .unwrap_err(); + assert_eq!(error.phase, CodegenPhase::Validation); + assert_eq!(error.operation, "inline assembly"); + assert_eq!(error.span.unwrap().file, "backend-errors.wave"); +} + +#[test] +fn failed_emission_preserves_old_output_and_cleans_staging_files() { + let dir = directory(); + let output = dir.join("old.o"); + std::fs::write(&output, "previous artifact").unwrap(); + let invalid = program("fun main() { asm { clobber(\"not_a_register\") } }"); + assert!(unsafe { + emit_codegen_file(&invalid, "", &options(), &output, CodegenFileKind::Object) + } + .is_err()); + assert_eq!( + std::fs::read_to_string(&output).unwrap(), + "previous artifact" + ); + let valid = program("fun main() -> i32 { return 0; }"); + let error = unsafe { + emit_codegen_file( + &valid, + "", + &options(), + &dir.join("missing/out.o"), + CodegenFileKind::Object, + ) + } + .unwrap_err(); + assert_eq!(error.phase, CodegenPhase::Emission); + // Failed publication after LLVM has written the staging file also cleans it. + let blocked = dir.join("directory.o"); + std::fs::create_dir(&blocked).unwrap(); + assert!(unsafe { + emit_codegen_file(&valid, "", &options(), &blocked, CodegenFileKind::Object) + } + .is_err()); + assert!(blocked.is_dir()); + assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2); +} + +#[test] +fn missing_linker_returns_a_typed_error_and_preserves_destination() { + let dir = directory(); + let output = dir.join("old.exe"); + std::fs::write(&output, "previous artifact").unwrap(); + let mut backend = options(); + backend.linker = Some(dir.join("missing-linker").to_string_lossy().into_owned()); + let error = link_objects(&[], output.to_str().unwrap(), &[], &[], &backend).unwrap_err(); + assert_eq!(error.phase, CodegenPhase::Link); + assert_eq!( + std::fs::read_to_string(&output).unwrap(), + "previous artifact" + ); + assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1); +} diff --git a/src/cli.rs b/src/cli.rs index 01550380..05a04ccd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,6 +25,7 @@ use crate::link_validation::{ validate_loongarch64_link_inputs, validate_riscv_link_inputs, LoongArchFloatAbi, RiscvFloatAbi, }; use crate::{runner, std as wave_std, version}; +use llvm::diagnostic::{CodegenError, CodegenPhase, PendingOutput}; use crate::version::get_os_pretty_name; use llvm::codegen::target::{ @@ -32,7 +33,7 @@ use llvm::codegen::target::{ EffectiveTargetOptions, TargetSpec, }; use std::collections::BTreeSet; -use std::io::{ErrorKind, Read}; +use std::io::Read; use std::path::{Path, PathBuf}; use std::process::{self, Command as ProcessCommand, Stdio}; use std::{env, fs}; @@ -1549,7 +1550,7 @@ fn infer_input_kind(path: &Path) -> Option { "bc" => Some(InputKind::Bc), "s" | "asm" => Some(InputKind::Asm), "o" | "obj" => Some(InputKind::Obj), - "a" => Some(InputKind::Archive), + "a" | "lib" => Some(InputKind::Archive), _ => None, } } @@ -1682,6 +1683,24 @@ fn validate_build_request( } let need_link = emit_set.contains(&EmitKind::Bin) || build.run; + if need_link && llvm::backend::is_windows_msvc_target(&target_triple_for_global(global)) { + if build.linker_script.is_some() { + return Err(CliError::usage( + "MSVC linking does not support GNU linker scripts; use COFF linker options", + )); + } + if build.no_start_files && build.entry.is_none() { + return Err(CliError::usage( + "MSVC --no-start-files requires --entry and -Cno-default-libs", + )); + } + if build.no_start_files && !global.llvm.no_default_libs { + return Err(CliError::usage( + "MSVC --no-start-files requires -Cno-default-libs", + )); + } + } + if (build.entry.is_some() || build.linker_script.is_some() || build.no_start_files) && !need_link { @@ -1848,7 +1867,9 @@ fn resolve_binary_output_path( .and_then(|s| s.to_str()) .filter(|s| !s.is_empty()) .unwrap_or("a.out"); - let stem = if is_windows_gnu_target_global(global) { + let stem = if llvm::backend::is_windows_msvc_target(&target_triple_for_global(global)) { + format!("{}.{}", stem, if build.shared { "dll" } else { "exe" }) + } else if is_windows_gnu_target_global(global) { format!("{}.exe", stem) } else if global.llvm.target.as_deref().is_some_and(is_wasm_target) { format!("{}.wasm", stem) @@ -2093,20 +2114,19 @@ fn compile_lowering_with_llvm_tools( output: &Path, emit_kind: EmitKind, ) -> Result<(), CliError> { - let (bin, args) = build_llvm_lowering_args(global, input, input_kind, output, emit_kind); + let pending = PendingOutput::new(output)?; + let (bin, args) = + build_llvm_lowering_args(global, input, input_kind, pending.path(), emit_kind); let mut command = ProcessCommand::new(&bin); configure_bundled_llvm_tool_env(&mut command, &bin); - let process_output = command.args(&args).output().map_err(|e| { - if e.kind() == ErrorKind::NotFound { - CliError::ExternalToolMissing(linker_tool_name(&bin)) - } else { - CliError::Io(e) - } - })?; + let process_output = command + .args(&args) + .output() + .map_err(|error| CodegenError::tool_launch(CodegenPhase::Tool, &bin, error))?; if process_output.status.success() { - return Ok(()); + return pending.commit().map_err(CliError::from); } let stderr = String::from_utf8_lossy(&process_output.stderr) @@ -2116,13 +2136,18 @@ fn compile_lowering_with_llvm_tools( .trim() .to_string(); - Err(CliError::CommandFailed(format!( - "{} failed (status={})\nstdout: {}\nstderr: {}", + Err(CodegenError::new( + CodegenPhase::Tool, emit_kind_name(emit_kind), - process_output.status, - stdout, - stderr - ))) + format!( + "{} failed (status={})\nstdout: {}\nstderr: {}", + emit_kind_name(emit_kind), + process_output.status, + stdout, + stderr + ), + ) + .into()) } fn build_llvm_lowering_args( @@ -2254,7 +2279,7 @@ fn link_objects( validate_default_elf_runtime(global, build)?; } - let (bin, args) = build_linker_args(global, build, objects, output); + let (_, args) = build_linker_args(global, build, objects, output); if matches!( target_spec_for_triple(&target).map(|spec| spec.codegen), Some(CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64) @@ -2278,28 +2303,40 @@ fn link_objects( validate_loongarch64_link_inputs(target_abi, &validation_inputs) .map_err(|error| CliError::CommandFailed(error.to_string()))?; } + let pending = PendingOutput::new(output)?; + let (bin, args) = build_linker_args(global, build, objects, pending.path()); let mut command = ProcessCommand::new(&bin); configure_bundled_llvm_tool_env(&mut command, &bin); - let out = command.args(&args).output().map_err(|e| { - if e.kind() == ErrorKind::NotFound { - CliError::ExternalToolMissing(missing_linker_tool_name(global, &bin)) - } else { - CliError::Io(e) - } + let out = command.args(&args).output().map_err(|error| { + CodegenError::tool_launch( + CodegenPhase::Link, + &missing_linker_tool_name(global, &bin), + error, + ) })?; if out.status.success() { - return Ok(()); + return pending.commit().map_err(CliError::from); } let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string(); - Err(CliError::CommandFailed(format!( - "link failed (status={})\nstdout: {}\nstderr: {}", - out.status, stdout, stderr - ))) + let hint = if llvm::backend::is_windows_msvc_target(&target) { + "\nMSVC output requires matching Windows SDK (UM/UCRT) and VC runtime libraries; run a Developer Command Prompt for the target architecture or supply their directories with -L." + } else { + "" + }; + Err(CodegenError::new( + CodegenPhase::Link, + "link objects", + format!( + "link failed (status={})\nstdout: {}\nstderr: {}{}", + out.status, stdout, stderr, hint + ), + ) + .into()) } fn validate_default_elf_runtime(global: &Global, build: &BuildRequest) -> Result<(), CliError> { @@ -2386,6 +2423,29 @@ fn build_linker_args( objects: &[String], output: &Path, ) -> (String, Vec) { + let target = target_triple_for_global(global); + if llvm::backend::is_windows_msvc_target(&target) { + let args = llvm::backend::msvc_link_args( + &target, + objects, + &output.to_string_lossy(), + &global.link.libs, + &global.link.paths, + global.llvm.no_default_libs, + build.static_link, + build.shared, + build.entry.as_deref(), + &global.llvm.link_args, + ); + return ( + global + .llvm + .linker + .clone() + .unwrap_or_else(|| resolve_bundled_tool("lld-link")), + args, + ); + } if let Some(linker) = &global.llvm.linker { return build_user_linker_args(linker, global, build, objects, output); } @@ -3401,6 +3461,9 @@ fn default_linker_name(global: &Global) -> String { } let target = target_triple_for_global(global); + if llvm::backend::is_windows_msvc_target(&target) { + return resolve_bundled_tool("lld-link"); + } if matches!( target_spec_for_triple(&target).map(|spec| spec.codegen), Some(CodegenTarget::WindowsArm64Gnu) diff --git a/src/errors.rs b/src/errors.rs index 2a50c9a5..49e6822f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -22,6 +22,7 @@ use std::path::PathBuf; #[derive(Debug)] pub enum CliError { Usage(String), + Backend(llvm::diagnostic::CodegenError), // std StdAlreadyInstalled { path: PathBuf }, @@ -41,6 +42,13 @@ impl CliError { pub fn kind(&self) -> &'static str { match self { CliError::Usage(_) => "usage", + CliError::Backend(error) => { + if error.kind == llvm::diagnostic::CodegenErrorKind::MissingTool { + "external-tool-missing" + } else { + "command-failed" + } + } CliError::StdAlreadyInstalled { .. } => "std-already-installed", CliError::ExternalToolMissing(_) => "external-tool-missing", CliError::CommandFailed(_) => "command-failed", @@ -52,6 +60,7 @@ impl CliError { pub fn message(&self) -> String { match self { CliError::Usage(msg) => msg.clone(), + CliError::Backend(error) => error.to_string(), CliError::StdAlreadyInstalled { path } => { format!("std already installed at '{}'", path.display()) } @@ -63,6 +72,10 @@ impl CliError { } pub fn to_json(&self) -> String { + if let Self::Backend(error) = self { + return format!("{{\"error\":{{\"kind\":{},\"message\":{},\"exit_code\":{},\"phase\":{},\"operation\":{}}}}}", + json_string(self.kind()), json_string(&self.message()), self.exit_code(), json_string(&error.phase.to_string()), json_string(&error.operation)); + } format!( "{{\"error\":{{\"kind\":{},\"message\":{},\"exit_code\":{}}}}}", json_string(self.kind()), @@ -74,6 +87,13 @@ impl CliError { pub fn exit_code(&self) -> i32 { match self { CliError::Usage(_) => 2, + CliError::Backend(error) => { + if error.kind == llvm::diagnostic::CodegenErrorKind::MissingTool { + 3 + } else { + 1 + } + } CliError::ExternalToolMissing(_) | CliError::HomeNotSet | CliError::Io(_) => 3, CliError::StdAlreadyInstalled { .. } | CliError::CommandFailed(_) => 1, } @@ -84,6 +104,7 @@ impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { CliError::Usage(msg) => write!(f, "Error: {}", msg), + CliError::Backend(error) => write!(f, "Error: {error}"), CliError::StdAlreadyInstalled { path } => { write!(f, "Error: std already installed at '{}'", path.display()) } @@ -117,3 +138,9 @@ fn json_string(value: &str) -> String { out.push('"'); out } + +impl From for CliError { + fn from(error: llvm::diagnostic::CodegenError) -> Self { + Self::Backend(error) + } +} diff --git a/src/module_resolver.rs b/src/module_resolver.rs index 25cd7607..28b4308c 100644 --- a/src/module_resolver.rs +++ b/src/module_resolver.rs @@ -551,6 +551,9 @@ fn resolve_name( fn rewrite_type(ty: WaveType, names: &NameContext, path: &Path) -> Result { match ty { + WaveType::Future(inner) => Ok(WaveType::Future(Box::new(rewrite_type( + *inner, names, path, + )?))), WaveType::Pointer(inner) => Ok(WaveType::Pointer(Box::new(rewrite_type( *inner, names, path, )?))), @@ -1071,9 +1074,18 @@ fn rewrite_expression( }, } } - Expression::MethodCall { object, name, args } => Expression::MethodCall { + Expression::MethodCall { + object, + name, + args, + type_args, + } => Expression::MethodCall { object: Box::new(rewrite_expression(*object, names, path, locals)?), name, + type_args: type_args + .into_iter() + .map(|ty| rewrite_type(ty, names, path)) + .collect::>()?, args: rewrite_expressions(args, names, path, locals)?, }, Expression::Variable(name) => Expression::Variable(if locals.contains(&name) { @@ -1103,6 +1115,9 @@ fn rewrite_expression( Expression::ArrayLiteral(values) => { Expression::ArrayLiteral(rewrite_expressions(values, names, path, locals)?) } + Expression::Await(inner) => { + Expression::Await(Box::new(rewrite_expression(*inner, names, path, locals)?)) + } Expression::Grouped(inner) => { Expression::Grouped(Box::new(rewrite_expression(*inner, names, path, locals)?)) } diff --git a/src/runner.rs b/src/runner.rs index ad792f49..cbc4b690 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -32,7 +32,6 @@ use lexer::Lexer; use llvm::backend::*; use llvm::codegen::target::target_spec_for_triple; use llvm::codegen::*; -use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::{Arc, Mutex}; @@ -85,7 +84,9 @@ fn parse_wave_tokens_or_exit( .with_code(code) .with_source_code(source.to_string()); - wave_err = wave_err.with_span(err.span()); + wave_err = wave_err + .with_span(err.span()) + .with_related(err.related().iter().cloned()); if let Some(ctx) = err.context() { wave_err = wave_err.with_context(ctx.to_string()); } @@ -253,368 +254,63 @@ where } } -fn classify_codegen_panic(panic_message: &str) -> (&'static str, &'static str, &'static str) { - if panic_message.contains("null literal can only be assigned to ptr") { - return ( - "E3102", - "invalid null assignment", - "use `null` only where the target type is `ptr`", - ); - } - - if panic_message.contains("integer literals cannot initialize pointers") { - return ( - "E3103", - "invalid pointer literal", - "use `null` or an explicit cast when initializing a pointer", - ); - } - - if panic_message.contains("implicit integer narrowing is forbidden") { - return ( - "E3201", - "implicit integer narrowing is forbidden", - "insert an explicit cast or widen the destination type", - ); - } - - if panic_message.contains("missing a return statement") { - return ( - "E3004", - "non-void function is missing return", - "ensure every control-flow path returns a value", - ); - } - - if panic_message.contains("unsupported extern ABI") { - return ( - "E3006", - "unsupported extern ABI", - "Wave currently supports `extern(c)` only", - ); - } - - if panic_message.contains("match value must be integer/enum type") - || panic_message.contains("match case identifier") - { - return ( - "E3010", - "invalid match operand", - "use `match` only with integer/enum values and integer/enum case labels", - ); - } - - if panic_message.contains("duplicate match case value") - || panic_message.contains("duplicate wildcard match arm") - { - return ( - "E3011", - "duplicate match case", - "remove duplicate case labels so every match case value is unique", - ); - } - - if panic_message.contains("asm input register/constraint") - || panic_message.contains("asm output register/constraint") - || panic_message.contains("Invalid clobber token") - || panic_message.contains("asm touches the stack") - || panic_message.contains("asm contains a non-returning branch") - || panic_message.contains("asm stack delta is not balanced") - || panic_message.contains("asm writes the stack pointer") - || panic_message.contains("asm cannot declare both") - || panic_message.contains("conflicts with an input/output operand register") - || panic_message.contains("asm expression cannot declare") - { - return ( - "E3401", - "invalid inline assembly contract", - "use registers valid for the selected target and declare stack, clobber, and control-flow effects explicitly", - ); - } - - ( - "E9001", - "compiler internal error during code generation", - "this looks like a compiler bug; please report the panic details below", +fn emit_backend_error_and_exit( + file_path: &Path, + source: &str, + error: llvm::diagnostic::CodegenError, +) -> ! { + let mut diagnostic = WaveError::new( + WaveErrorKind::CompilationFailed(error.message.clone()), + format!("{}: {}", error.operation, error.message), + file_path.display().to_string(), + 0, + 0, ) -} - -#[derive(Debug, Clone)] -struct InferredSourceLoc { - line: usize, - column: usize, - span_len: usize, - label: String, - note: String, -} - -fn extract_between(s: &str, start: &str, end: &str) -> Option { - let st = s.find(start)? + start.len(); - let rest = &s[st..]; - let en = rest.find(end)?; - Some(rest[..en].to_string()) -} - -fn byte_index_to_line_col(source: &str, byte_index: usize) -> (usize, usize) { - let idx = byte_index.min(source.len()); - let mut line = 1usize; - let mut line_start = 0usize; - - for (i, ch) in source.char_indices() { - if i >= idx { - break; - } - if ch == '\n' { - line += 1; - line_start = i + 1; - } - } - - let col = source[line_start..idx].chars().count() + 1; - (line, col) -} - -fn find_function_call_site(source: &str, fn_name: &str) -> Option { - let needle = format!("{}(", fn_name); - let mut pos = 0usize; - - while pos < source.len() { - let rel = source[pos..].find(&needle)?; - let idx = pos + rel; - - let prefix_start = idx.saturating_sub(6); - let prefix = &source[prefix_start..idx]; - if !prefix.ends_with("fun ") { - return Some(idx); - } - - pos = idx + needle.len(); - } - - None -} - -fn find_function_decl(source: &str, fn_name: &str) -> Option { - let pattern = format!("fun {}", fn_name); - let idx = source.find(&pattern)?; - Some(idx + "fun ".len()) -} - -fn find_variable_decl(source: &str, var_name: &str) -> Option { - let patterns = [format!("var {}", var_name), format!("const {}", var_name)]; - - for p in patterns { - if let Some(idx) = source.find(&p) { - if let Some(off) = p.find(var_name) { - return Some(idx + off); - } - } - } - - None -} - -fn is_ident_char(ch: char) -> bool { - ch.is_ascii_alphanumeric() || ch == '_' -} - -fn find_identifier_occurrence(source: &str, ident: &str) -> Option { - if ident.is_empty() { - return None; - } - - let mut pos = 0usize; - while pos < source.len() { - let rel = source[pos..].find(ident)?; - let idx = pos + rel; - let end = idx + ident.len(); - - let before_ok = source[..idx] - .chars() - .next_back() - .map(|ch| !is_ident_char(ch)) - .unwrap_or(true); - - let after_ok = source[end..] - .chars() - .next() - .map(|ch| !is_ident_char(ch)) - .unwrap_or(true); - - if before_ok && after_ok { - return Some(idx); - } - - pos = end; - } - - None -} - -fn extract_single_quoted_identifiers(message: &str) -> Vec { - let mut out = Vec::new(); - let mut seen = HashSet::new(); - let mut rest = message; - - while let Some(start) = rest.find('\'') { - let tail = &rest[start + 1..]; - let Some(end) = tail.find('\'') else { - break; + .with_code( + if error.kind == llvm::diagnostic::CodegenErrorKind::InvalidAssembly { + "E3401" + } else { + "E9002" + }, + ) + .with_context(format!("compiler phase: {}", error.phase)); + if let Some(span) = error.span { + let contents = if Path::new(&span.file) == file_path { + Some(source.to_string()) + } else { + fs::read_to_string(&span.file).ok() }; - - let candidate = &tail[..end]; - let is_symbol = !candidate.is_empty() - && candidate.chars().any(is_ident_char) - && candidate.chars().all(|ch| is_ident_char(ch) || ch == '.'); - - if is_symbol && seen.insert(candidate.to_string()) { - out.push(candidate.to_string()); - } - - rest = &tail[end + 1..]; - } - - out -} - -fn find_best_symbol_site(source: &str, symbol: &str) -> Option { - if symbol.is_empty() { - return None; - } - - if let Some(idx) = find_function_call_site(source, symbol) { - return Some(idx); - } - if let Some(idx) = find_variable_decl(source, symbol) { - return Some(idx); - } - if let Some(idx) = find_function_decl(source, symbol) { - return Some(idx); - } - if let Some(idx) = find_identifier_occurrence(source, symbol) { - return Some(idx); - } - - if let Some(base) = symbol.split('.').next() { - if base != symbol { - return find_best_symbol_site(source, base); - } - } - - None -} - -fn infer_codegen_source_location(source: &str, panic_message: &str) -> Option { - if let Some(fn_name) = extract_between(panic_message, "Function '", "' not found") { - if let Some(idx) = find_function_call_site(source, &fn_name) { - let (line, column) = byte_index_to_line_col(source, idx); - return Some(InferredSourceLoc { - line, - column, - span_len: fn_name.chars().count().max(1), - label: format!("unresolved function `{}` is called here", fn_name), - note: "source position inferred from unresolved function name in backend panic" - .to_string(), - }); - } - } - - if let Some(fn_name) = extract_between( - panic_message, - "Non-void function '", - "' is missing a return statement", - ) { - if let Some(idx) = find_function_decl(source, &fn_name) { - let (line, column) = byte_index_to_line_col(source, idx); - return Some(InferredSourceLoc { - line, - column, - span_len: fn_name.chars().count().max(1), - label: format!("function `{}` declaration", fn_name), - note: "source position inferred from function name in backend panic".to_string(), - }); - } - } - - if let Some(var_name) = extract_between(panic_message, "(variable '", "':") { - if let Some(idx) = find_variable_decl(source, &var_name) { - let (line, column) = byte_index_to_line_col(source, idx); - return Some(InferredSourceLoc { - line, - column, - span_len: var_name.chars().count().max(1), - label: format!("variable `{}` declaration", var_name), - note: "source position inferred from variable name in backend panic".to_string(), - }); + diagnostic = diagnostic.with_span(Some(&span)); + if let Some(contents) = contents { + diagnostic = diagnostic.with_source_code(contents); } } - - for symbol in extract_single_quoted_identifiers(panic_message) { - if let Some(idx) = find_best_symbol_site(source, &symbol) { - let (line, column) = byte_index_to_line_col(source, idx); - return Some(InferredSourceLoc { - line, - column, - span_len: symbol - .split('.') - .next_back() - .unwrap_or(&symbol) - .chars() - .count() - .max(1), - label: format!("related symbol `{}` appears here", symbol), - note: "source position inferred from backend panic symbol".to_string(), - }); - } - } - - None + diagnostic.display_auto(); + process::exit(1); } fn emit_codegen_panic_and_exit( file_path: &Path, - source: &str, + _source: &str, phase: &str, - panic_message: String, - panic_location: Option, + message: String, + location: Option, ) -> ! { - let (code, headline, help) = classify_codegen_panic(&panic_message); - let inferred = infer_codegen_source_location(source, &panic_message); - - let (line, column) = inferred - .as_ref() - .map(|x| (x.line, x.column)) - .unwrap_or((1, 1)); - - let mut err = WaveError::new( - WaveErrorKind::CompilationFailed(panic_message.clone()), - format!("{} ({})", headline, phase), + let mut diagnostic = WaveError::new( + WaveErrorKind::CompilationFailed(message.clone()), + "compiler internal error during code generation", file_path.display().to_string(), - line, - column, + 0, + 0, ) - .with_code(code) - .with_source_code(source.to_string()) - .with_context(format!("compiler phase: {}", phase)) - .with_found(panic_message) - .with_help(help); - - if let Some(loc) = inferred { - err = err - .with_span_len(loc.span_len) - .with_label(loc.label) - .with_note(loc.note); - } else { - err = err.with_note("no precise source span was available for this backend diagnostic"); - } - - if code == "E9001" { - if let Some(loc) = panic_location { - err = err.with_suggestion(format!("compiler panic location: {}", loc)); - } - } - - err.display_auto(); + .with_code("E9001") + .with_context(format!("compiler phase: {phase}")) + .with_found(message) + .with_help("report this compiler invariant failure with the source and target"); + if let Some(location) = location { + diagnostic = diagnostic.with_note(format!("compiler panic location: {location}")); + } + diagnostic.display_auto(); process::exit(1); } @@ -867,7 +563,8 @@ pub(crate) unsafe fn emit_wave_ir_text( let backend_opts = build_backend_options(llvm); let ir = match run_panic_guarded(|| unsafe { generate_ir(&hir, opt_flag, &backend_opts) }) { - Ok(ir) => ir, + Ok(Ok(ir)) => ir, + Ok(Err(error)) => emit_backend_error_and_exit(file_path, &code, error), Err((msg, loc)) => { emit_codegen_panic_and_exit(file_path, &code, "llvm-ir-generation", msg, loc) } @@ -915,7 +612,8 @@ unsafe fn emit_wave_codegen_file_from_hir( if debug.ir { let ir = match run_panic_guarded(|| unsafe { generate_ir(hir, opt_flag, &backend_opts) }) { - Ok(ir) => ir, + Ok(Ok(ir)) => ir, + Ok(Err(error)) => emit_backend_error_and_exit(file_path, code, error), Err((msg, loc)) => { emit_codegen_panic_and_exit(file_path, code, "llvm-ir-generation", msg, loc) } @@ -926,25 +624,27 @@ unsafe fn emit_wave_codegen_file_from_hir( if let Some(parent) = output.parent() { if !parent.as_os_str().is_empty() { fs::create_dir_all(parent).unwrap_or_else(|e| { - emit_codegen_panic_and_exit( + emit_backend_error_and_exit( file_path, code, - codegen_file_phase(kind), - format!( - "failed to create output directory '{}': {}", - parent.display(), - e + llvm::diagnostic::CodegenError::new( + llvm::diagnostic::CodegenPhase::Emission, + format!("create output directory {}", parent.display()), + e, ), - None, ) }); } } - if let Err((msg, loc)) = run_panic_guarded(|| unsafe { - emit_codegen_file(hir, opt_flag, &backend_opts, output, kind); + match run_panic_guarded(|| unsafe { + emit_codegen_file(hir, opt_flag, &backend_opts, output, kind) }) { - emit_codegen_panic_and_exit(file_path, code, codegen_file_phase(kind), msg, loc); + Ok(Ok(())) => {} + Ok(Err(error)) => emit_backend_error_and_exit(file_path, code, error), + Err((msg, loc)) => { + emit_codegen_panic_and_exit(file_path, code, codegen_file_phase(kind), msg, loc) + } } } @@ -1098,16 +798,18 @@ pub(crate) unsafe fn run_wave_file( let exe_patch = format!("target/{}", file_stem); let backend_opts = build_backend_options(llvm); - if let Err((msg, loc)) = run_panic_guarded(|| { + match run_panic_guarded(|| { link_objects( std::slice::from_ref(&object_patch), &exe_patch, &link.libs, &link.paths, &backend_opts, - ); + ) }) { - emit_codegen_panic_and_exit(file_path, &code, "native-link", msg, loc); + Ok(Ok(())) => {} + Ok(Err(error)) => emit_backend_error_and_exit(file_path, &code, error), + Err((msg, loc)) => emit_codegen_panic_and_exit(file_path, &code, "native-link", msg, loc), } let status = Command::new(&exe_patch) @@ -1248,16 +950,18 @@ pub(crate) unsafe fn build_wave_file( resolve_output_target(&default_exe_path, output, file_path, &source, "native-link"); let backend_opts = build_backend_options(llvm); - if let Err((msg, loc)) = run_panic_guarded(|| { + match run_panic_guarded(|| { link_objects( &[object_path], &exe_path, &link.libs, &link.paths, &backend_opts, - ); + ) }) { - emit_codegen_panic_and_exit(file_path, &source, "native-link", msg, loc); + Ok(Ok(())) => {} + Ok(Err(error)) => emit_backend_error_and_exit(file_path, &source, error), + Err((msg, loc)) => emit_codegen_panic_and_exit(file_path, &source, "native-link", msg, loc), } if debug.mc { diff --git a/std/sys/linux/event.wave b/std/sys/linux/event.wave index 2e17edde..3db94a46 100644 --- a/std/sys/linux/event.wave +++ b/std/sys/linux/event.wave @@ -17,20 +17,32 @@ const EPOLLRDHUP: u32 = 8192; pub struct NativeEvent { fd: i64; token: u64; flags: i32; } -extern(c, "epoll_create1") fun native_epoll_create1(flags: i32) -> i32; -extern(c, "epoll_ctl") fun native_epoll_ctl( - epoll: i32, operation: i32, fd: i32, event: ptr -) -> i32; -extern(c, "epoll_wait") fun native_epoll_wait( - epoll: i32, events: ptr, capacity: i32, timeout_ms: i32 -) -> i32; -extern(c, "__errno_location") fun native_errno_location() -> ptr; +import("std::sys::linux::syscall")::{syscall1, syscall4, syscall6}; +#[target(arch="x86_64")] +const EPOLL_CREATE_SYSCALL: i64 = 291; +#[target(arch="x86_64")] +const EPOLL_CTL_SYSCALL: i64 = 233; +#[target(arch="x86_64")] +const EPOLL_PWAIT_SYSCALL: i64 = 281; +#[target(arch="aarch64")] +const EPOLL_CREATE_SYSCALL: i64 = 20; +#[target(arch="aarch64")] +const EPOLL_CTL_SYSCALL: i64 = 21; +#[target(arch="aarch64")] +const EPOLL_PWAIT_SYSCALL: i64 = 22; +#[target(arch="riscv64")] +const EPOLL_CREATE_SYSCALL: i64 = 20; +#[target(arch="riscv64")] +const EPOLL_CTL_SYSCALL: i64 = 21; +#[target(arch="riscv64")] +const EPOLL_PWAIT_SYSCALL: i64 = 22; +#[target(arch="loongarch64")] +const EPOLL_CREATE_SYSCALL: i64 = 20; +#[target(arch="loongarch64")] +const EPOLL_CTL_SYSCALL: i64 = 21; +#[target(arch="loongarch64")] +const EPOLL_PWAIT_SYSCALL: i64 = 22; -fun _errno_result(value: i32) -> i64 { - if (value >= 0) { return value as i64; } - var error_pointer: ptr = native_errno_location(); - return -(deref error_pointer) as i64; -} fun _store_u32(output: ptr, value: u32) { deref output[0] = (value & 255) as u8; deref output[1] = ((value >> 8) & 255) as u8; @@ -98,11 +110,11 @@ fun _control(handle: i64, operation: i32, fd: i64, token: u64, flags: i32) -> i6 var data_offset: i32 = _epoll_data_offset(); _store_u32(&raw[0], _epoll_flags(flags)); _store_u64(&raw[data_offset], token); - return _errno_result(native_epoll_ctl(handle as i32, operation, fd as i32, &raw[0])); + return syscall4(EPOLL_CTL_SYSCALL, handle, operation as i64, fd, &raw[0] as i64); } pub fun event_create(capacity: i32) -> i64 { if (capacity <= 0) { return -22; } - return _errno_result(native_epoll_create1(0)); + return syscall1(EPOLL_CREATE_SYSCALL, 0); } pub fun event_add(handle: i64, fd: i64, token: u64, flags: i32) -> i64 { if ((flags & (EVENT_READABLE | EVENT_WRITABLE)) == 0) { return -22; } @@ -113,16 +125,14 @@ pub fun event_modify(handle: i64, fd: i64, token: u64, flags: i32) -> i64 { return _control(handle, EPOLL_CTL_MOD, fd, token, flags); } pub fun event_remove(handle: i64, fd: i64) -> i64 { - return _errno_result(native_epoll_ctl(handle as i32, EPOLL_CTL_DEL, fd as i32, null)); + return syscall4(EPOLL_CTL_SYSCALL, handle, EPOLL_CTL_DEL as i64, fd, 0); } pub fun event_wait( handle: i64, output: ptr, capacity: i32, timeout_ms: i32 ) -> i64 { if (capacity <= 0 || capacity > 64 || output == null || timeout_ms < -1) { return -22; } var raw: array; - var count: i64 = _errno_result( - native_epoll_wait(handle as i32, &raw[0], capacity, timeout_ms) - ); + var count: i64 = syscall6(EPOLL_PWAIT_SYSCALL, handle, &raw[0] as i64, capacity as i64, timeout_ms as i64, 0, 8); if (count < 0) { return count; } var i: i32 = 0; while (i < count as i32) { diff --git a/std/sys/windows/iocp.wave b/std/sys/windows/iocp.wave new file mode 100644 index 00000000..590d446a --- /dev/null +++ b/std/sys/windows/iocp.wave @@ -0,0 +1,157 @@ +// IOCP operations own their OVERLAPPED storage and kernel-visible buffers. +// Cancellation only releases storage after its completion packet is drained. +import("std::mem::alloc")::{mem_alloc_zeroed, mem_free}; +import("std::mem::ops")::{mem_copy}; +import("std::process::core")::{proc_exit}; + +struct Overlapped { internal: u64; internal_high: u64; offset: u32; offset_high: u32; event: ptr; } +struct WsaBuffer { length: u32; data: ptr; } +struct NetworkEvents { events: i32; errors: array; } +struct Operation { + overlapped: Overlapped; + port: ptr; token: i64; socket: i64; kind: i32; + storage: ptr; destination: ptr; length: i64; writing: bool; + buffer: WsaBuffer; flags: u32; interests: i32; + event: ptr; wait: ptr; posted: i32; cancelled: bool; + next: ptr; +} +pub struct IocpCompletion { token: i64; value: i64; kind: i32; } +static _port: ptr = null; +static _operations: ptr = null; +extern(system,"CreateIoCompletionPort") fun win_create_port(file:ptr,port:ptr,key:u64,threads:u32)->ptr; +extern(system,"GetQueuedCompletionStatus") fun win_dequeue(port:ptr,bytes:ptr,key:ptr,overlapped:ptr>,timeout:u32)->i32; +extern(system,"PostQueuedCompletionStatus") fun win_post(port:ptr,bytes:u32,key:u64,overlapped:ptr)->i32; +extern(system,"CancelIoEx") fun win_cancel(file:ptr,overlapped:ptr)->i32; +extern(system,"CloseHandle") fun win_close(handle:ptr)->i32; +extern(system,"GetLastError") fun win_error()->u32; +extern(system,"WSAGetLastError") fun wsa_error()->i32; +extern(system,"WSASend") fun wsa_send(socket:u64,buffers:ptr,count:u32,bytes:ptr,flags:u32,overlapped:ptr,callback:ptr)->i32; +extern(system,"WSARecv") fun wsa_recv(socket:u64,buffers:ptr,count:u32,bytes:ptr,flags:ptr,overlapped:ptr,callback:ptr)->i32; +extern(system,"WSACreateEvent") fun wsa_create_event()->ptr; +extern(system,"WSACloseEvent") fun wsa_close_event(event:ptr)->i32; +extern(system,"WSAEventSelect") fun wsa_select(socket:u64,event:ptr,events:i32)->i32; +extern(system,"WSAEnumNetworkEvents") fun wsa_events(socket:u64,event:ptr,events:ptr)->i32; +extern(system,"RegisterWaitForSingleObject") fun win_register(wait:ptr>,event:ptr,callback:ptr,context:ptr,timeout:u32,flags:u32)->i32; +extern(system,"UnregisterWaitEx") fun win_unregister(wait:ptr,completion:ptr)->i32; + +fun _ensure_port()->i64 { + if (_port==null) { _port=win_create_port(-1 as ptr,null,0,1); } + if (_port==null) { return -(win_error() as i64); } + return 0; +} +fun _link(op:ptr) { op.next=_operations; _operations=op; } +fun _unlink(op:ptr) { + var previous:ptr =null; var cursor:ptr =_operations; + while(cursor!=null){ + if(cursor==op){if(previous==null){_operations=cursor.next;}else{previous.next=cursor.next;}return;} + previous=cursor;cursor=cursor.next; + } +} +fun _detach_wait(op:ptr) { + if(op.wait!=null){ + // A failed join must never turn into freeing a live callback context. + if(win_unregister(op.wait,-1 as ptr)==0){proc_exit(73);} + op.wait=null; + } + if(op.event!=null){wsa_select(op.socket as u64,null,0);wsa_close_event(op.event);op.event=null;} +} +fun _destroy(op:ptr) { + _detach_wait(op);_unlink(op); + if(op.storage!=null){mem_free(op.storage,op.length);} + __wave_async_free_slot(op); +} +// A Windows worker only posts a packet. Scheduler and frame state stay on the executor thread. +export(system,"__wave_task_windows_notify") fun _notify(context:ptr,timed_out:u8) { + var op:ptr =context as ptr; + if(win_post(op.port,0,0,context)==0){proc_exit(73);} + op.posted=1; +} +pub fun iocp_start_io(token:i64,socket:i64,destination:ptr,length:i64,writing:bool)->i64 { + if(socket<0||destination==null||length<=0||length>4294967295){return -22;} + var status:i64=_ensure_port();if(status<0){return status;} + if(win_create_port(socket as ptr,_port,0,1)==null){return -(win_error() as i64);} + var op:ptr =__wave_async_alloc(); + op.storage=mem_alloc_zeroed(length); + if(op.storage==null){__wave_async_free_slot(op);return -12;} + op.port=_port;op.token=token;op.socket=socket;op.kind=0;op.destination=destination;op.length=length;op.writing=writing; + op.buffer=WsaBuffer{length:length as u32,data:op.storage}; + if(writing){mem_copy(op.storage,destination,length);} + var started:i32=0; + if(writing){started=wsa_send(socket as u64,&op.buffer,1,null,0,&op.overlapped as ptr,null);} + else{started=wsa_recv(socket as u64,&op.buffer,1,null,&op.flags,&op.overlapped as ptr,null);} + if(started!=0){var code:i32=wsa_error();if(code!=997){mem_free(op.storage,length);__wave_async_free_slot(op);return -(code as i64);}} + // Immediate success also queues a completion; no skip-on-success mode is enabled. + _link(op);return 0; +} +pub fun iocp_watch(token:i64,socket:i64,interests:i32)->i64 { + var status:i64=_ensure_port();if(status<0){return status;} + var op:ptr =__wave_async_alloc(); + op.port=_port;op.token=token;op.socket=socket;op.kind=1;op.interests=interests;op.event=wsa_create_event(); + if(op.event==null){var code:i64=-(wsa_error() as i64);__wave_async_free_slot(op);return code;} + var mask:i32=32; + if((interests&1)!=0){mask=mask|1|8;} + if((interests&2)!=0){mask=mask|2|16;} + if(wsa_select(socket as u64,op.event,mask)!=0){var code:i64=-(wsa_error() as i64);_destroy(op);return code;} + var callback:ptr =__wave_async_windows_notify_address(); + // WT_EXECUTEONLYONCE: the executor rearms after consuming each notification. + if(win_register(&op.wait,op.event,callback,op as ptr,4294967295 as u32,8)==0){var code:i64=-(win_error() as i64);_destroy(op);return code;} + _link(op);return 0; +} +// Returns 1 if an acknowledgement packet is still pending, 0 if already detached. +pub fun iocp_cancel(token:i64)->i32 { + var op:ptr =_operations; + while(op!=null){ + if(op.token==token){ + op.cancelled=true; + if(op.kind==0){win_cancel(op.socket as ptr,&op.overlapped as ptr);return 1;} + _detach_wait(op); + if(op.posted!=0){return 1;} + _destroy(op);return 0; + } + op=op.next; + } + return 0; +} +// 1 = completion, 0 = wait timeout, negative = port error. +pub fun iocp_wait(output:ptr,timeout_ms:i32)->i64 { + if(output==null||timeout_ms< -1){return -22;} + if(_port==null){return 0;} + var bytes:u32=0;var key:u64=0;var pointer:ptr =null; + var ok:i32=win_dequeue(_port,&bytes,&key,&pointer,timeout_ms as u32); + var code:u32=0;if(ok==0){code=win_error();} + if(pointer==null){if(code==258){return 0;}return -(code as i64);} + var op:ptr =_operations; + while(op!=null&&op as ptr!=pointer){op=op.next;} + // Never dereference a completion pointer before finding its live registration. + if(op==null){return 0;} + var value:i64=bytes as i64; + if(op.cancelled){value=-995;} + else if(op.kind==0){ + if(ok==0){value=-(code as i64);} + else if(!op.writing){ + if(value>op.length){value=-5;}else if(value>0){mem_copy(op.destination,op.storage,value);} + } + }else{ + var events:NetworkEvents; + value=0; + if(wsa_events(op.socket as u64,op.event,&events)!=0){value=-(wsa_error() as i64);} + else{ + if((events.events&(1|8))!=0){value=value|1;} + if((events.events&(2|16))!=0){value=value|2;} + if((events.events&32)!=0){value=value|8;} + var i:i32=0;while(i<10){if(events.errors[i]!=0){value=value|4;}i+=1;} + } + } + deref output=IocpCompletion{token:op.token,value:value,kind:op.kind}; + _destroy(op);return 1; +} +pub fun iocp_shutdown() { + var op:ptr =_operations; + while(op!=null){var next:ptr =op.next;iocp_cancel(op.token);op=next;} + // Draining acknowledgements is necessary even after CancelIoEx succeeds. + while(_operations!=null){ + var result:IocpCompletion; + if(iocp_wait(&result,-1)<0){proc_exit(73);} + } + if(_port!=null){win_close(_port);_port=null;} +} diff --git a/std/task.wave b/std/task.wave new file mode 100644 index 00000000..a2253c7c --- /dev/null +++ b/std/task.wave @@ -0,0 +1,531 @@ +// Cooperative async tasks. Futures are lazy and single-consumer handles. +// Complete or cancel outstanding work, then call shutdown to release the executor. +import("std::mem::alloc")::{mem_alloc_zeroed, mem_free}; +import("std::process::core")::{proc_exit}; +import("std::net::event")::{NetEventLoop, NetEvent, NetEventWaitResult, net_event_loop_create, net_event_loop_close, net_event_add, net_event_remove, net_event_wait}; +import("std::net::error")::{NetError, NetResult}; +import("std::time::clock")::{TimeSpec, time_now_monotonic}; +import("std::time::sleep")::{time_sleep_ms}; +#[target(os="windows")] +import("std::sys::windows::iocp")::{IocpCompletion, iocp_start_io, iocp_watch, iocp_cancel, iocp_wait, iocp_shutdown}; + +struct TaskSlot { + id: i64; frame: ptr; size: i64; result: ptr; poll: ptr; + state: i32; waiting: i64; yield_step: i32; next: ptr; + kind: i32; fd: i64; flags: i32; deadline: i64; registered: bool; + buffer: ptr; length: i64; writing: bool; terminal: i64; +} +static _head: ptr = null; +static _next_id: i64 = 1; +static _driving: bool = false; +static _event_handle: i64 = -1; +static _event_backend: i32 = 0; +fun _now_ms() -> i64 { + var ts: TimeSpec; + if (time_now_monotonic(&ts) < 0) { proc_exit(73); } + return ts.sec * 1000 + ts.nsec / 1000000; +} +// A descriptor owns one native watch. Logical waiters retain separate masks and deadlines. +struct WatchGroup { + fd: i64; token: i64; flags: i32; active: bool; draining: bool; next: ptr; +} +static _watches: ptr = null; +fun _identity() -> i64 { + if (_next_id <= 0 || _next_id == 9223372036854775807) { proc_exit(71); } + var id: i64 = _next_id; _next_id += 1; return id; +} +fun _watch_for(fd: i64) -> ptr { + var group: ptr = _watches; + while (group != null) { if (group.fd == fd) { return group; } group = group.next; } + group = __wave_async_alloc(); + group.fd = fd; group.next = _watches; _watches = group; + return group; +} +fun _store_event(slot: ptr, value: i32) { + var output: ptr = slot.result as ptr; + deref output = value; _complete(slot.id); +} +fun _refresh_watch(fd: i64) { + if (fd < 0) { return; } + var group: ptr = _watch_for(fd); + var flags: i32 = 0; + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 4 && slot.kind == 1 && slot.fd == fd) { flags = flags | slot.flags; } + slot = slot.next; + } + if (group.draining) { return; } + if (group.active && group.flags == flags) { return; } + if (group.active) { + group.draining = _watch_stop(group.token, fd); + group.active = false; + if (group.draining) { return; } + } + group.flags = flags; + if (flags == 0) { return; } + group.token = _identity(); + var status: i64 = _watch_start(group.token, fd, flags); + if (status >= 0) { group.active = true; return; } + slot = _head; + while (slot != null) { + if (slot.state == 4 && slot.kind == 1 && slot.fd == fd) { _store_event(slot, status as i32); } + slot = slot.next; + } +} +fun _deliver_watch(token: i64, value: i32) { + var group: ptr = _watches; + while (group != null) { + if (group.token == token && (group.active || group.draining)) { + if (group.draining) { group.draining = false; _refresh_watch(group.fd); return; } + // IOCP has consumed its one-shot packet; epoll/kqueue detach here. + group.draining = _watch_stop(group.token, group.fd); + group.active = false; + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 4 && slot.kind == 1 && slot.fd == group.fd) { + if (value < 0 || (value & (slot.flags | 4 | 8)) != 0) { _store_event(slot, value); } + } + slot = slot.next; + } + _refresh_watch(group.fd); return; + } + group = group.next; + } +} +fun _event_result(slot: ptr, value: i32) { + _store_event(slot, value); _refresh_watch(slot.fd); +} +fun _unregister_portable(slot: ptr) { _refresh_watch(slot.fd); } +fun _watch_start_portable(token: i64, fd: i64, flags: i32) -> i64 { + if (_event_handle < 0) { + var created: NetResult = net_event_loop_create(64); + if (!created.ok) { return created.error.native_code; } + _event_handle = created.value.handle; _event_backend = created.value.backend; + } + var status: NetError = net_event_add(NetEventLoop { handle: _event_handle, backend: _event_backend }, fd, token as u64, flags); + if (status.kind != 0) { return status.native_code; } + return 0; +} +fun _watch_stop_portable(fd: i64) -> bool { + if (_event_handle >= 0) { net_event_remove(NetEventLoop { handle: _event_handle, backend: _event_backend }, fd); } + return false; +} +fun _arm_portable(slot: ptr) { slot.state = 4; _refresh_watch(slot.fd); } +fun _park_portable(blocking: bool) -> bool { + var deadline: i64 = -1; + var registered: bool = false; + var waiting: bool = false; + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 4 && slot.kind == 1) { + waiting = true; + if (slot.fd >= 0) { registered = true; } + if (slot.deadline >= 0 && (deadline < 0 || slot.deadline < deadline)) { deadline = slot.deadline; } + } + slot = slot.next; + } + if (!waiting) { return false; } + var timeout: i32 = -1; + if (deadline >= 0) { + var remaining: i64 = deadline - _now_ms(); + if (remaining < 0) { remaining = 0; } + if (remaining > 2147483647) { remaining = 2147483647; } + timeout = remaining as i32; + } + if (!blocking) { timeout = 0; } + if (registered) { + var events: array; + var batch: NetEventWaitResult = net_event_wait(NetEventLoop { handle: _event_handle, backend: _event_backend }, &events[0], 64, timeout); + if (batch.error.kind != 0 && batch.error.kind != 2) { proc_exit(73); } + var i: i32 = 0; + while (i < batch.count) { + var event: NetEvent = events[i]; + var flags: i32 = 0; + if (event.readable) { flags = flags | 1; } + if (event.writable) { flags = flags | 2; } + if (event.error) { flags = flags | 4; } + if (event.hangup) { flags = flags | 8; } + _deliver_watch(event.token as i64, flags); + i += 1; + } + } else { + if (timeout < 0) { return false; } + time_sleep_ms(timeout as i64); + } + var now: i64 = _now_ms(); + slot = _head; + while (slot != null) { + if (slot.state == 4 && slot.kind == 1 && slot.deadline >= 0 && slot.deadline <= now) { + if (slot.fd < 0) { _event_result(slot, 0); } + else { _event_result(slot, -110); } + } + slot = slot.next; + } + return true; +} + +fun _find(id: i64) -> ptr { + var slot: ptr = _head; + while (slot != null) { + if (slot.id == id && id > 0) { return slot; } + slot = slot.next; + } + proc_exit(70); +} +fun _wake_dependents(id: i64) { + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 4 && slot.waiting == id) { slot.state = 2; } + slot = slot.next; + } +} +// These exported symbols form the private compiler/runtime boundary. +export(c, "__wave_task_alloc") fun _allocate(size: i64) -> ptr { + var memory: ptr = mem_alloc_zeroed(size); + if (memory == null) { proc_exit(71); } + return memory; +} +export(c, "__wave_task_free") fun _free(memory: ptr, size: i64) { mem_free(memory, size); } +export(c, "__wave_task_new") fun _new( + frame: ptr, size: i64, result: ptr, poll: ptr +) -> i64 { + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 0) { break; } + slot = slot.next; + } + if (slot == null) { + slot = __wave_async_alloc(); + slot.next = _head; + _head = slot; + } + slot.id = _identity(); + slot.frame = frame; + slot.size = size; + slot.result = result; + slot.poll = poll; + slot.state = 1; + slot.waiting = 0; + slot.yield_step = 0; + slot.kind = 0; slot.fd = -1; slot.flags = 0; slot.deadline = -1; slot.registered = false; + slot.buffer = null; slot.length = 0; slot.writing = false; slot.terminal = 0; + return slot.id; +} +export(c, "__wave_task_spawn") fun _spawn(id: i64) -> i64 { + var slot: ptr = _find(id); + if (slot.state == 1) { + if (slot.kind == 1 || slot.kind == 2) { _arm(slot); } + else if (slot.kind == 3) { _cancel(slot.waiting); slot.state = 4; _settle_cancelled(); _finish_cancel_waiter(slot); } + else { slot.state = 2; } + } + if (slot.state == 0 || slot.state == 6 || slot.state == 7) { proc_exit(70); } + return id; +} +export(c, "__wave_task_ready") fun _ready(id: i64) -> i32 { + var slot: ptr = _find(id); + if (slot.state == 0 || slot.state == 6 || slot.state == 7) { proc_exit(70); } + if (slot.state == 5) { return 1; } + return 0; +} +export(c, "__wave_task_wait") fun _wait(parent: i64, child: i64) { + if (parent == child) { proc_exit(72); } + var p: ptr = _find(parent); + p.waiting = child; + p.state = 4; + _spawn(child); + if (_ready(child) == 1) { p.state = 2; } +} +export(c, "__wave_task_complete") fun _complete(id: i64) { + var slot: ptr = _find(id); + slot.state = 5; + slot.waiting = 0; + _wake_dependents(id); +} +export(c, "__wave_task_result") fun _result(id: i64) -> ptr { + if (_ready(id) != 1) { proc_exit(70); } + return _find(id).result; +} +export(c, "__wave_task_release") fun _release(id: i64) { + var slot: ptr = _find(id); + if (slot.state != 5 && slot.state != 6) { proc_exit(70); } + if (slot.frame != null) { mem_free(slot.frame, slot.size); } + slot.frame = null; + slot.result = null; + slot.poll = null; + slot.state = 0; + slot.id = 0; +} +fun _finish_cancel_waiter(slot: ptr) { + var child: ptr = _find(slot.waiting); + if (child.state == 5 || child.state == 6) { _release(child.id); slot.waiting = 0; _complete(slot.id); } +} +fun _settle_cancelled() { + var changed: bool = true; + while (changed) { + changed = false; + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 7 && !slot.registered) { + var ready: bool = slot.waiting == 0; + if (!ready) { var child: ptr = _find(slot.waiting); ready = child.state == 5 || child.state == 6; } + if (ready) { + // Await transfers the child handle to this task. Reclaim its slot + // only after native cancellation has acknowledged all borrowed buffers. + if (slot.waiting != 0) { _release(slot.waiting); } + if (slot.frame != null) { mem_free(slot.frame, slot.size); slot.frame = null; } + slot.result = null; slot.poll = null; + slot.state = 6; slot.waiting = 0; _wake_dependents(slot.id); changed = true; + } + } + slot = slot.next; + } + } +} +export(c, "__wave_task_cancel") fun _cancel(id: i64) -> i32 { + var first: ptr = _find(id); + if (first.state == 5 || first.state == 6 || first.state == 7) { return 0; } + var cursor: i64 = id; + while (cursor > 0) { + var slot: ptr = _find(cursor); + if (slot.state == 0 || slot.state == 3) { proc_exit(70); } + if (slot.state == 5 || slot.state == 6 || slot.state == 7) { break; } + slot.state = 7; + _cancel_registration(slot); + cursor = slot.waiting; + } + _settle_cancelled(); + return 1; +} +export(c, "__wave_task_cancel_join") fun _cancel_join(id: i64) -> i64 { + _find(id); + var waiter: i64 = _new(null, 0, null, null); + var slot: ptr = _find(waiter); + slot.kind = 3; slot.waiting = id; + return waiter; +} +export(c, "__wave_task_drive") fun _drive(id: i64) { + if (_driving) { proc_exit(72); } + _driving = true; + _spawn(id); + while (_ready(id) == 0) { + var progress: bool = false; + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 2) { + progress = true; + slot.state = 3; + if (slot.poll == null) { + if (slot.kind == 3) { slot.state = 4; _finish_cancel_waiter(slot); } + else if (slot.yield_step == 0) { slot.yield_step = 1; slot.state = 2; } + else { _complete(slot.id); } + } else { + __wave_async_invoke(slot.poll, slot.frame, slot.id); + } + } + slot = slot.next; + } + if (!progress) { if (!_park(true)) { proc_exit(72); } } else { _park(false); } + } + _driving = false; +} +export(c, "__wave_task_yield") fun _yield() -> i64 { + return _new(null, 0, null, null); +} +export(c, "__wave_task_shutdown") fun _shutdown() { + if (_driving) { proc_exit(72); } + _shutdown_native(); + var slot: ptr = _head; + while (slot != null) { + var next: ptr = slot.next; + if (slot.frame != null) { mem_free(slot.frame, slot.size); } + // Compiler supplies the exact target layout, including padding. + __wave_async_free_slot(slot); + slot = next; + } + _head = null; + var group: ptr = _watches; + while (group != null) { var next: ptr = group.next; __wave_async_free_slot(group); group = next; } + _watches = null; + if (_event_handle >= 0) { net_event_loop_close(NetEventLoop { handle: _event_handle, backend: _event_backend }); _event_handle = -1; } +} +pub fun block_on(future: Future) -> T { return __wave_async_block_on(future); } +pub fun spawn(future: Future) -> Future { return __wave_async_spawn(future); } +pub async fun join(future: Future) -> T { return await future; } +pub fun cancel(future: Future) -> bool { return __wave_async_cancel(future); } +pub fun yield_now() -> Future { return __wave_async_yield(); } +pub fun shutdown() { __wave_async_shutdown(); } + +export(c, "__wave_task_interest") fun _interest(fd: i64, flags: i32, timeout_ms: i64) -> i64 { + var memory: ptr = _allocate(4); + var id: i64 = _new(memory, 4, memory, null); + var slot: ptr = _find(id); + slot.kind = 1; slot.fd = fd; slot.flags = flags; + if (timeout_ms < -1 || (fd >= 0 && (flags < 1 || flags > 3)) || (fd < 0 && timeout_ms < 0)) { _event_result(slot, -22); return id; } + if (timeout_ms >= 0) { + var now: i64 = _now_ms(); + if (timeout_ms > 9223372036854775807 - now) { _event_result(slot, -22); return id; } + slot.deadline = now + timeout_ms; + } + return id; +} +export(c, "__wave_task_close_fd") fun _close_fd(fd: i64) { + var slot: ptr = _head; + while (slot != null) { + if (slot.fd == fd && slot.state != 0 && slot.state != 5 && slot.state != 6 && slot.state != 7) { + if (slot.kind == 1) { _store_event(slot, -9); } + else if (slot.kind == 2) { _close_io(slot); } + } + slot = slot.next; + } + _refresh_watch(fd); +} +// Ready bits: readable=1, writable=2, error=4, hangup=8; failures are negative. +// Waiters on the same descriptor share a native watch; cancellation affects only that waiter. +pub fun wait_fd(fd: i64, flags: i32, timeout_ms: i64) -> Future { return __wave_async_interest(fd, flags, timeout_ms); } +pub fun sleep_ms(milliseconds: i64) -> Future { return __wave_async_sleep(milliseconds); } +pub fun closing_fd(fd: i64) { __wave_async_close_fd(fd); } + +#[target(os="linux")] fun _arm(slot: ptr) { _arm_portable(slot); } +#[target(os="linux")] fun _park(blocking: bool) -> bool { return _park_portable(blocking); } +#[target(os="linux")] fun _unregister(slot: ptr) { _unregister_portable(slot); } +#[target(os="linux")] fun _cancel_registration(slot: ptr) { _unregister_portable(slot); } +#[target(os="linux")] fun _shutdown_native() { } +#[target(os="linux")] fun _watch_start(token: i64, fd: i64, flags: i32) -> i64 { return _watch_start_portable(token, fd, flags); } +#[target(os="linux")] fun _watch_stop(token: i64, fd: i64) -> bool { return _watch_stop_portable(fd); } +#[target(os="linux")] fun _close_io(slot: ptr) { } + +#[target(os="macos")] fun _arm(slot: ptr) { _arm_portable(slot); } +#[target(os="macos")] fun _park(blocking: bool) -> bool { return _park_portable(blocking); } +#[target(os="macos")] fun _unregister(slot: ptr) { _unregister_portable(slot); } +#[target(os="macos")] fun _cancel_registration(slot: ptr) { _unregister_portable(slot); } +#[target(os="macos")] fun _shutdown_native() { } +#[target(os="macos")] fun _watch_start(token: i64, fd: i64, flags: i32) -> i64 { return _watch_start_portable(token, fd, flags); } +#[target(os="macos")] fun _watch_stop(token: i64, fd: i64) -> bool { return _watch_stop_portable(fd); } +#[target(os="macos")] fun _close_io(slot: ptr) { } + +#[target(os="freebsd")] fun _arm(slot: ptr) { _arm_portable(slot); } +#[target(os="freebsd")] fun _park(blocking: bool) -> bool { return _park_portable(blocking); } +#[target(os="freebsd")] fun _unregister(slot: ptr) { _unregister_portable(slot); } +#[target(os="freebsd")] fun _cancel_registration(slot: ptr) { _unregister_portable(slot); } +#[target(os="freebsd")] fun _shutdown_native() { } +#[target(os="freebsd")] fun _watch_start(token: i64, fd: i64, flags: i32) -> i64 { return _watch_start_portable(token, fd, flags); } +#[target(os="freebsd")] fun _watch_stop(token: i64, fd: i64) -> bool { return _watch_stop_portable(fd); } +#[target(os="freebsd")] fun _close_io(slot: ptr) { } + +#[target(os="windows")] +fun _unregister(slot: ptr) { _cancel_registration(slot); } +#[target(os="windows")] +fun _cancel_registration(slot: ptr) { + if (slot.kind == 1) { _refresh_watch(slot.fd); return; } + if (slot.registered && iocp_cancel(slot.id) == 0) { slot.registered = false; } +} +#[target(os="windows")] +fun _watch_start(token: i64, fd: i64, flags: i32) -> i64 { return iocp_watch(token, fd, flags); } +#[target(os="windows")] +fun _watch_stop(token: i64, fd: i64) -> bool { return iocp_cancel(token) != 0; } +#[target(os="windows")] +fun _close_io(slot: ptr) { + slot.terminal = -9; slot.deadline = -1; + _cancel_registration(slot); + if (!slot.registered) { _io_result(slot, -9); } +} +#[target(os="windows")] +fun _shutdown_native() { iocp_shutdown(); } +#[target(os="windows")] +fun _io_result(slot: ptr, value: i64) { + var output: ptr = slot.result as ptr; + deref output = value; slot.registered = false; _complete(slot.id); +} +#[target(os="windows")] +fun _arm(slot: ptr) { + slot.state = 4; + if (slot.fd < 0) { return; } + if (slot.kind == 1) { _refresh_watch(slot.fd); return; } + var status: i64 = iocp_start_io(slot.id, slot.fd, slot.buffer, slot.length, slot.writing); + if (status < 0) { _io_result(slot, status); } else { slot.registered = true; } +} +#[target(os="windows")] +fun _park(blocking: bool) -> bool { + var deadline: i64 = -1; var registered: bool = false; var waiting: bool = false; + var group: ptr = _watches; + while (group != null) { + if (group.active || group.draining) { registered = true; waiting = true; } + group = group.next; + } + var slot: ptr = _head; + while (slot != null) { + if (slot.state == 4 || slot.state == 7) { + if (slot.registered) { registered = true; waiting = true; } + if (slot.state == 4 && (slot.kind == 1 || slot.kind == 2)) { + waiting = true; + if (slot.deadline >= 0 && (deadline < 0 || slot.deadline < deadline)) { deadline = slot.deadline; } + } + } + slot = slot.next; + } + if (!waiting) { return false; } + var timeout: i32 = -1; + if (deadline >= 0) { + var remaining: i64 = deadline - _now_ms(); + if (remaining < 0) { remaining = 0; } + if (remaining > 2147483647) { remaining = 2147483647; } + timeout = remaining as i32; + } + if (!blocking) { timeout = 0; } + if (registered) { + var completion: IocpCompletion; + var count: i64 = iocp_wait(&completion, timeout); + if (count < 0) { proc_exit(73); } + if (count > 0) { + if (completion.kind == 1) { _deliver_watch(completion.token, completion.value as i32); } + else { + slot = _head; + while (slot != null) { + if (slot.id == completion.token && slot.registered) { + slot.registered = false; + if (slot.state == 7) { _settle_cancelled(); } + else { + var value: i64 = completion.value; + if (slot.terminal != 0) { value = slot.terminal; } + _io_result(slot, value); + } + break; + } + slot = slot.next; + } + } + } + } else { if (timeout < 0) { return false; } time_sleep_ms(timeout as i64); } + var now: i64 = _now_ms(); slot = _head; + while (slot != null) { + if (slot.state == 4 && (slot.kind == 1 || slot.kind == 2) && slot.deadline >= 0 && slot.deadline <= now) { + slot.deadline = -1; + if (slot.kind == 1) { + if (slot.fd < 0) { _event_result(slot, 0); } else { _event_result(slot, -110); } + } else { + slot.terminal = -110; _cancel_registration(slot); + if (!slot.registered) { _io_result(slot, -110); } + } + } + slot = slot.next; + } + _settle_cancelled(); return true; +} +#[target(os="windows")] +export(c, "__wave_task_io") fun _io(fd: i64, buffer: ptr, length: i64, writing: i32, timeout_ms: i64) -> i64 { + var memory: ptr = _allocate(8); + var id: i64 = _new(memory, 8, memory, null); var slot: ptr = _find(id); + slot.kind = 2; slot.fd = fd; slot.buffer = buffer; slot.length = length; slot.writing = writing != 0; + if (fd < 0 || buffer == null || length <= 0 || length > 4294967295 || timeout_ms < -1) { _io_result(slot, -22); return id; } + if (timeout_ms >= 0) { + var now: i64 = _now_ms(); if (timeout_ms > 9223372036854775807 - now) { _io_result(slot, -22); return id; } + slot.deadline = now + timeout_ms; + } + return id; +} +// cancel() requests cancellation; cancel_and_join() consumes the handle and waits for cleanup. +pub fun cancel_and_join(future: Future) -> Future { return __wave_async_cancel_join(future); } +#[target(os="windows")] +pub fun overlapped_read(fd: i64, buffer: ptr, length: i64, timeout_ms: i64) -> Future { return __wave_async_io(fd, buffer, length, 0, timeout_ms); } +#[target(os="windows")] +pub fun overlapped_write(fd: i64, buffer: ptr, length: i64, timeout_ms: i64) -> Future { return __wave_async_io(fd, buffer, length, 1, timeout_ms); } diff --git a/std/task/net.wave b/std/task/net.wave new file mode 100644 index 00000000..da0df50c --- /dev/null +++ b/std/task/net.wave @@ -0,0 +1,201 @@ +// Async socket I/O uses readiness on Unix and overlapped completion on Windows. +// Keep borrowed buffers alive until completion or until cancel_and_join() completes. +// Use close() to detach task registrations before closing a descriptor. +import("std::task")::{wait_fd, closing_fd, yield_now}; +import("std::net::socket_base")::{net_recv_once, net_send_once, net_accept_socket_v4, net_connect_socket_v4, net_accept_v6, net_connect_v6, net_close}; +import("std::net::socketopt")::{net_set_nonblock}; +import("std::net::poll")::{net_socket_error}; +import("std::net::address")::{SocketAddrV4, SocketAddrV6}; +import("std::net::error")::{NetError, NetIoResult, net_io_ok, net_io_eof, net_io_error, net_error_from_native, NET_ERROR_WOULD_BLOCK, NET_ERROR_INTERRUPTED}; +import("std::time::clock")::{TimeSpec, time_now_monotonic}; +fun _now() -> i64 { + var ts: TimeSpec; + if (time_now_monotonic(&ts) < 0) { return -1; } + return ts.sec * 1000 + ts.nsec / 1000000; +} +fun _deadline(timeout_ms: i64) -> i64 { + if (timeout_ms == -1) { return -1; } + var now: i64 = _now(); + if (timeout_ms < -1 || now < 0 || timeout_ms > 9223372036854775807 - now) { return -2; } + return now + timeout_ms; +} +fun _remaining(deadline: i64) -> i64 { + if (deadline < 0) { return -1; } + var now: i64 = _now(); + if (now < 0 || now >= deadline) { return 0; } + return deadline - now; +} +fun _retry(value: i64) -> bool { + var error: NetError = net_error_from_native(value); + return error.kind == NET_ERROR_WOULD_BLOCK || error.kind == NET_ERROR_INTERRUPTED; +} +async fun _read_readiness(fd: i64, buffer: ptr, size: i64, timeout_ms: i64) -> NetIoResult { + if ((buffer == null && size > 0) || size < 0) { return net_io_error(0, -22); } + if (size == 0) { return net_io_ok(0); } + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return net_io_error(0, status); } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return net_io_error(0, -22); } + while (true) { + var count: i64 = net_recv_once(fd, buffer, size, 0); + if (count > 0) { return net_io_ok(count); } + if (count == 0) { return net_io_eof(0); } + if (!_retry(count)) { return net_io_error(0, count); } + var remaining: i64 = _remaining(deadline); + if (remaining == 0) { return net_io_error(0, -110); } + var ready: i32 = await wait_fd(fd, 1, remaining); + if (ready < 0) { return net_io_error(0, ready as i64); } + // Retry recv even on hangup: a peer may have sent final readable bytes. + } +} +async fun _write_readiness(fd: i64, buffer: ptr, size: i64, timeout_ms: i64) -> NetIoResult { + if ((buffer == null && size > 0) || size < 0) { return net_io_error(0, -22); } + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return net_io_error(0, status); } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return net_io_error(0, -22); } + var written: i64 = 0; + while (written < size) { + var count: i64 = net_send_once(fd, buffer + written, size - written, 0); + if (count > 0) { + written += count; + if (written < size) { + if (_remaining(deadline) == 0) { return net_io_error(written, -110); } + await yield_now(); + } + } + else { + if (count == 0) { return net_io_error(written, -32); } + if (!_retry(count)) { return net_io_error(written, count); } + var remaining: i64 = _remaining(deadline); + if (remaining == 0) { return net_io_error(written, -110); } + var ready: i32 = await wait_fd(fd, 2, remaining); + if (ready < 0) { return net_io_error(written, ready as i64); } + } + } + return net_io_ok(written); +} +pub async fun accept(fd: i64, timeout_ms: i64) -> i64 { + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return status; } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return -22; } + while (true) { + var address: SocketAddrV4; + var accepted: i64 = net_accept_socket_v4(fd, &address); + if (accepted >= 0) { + status = net_set_nonblock(accepted, 1); + if (status < 0) { net_close(accepted); return status; } + return accepted; + } + if (!_retry(accepted)) { return accepted; } + var remaining: i64 = _remaining(deadline); + if (remaining == 0) { return -110; } + var ready: i32 = await wait_fd(fd, 1, remaining); + if (ready < 0) { return ready as i64; } + } +} +pub async fun connect(fd: i64, address: SocketAddrV4, timeout_ms: i64) -> i64 { + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return status; } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return -22; } + status = net_connect_socket_v4(fd, address); + if (status >= 0) { return 0; } + return await _finish_connect(fd, status, deadline); +} +async fun _finish_connect(fd: i64, status: i64, deadline: i64) -> i64 { + // EINPROGRESS/EALREADY and WSAEWOULDBLOCK are pending connects. + if (status != -115 && status != -36 && status != -37 && status != -114 && status != -10035 && status != -10036 && status != -10037 && !_retry(status)) { return status; } + var ready: i32 = await wait_fd(fd, 2, _remaining(deadline)); + if (ready < 0) { return ready as i64; } + return net_socket_error(fd); +} +pub async fun accept_v6(fd: i64, timeout_ms: i64) -> i64 { + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return status; } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return -22; } + while (true) { + var address: SocketAddrV6; + var accepted: i64 = net_accept_v6(fd, &address); + if (accepted >= 0) { + status = net_set_nonblock(accepted, 1); + if (status < 0) { net_close(accepted); return status; } + return accepted; + } + if (!_retry(accepted)) { return accepted; } + var remaining: i64 = _remaining(deadline); + if (remaining == 0) { return -110; } + var ready: i32 = await wait_fd(fd, 1, remaining); + if (ready < 0) { return ready as i64; } + } +} +pub async fun connect_v6(fd: i64, address: SocketAddrV6, timeout_ms: i64) -> i64 { + var status: i64 = net_set_nonblock(fd, 1); + if (status < 0) { return status; } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return -22; } + status = net_connect_v6(fd, address); + if (status >= 0) { return 0; } + return await _finish_connect(fd, status, deadline); +} +// Preserve partial progress on EOF and errors; one deadline covers the entire read. +pub async fun read_exact(fd: i64, buffer: ptr, size: i64, timeout_ms: i64) -> NetIoResult { + if (size < 0 || (buffer == null && size > 0)) { return net_io_error(0, -22); } + var deadline: i64 = _deadline(timeout_ms); + if (deadline == -2) { return net_io_error(0, -22); } + var received: i64 = 0; + while (received < size) { + var result: NetIoResult = await read(fd, buffer + received, size - received, _remaining(deadline)); + received += result.count; + if (result.eof) { return net_io_eof(received); } + if (result.error.kind != 0) { return NetIoResult { count: received, eof: false, error: result.error }; } + if (result.count == 0) { return net_io_error(received, -5); } + if (received < size && _remaining(deadline) == 0) { return net_io_error(received, -110); } + } + return net_io_ok(received); +} +pub fun close(fd: i64) -> i64 { closing_fd(fd); return net_close(fd); } + +#[target(os="linux")] +pub async fun read(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _read_readiness(fd,buffer,size,timeout_ms); } +#[target(os="linux")] +pub async fun write_all(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _write_readiness(fd,buffer,size,timeout_ms); } + +#[target(os="macos")] +pub async fun read(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _read_readiness(fd,buffer,size,timeout_ms); } +#[target(os="macos")] +pub async fun write_all(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _write_readiness(fd,buffer,size,timeout_ms); } + +#[target(os="freebsd")] +pub async fun read(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _read_readiness(fd,buffer,size,timeout_ms); } +#[target(os="freebsd")] +pub async fun write_all(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { return await _write_readiness(fd,buffer,size,timeout_ms); } + +#[target(os="windows")] +import("std::task")::{overlapped_read, overlapped_write}; +#[target(os="windows")] +pub async fun read(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { + if((buffer==null&&size>0)||size<0){return net_io_error(0,-22);} + if(size==0){return net_io_ok(0);} + var length:i64=size;if(length>4294967295){length=4294967295;} + var count:i64=await overlapped_read(fd,buffer,length,timeout_ms); + if(count<0){return net_io_error(0,count);} + if(count==0){return net_io_eof(0);} + return net_io_ok(count); +} +#[target(os="windows")] +pub async fun write_all(fd:i64,buffer:ptr,size:i64,timeout_ms:i64)->NetIoResult { + if((buffer==null&&size>0)||size<0){return net_io_error(0,-22);} + var deadline:i64=_deadline(timeout_ms);if(deadline== -2){return net_io_error(0,-22);} + var written:i64=0; + while(written4294967295){length=4294967295;} + var count:i64=await overlapped_write(fd,buffer+written,length,_remaining(deadline)); + if(count<0){return net_io_error(written,count);} + if(count==0){return net_io_error(written,-32);} + written+=count; + } + return net_io_ok(written); +} diff --git a/tests/cases/cases.toml b/tests/cases/cases.toml index c418456c..0ab91cd7 100644 --- a/tests/cases/cases.toml +++ b/tests/cases/cases.toml @@ -1,35 +1,44 @@ version = 2 -# OS rows and architecture columns are the source of truth. Every cell must -# appear in both tables so support and CI coverage can be reviewed at a glance. +# List only existing OS ports or publicly documented porting work. Every listed +# cell must appear in both tables; false means planned in Wave, not an invented +# OS/architecture combination. # # Named processor/project cells remain planned until Wave and Whale define # their target, ABI, and runtime contracts. +# OpenBSD: https://www.openbsd.org/plat.html +# NetBSD RISC-V: https://wiki.netbsd.org/ports/riscv/ +# DragonFly amd64: https://www.dragonflybsd.org/docs/faq/FAQ-English/ (Wave #497) +# Android RISC-V: https://github.com/google/android-riscv64 (provisional ABI) +# Fuchsia RISC-V: https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0211_fuchsia_on_risc-v +# iOS amd64 is reserved for the simulator, not physical iOS devices (Wave #473). [supported] linux = { amd64 = true, arm64 = true, riscv64 = true, loong64 = true, rhea = false, shakti = false, xiangshan = false, t-head = false } -macos = { amd64 = true, arm64 = true, riscv64 = false } -windows = { amd64 = true, arm64 = true, riscv64 = false } +macos = { amd64 = true, arm64 = true } +windows = { amd64 = true, arm64 = true } freebsd = { amd64 = true, arm64 = true, riscv64 = true } freestanding = { amd64 = true, arm64 = false, riscv64 = false } openbsd = { amd64 = false, arm64 = false, riscv64 = false } netbsd = { amd64 = false, arm64 = false, riscv64 = false } +dragonfly = { amd64 = false } fuchsia = { amd64 = false, arm64 = false, riscv64 = false } android = { amd64 = false, arm64 = false, riscv64 = false } -ios = { amd64 = false, arm64 = false, riscv64 = false } +ios = { amd64 = false, arm64 = false } wasm = { wasm32 = true, wasm64 = true } wasi = { wasm32 = true, wasm64 = false } [ci] linux = { amd64 = true, arm64 = true, riscv64 = true, loong64 = true, rhea = false, shakti = false, xiangshan = false, t-head = false } -macos = { amd64 = true, arm64 = true, riscv64 = false } -windows = { amd64 = true, arm64 = true, riscv64 = false } +macos = { amd64 = true, arm64 = true } +windows = { amd64 = true, arm64 = true } freebsd = { amd64 = true, arm64 = true, riscv64 = true } freestanding = { amd64 = true, arm64 = false, riscv64 = false } openbsd = { amd64 = false, arm64 = false, riscv64 = false } netbsd = { amd64 = false, arm64 = false, riscv64 = false } +dragonfly = { amd64 = false } fuchsia = { amd64 = false, arm64 = false, riscv64 = false } android = { amd64 = false, arm64 = false, riscv64 = false } -ios = { amd64 = false, arm64 = false, riscv64 = false } +ios = { amd64 = false, arm64 = false } wasm = { wasm32 = true, wasm64 = true } wasi = { wasm32 = true, wasm64 = false } diff --git a/tests/cases/windows/riscv64/test1.wave b/tests/cases/dragonfly/amd64/test1.wave similarity index 96% rename from tests/cases/windows/riscv64/test1.wave rename to tests/cases/dragonfly/amd64/test1.wave index fba78368..0ea40ccc 100644 --- a/tests/cases/windows/riscv64/test1.wave +++ b/tests/cases/dragonfly/amd64/test1.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 1 and target data transport. +// dragonfly/amd64: functional workload 1 and target data transport. // Stack-machine execution with payload variants, generic results and large // aggregate returns. Error paths must preserve the last committed stack. variant Op { Push(i64), Add, Multiply, Divide, Duplicate, Halt } @@ -111,13 +111,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test10.wave b/tests/cases/dragonfly/amd64/test10.wave similarity index 94% rename from tests/cases/windows/riscv64/test10.wave rename to tests/cases/dragonfly/amd64/test10.wave index 47b3593e..dc851acc 100644 --- a/tests/cases/windows/riscv64/test10.wave +++ b/tests/cases/dragonfly/amd64/test10.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 10 and target data transport. +// dragonfly/amd64: functional workload 10 and target data transport. // Recursively build and traverse an indexed binary search tree. Validate // subtree totals, unsuccessful searches and traversal order across 63 nodes. struct Node { key: i64; left: i64; right: i64; } @@ -64,13 +64,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test2.wave b/tests/cases/dragonfly/amd64/test2.wave similarity index 95% rename from tests/cases/windows/riscv64/test2.wave rename to tests/cases/dragonfly/amd64/test2.wave index b3e87b73..d3ab1624 100644 --- a/tests/cases/windows/riscv64/test2.wave +++ b/tests/cases/dragonfly/amd64/test2.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 2 and target data transport. +// dragonfly/amd64: functional workload 2 and target data transport. // Stable sorting of wide records through pointers and nested aggregate fields. // A returned snapshot must own its array values, while explicit pointer writes // must affect the caller. Duplicate keys expose accidental unstable reordering. @@ -85,13 +85,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test3.wave b/tests/cases/dragonfly/amd64/test3.wave similarity index 95% rename from tests/cases/windows/riscv64/test3.wave rename to tests/cases/dragonfly/amd64/test3.wave index 13b12c1b..59ff46b7 100644 --- a/tests/cases/windows/riscv64/test3.wave +++ b/tests/cases/dragonfly/amd64/test3.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 3 and target data transport. +// dragonfly/amd64: functional workload 3 and target data transport. // Weighted routing: disconnected vertices, predecessor reconstruction and // independently known distances. Graph and route snapshots travel by value. struct Graph { weights: array; } @@ -99,13 +99,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test4.wave b/tests/cases/dragonfly/amd64/test4.wave similarity index 96% rename from tests/cases/windows/riscv64/test4.wave rename to tests/cases/dragonfly/amd64/test4.wave index 667e2020..6938f765 100644 --- a/tests/cases/windows/riscv64/test4.wave +++ b/tests/cases/dragonfly/amd64/test4.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 4 and target data transport. +// dragonfly/amd64: functional workload 4 and target data transport. // Open-addressed key/value store: long collisions, tombstones, replacements, // full-table termination and immutable snapshots of payload-variant arrays. variant Slot { Empty, Deleted, Used(i64, i64) } @@ -121,13 +121,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test5.wave b/tests/cases/dragonfly/amd64/test5.wave similarity index 96% rename from tests/cases/windows/riscv64/test5.wave rename to tests/cases/dragonfly/amd64/test5.wave index f7a78e69..690e9373 100644 --- a/tests/cases/windows/riscv64/test5.wave +++ b/tests/cases/dragonfly/amd64/test5.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 5 and target data transport. +// dragonfly/amd64: functional workload 5 and target data transport. // Bounded run-length packet codec. Round trips alone are insufficient: check // wire bytes, truncated pairs, zero runs, capacity and checksum corruption. struct Bytes { data: array; length: i64; } @@ -116,13 +116,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test6.wave b/tests/cases/dragonfly/amd64/test6.wave similarity index 94% rename from tests/cases/windows/riscv64/test6.wave rename to tests/cases/dragonfly/amd64/test6.wave index c2ec0a48..277960cd 100644 --- a/tests/cases/windows/riscv64/test6.wave +++ b/tests/cases/dragonfly/amd64/test6.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 6 and target data transport. +// dragonfly/amd64: functional workload 6 and target data transport. // Base-10000 multi-precision multiplication, with carries crossing every limb. // Exact independently computed limbs exercise wide intermediates and copies. struct Big { limb: array; } @@ -71,13 +71,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test7.wave b/tests/cases/dragonfly/amd64/test7.wave similarity index 95% rename from tests/cases/windows/riscv64/test7.wave rename to tests/cases/dragonfly/amd64/test7.wave index 88c45a38..437c1c51 100644 --- a/tests/cases/windows/riscv64/test7.wave +++ b/tests/cases/dragonfly/amd64/test7.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 7 and target data transport. +// dragonfly/amd64: functional workload 7 and target data transport. // A bounded arena with generation-tagged handles. Reuse must invalidate stale // handles, preserve neighboring allocations, and clear newly allocated bytes. struct Handle { index: i64; generation: i64; } @@ -84,13 +84,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test8.wave b/tests/cases/dragonfly/amd64/test8.wave similarity index 94% rename from tests/cases/windows/riscv64/test8.wave rename to tests/cases/dragonfly/amd64/test8.wave index 2c0cd00a..53b945d5 100644 --- a/tests/cases/windows/riscv64/test8.wave +++ b/tests/cases/dragonfly/amd64/test8.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 8 and target data transport. +// dragonfly/amd64: functional workload 8 and target data transport. // Cooperative round-robin scheduling with payload states, cancellation, // repeated queue wraparound and exact per-task accounting. struct Progress { remaining: i64; sum: i64; } @@ -78,13 +78,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/windows/riscv64/test9.wave b/tests/cases/dragonfly/amd64/test9.wave similarity index 94% rename from tests/cases/windows/riscv64/test9.wave rename to tests/cases/dragonfly/amd64/test9.wave index 4989b1ee..6f1a0999 100644 --- a/tests/cases/windows/riscv64/test9.wave +++ b/tests/cases/dragonfly/amd64/test9.wave @@ -1,5 +1,5 @@ // Generated by tools/populate_case_matrix.py. -// windows/riscv64: functional workload 9 and target data transport. +// dragonfly/amd64: functional workload 9 and target data transport. // Image convolution with clamped boundaries and a separate intermediate buffer. // Every output is checked against an independently calculated golden image. struct Image { pixels: array; guard: u64; } @@ -68,13 +68,11 @@ fun exercise_language() -> i32 { fun target_mix(state: u64, word: u64) -> u64 { var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") + "imul rax, rax, 33" + "xor rax, rcx" + in("rax") state + in("rcx") word + out("rax") result }; return result % 65521; } diff --git a/tests/cases/freebsd/amd64/test6.wave b/tests/cases/freebsd/amd64/test6.wave index d8021539..120ebde1 100644 --- a/tests/cases/freebsd/amd64/test6.wave +++ b/tests/cases/freebsd/amd64/test6.wave @@ -1,126 +1,130 @@ // wave-test: mode=build, runner=compile, target=x86_64-unknown-freebsd, emit=obj -// Generated by tools/populate_case_matrix.py. -// freebsd/amd64: functional workload 6 and target data transport. -// Base-10000 multi-precision multiplication, with carries crossing every limb. -// Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } -fun zero() -> Big { - var result: Big; - var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } - return result; +// A page-backed 256-vertex graph uses one bit per edge and a fixed BFS queue. +// The oracle minimizes modular +1/+17 paths independently of graph traversal. +import("std::mem::alloc")::{mem_alloc_zeroed, mem_free, mem_page_size}; + +fun add_edge(graph: ptr, from: i64, to: i64) { + var offset: i64 = from * 32 + to / 8; + var mask: u8 = (1 as u8) << ((to % 8) as u8); + graph[offset] = graph[offset] | mask; } -fun multiply(a: Big, b: Big) -> Big { - var result: Big = zero(); - var i: i64 = 0; - while (i < 8) { - var carry: u64 = 0; - var j: i64 = 0; - while (j < 8) { - var index: i64 = i + j; - var total: u64 = result.limb[index] + a.limb[i] * b.limb[j] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - j += 1; - } - var index: i64 = i + 8; - while (carry != 0 && index < 16) { - var total: u64 = result.limb[index] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - index += 1; + +fun connected(graph: ptr, from: i64, to: i64) -> bool { + var offset: i64 = from * 32 + to / 8; + var mask: u8 = (1 as u8) << ((to % 8) as u8); + return (graph[offset] & mask) != 0; +} + +fun shortest_reference(vertex: i64) -> i64 { + var best: i64 = 256; + var jumps: i64 = 0; + while (jumps < 256) { + var residue: i64 = (17 * jumps) % 256; + var singles: i64 = (vertex + 256 - residue) % 256; + if (singles + jumps < best) { + best = singles + jumps; } - i += 1; + jumps += 1; } - return result; + return best; } -fun equal(a: Big, b: Big) -> bool { + +fun exercise(storage: ptr) -> i32 { + var graph: ptr = storage + 8; var i: i64 = 0; - while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } + while (i < 8208) { + if (storage[i] != 0) { + return 2; + } i += 1; } - return true; -} -fun exercise_language() -> i32 { - var a: Big = zero(); - var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; - var product: Big = multiply(a, b); - // 1234567890 * 987654321 = 1219326311126352690. - var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } - var maximal: Big = zero(); - var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } - var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } - i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } - i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "imul rax, rax, 33" - "xor rax, rcx" - in("rax") state - in("rcx") word - out("rax") result - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; + i = 0; + while (i < 8) { + storage[i] = 173; + var tail: i64 = 8200 + i; + storage[tail] = 219; i += 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + i = 0; + while (i < 256) { + add_edge(graph, i, (i + 1) % 256); + add_edge(graph, i, (i + 17) % 256); + i += 1; + } + var distances: array; + var queue: array; + queue[256] = -9000000000; + i = 0; + while (i < 256) { + distances[i] = -1; + i += 1; + } + distances[0] = 0; + queue[0] = 0; + var head: i64 = 0; + var tail: i64 = 1; + while (head < tail) { + var from: i64 = queue[head]; + head += 1; + var to: i64 = 0; + while (to < 256) { + if (connected(graph, from, to) && distances[to] == -1) { + if (tail >= 256) { + return 3; + } + distances[to] = distances[from] + 1; + queue[tail] = to; + tail += 1; + } + to += 1; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; + } + if (head != 256 || tail != 256 || queue[256] != -9000000000) { + return 4; + } + i = 0; + while (i < 256) { + if (distances[i] != shortest_reference(i)) { + return 5; + } + var edges: i64 = 0; + var to: i64 = 0; + while (to < 256) { + if (connected(graph, i, to)) { + edges += 1; + if (to != (i + 1) % 256 && to != (i + 17) % 256) { + return 6; + } + } + to += 1; + } + if (edges != 2) { + return 7; + } + i += 1; + } + i = 0; + while (i < 8) { + var offset: i64 = 8200 + i; + if (storage[i] != 173 || storage[offset] != 219) { + return 8; + } + i += 1; } return 0; } fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + if (mem_page_size() <= 0) { + return 1; + } + var storage: ptr = mem_alloc_zeroed(8208); + if (storage == null) { + return 1; + } + var status: i32 = exercise(storage); + if (mem_free(storage, 8208) < 0 && status == 0) { + status = 9; + } + return status; } diff --git a/tests/cases/ios/riscv64/test1.wave b/tests/cases/ios/riscv64/test1.wave deleted file mode 100644 index f2f65be4..00000000 --- a/tests/cases/ios/riscv64/test1.wave +++ /dev/null @@ -1,167 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 1 and target data transport. -// Stack-machine execution with payload variants, generic results and large -// aggregate returns. Error paths must preserve the last committed stack. -variant Op { Push(i64), Add, Multiply, Divide, Duplicate, Halt } -struct Stack { slots: array; used: i64; steps: i64; } -variant Run { Done(T), Fault(i32, T) } - -fun empty_stack() -> Stack { - var result: Stack; - var i: i64 = 0; - while (i < 16) { result.slots[i] = 0; i += 1; } - result.used = 0; - result.steps = 0; - return result; -} - -fun execute(code: ptr, count: i64, initial: Stack) -> Run { - var state: Stack = initial; - var pc: i64 = 0; - while (pc < count) { - var op: Op = code[pc]; - match op { - Op::Push(value) => { - if (state.used == 16) { return Run::Fault(1, state); } - state.slots[state.used] = value; - state.used += 1; - } - Op::Add => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - state.slots[left] += rhs; - state.used -= 1; - } - Op::Multiply => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - state.slots[left] *= rhs; - state.used -= 1; - } - Op::Divide => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - if (rhs == 0) { return Run::Fault(3, state); } - state.slots[left] /= rhs; - state.used -= 1; - } - Op::Duplicate => { - if (state.used == 0) { return Run::Fault(2, state); } - if (state.used == 16) { return Run::Fault(1, state); } - var last: i64 = state.used - 1; - state.slots[state.used] = state.slots[last]; - state.used += 1; - } - Op::Halt => { return Run::Done(state); } - } - state.steps += 1; - pc += 1; - } - return Run::Fault(4, state); -} - -fun exercise_language() -> i32 { - var initial: Stack = empty_stack(); - var program: array = [ - Op::Push(7), Op::Push(5), Op::Add, Op::Duplicate, - Op::Multiply, Op::Push(3), Op::Divide, Op::Halt - ]; - var success: Run = execute(&program[0], 8, initial); - match success { - Run::Done(state) => { - if (state.used != 1 || state.slots[0] != 48 || state.steps != 7) { return 1; } - } - Run::Fault(_, _) => { return 2; } - } - if (initial.used != 0 || initial.steps != 0 || initial.slots[0] != 0) { return 3; } - var broken: array = [Op::Push(41), Op::Push(0), Op::Divide, Op::Halt]; - var failure: Run = execute(&broken[0], 4, initial); - match failure { - Run::Done(_) => { return 4; } - Run::Fault(code, state) => { - if (code != 3 || state.used != 2 || state.steps != 2) { return 5; } - if (state.slots[0] != 41 || state.slots[1] != 0) { return 6; } - } - } - var underflow: array = [Op::Add]; - match execute(&underflow[0], 1, initial) { - Run::Done(_) => { return 7; } - Run::Fault(code, state) => { - if (code != 2 || state.used != 0 || state.steps != 0) { return 8; } - } - } - var full: Stack = empty_stack(); - full.used = 16; - full.slots[15] = 9000000000; - var overflow: array = [Op::Duplicate]; - match execute(&overflow[0], 1, full) { - Run::Done(_) => { return 9; } - Run::Fault(code, state) => { - if (code != 1 || state.used != 16 || state.slots[15] != 9000000000) { return 10; } - } - } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test10.wave b/tests/cases/ios/riscv64/test10.wave deleted file mode 100644 index 2bb82039..00000000 --- a/tests/cases/ios/riscv64/test10.wave +++ /dev/null @@ -1,120 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 10 and target data transport. -// Recursively build and traverse an indexed binary search tree. Validate -// subtree totals, unsuccessful searches and traversal order across 63 nodes. -struct Node { key: i64; left: i64; right: i64; } -struct Tree { nodes: array; root: i64; } -fun build(nodes: ptr, low: i64, high: i64) -> i64 { - if (low >= high) { return -1; } - var mid: i64 = (low + high) / 2; - var left: i64 = build(nodes, low, mid); - var right: i64 = build(nodes, mid + 1, high); - nodes[mid] = Node { key: mid * 3 + 1, left: left, right: right }; - return mid; -} -fun total(nodes: ptr, root: i64) -> i64 { - if (root == -1) { return 0; } - var node: Node = nodes[root]; - return node.key + total(nodes, node.left) + total(nodes, node.right); -} -fun search(nodes: ptr, root: i64, key: i64) -> i64 { - var cursor: i64 = root; - var steps: i64 = 0; - while (cursor != -1 && steps < 7) { - var node: Node = nodes[cursor]; - if (key == node.key) { return cursor; } - if (key < node.key) { cursor = node.left; } - else { cursor = node.right; } - steps += 1; - } - return -1; -} -fun traverse(nodes: ptr, root: i64, output: ptr, used: ptr) { - if (root == -1) { return; } - var node: Node = nodes[root]; - traverse(nodes, node.left, output, used); - var index: i64 = deref used; - output[index] = node.key; - deref used += 1; - traverse(nodes, node.right, output, used); -} -fun exercise_language() -> i32 { - var tree: Tree; - tree.root = build(&tree.nodes[0], 0, 63); - if (tree.root != 31 || total(&tree.nodes[0], tree.root) != 5922) { return 1; } - var i: i64 = 0; - while (i < 63) { - if (search(&tree.nodes[0], tree.root, i * 3 + 1) != i) { return 2; } - if (search(&tree.nodes[0], tree.root, i * 3 + 2) != -1) { return 3; } - i += 1; - } - var output: array; - output[63] = 9000000000; - var used: i64 = 0; - traverse(&tree.nodes[0], tree.root, &output[0], &used); - if (used != 63 || output[63] != 9000000000) { return 4; } - i = 0; - while (i < 63) { if (output[i] != i * 3 + 1) { return 5; } i += 1; } - if (total(&tree.nodes[0], -1) != 0 || search(&tree.nodes[0], -1, 1) != -1) { return 6; } - var copy: Tree = tree; - copy.nodes[31].key = 9000000000; - if (tree.nodes[31].key != 94 || total(©.nodes[0], copy.root) != 9000005828) { return 7; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test2.wave b/tests/cases/ios/riscv64/test2.wave deleted file mode 100644 index 37af50a2..00000000 --- a/tests/cases/ios/riscv64/test2.wave +++ /dev/null @@ -1,141 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 2 and target data transport. -// Stable sorting of wide records through pointers and nested aggregate fields. -// A returned snapshot must own its array values, while explicit pointer writes -// must affect the caller. Duplicate keys expose accidental unstable reordering. -struct Item { key: i32; ordinal: i32; value: i64; } -struct Box { value: T; } -struct Batch { guard_before: u64; records: array; guard_after: u64; } - -fun identity(value: T) -> T { return value; } - -fun prepare() -> Box { - var batch: Batch; - batch.guard_before = 0xFEDCBA9876543210; - batch.guard_after = 0x8000000000000001; - var i: i64 = 0; - while (i < 32) { - batch.records[i] = Item { - key: ((31 - i) % 7) as i32, - ordinal: i as i32, - value: 9000000000 + i * 17 - }; - i += 1; - } - return Box { value: batch }; -} - -fun stable_sort(items: ptr, length: i64) -> i64 { - var moves: i64 = 0; - var i: i64 = 1; - while (i < length) { - var next: Item = items[i]; - var j: i64 = i; - while (j > 0) { - var previous_index: i64 = j - 1; - if (items[previous_index].key <= next.key) { break; } - items[j] = items[previous_index]; - moves += 1; - j -= 1; - } - items[j] = next; - i += 1; - } - return moves; -} - -fun bump(value: ptr, amount: i64) { deref value += amount; } - -fun exercise_language() -> i32 { - var original: Box = prepare(); - var sorted: Box = identity>(original); - var moves: i64 = stable_sort(&sorted.value.records[0], 32); - if (moves != 240) { return 1; } - var i: i64 = 0; - var sum: i64 = 0; - var seen: u64 = 0; - while (i < 32) { - var item: Item = sorted.value.records[i]; - if (item.ordinal < 0 || item.ordinal >= 32) { return 2; } - var bit: u64 = (1 as u64) << (item.ordinal as u64); - if ((seen & bit) != 0) { return 3; } - seen = seen | bit; - if (item.value != 9000000000 + (item.ordinal as i64) * 17) { return 4; } - if (i > 0) { - var previous_index: i64 = i - 1; - var previous: Item = sorted.value.records[previous_index]; - if (previous.key > item.key) { return 5; } - if (previous.key == item.key && previous.ordinal > item.ordinal) { return 6; } - } - if (original.value.records[i].ordinal != (i as i32)) { return 7; } - sum += item.value; - i += 1; - } - if (seen != 0xFFFFFFFF || sum != 288000008432) { return 8; } - if (sorted.value.guard_before != 0xFEDCBA9876543210) { return 9; } - if (sorted.value.guard_after != 0x8000000000000001) { return 10; } - var saved: i64 = sorted.value.records[15].value; - bump(&sorted.value.records[15].value, -9000000000); - if (sorted.value.records[15].value != saved - 9000000000) { return 11; } - if (original.value.records[15].value != 9000000255) { return 12; } - if (stable_sort(&sorted.value.records[0], 32) != 0) { return 13; } - if (stable_sort(&sorted.value.records[0], 0) != 0) { return 14; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test3.wave b/tests/cases/ios/riscv64/test3.wave deleted file mode 100644 index 0084f7f4..00000000 --- a/tests/cases/ios/riscv64/test3.wave +++ /dev/null @@ -1,155 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 3 and target data transport. -// Weighted routing: disconnected vertices, predecessor reconstruction and -// independently known distances. Graph and route snapshots travel by value. -struct Graph { weights: array; } -struct Routes { distance: array; previous: array; } -fun edge(weights: ptr, a: i64, b: i64, cost: i64) { - var forward: i64 = a * 9 + b; - var reverse: i64 = b * 9 + a; - weights[forward] = cost; - weights[reverse] = cost; -} -fun network() -> Graph { - var graph: Graph; - var i: i64 = 0; - while (i < 81) { graph.weights[i] = 0; i += 1; } - edge(&graph.weights[0], 0, 1, 4); - edge(&graph.weights[0], 0, 7, 8); - edge(&graph.weights[0], 1, 2, 8); - edge(&graph.weights[0], 1, 7, 11); - edge(&graph.weights[0], 2, 3, 7); - edge(&graph.weights[0], 2, 5, 4); - edge(&graph.weights[0], 3, 4, 9); - edge(&graph.weights[0], 3, 5, 14); - edge(&graph.weights[0], 4, 5, 10); - edge(&graph.weights[0], 5, 6, 2); - edge(&graph.weights[0], 6, 7, 1); - return graph; -} -fun shortest(graph: Graph, start: i64) -> Routes { - var result: Routes; - var settled: array; - var i: i64 = 0; - while (i < 9) { - result.distance[i] = 1000000; - result.previous[i] = -1; - settled[i] = false; - i += 1; - } - result.distance[start] = 0; - var pass: i64 = 0; - while (pass < 9) { - var best: i64 = -1; - var cost: i64 = 1000000; - i = 0; - while (i < 9) { - if (!settled[i] && result.distance[i] < cost) { - best = i; - cost = result.distance[i]; - } - i += 1; - } - if (best == -1) { break; } - settled[best] = true; - i = 0; - while (i < 9) { - var index: i64 = best * 9 + i; - var weight: i64 = graph.weights[index]; - if (weight != 0 && cost + weight < result.distance[i]) { - result.distance[i] = cost + weight; - result.previous[i] = best; - } - i += 1; - } - pass += 1; - } - return result; -} -fun exercise_language() -> i32 { - var graph: Graph = network(); - var routes: Routes = shortest(graph, 0); - var expected: array = [0, 4, 12, 19, 21, 11, 9, 8, 1000000]; - var i: i64 = 0; - while (i < 9) { - if (routes.distance[i] != expected[i]) { return 1; } - i += 1; - } - var cursor: i64 = 4; - var total: i64 = 0; - var hops: i64 = 0; - while (cursor != 0 && hops < 9) { - var parent: i64 = routes.previous[cursor]; - if (parent < 0 || parent >= 9) { return 2; } - var index: i64 = parent * 9 + cursor; - if (graph.weights[index] == 0) { return 3; } - total += graph.weights[index]; - cursor = parent; - hops += 1; - } - if (cursor != 0 || total != 21 || hops != 4) { return 4; } - if (routes.previous[8] != -1) { return 5; } - var isolated: Routes = shortest(graph, 8); - if (isolated.distance[8] != 0 || isolated.distance[0] != 1000000) { return 6; } - edge(&graph.weights[0], 0, 4, 1); - var shortcut: Routes = shortest(graph, 0); - if (shortcut.distance[4] != 1 || routes.distance[4] != 21) { return 7; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test4.wave b/tests/cases/ios/riscv64/test4.wave deleted file mode 100644 index c232d937..00000000 --- a/tests/cases/ios/riscv64/test4.wave +++ /dev/null @@ -1,177 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 4 and target data transport. -// Open-addressed key/value store: long collisions, tombstones, replacements, -// full-table termination and immutable snapshots of payload-variant arrays. -variant Slot { Empty, Deleted, Used(i64, i64) } -variant Found { Value(T), Missing } -struct Table { slots: array; count: i64; rejected: bool; } -fun empty_table() -> Table { - var table: Table; - var i: i64 = 0; - while (i < 32) { table.slots[i] = Slot::Empty; i += 1; } - table.count = 0; - table.rejected = false; - return table; -} -fun put(table: Table, key: i64, value: i64) -> Table { - var result: Table = table; - result.rejected = false; - var index: i64 = key % 32; - var vacant: i64 = -1; - var probes: i64 = 0; - while (probes < 32) { - match result.slots[index] { - Slot::Used(stored, _) => { - if (stored == key) { - result.slots[index] = Slot::Used(key, value); - return result; - } - } - Slot::Deleted => { if (vacant == -1) { vacant = index; } } - Slot::Empty => { - if (vacant == -1) { vacant = index; } - break; - } - } - index = (index + 1) % 32; - probes += 1; - } - if (vacant == -1) { result.rejected = true; return result; } - result.slots[vacant] = Slot::Used(key, value); - result.count += 1; - return result; -} -fun get(table: Table, key: i64) -> Found { - var index: i64 = key % 32; - var probes: i64 = 0; - while (probes < 32) { - match table.slots[index] { - Slot::Empty => { return Found::Missing; } - Slot::Deleted => {} - Slot::Used(stored, value) => { - if (stored == key) { return Found::Value(value); } - } - } - index = (index + 1) % 32; - probes += 1; - } - return Found::Missing; -} -fun erase(table: Table, key: i64) -> Table { - var result: Table = table; - var index: i64 = key % 32; - var probes: i64 = 0; - while (probes < 32) { - match result.slots[index] { - Slot::Empty => { return result; } - Slot::Deleted => {} - Slot::Used(stored, _) => { - if (stored == key) { - result.slots[index] = Slot::Deleted; - result.count -= 1; - return result; - } - } - } - index = (index + 1) % 32; - probes += 1; - } - return result; -} -fun matches_value(value: Found, expected: i64) -> bool { - match value { - Found::Value(actual) => { return actual == expected; } - Found::Missing => { return false; } - } -} -fun exercise_language() -> i32 { - var table: Table = empty_table(); - var i: i64 = 0; - while (i < 24) { table = put(table, i * 32 + 3, i * i - 17); i += 1; } - var snapshot: Table = table; - i = 0; - while (i < 24) { table = erase(table, i * 32 + 3); i += 2; } - if (table.count != 12 || snapshot.count != 24) { return 1; } - i = 24; - while (i < 44) { table = put(table, i * 32 + 3, -i); i += 1; } - if (table.count != 32 || table.rejected) { return 2; } - i = 0; - while (i < 44) { - if (i < 24 && i % 2 == 0) { - match get(table, i * 32 + 3) { - Found::Value(_) => { return 3; } - Found::Missing => {} - } - } else { - var expected: i64 = -i; - if (i < 24) { expected = i * i - 17; } - if (!matches_value(get(table, i * 32 + 3), expected)) { return 4; } - } - i += 1; - } - table = put(table, 7 * 32 + 3, -9000000000); - if (table.count != 32 || table.rejected) { return 5; } - if (!matches_value(get(table, 227), -9000000000)) { return 6; } - table = put(table, 55 * 32 + 3, 99); - if (!table.rejected || table.count != 32) { return 7; } - if (!matches_value(get(snapshot, 3), -17)) { return 8; } - if (!matches_value(get(snapshot, 227), 32)) { return 9; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test5.wave b/tests/cases/ios/riscv64/test5.wave deleted file mode 100644 index 6cdef4aa..00000000 --- a/tests/cases/ios/riscv64/test5.wave +++ /dev/null @@ -1,172 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 5 and target data transport. -// Bounded run-length packet codec. Round trips alone are insufficient: check -// wire bytes, truncated pairs, zero runs, capacity and checksum corruption. -struct Bytes { data: array; length: i64; } -variant Decoded { Packet(Bytes), Invalid(i32) } -fun blank() -> Bytes { - var result: Bytes; - var i: i64 = 0; - while (i < 128) { result.data[i] = 0; i += 1; } - result.length = 0; - return result; -} -fun pack(source: Bytes) -> Bytes { - var result: Bytes = blank(); - result.data[0] = 165; - result.data[1] = source.length as u8; - result.length = 2; - var checksum: u64 = 0; - var i: i64 = 0; - while (i < source.length) { - var value: u8 = source.data[i]; - var count: i64 = 1; - var next: i64 = i + 1; - while (next < source.length && source.data[next] == value && count < 255) { - count += 1; - next += 1; - } - result.data[result.length] = count as u8; - result.length += 1; - result.data[result.length] = value; - result.length += 1; - checksum = (checksum + (value as u64) * (count as u64)) % 251; - i = next; - } - result.data[result.length] = checksum as u8; - result.length += 1; - return result; -} -fun unpack(packet: Bytes) -> Decoded { - if (packet.length < 3 || packet.data[0] != 165) { return Decoded::Invalid(1); } - var payload_end: i64 = packet.length - 1; - if ((payload_end - 2) % 2 != 0) { return Decoded::Invalid(2); } - var result: Bytes = blank(); - var checksum: u64 = 0; - var i: i64 = 2; - while (i < payload_end) { - var count: i64 = packet.data[i] as i64; - i += 1; - var value: u8 = packet.data[i]; - i += 1; - if (count == 0) { return Decoded::Invalid(3); } - if (result.length + count > 128) { return Decoded::Invalid(4); } - var n: i64 = 0; - while (n < count) { - result.data[result.length] = value; - result.length += 1; - checksum = (checksum + (value as u64)) % 251; - n += 1; - } - } - if (result.length != packet.data[1] as i64) { return Decoded::Invalid(5); } - if (checksum != packet.data[payload_end] as u64) { return Decoded::Invalid(6); } - return Decoded::Packet(result); -} -fun rejected(packet: Bytes, expected: i32) -> bool { - match unpack(packet) { - Decoded::Packet(_) => { return false; } - Decoded::Invalid(code) => { return code == expected; } - } -} -fun exercise_language() -> i32 { - var source: Bytes = blank(); - source.length = 48; - var i: i64 = 0; - while (i < 48) { source.data[i] = ((i / 6) * 17) as u8; i += 1; } - var packet: Bytes = pack(source); - if (packet.length != 19 || packet.data[0] != 165 || packet.data[1] != 48) { return 1; } - i = 0; - while (i < 8) { - var offset: i64 = 2 + i * 2; - var value_offset: i64 = offset + 1; - if (packet.data[offset] != 6 || packet.data[value_offset] != (i * 17) as u8) { return 2; } - i += 1; - } - if (packet.data[18] != 95) { return 3; } - match unpack(packet) { - Decoded::Invalid(_) => { return 4; } - Decoded::Packet(decoded) => { - if (decoded.length != 48) { return 5; } - i = 0; - while (i < 48) { - if (decoded.data[i] != source.data[i]) { return 6; } - i += 1; - } - } - } - var broken: Bytes = packet; - broken.length = 18; - if (!rejected(broken, 2)) { return 7; } - broken = packet; - broken.data[2] = 0; - if (!rejected(broken, 3)) { return 8; } - broken.data[2] = 129; - if (!rejected(broken, 4)) { return 9; } - broken = packet; - broken.data[18] = 0; - if (!rejected(broken, 6)) { return 10; } - match unpack(pack(blank())) { - Decoded::Invalid(_) => { return 11; } - Decoded::Packet(empty) => { if (empty.length != 0) { return 12; } } - } - if (packet.data[2] != 6 || source.data[47] != 119) { return 13; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test6.wave b/tests/cases/ios/riscv64/test6.wave deleted file mode 100644 index 221e13bc..00000000 --- a/tests/cases/ios/riscv64/test6.wave +++ /dev/null @@ -1,127 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 6 and target data transport. -// Base-10000 multi-precision multiplication, with carries crossing every limb. -// Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } -fun zero() -> Big { - var result: Big; - var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } - return result; -} -fun multiply(a: Big, b: Big) -> Big { - var result: Big = zero(); - var i: i64 = 0; - while (i < 8) { - var carry: u64 = 0; - var j: i64 = 0; - while (j < 8) { - var index: i64 = i + j; - var total: u64 = result.limb[index] + a.limb[i] * b.limb[j] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - j += 1; - } - var index: i64 = i + 8; - while (carry != 0 && index < 16) { - var total: u64 = result.limb[index] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - index += 1; - } - i += 1; - } - return result; -} -fun equal(a: Big, b: Big) -> bool { - var i: i64 = 0; - while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } - i += 1; - } - return true; -} -fun exercise_language() -> i32 { - var a: Big = zero(); - var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; - var product: Big = multiply(a, b); - // 1234567890 * 987654321 = 1219326311126352690. - var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } - var maximal: Big = zero(); - var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } - var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } - i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } - i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test7.wave b/tests/cases/ios/riscv64/test7.wave deleted file mode 100644 index 296f71e5..00000000 --- a/tests/cases/ios/riscv64/test7.wave +++ /dev/null @@ -1,140 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 7 and target data transport. -// A bounded arena with generation-tagged handles. Reuse must invalidate stale -// handles, preserve neighboring allocations, and clear newly allocated bytes. -struct Handle { index: i64; generation: i64; } -struct Pool { live: array; generation: array; values: array; } -struct Allocation { pool: Pool; handle: Handle; ok: bool; } -fun empty_pool() -> Pool { - var pool: Pool; - var i: i64 = 0; - while (i < 16) { - pool.live[i] = false; pool.generation[i] = 0; pool.values[i] = 0; - i += 1; - } - return pool; -} -fun allocate(pool: Pool, value: i64) -> Allocation { - var result: Pool = pool; - var i: i64 = 0; - while (i < 16) { - if (!result.live[i]) { - result.live[i] = true; - result.generation[i] += 1; - result.values[i] = value; - return Allocation { pool: result, - handle: Handle { index: i, generation: result.generation[i] }, ok: true }; - } - i += 1; - } - return Allocation { pool: result, handle: Handle { index: -1, generation: 0 }, ok: false }; -} -fun valid(pool: Pool, handle: Handle) -> bool { - if (handle.index < 0 || handle.index >= 16) { return false; } - return pool.live[handle.index] && pool.generation[handle.index] == handle.generation; -} -fun release(pool: Pool, handle: Handle) -> Pool { - var result: Pool = pool; - if (valid(result, handle)) { - result.live[handle.index] = false; - result.values[handle.index] = 0; - } - return result; -} -fun exercise_language() -> i32 { - var pool: Pool = empty_pool(); - var handles: array; - var i: i64 = 0; - while (i < 16) { - var allocation: Allocation = allocate(pool, 9000000000 + i); - if (!allocation.ok || allocation.handle.index != i) { return 1; } - pool = allocation.pool; - handles[i] = allocation.handle; - i += 1; - } - var full: Allocation = allocate(pool, -1); - if (full.ok || full.handle.index != -1) { return 2; } - var snapshot: Pool = pool; - i = 0; - while (i < 16) { - pool = release(pool, handles[i]); - if (pool.values[i] != 0 || valid(pool, handles[i])) { return 3; } - i += 2; - } - i = 0; - while (i < 16) { - var reused: Allocation = allocate(pool, 700 + i); - if (!reused.ok || reused.handle.index != i || reused.handle.generation != 2) { return 4; } - pool = reused.pool; - // A stale release must not free the replacement object. - pool = release(pool, handles[i]); - if (!valid(pool, reused.handle) || pool.values[i] != 700 + i) { return 5; } - i += 2; - } - i = 1; - while (i < 16) { - if (!valid(pool, handles[i]) || pool.values[i] != 9000000000 + i) { return 6; } - i += 2; - } - if (valid(pool, Handle { index: -1, generation: 1 })) { return 7; } - if (valid(pool, Handle { index: 16, generation: 1 })) { return 8; } - if (!valid(snapshot, handles[0]) || snapshot.values[0] != 9000000000) { return 9; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test8.wave b/tests/cases/ios/riscv64/test8.wave deleted file mode 100644 index ae0f9930..00000000 --- a/tests/cases/ios/riscv64/test8.wave +++ /dev/null @@ -1,134 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 8 and target data transport. -// Cooperative round-robin scheduling with payload states, cancellation, -// repeated queue wraparound and exact per-task accounting. -struct Progress { remaining: i64; sum: i64; } -variant Task { Ready(Progress), Done(i64), Cancelled } -struct Scheduler { tasks: array; queue: array; head: i64; count: i64; ticks: i64; } -fun schedule() -> Scheduler { - var state: Scheduler; - var i: i64 = 0; - while (i < 8) { - state.tasks[i] = Task::Ready(Progress { remaining: i + 1, sum: 0 }); - state.queue[i] = i; - i += 1; - } - state.head = 0; state.count = 8; state.ticks = 0; - return state; -} -fun drive(initial: Scheduler) -> Scheduler { - var state: Scheduler = initial; - var watchdog: i64 = 0; - while (state.count > 0 && watchdog < 100) { - var id: i64 = state.queue[state.head]; - state.head = (state.head + 1) % 8; - state.count -= 1; - match state.tasks[id] { - Task::Ready(progress) => { - var next: Progress = progress; - next.remaining -= 1; - next.sum += id + 1; - state.ticks += 1; - if (next.remaining == 0) { - state.tasks[id] = Task::Done(next.sum); - } else { - state.tasks[id] = Task::Ready(next); - var tail: i64 = (state.head + state.count) % 8; - state.queue[tail] = id; - state.count += 1; - } - } - Task::Cancelled => {} - Task::Done(_) => {} - } - watchdog += 1; - } - return state; -} -fun exercise_language() -> i32 { - var initial: Scheduler = schedule(); - initial.tasks[3] = Task::Cancelled; - var result: Scheduler = drive(initial); - if (result.count != 0 || result.ticks != 32) { return 1; } - var total: i64 = 0; - var i: i64 = 0; - while (i < 8) { - match result.tasks[i] { - Task::Ready(_) => { return 2; } - Task::Cancelled => { if (i != 3) { return 3; } } - Task::Done(sum) => { - if (sum != (i + 1) * (i + 1)) { return 4; } - total += sum; - } - } - i += 1; - } - if (total != 188 || initial.count != 8 || initial.ticks != 0) { return 5; } - var again: Scheduler = drive(result); - if (again.ticks != 32 || again.count != 0) { return 6; } - match initial.tasks[7] { - Task::Ready(progress) => { - if (progress.remaining != 8 || progress.sum != 0) { return 7; } - } - Task::Done(_) => { return 8; } - Task::Cancelled => { return 9; } - } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/ios/riscv64/test9.wave b/tests/cases/ios/riscv64/test9.wave deleted file mode 100644 index cd8b2be5..00000000 --- a/tests/cases/ios/riscv64/test9.wave +++ /dev/null @@ -1,124 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// ios/riscv64: functional workload 9 and target data transport. -// Image convolution with clamped boundaries and a separate intermediate buffer. -// Every output is checked against an independently calculated golden image. -struct Image { pixels: array; guard: u64; } -fun clamp(value: i64) -> i64 { - if (value < 0) { return 0; } - if (value > 4) { return 4; } - return value; -} -fun blur(source: Image) -> Image { - var horizontal: Image = source; - var result: Image = source; - var y: i64 = 0; - while (y < 5) { - var x: i64 = 0; - while (x < 5) { - var center: i64 = y * 5 + x; - var left: i64 = y * 5 + clamp(x - 1); - var right: i64 = y * 5 + clamp(x + 1); - horizontal.pixels[center] = source.pixels[left] + source.pixels[center] * 2 + source.pixels[right]; - x += 1; - } - y += 1; - } - y = 0; - while (y < 5) { - var x: i64 = 0; - while (x < 5) { - var center: i64 = y * 5 + x; - var top: i64 = clamp(y - 1) * 5 + x; - var bottom: i64 = clamp(y + 1) * 5 + x; - result.pixels[center] = (horizontal.pixels[top] + horizontal.pixels[center] * 2 + horizontal.pixels[bottom]) / 16; - x += 1; - } - y += 1; - } - return result; -} -fun exercise_language() -> i32 { - var source: Image; - source.guard = 0xFEDCBA9876543210; - var i: i64 = 0; - while (i < 25) { - var x: i64 = i % 5; - var y: i64 = i / 5; - source.pixels[i] = x * x + y * 3; - i += 1; - } - var expected: array = [1, 2, 5, 10, 15, 3, 4, 7, 12, 17, 6, 7, 10, 15, 20, 9, 10, 13, 18, 23, 11, 12, 15, 20, 25]; - var result: Image = blur(source); - i = 0; - while (i < 25) { - if (result.pixels[i] != expected[i]) { return 1; } - var x: i64 = i % 5; - var y: i64 = i / 5; - if (source.pixels[i] != x * x + y * 3) { return 2; } - i += 1; - } - if (result.guard != source.guard) { return 3; } - i = 0; - while (i < 25) { source.pixels[i] = 9000000000; i += 1; } - result = blur(source); - i = 0; - while (i < 25) { if (result.pixels[i] != 9000000000) { return 4; } i += 1; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/linux/amd64/test6.wave b/tests/cases/linux/amd64/test6.wave index ea5a2634..c943554e 100644 --- a/tests/cases/linux/amd64/test6.wave +++ b/tests/cases/linux/amd64/test6.wave @@ -1,99 +1,184 @@ -const SYS_WRITE: i32 = 1; +// Three independent Linux pipes are demultiplexed by epoll tokens. Seven-byte +// reads exercise short records, re-registration, EOF, and bounded cleanup. +import("std::io::fd")::{io_pipe, io_read, io_write_all, io_close}; +import("std::sys::linux::event")::{ + NativeEvent, event_create, event_add, event_remove, event_wait, event_close +}; -const SYS_READ: i32 = 0; - -const SYS_MMAP: i32 = 9; -const SYS_MUNMAP: i32 = 11; -const SYS_CLOSE: i32 = 3; -const SYS_SOCKET: i32 = 41; -const SYS_CONNECT: i32 = 42; -const SYS_BIND: i32 = 49; -const SYS_LISTEN: i32 = 50; -const SYS_ACCEPT: i32 = 43; -const SYS_SENDTO: i32 = 44; -const SYS_RECVFROM: i32 = 45; - -const PROT_READ: i32 = 1; -const PROT_WRITE: i32 = 2; -const MAP_PRIVATE: i32 = 2; -const MAP_ANONYMOUS: i32 = 32; - -fun syscall_write(fd: i32, buf: ptr, len: i32) { - var r: i32; - asm { - "mov rax, 1" - "syscall" - in("rdi") fd - in("rsi") buf - in("rdx") len - out("rax") r - } +struct Channel { + reader: i64; + writer: i64; + token: u64; + consumed: i64; + weighted: i64; + retagged: bool; } -fun syscall_read(fd: i32, buf: ptr, len:i32) -> i32 { - var r: i32; - asm { - "mov rax, 0" - "syscall" - in("rdi") fd - in("rsi") buf - in("rdx") len - out("rax") r - } - return r; +struct Resources { + queue: i64; + channels: array; } -fun syscall_close(fd: i32) -> i32 { - var r: i32; - asm { - "mov rax, 3" - "syscall" - in("rdi") fd - out("rax") r +fun exercise(resources: ptr) -> i32 { + resources.queue = event_create(3); + if (resources.queue < 0) { + return 1; } - return r; -} - -fun syscall_mmap(addr: ptr, length: i32, prot: i32, flags: i32, fd: i32, offset: i32) -> ptr { - var r: ptr; - asm { - "mov rax, 9" - "syscall" - in("rdi") addr - in("rsi") length - in("rdx") prot - in("r10") flags - in("r8") fd - in("r9") offset - out("rax") r + var channel: i64 = 0; + while (channel < 3) { + var descriptors: array = [-1, -1]; + if (io_pipe(&descriptors[0]) < 0) { + return 2; + } + resources.channels[channel].reader = descriptors[0] as i64; + resources.channels[channel].writer = descriptors[1] as i64; + resources.channels[channel].token = 4294967296 + (channel as u64); + var bytes: array; + var i: i64 = 0; + while (i < 96) { + bytes[i] = ((channel * 31 + i * 7) % 251) as u8; + i += 1; + } + var current: Channel = resources.channels[channel]; + if (event_add(resources.queue, current.reader, current.token, 1) < 0) { + return 3; + } + if (io_write_all(current.writer, &bytes[0], 96) != 96) { + return 4; + } + if (io_close(current.writer) < 0) { + return 5; + } + resources.channels[channel].writer = -1; + channel += 1; } - return r; -} - -fun syscall_munmap(addr: ptr, length: i32) -> i32 { - var r: i32; - asm { - "mov rax, 11" - "syscall" - in("rdi") addr - in("rsi") length - out("rax") r + var finished: i64 = 0; + var turns: i64 = 0; + while (finished < 3 && turns < 128) { + var events: array; + var count: i64 = event_wait(resources.queue, &events[0], 3, 20); + if (count < 0) { + return 6; + } + var index: i64 = 0; + while (index < count) { + var event: NativeEvent = events[index]; + var owner: i64 = -1; + channel = 0; + while (channel < 3) { + if (resources.channels[channel].reader >= 0 && resources.channels[channel].token == event.token) { + owner = channel; + } + channel += 1; + } + if (owner < 0) { + return 7; + } + var current: Channel = resources.channels[owner]; + var buffer: array; + buffer[0] = 173; + buffer[8] = 219; + var received: i64 = io_read(current.reader, &buffer[1], 7); + if (received < 0 || received > 7 || buffer[0] != 173 || buffer[8] != 219) { + return 8; + } + var i: i64 = 0; + while (i < received) { + var position: i64 = current.consumed + i; + var offset: i64 = i + 1; + var expected: i64 = (owner * 31 + position * 7) % 251; + if (position >= 96 || buffer[offset] as i64 != expected) { + return 9; + } + current.weighted += (position + 1) * (buffer[offset] as i64); + i += 1; + } + current.consumed += received; + resources.channels[owner] = current; + if (received == 0) { + if (current.consumed != 96) { + return 10; + } + if (event_remove(resources.queue, current.reader) < 0) { + return 11; + } + if (io_close(current.reader) < 0) { + return 12; + } + resources.channels[owner].reader = -1; + finished += 1; + } else if (current.consumed >= 40 && !current.retagged) { + if (event_remove(resources.queue, current.reader) < 0) { + return 13; + } + current.token += 65536; + current.retagged = true; + if (event_add(resources.queue, current.reader, current.token, 1) < 0) { + return 14; + } + resources.channels[owner] = current; + } + index += 1; + } + turns += 1; } - return r; + if (finished != 3) { + return 15; + } + channel = 0; + while (channel < 3) { + var expected: i64 = 0; + var i: i64 = 0; + while (i < 96) { + expected += (i + 1) * ((channel * 31 + i * 7) % 251); + i += 1; + } + if (!resources.channels[channel].retagged || resources.channels[channel].weighted != expected) { + return 16; + } + channel += 1; + } + var empty: NativeEvent; + if (event_wait(resources.queue, &empty, 1, 0) != 0) { + return 17; + } + return 0; } -fun main() { - var length: i32 = 4096; - var mem: ptr = syscall_mmap( - null, length, - PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, - -1, 0 - ); - - var msg: str = "Hello from Wave syscall!\n"; - var msg_ptr: ptr = msg; - - syscall_write(1, msg_ptr, 24); - syscall_munmap(mem, length); +fun main() -> i32 { + var resources: Resources; + resources.queue = -1; + var i: i64 = 0; + while (i < 3) { + resources.channels[i] = Channel { + reader: -1, + writer: -1, + token: 0, + consumed: 0, + weighted: 0, + retagged: false + }; + i += 1; + } + var status: i32 = exercise(&resources); + i = 0; + while (i < 3) { + if (resources.channels[i].writer >= 0) { + if (io_close(resources.channels[i].writer) < 0 && status == 0) { + status = 18; + } + } + if (resources.channels[i].reader >= 0) { + if (io_close(resources.channels[i].reader) < 0 && status == 0) { + status = 19; + } + } + i += 1; + } + if (resources.queue >= 0) { + if (event_close(resources.queue) < 0 && status == 0) { + status = 20; + } + } + return status; } diff --git a/tests/cases/macos/arm64/test6.wave b/tests/cases/macos/arm64/test6.wave index ff3a36db..f36e4417 100644 --- a/tests/cases/macos/arm64/test6.wave +++ b/tests/cases/macos/arm64/test6.wave @@ -1,125 +1,217 @@ -// Generated by tools/populate_case_matrix.py. -// macos/arm64: functional workload 6 and target data transport. -// Base-10000 multi-precision multiplication, with carries crossing every limb. -// Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } -fun zero() -> Big { - var result: Big; - var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } - return result; +// Darwin kqueue drives a three-byte streaming number decoder. Read readiness +// remains level-triggered until drained, and EOF must not discard buffered data. +import("std::io::fd")::{io_pipe, io_read, io_write_all, io_close}; +import("std::sys::macos::event")::{ + NativeEvent, event_create, event_add, event_remove, event_wait, event_close +}; + +struct Decoder { + values: array; + count: i64; + magnitude: i64; + digits: i64; + negative: bool; + error: i32; } -fun multiply(a: Big, b: Big) -> Big { - var result: Big = zero(); - var i: i64 = 0; - while (i < 8) { - var carry: u64 = 0; - var j: i64 = 0; - while (j < 8) { - var index: i64 = i + j; - var total: u64 = result.limb[index] + a.limb[i] * b.limb[j] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - j += 1; + +struct Resources { + reader: i64; + writer: i64; + queue: i64; +} + +fun decoder() -> Decoder { + return Decoder { + values: [0, 0, 0, 0, 0, 0, 0, 9000000000], + count: 0, + magnitude: 0, + digits: 0, + negative: false, + error: 0 + }; +} + +fun consume(state: Decoder, byte: u8) -> Decoder { + var result: Decoder = state; + if (result.error != 0) { + return result; + } + if (byte == 45) { + if (result.negative || result.digits != 0) { + result.error = 1; + } else { + result.negative = true; } - var index: i64 = i + 8; - while (carry != 0 && index < 16) { - var total: u64 = result.limb[index] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - index += 1; + } else if (byte >= 48 && byte <= 57) { + var digit: i64 = (byte as i64) - 48; + if (result.magnitude > (1000 - digit) / 10) { + result.error = 2; + } else { + result.magnitude = result.magnitude * 10 + digit; + result.digits += 1; } - i += 1; + } else if (byte == 44 || byte == 10) { + if (result.digits == 0 || result.count >= 7) { + result.error = 3; + } else { + var value: i64 = result.magnitude; + if (result.negative) { + value = -value; + } + result.values[result.count] = value; + result.count += 1; + result.magnitude = 0; + result.digits = 0; + result.negative = false; + } + } else { + result.error = 4; } return result; } -fun equal(a: Big, b: Big) -> bool { + +fun exercise(resources: ptr) -> i32 { + var descriptors: array = [-1, -1]; + if (io_pipe(&descriptors[0]) < 0) { + return 1; + } + resources.reader = descriptors[0] as i64; + resources.writer = descriptors[1] as i64; + resources.queue = event_create(1); + if (resources.queue < 0) { + return 2; + } + var token: u64 = 0x123456789ABCDE; + if (event_add(resources.queue, resources.reader, token, 1) < 0) { + return 3; + } + // "12,-3,45,0,999,-128\n" deliberately splits signs and multi-digit numbers. + var input: array = [ + 49, 50, 44, + 45, 51, 44, + 52, 53, 44, + 48, 44, + 57, 57, 57, 44, + 45, 49, 50, 56, 10 + ]; + if (io_write_all(resources.writer, &input[0], 20) != 20) { + return 4; + } + if (io_close(resources.writer) < 0) { + return 5; + } + resources.writer = -1; + var state: Decoder = decoder(); + var turns: i64 = 0; + var ended: bool = false; + while (turns < 32 && !ended) { + var event: NativeEvent; + var ready: i64 = event_wait(resources.queue, &event, 1, 20); + if (ready != 1 || event.token != token || event.fd != resources.reader) { + return 6; + } + if (turns == 0) { + var repeated: NativeEvent; + if (event_wait(resources.queue, &repeated, 1, 0) != 1 || repeated.token != token) { + return 7; + } + } + var bytes: array = [173, 0, 0, 0, 219]; + var count: i64 = io_read(resources.reader, &bytes[1], 3); + if (count < 0 || count > 3 || bytes[0] != 173 || bytes[4] != 219) { + return 8; + } + var i: i64 = 0; + while (i < count) { + var offset: i64 = i + 1; + state = consume(state, bytes[offset]); + i += 1; + } + ended = count == 0; + turns += 1; + } + if (!ended || state.error != 0 || state.count != 6 || state.digits != 0) { + return 9; + } + if (event_remove(resources.queue, resources.reader) < 0) { + return 10; + } + var empty: NativeEvent; + if (event_wait(resources.queue, &empty, 1, 0) != 0) { + return 11; + } + var expected: array = [12, -3, 45, 0, 999, -128]; var i: i64 = 0; - while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } + while (i < 6) { + if (state.values[i] != expected[i]) { + return 12; + } i += 1; } - return true; -} -fun exercise_language() -> i32 { - var a: Big = zero(); - var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; - var product: Big = multiply(a, b); - // 1234567890 * 987654321 = 1219326311126352690. - var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } - var maximal: Big = zero(); - var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } - var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } + var sorted: Decoder = state; i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } - i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "add x0, x0, x0, lsl #5" - "eor x0, x0, x1" - in("x0") state - in("x1") word - out("x0") result - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; + while (i < 6) { + var value: i64 = sorted.values[i]; + var position: i64 = i; + while (position > 0) { + var previous: i64 = position - 1; + if (sorted.values[previous] <= value) { + break; + } + sorted.values[position] = sorted.values[previous]; + position -= 1; + } + sorted.values[position] = value; i += 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + var ordered: array = [-128, -3, 0, 12, 45, 999]; + i = 0; + while (i < 6) { + if (sorted.values[i] != ordered[i] || state.values[i] != expected[i]) { + return 13; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; + i += 1; + } + var broken: Decoder = consume(consume(state, 45), 45); + if (broken.error != 1 || broken.count != 6 || broken.values[5] != -128) { + return 14; + } + var overflow: Decoder = decoder(); + i = 0; + while (i < 4) { + overflow = consume(overflow, 57); + i += 1; + } + if (overflow.error != 2 || overflow.count != 0) { + return 15; + } + if (state.values[7] != 9000000000 || sorted.values[7] != 9000000000) { + return 16; } return 0; } fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + var resources: Resources = Resources { + reader: -1, + writer: -1, + queue: -1 + }; + var status: i32 = exercise(&resources); + if (resources.queue >= 0) { + if (event_close(resources.queue) < 0 && status == 0) { + status = 17; + } + } + if (resources.writer >= 0) { + if (io_close(resources.writer) < 0 && status == 0) { + status = 18; + } + } + if (resources.reader >= 0) { + if (io_close(resources.reader) < 0 && status == 0) { + status = 19; + } + } + return status; } diff --git a/tests/cases/macos/riscv64/test1.wave b/tests/cases/macos/riscv64/test1.wave deleted file mode 100644 index 1285d517..00000000 --- a/tests/cases/macos/riscv64/test1.wave +++ /dev/null @@ -1,167 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 1 and target data transport. -// Stack-machine execution with payload variants, generic results and large -// aggregate returns. Error paths must preserve the last committed stack. -variant Op { Push(i64), Add, Multiply, Divide, Duplicate, Halt } -struct Stack { slots: array; used: i64; steps: i64; } -variant Run { Done(T), Fault(i32, T) } - -fun empty_stack() -> Stack { - var result: Stack; - var i: i64 = 0; - while (i < 16) { result.slots[i] = 0; i += 1; } - result.used = 0; - result.steps = 0; - return result; -} - -fun execute(code: ptr, count: i64, initial: Stack) -> Run { - var state: Stack = initial; - var pc: i64 = 0; - while (pc < count) { - var op: Op = code[pc]; - match op { - Op::Push(value) => { - if (state.used == 16) { return Run::Fault(1, state); } - state.slots[state.used] = value; - state.used += 1; - } - Op::Add => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - state.slots[left] += rhs; - state.used -= 1; - } - Op::Multiply => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - state.slots[left] *= rhs; - state.used -= 1; - } - Op::Divide => { - if (state.used < 2) { return Run::Fault(2, state); } - var right: i64 = state.used - 1; - var left: i64 = state.used - 2; - var rhs: i64 = state.slots[right]; - if (rhs == 0) { return Run::Fault(3, state); } - state.slots[left] /= rhs; - state.used -= 1; - } - Op::Duplicate => { - if (state.used == 0) { return Run::Fault(2, state); } - if (state.used == 16) { return Run::Fault(1, state); } - var last: i64 = state.used - 1; - state.slots[state.used] = state.slots[last]; - state.used += 1; - } - Op::Halt => { return Run::Done(state); } - } - state.steps += 1; - pc += 1; - } - return Run::Fault(4, state); -} - -fun exercise_language() -> i32 { - var initial: Stack = empty_stack(); - var program: array = [ - Op::Push(7), Op::Push(5), Op::Add, Op::Duplicate, - Op::Multiply, Op::Push(3), Op::Divide, Op::Halt - ]; - var success: Run = execute(&program[0], 8, initial); - match success { - Run::Done(state) => { - if (state.used != 1 || state.slots[0] != 48 || state.steps != 7) { return 1; } - } - Run::Fault(_, _) => { return 2; } - } - if (initial.used != 0 || initial.steps != 0 || initial.slots[0] != 0) { return 3; } - var broken: array = [Op::Push(41), Op::Push(0), Op::Divide, Op::Halt]; - var failure: Run = execute(&broken[0], 4, initial); - match failure { - Run::Done(_) => { return 4; } - Run::Fault(code, state) => { - if (code != 3 || state.used != 2 || state.steps != 2) { return 5; } - if (state.slots[0] != 41 || state.slots[1] != 0) { return 6; } - } - } - var underflow: array = [Op::Add]; - match execute(&underflow[0], 1, initial) { - Run::Done(_) => { return 7; } - Run::Fault(code, state) => { - if (code != 2 || state.used != 0 || state.steps != 0) { return 8; } - } - } - var full: Stack = empty_stack(); - full.used = 16; - full.slots[15] = 9000000000; - var overflow: array = [Op::Duplicate]; - match execute(&overflow[0], 1, full) { - Run::Done(_) => { return 9; } - Run::Fault(code, state) => { - if (code != 1 || state.used != 16 || state.slots[15] != 9000000000) { return 10; } - } - } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test10.wave b/tests/cases/macos/riscv64/test10.wave deleted file mode 100644 index ecc4bda8..00000000 --- a/tests/cases/macos/riscv64/test10.wave +++ /dev/null @@ -1,120 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 10 and target data transport. -// Recursively build and traverse an indexed binary search tree. Validate -// subtree totals, unsuccessful searches and traversal order across 63 nodes. -struct Node { key: i64; left: i64; right: i64; } -struct Tree { nodes: array; root: i64; } -fun build(nodes: ptr, low: i64, high: i64) -> i64 { - if (low >= high) { return -1; } - var mid: i64 = (low + high) / 2; - var left: i64 = build(nodes, low, mid); - var right: i64 = build(nodes, mid + 1, high); - nodes[mid] = Node { key: mid * 3 + 1, left: left, right: right }; - return mid; -} -fun total(nodes: ptr, root: i64) -> i64 { - if (root == -1) { return 0; } - var node: Node = nodes[root]; - return node.key + total(nodes, node.left) + total(nodes, node.right); -} -fun search(nodes: ptr, root: i64, key: i64) -> i64 { - var cursor: i64 = root; - var steps: i64 = 0; - while (cursor != -1 && steps < 7) { - var node: Node = nodes[cursor]; - if (key == node.key) { return cursor; } - if (key < node.key) { cursor = node.left; } - else { cursor = node.right; } - steps += 1; - } - return -1; -} -fun traverse(nodes: ptr, root: i64, output: ptr, used: ptr) { - if (root == -1) { return; } - var node: Node = nodes[root]; - traverse(nodes, node.left, output, used); - var index: i64 = deref used; - output[index] = node.key; - deref used += 1; - traverse(nodes, node.right, output, used); -} -fun exercise_language() -> i32 { - var tree: Tree; - tree.root = build(&tree.nodes[0], 0, 63); - if (tree.root != 31 || total(&tree.nodes[0], tree.root) != 5922) { return 1; } - var i: i64 = 0; - while (i < 63) { - if (search(&tree.nodes[0], tree.root, i * 3 + 1) != i) { return 2; } - if (search(&tree.nodes[0], tree.root, i * 3 + 2) != -1) { return 3; } - i += 1; - } - var output: array; - output[63] = 9000000000; - var used: i64 = 0; - traverse(&tree.nodes[0], tree.root, &output[0], &used); - if (used != 63 || output[63] != 9000000000) { return 4; } - i = 0; - while (i < 63) { if (output[i] != i * 3 + 1) { return 5; } i += 1; } - if (total(&tree.nodes[0], -1) != 0 || search(&tree.nodes[0], -1, 1) != -1) { return 6; } - var copy: Tree = tree; - copy.nodes[31].key = 9000000000; - if (tree.nodes[31].key != 94 || total(©.nodes[0], copy.root) != 9000005828) { return 7; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test2.wave b/tests/cases/macos/riscv64/test2.wave deleted file mode 100644 index d0f62a1d..00000000 --- a/tests/cases/macos/riscv64/test2.wave +++ /dev/null @@ -1,141 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 2 and target data transport. -// Stable sorting of wide records through pointers and nested aggregate fields. -// A returned snapshot must own its array values, while explicit pointer writes -// must affect the caller. Duplicate keys expose accidental unstable reordering. -struct Item { key: i32; ordinal: i32; value: i64; } -struct Box { value: T; } -struct Batch { guard_before: u64; records: array; guard_after: u64; } - -fun identity(value: T) -> T { return value; } - -fun prepare() -> Box { - var batch: Batch; - batch.guard_before = 0xFEDCBA9876543210; - batch.guard_after = 0x8000000000000001; - var i: i64 = 0; - while (i < 32) { - batch.records[i] = Item { - key: ((31 - i) % 7) as i32, - ordinal: i as i32, - value: 9000000000 + i * 17 - }; - i += 1; - } - return Box { value: batch }; -} - -fun stable_sort(items: ptr, length: i64) -> i64 { - var moves: i64 = 0; - var i: i64 = 1; - while (i < length) { - var next: Item = items[i]; - var j: i64 = i; - while (j > 0) { - var previous_index: i64 = j - 1; - if (items[previous_index].key <= next.key) { break; } - items[j] = items[previous_index]; - moves += 1; - j -= 1; - } - items[j] = next; - i += 1; - } - return moves; -} - -fun bump(value: ptr, amount: i64) { deref value += amount; } - -fun exercise_language() -> i32 { - var original: Box = prepare(); - var sorted: Box = identity>(original); - var moves: i64 = stable_sort(&sorted.value.records[0], 32); - if (moves != 240) { return 1; } - var i: i64 = 0; - var sum: i64 = 0; - var seen: u64 = 0; - while (i < 32) { - var item: Item = sorted.value.records[i]; - if (item.ordinal < 0 || item.ordinal >= 32) { return 2; } - var bit: u64 = (1 as u64) << (item.ordinal as u64); - if ((seen & bit) != 0) { return 3; } - seen = seen | bit; - if (item.value != 9000000000 + (item.ordinal as i64) * 17) { return 4; } - if (i > 0) { - var previous_index: i64 = i - 1; - var previous: Item = sorted.value.records[previous_index]; - if (previous.key > item.key) { return 5; } - if (previous.key == item.key && previous.ordinal > item.ordinal) { return 6; } - } - if (original.value.records[i].ordinal != (i as i32)) { return 7; } - sum += item.value; - i += 1; - } - if (seen != 0xFFFFFFFF || sum != 288000008432) { return 8; } - if (sorted.value.guard_before != 0xFEDCBA9876543210) { return 9; } - if (sorted.value.guard_after != 0x8000000000000001) { return 10; } - var saved: i64 = sorted.value.records[15].value; - bump(&sorted.value.records[15].value, -9000000000); - if (sorted.value.records[15].value != saved - 9000000000) { return 11; } - if (original.value.records[15].value != 9000000255) { return 12; } - if (stable_sort(&sorted.value.records[0], 32) != 0) { return 13; } - if (stable_sort(&sorted.value.records[0], 0) != 0) { return 14; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test3.wave b/tests/cases/macos/riscv64/test3.wave deleted file mode 100644 index a3dfc70c..00000000 --- a/tests/cases/macos/riscv64/test3.wave +++ /dev/null @@ -1,155 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 3 and target data transport. -// Weighted routing: disconnected vertices, predecessor reconstruction and -// independently known distances. Graph and route snapshots travel by value. -struct Graph { weights: array; } -struct Routes { distance: array; previous: array; } -fun edge(weights: ptr, a: i64, b: i64, cost: i64) { - var forward: i64 = a * 9 + b; - var reverse: i64 = b * 9 + a; - weights[forward] = cost; - weights[reverse] = cost; -} -fun network() -> Graph { - var graph: Graph; - var i: i64 = 0; - while (i < 81) { graph.weights[i] = 0; i += 1; } - edge(&graph.weights[0], 0, 1, 4); - edge(&graph.weights[0], 0, 7, 8); - edge(&graph.weights[0], 1, 2, 8); - edge(&graph.weights[0], 1, 7, 11); - edge(&graph.weights[0], 2, 3, 7); - edge(&graph.weights[0], 2, 5, 4); - edge(&graph.weights[0], 3, 4, 9); - edge(&graph.weights[0], 3, 5, 14); - edge(&graph.weights[0], 4, 5, 10); - edge(&graph.weights[0], 5, 6, 2); - edge(&graph.weights[0], 6, 7, 1); - return graph; -} -fun shortest(graph: Graph, start: i64) -> Routes { - var result: Routes; - var settled: array; - var i: i64 = 0; - while (i < 9) { - result.distance[i] = 1000000; - result.previous[i] = -1; - settled[i] = false; - i += 1; - } - result.distance[start] = 0; - var pass: i64 = 0; - while (pass < 9) { - var best: i64 = -1; - var cost: i64 = 1000000; - i = 0; - while (i < 9) { - if (!settled[i] && result.distance[i] < cost) { - best = i; - cost = result.distance[i]; - } - i += 1; - } - if (best == -1) { break; } - settled[best] = true; - i = 0; - while (i < 9) { - var index: i64 = best * 9 + i; - var weight: i64 = graph.weights[index]; - if (weight != 0 && cost + weight < result.distance[i]) { - result.distance[i] = cost + weight; - result.previous[i] = best; - } - i += 1; - } - pass += 1; - } - return result; -} -fun exercise_language() -> i32 { - var graph: Graph = network(); - var routes: Routes = shortest(graph, 0); - var expected: array = [0, 4, 12, 19, 21, 11, 9, 8, 1000000]; - var i: i64 = 0; - while (i < 9) { - if (routes.distance[i] != expected[i]) { return 1; } - i += 1; - } - var cursor: i64 = 4; - var total: i64 = 0; - var hops: i64 = 0; - while (cursor != 0 && hops < 9) { - var parent: i64 = routes.previous[cursor]; - if (parent < 0 || parent >= 9) { return 2; } - var index: i64 = parent * 9 + cursor; - if (graph.weights[index] == 0) { return 3; } - total += graph.weights[index]; - cursor = parent; - hops += 1; - } - if (cursor != 0 || total != 21 || hops != 4) { return 4; } - if (routes.previous[8] != -1) { return 5; } - var isolated: Routes = shortest(graph, 8); - if (isolated.distance[8] != 0 || isolated.distance[0] != 1000000) { return 6; } - edge(&graph.weights[0], 0, 4, 1); - var shortcut: Routes = shortest(graph, 0); - if (shortcut.distance[4] != 1 || routes.distance[4] != 21) { return 7; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test4.wave b/tests/cases/macos/riscv64/test4.wave deleted file mode 100644 index 42efdaab..00000000 --- a/tests/cases/macos/riscv64/test4.wave +++ /dev/null @@ -1,177 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 4 and target data transport. -// Open-addressed key/value store: long collisions, tombstones, replacements, -// full-table termination and immutable snapshots of payload-variant arrays. -variant Slot { Empty, Deleted, Used(i64, i64) } -variant Found { Value(T), Missing } -struct Table { slots: array; count: i64; rejected: bool; } -fun empty_table() -> Table { - var table: Table; - var i: i64 = 0; - while (i < 32) { table.slots[i] = Slot::Empty; i += 1; } - table.count = 0; - table.rejected = false; - return table; -} -fun put(table: Table, key: i64, value: i64) -> Table { - var result: Table = table; - result.rejected = false; - var index: i64 = key % 32; - var vacant: i64 = -1; - var probes: i64 = 0; - while (probes < 32) { - match result.slots[index] { - Slot::Used(stored, _) => { - if (stored == key) { - result.slots[index] = Slot::Used(key, value); - return result; - } - } - Slot::Deleted => { if (vacant == -1) { vacant = index; } } - Slot::Empty => { - if (vacant == -1) { vacant = index; } - break; - } - } - index = (index + 1) % 32; - probes += 1; - } - if (vacant == -1) { result.rejected = true; return result; } - result.slots[vacant] = Slot::Used(key, value); - result.count += 1; - return result; -} -fun get(table: Table, key: i64) -> Found { - var index: i64 = key % 32; - var probes: i64 = 0; - while (probes < 32) { - match table.slots[index] { - Slot::Empty => { return Found::Missing; } - Slot::Deleted => {} - Slot::Used(stored, value) => { - if (stored == key) { return Found::Value(value); } - } - } - index = (index + 1) % 32; - probes += 1; - } - return Found::Missing; -} -fun erase(table: Table, key: i64) -> Table { - var result: Table = table; - var index: i64 = key % 32; - var probes: i64 = 0; - while (probes < 32) { - match result.slots[index] { - Slot::Empty => { return result; } - Slot::Deleted => {} - Slot::Used(stored, _) => { - if (stored == key) { - result.slots[index] = Slot::Deleted; - result.count -= 1; - return result; - } - } - } - index = (index + 1) % 32; - probes += 1; - } - return result; -} -fun matches_value(value: Found, expected: i64) -> bool { - match value { - Found::Value(actual) => { return actual == expected; } - Found::Missing => { return false; } - } -} -fun exercise_language() -> i32 { - var table: Table = empty_table(); - var i: i64 = 0; - while (i < 24) { table = put(table, i * 32 + 3, i * i - 17); i += 1; } - var snapshot: Table = table; - i = 0; - while (i < 24) { table = erase(table, i * 32 + 3); i += 2; } - if (table.count != 12 || snapshot.count != 24) { return 1; } - i = 24; - while (i < 44) { table = put(table, i * 32 + 3, -i); i += 1; } - if (table.count != 32 || table.rejected) { return 2; } - i = 0; - while (i < 44) { - if (i < 24 && i % 2 == 0) { - match get(table, i * 32 + 3) { - Found::Value(_) => { return 3; } - Found::Missing => {} - } - } else { - var expected: i64 = -i; - if (i < 24) { expected = i * i - 17; } - if (!matches_value(get(table, i * 32 + 3), expected)) { return 4; } - } - i += 1; - } - table = put(table, 7 * 32 + 3, -9000000000); - if (table.count != 32 || table.rejected) { return 5; } - if (!matches_value(get(table, 227), -9000000000)) { return 6; } - table = put(table, 55 * 32 + 3, 99); - if (!table.rejected || table.count != 32) { return 7; } - if (!matches_value(get(snapshot, 3), -17)) { return 8; } - if (!matches_value(get(snapshot, 227), 32)) { return 9; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test5.wave b/tests/cases/macos/riscv64/test5.wave deleted file mode 100644 index 940bb067..00000000 --- a/tests/cases/macos/riscv64/test5.wave +++ /dev/null @@ -1,172 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 5 and target data transport. -// Bounded run-length packet codec. Round trips alone are insufficient: check -// wire bytes, truncated pairs, zero runs, capacity and checksum corruption. -struct Bytes { data: array; length: i64; } -variant Decoded { Packet(Bytes), Invalid(i32) } -fun blank() -> Bytes { - var result: Bytes; - var i: i64 = 0; - while (i < 128) { result.data[i] = 0; i += 1; } - result.length = 0; - return result; -} -fun pack(source: Bytes) -> Bytes { - var result: Bytes = blank(); - result.data[0] = 165; - result.data[1] = source.length as u8; - result.length = 2; - var checksum: u64 = 0; - var i: i64 = 0; - while (i < source.length) { - var value: u8 = source.data[i]; - var count: i64 = 1; - var next: i64 = i + 1; - while (next < source.length && source.data[next] == value && count < 255) { - count += 1; - next += 1; - } - result.data[result.length] = count as u8; - result.length += 1; - result.data[result.length] = value; - result.length += 1; - checksum = (checksum + (value as u64) * (count as u64)) % 251; - i = next; - } - result.data[result.length] = checksum as u8; - result.length += 1; - return result; -} -fun unpack(packet: Bytes) -> Decoded { - if (packet.length < 3 || packet.data[0] != 165) { return Decoded::Invalid(1); } - var payload_end: i64 = packet.length - 1; - if ((payload_end - 2) % 2 != 0) { return Decoded::Invalid(2); } - var result: Bytes = blank(); - var checksum: u64 = 0; - var i: i64 = 2; - while (i < payload_end) { - var count: i64 = packet.data[i] as i64; - i += 1; - var value: u8 = packet.data[i]; - i += 1; - if (count == 0) { return Decoded::Invalid(3); } - if (result.length + count > 128) { return Decoded::Invalid(4); } - var n: i64 = 0; - while (n < count) { - result.data[result.length] = value; - result.length += 1; - checksum = (checksum + (value as u64)) % 251; - n += 1; - } - } - if (result.length != packet.data[1] as i64) { return Decoded::Invalid(5); } - if (checksum != packet.data[payload_end] as u64) { return Decoded::Invalid(6); } - return Decoded::Packet(result); -} -fun rejected(packet: Bytes, expected: i32) -> bool { - match unpack(packet) { - Decoded::Packet(_) => { return false; } - Decoded::Invalid(code) => { return code == expected; } - } -} -fun exercise_language() -> i32 { - var source: Bytes = blank(); - source.length = 48; - var i: i64 = 0; - while (i < 48) { source.data[i] = ((i / 6) * 17) as u8; i += 1; } - var packet: Bytes = pack(source); - if (packet.length != 19 || packet.data[0] != 165 || packet.data[1] != 48) { return 1; } - i = 0; - while (i < 8) { - var offset: i64 = 2 + i * 2; - var value_offset: i64 = offset + 1; - if (packet.data[offset] != 6 || packet.data[value_offset] != (i * 17) as u8) { return 2; } - i += 1; - } - if (packet.data[18] != 95) { return 3; } - match unpack(packet) { - Decoded::Invalid(_) => { return 4; } - Decoded::Packet(decoded) => { - if (decoded.length != 48) { return 5; } - i = 0; - while (i < 48) { - if (decoded.data[i] != source.data[i]) { return 6; } - i += 1; - } - } - } - var broken: Bytes = packet; - broken.length = 18; - if (!rejected(broken, 2)) { return 7; } - broken = packet; - broken.data[2] = 0; - if (!rejected(broken, 3)) { return 8; } - broken.data[2] = 129; - if (!rejected(broken, 4)) { return 9; } - broken = packet; - broken.data[18] = 0; - if (!rejected(broken, 6)) { return 10; } - match unpack(pack(blank())) { - Decoded::Invalid(_) => { return 11; } - Decoded::Packet(empty) => { if (empty.length != 0) { return 12; } } - } - if (packet.data[2] != 6 || source.data[47] != 119) { return 13; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test6.wave b/tests/cases/macos/riscv64/test6.wave deleted file mode 100644 index e8e9de61..00000000 --- a/tests/cases/macos/riscv64/test6.wave +++ /dev/null @@ -1,127 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 6 and target data transport. -// Base-10000 multi-precision multiplication, with carries crossing every limb. -// Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } -fun zero() -> Big { - var result: Big; - var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } - return result; -} -fun multiply(a: Big, b: Big) -> Big { - var result: Big = zero(); - var i: i64 = 0; - while (i < 8) { - var carry: u64 = 0; - var j: i64 = 0; - while (j < 8) { - var index: i64 = i + j; - var total: u64 = result.limb[index] + a.limb[i] * b.limb[j] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - j += 1; - } - var index: i64 = i + 8; - while (carry != 0 && index < 16) { - var total: u64 = result.limb[index] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - index += 1; - } - i += 1; - } - return result; -} -fun equal(a: Big, b: Big) -> bool { - var i: i64 = 0; - while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } - i += 1; - } - return true; -} -fun exercise_language() -> i32 { - var a: Big = zero(); - var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; - var product: Big = multiply(a, b); - // 1234567890 * 987654321 = 1219326311126352690. - var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } - var maximal: Big = zero(); - var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } - var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } - i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } - i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test7.wave b/tests/cases/macos/riscv64/test7.wave deleted file mode 100644 index d1dd7502..00000000 --- a/tests/cases/macos/riscv64/test7.wave +++ /dev/null @@ -1,140 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 7 and target data transport. -// A bounded arena with generation-tagged handles. Reuse must invalidate stale -// handles, preserve neighboring allocations, and clear newly allocated bytes. -struct Handle { index: i64; generation: i64; } -struct Pool { live: array; generation: array; values: array; } -struct Allocation { pool: Pool; handle: Handle; ok: bool; } -fun empty_pool() -> Pool { - var pool: Pool; - var i: i64 = 0; - while (i < 16) { - pool.live[i] = false; pool.generation[i] = 0; pool.values[i] = 0; - i += 1; - } - return pool; -} -fun allocate(pool: Pool, value: i64) -> Allocation { - var result: Pool = pool; - var i: i64 = 0; - while (i < 16) { - if (!result.live[i]) { - result.live[i] = true; - result.generation[i] += 1; - result.values[i] = value; - return Allocation { pool: result, - handle: Handle { index: i, generation: result.generation[i] }, ok: true }; - } - i += 1; - } - return Allocation { pool: result, handle: Handle { index: -1, generation: 0 }, ok: false }; -} -fun valid(pool: Pool, handle: Handle) -> bool { - if (handle.index < 0 || handle.index >= 16) { return false; } - return pool.live[handle.index] && pool.generation[handle.index] == handle.generation; -} -fun release(pool: Pool, handle: Handle) -> Pool { - var result: Pool = pool; - if (valid(result, handle)) { - result.live[handle.index] = false; - result.values[handle.index] = 0; - } - return result; -} -fun exercise_language() -> i32 { - var pool: Pool = empty_pool(); - var handles: array; - var i: i64 = 0; - while (i < 16) { - var allocation: Allocation = allocate(pool, 9000000000 + i); - if (!allocation.ok || allocation.handle.index != i) { return 1; } - pool = allocation.pool; - handles[i] = allocation.handle; - i += 1; - } - var full: Allocation = allocate(pool, -1); - if (full.ok || full.handle.index != -1) { return 2; } - var snapshot: Pool = pool; - i = 0; - while (i < 16) { - pool = release(pool, handles[i]); - if (pool.values[i] != 0 || valid(pool, handles[i])) { return 3; } - i += 2; - } - i = 0; - while (i < 16) { - var reused: Allocation = allocate(pool, 700 + i); - if (!reused.ok || reused.handle.index != i || reused.handle.generation != 2) { return 4; } - pool = reused.pool; - // A stale release must not free the replacement object. - pool = release(pool, handles[i]); - if (!valid(pool, reused.handle) || pool.values[i] != 700 + i) { return 5; } - i += 2; - } - i = 1; - while (i < 16) { - if (!valid(pool, handles[i]) || pool.values[i] != 9000000000 + i) { return 6; } - i += 2; - } - if (valid(pool, Handle { index: -1, generation: 1 })) { return 7; } - if (valid(pool, Handle { index: 16, generation: 1 })) { return 8; } - if (!valid(snapshot, handles[0]) || snapshot.values[0] != 9000000000) { return 9; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test8.wave b/tests/cases/macos/riscv64/test8.wave deleted file mode 100644 index 06e89154..00000000 --- a/tests/cases/macos/riscv64/test8.wave +++ /dev/null @@ -1,134 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 8 and target data transport. -// Cooperative round-robin scheduling with payload states, cancellation, -// repeated queue wraparound and exact per-task accounting. -struct Progress { remaining: i64; sum: i64; } -variant Task { Ready(Progress), Done(i64), Cancelled } -struct Scheduler { tasks: array; queue: array; head: i64; count: i64; ticks: i64; } -fun schedule() -> Scheduler { - var state: Scheduler; - var i: i64 = 0; - while (i < 8) { - state.tasks[i] = Task::Ready(Progress { remaining: i + 1, sum: 0 }); - state.queue[i] = i; - i += 1; - } - state.head = 0; state.count = 8; state.ticks = 0; - return state; -} -fun drive(initial: Scheduler) -> Scheduler { - var state: Scheduler = initial; - var watchdog: i64 = 0; - while (state.count > 0 && watchdog < 100) { - var id: i64 = state.queue[state.head]; - state.head = (state.head + 1) % 8; - state.count -= 1; - match state.tasks[id] { - Task::Ready(progress) => { - var next: Progress = progress; - next.remaining -= 1; - next.sum += id + 1; - state.ticks += 1; - if (next.remaining == 0) { - state.tasks[id] = Task::Done(next.sum); - } else { - state.tasks[id] = Task::Ready(next); - var tail: i64 = (state.head + state.count) % 8; - state.queue[tail] = id; - state.count += 1; - } - } - Task::Cancelled => {} - Task::Done(_) => {} - } - watchdog += 1; - } - return state; -} -fun exercise_language() -> i32 { - var initial: Scheduler = schedule(); - initial.tasks[3] = Task::Cancelled; - var result: Scheduler = drive(initial); - if (result.count != 0 || result.ticks != 32) { return 1; } - var total: i64 = 0; - var i: i64 = 0; - while (i < 8) { - match result.tasks[i] { - Task::Ready(_) => { return 2; } - Task::Cancelled => { if (i != 3) { return 3; } } - Task::Done(sum) => { - if (sum != (i + 1) * (i + 1)) { return 4; } - total += sum; - } - } - i += 1; - } - if (total != 188 || initial.count != 8 || initial.ticks != 0) { return 5; } - var again: Scheduler = drive(result); - if (again.ticks != 32 || again.count != 0) { return 6; } - match initial.tasks[7] { - Task::Ready(progress) => { - if (progress.remaining != 8 || progress.sum != 0) { return 7; } - } - Task::Done(_) => { return 8; } - Task::Cancelled => { return 9; } - } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/macos/riscv64/test9.wave b/tests/cases/macos/riscv64/test9.wave deleted file mode 100644 index 3c17dd2c..00000000 --- a/tests/cases/macos/riscv64/test9.wave +++ /dev/null @@ -1,124 +0,0 @@ -// Generated by tools/populate_case_matrix.py. -// macos/riscv64: functional workload 9 and target data transport. -// Image convolution with clamped boundaries and a separate intermediate buffer. -// Every output is checked against an independently calculated golden image. -struct Image { pixels: array; guard: u64; } -fun clamp(value: i64) -> i64 { - if (value < 0) { return 0; } - if (value > 4) { return 4; } - return value; -} -fun blur(source: Image) -> Image { - var horizontal: Image = source; - var result: Image = source; - var y: i64 = 0; - while (y < 5) { - var x: i64 = 0; - while (x < 5) { - var center: i64 = y * 5 + x; - var left: i64 = y * 5 + clamp(x - 1); - var right: i64 = y * 5 + clamp(x + 1); - horizontal.pixels[center] = source.pixels[left] + source.pixels[center] * 2 + source.pixels[right]; - x += 1; - } - y += 1; - } - y = 0; - while (y < 5) { - var x: i64 = 0; - while (x < 5) { - var center: i64 = y * 5 + x; - var top: i64 = clamp(y - 1) * 5 + x; - var bottom: i64 = clamp(y + 1) * 5 + x; - result.pixels[center] = (horizontal.pixels[top] + horizontal.pixels[center] * 2 + horizontal.pixels[bottom]) / 16; - x += 1; - } - y += 1; - } - return result; -} -fun exercise_language() -> i32 { - var source: Image; - source.guard = 0xFEDCBA9876543210; - var i: i64 = 0; - while (i < 25) { - var x: i64 = i % 5; - var y: i64 = i / 5; - source.pixels[i] = x * x + y * 3; - i += 1; - } - var expected: array = [1, 2, 5, 10, 15, 3, 4, 7, 12, 17, 6, 7, 10, 15, 20, 9, 10, 13, 18, 23, 11, 12, 15, 20, 25]; - var result: Image = blur(source); - i = 0; - while (i < 25) { - if (result.pixels[i] != expected[i]) { return 1; } - var x: i64 = i % 5; - var y: i64 = i / 5; - if (source.pixels[i] != x * x + y * 3) { return 2; } - i += 1; - } - if (result.guard != source.guard) { return 3; } - i = 0; - while (i < 25) { source.pixels[i] = 9000000000; i += 1; } - result = blur(source); - i = 0; - while (i < 25) { if (result.pixels[i] != 9000000000) { return 4; } i += 1; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; - } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; - } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; - } - return 0; -} - -fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; -} diff --git a/tests/cases/shared/amd64/test6.wave b/tests/cases/shared/amd64/test6.wave index 0f7f0fcc..602d9196 100644 --- a/tests/cases/shared/amd64/test6.wave +++ b/tests/cases/shared/amd64/test6.wave @@ -1,14 +1,20 @@ -// Generated by tools/populate_case_matrix.py. -// shared/amd64: functional workload 6 and target data transport. // Base-10000 multi-precision multiplication, with carries crossing every limb. // Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } + +struct Big { + limb: array; +} + fun zero() -> Big { var result: Big; var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } + while (i < 16) { + result.limb[i] = 0; + i += 1; + } return result; } + fun multiply(a: Big, b: Big) -> Big { var result: Big = zero(); var i: i64 = 0; @@ -33,84 +39,123 @@ fun multiply(a: Big, b: Big) -> Big { } return result; } + fun equal(a: Big, b: Big) -> bool { var i: i64 = 0; while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } + if (a.limb[i] != b.limb[i]) { + return false; + } i += 1; } return true; } + fun exercise_language() -> i32 { var a: Big = zero(); var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; + a.limb[0] = 7890; + a.limb[1] = 3456; + a.limb[2] = 12; + b.limb[0] = 4321; + b.limb[1] = 8765; + b.limb[2] = 9; var product: Big = multiply(a, b); // 1234567890 * 987654321 = 1219326311126352690. var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } + expected.limb[0] = 2690; + expected.limb[1] = 2635; + expected.limb[2] = 3111; + expected.limb[3] = 9326; + expected.limb[4] = 121; + if (!equal(product, expected)) { + return 1; + } + if (!equal(multiply(b, a), product)) { + return 2; + } + if (!equal(multiply(a, zero()), zero())) { + return 3; + } + var one: Big = zero(); + one.limb[0] = 1; + if (!equal(multiply(a, one), a)) { + return 4; + } var maximal: Big = zero(); var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } + while (i < 8) { + maximal.limb[i] = 9999; + i += 1; + } var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } + if (square.limb[0] != 1 || square.limb[8] != 9998) { + return 5; + } i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } + while (i < 8) { + if (square.limb[i] != 0) { + return 6; + } + i += 1; + } i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } + while (i < 16) { + if (square.limb[i] != 9999) { + return 7; + } + i += 1; + } + if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { + return 8; + } return 0; } -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "imul rax, rax, 33" - "xor rax, rcx" - in("rax") state - in("rcx") word - out("rax") result - }; - return result % 65521; -} +// Ten mixed-width scalar arguments and a 128-byte integer cross the C boundary. +// This exercises stack arguments and an indirect aggregate result during carry propagation. -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; +export(c, "case_amd64_scale") fun scale_boundary( + a: i8, b: u8, c: i16, d: u16, e: i32, + f: u32, g: i64, h: u64, i: i64, j: i64, value: Big +) -> Big { + var factor: Big = zero(); + factor.limb[0] = 9999; + var result: Big = multiply(value, factor); + if ( + a != -7 || b != 250 || c != -30000 || d != 60000 || + e != -2000000000 || f != 4000000000 || g != 9000000000 || + h != 4294967297 || i != -17 || j != 23 + ) { + result.limb[15] = 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; + return result; } + fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + var round: i64 = 1; + while (round <= 64) { + var value: Big = zero(); + value.limb[0] = (round as u64) * 97; + value.limb[1] = 1; + var snapshot: Big = value; + var product: Big = scale_boundary( + -7, 250, -30000, 60000, -2000000000, + 4000000000, 9000000000, 4294967297, -17, 23, value + ); + // An independent scalar oracle covers every limb, not just a digest. + var reference: u64 = (10000 + (round as u64) * 97) * 9999; + var limb: i64 = 0; + while (limb < 16) { + if (product.limb[limb] != reference % 10000) { + return 201; + } + reference /= 10000; + if (value.limb[limb] != snapshot.limb[limb]) { + return 202; + } + limb += 1; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } round += 1; } return 0; @@ -118,8 +163,8 @@ fun target_workload() -> i32 { fun main() -> i32 { var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + if (status != 0) { + return status; + } + return target_workload(); } diff --git a/tests/cases/shared/arm64/test9.wave b/tests/cases/shared/arm64/test9.wave index becfc25a..0bae3241 100644 --- a/tests/cases/shared/arm64/test9.wave +++ b/tests/cases/shared/arm64/test9.wave @@ -1,13 +1,21 @@ -// Generated by tools/populate_case_matrix.py. -// shared/arm64: functional workload 9 and target data transport. // Image convolution with clamped boundaries and a separate intermediate buffer. // Every output is checked against an independently calculated golden image. -struct Image { pixels: array; guard: u64; } + +struct Image { + pixels: array; + guard: u64; +} + fun clamp(value: i64) -> i64 { - if (value < 0) { return 0; } - if (value > 4) { return 4; } + if (value < 0) { + return 0; + } + if (value > 4) { + return 4; + } return value; } + fun blur(source: Image) -> Image { var horizontal: Image = source; var result: Image = source; @@ -37,6 +45,7 @@ fun blur(source: Image) -> Image { } return result; } + fun exercise_language() -> i32 { var source: Image; source.guard = 0xFEDCBA9876543210; @@ -47,76 +56,107 @@ fun exercise_language() -> i32 { source.pixels[i] = x * x + y * 3; i += 1; } - var expected: array = [1, 2, 5, 10, 15, 3, 4, 7, 12, 17, 6, 7, 10, 15, 20, 9, 10, 13, 18, 23, 11, 12, 15, 20, 25]; + var expected: array = [ + 1, 2, 5, 10, 15, + 3, 4, 7, 12, 17, + 6, 7, 10, 15, 20, + 9, 10, 13, 18, 23, + 11, 12, 15, 20, 25 + ]; var result: Image = blur(source); i = 0; while (i < 25) { - if (result.pixels[i] != expected[i]) { return 1; } + if (result.pixels[i] != expected[i]) { + return 1; + } var x: i64 = i % 5; var y: i64 = i / 5; - if (source.pixels[i] != x * x + y * 3) { return 2; } + if (source.pixels[i] != x * x + y * 3) { + return 2; + } i += 1; } - if (result.guard != source.guard) { return 3; } + if (result.guard != source.guard) { + return 3; + } i = 0; - while (i < 25) { source.pixels[i] = 9000000000; i += 1; } + while (i < 25) { + source.pixels[i] = 9000000000; + i += 1; + } result = blur(source); i = 0; - while (i < 25) { if (result.pixels[i] != 9000000000) { return 4; } i += 1; } + while (i < 25) { + if (result.pixels[i] != 9000000000) { + return 4; + } + i += 1; + } return 0; } -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "add x0, x0, x0, lsl #5" - "eor x0, x0, x1" - in("x0") state - in("x1") word - out("x0") result +// A four-double homogeneous aggregate competes with eight scalar FP arguments. +// The filtered image feeds the boundary; exact binary fractions avoid tolerance masks. + +struct Color { + red: f64; + green: f64; + blue: f64; + alpha: f64; +} + +export(c, "case_arm64_color") fun color_boundary( + pixel: Color, a: f64, b: f64, c: f64, d: f64, + e: f64, f: f64, g: f64, h: f64 +) -> Color { + return Color { + red: pixel.red * a + pixel.green * b, + green: pixel.green * c + pixel.blue * d, + blue: pixel.blue * e + pixel.alpha * f, + alpha: pixel.alpha * g + h }; - return result % 65521; } -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; +fun target_workload() -> i32 { + var image: Image; + image.guard = 0xFEDCBA9876543210; var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; + while (i < 25) { + image.pixels[i] = i * 4; i += 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + var filtered: Image = blur(image); + i = 0; + while (i < 25) { + var sample: f64 = (filtered.pixels[i] as f64) / 2.0; + var pixel: Color = Color { + red: sample, + green: sample * 2.0, + blue: sample * 3.0, + alpha: 1.0 + }; + var result: Color = color_boundary(pixel, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0); + if (result.red != sample * 5.0 || result.green != sample * 18.0) { + return 201; + } + if (result.blue != sample * 15.0 + 6.0 || result.alpha != 15.0) { + return 202; + } + if (pixel.red != sample || pixel.alpha != 1.0 || image.pixels[i] != i * 4) { + return 203; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; + i += 1; + } + if (filtered.guard != 0xFEDCBA9876543210) { + return 204; } return 0; } fun main() -> i32 { var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + if (status != 0) { + return status; + } + return target_workload(); } diff --git a/tests/cases/shared/loong64/test5.wave b/tests/cases/shared/loong64/test5.wave index 24b5e8b2..14465839 100644 --- a/tests/cases/shared/loong64/test5.wave +++ b/tests/cases/shared/loong64/test5.wave @@ -1,16 +1,27 @@ -// Generated by tools/populate_case_matrix.py. -// shared/loong64: functional workload 5 and target data transport. // Bounded run-length packet codec. Round trips alone are insufficient: check // wire bytes, truncated pairs, zero runs, capacity and checksum corruption. -struct Bytes { data: array; length: i64; } -variant Decoded { Packet(Bytes), Invalid(i32) } + +struct Bytes { + data: array; + length: i64; +} + +variant Decoded { + Packet(Bytes), + Invalid(i32) +} + fun blank() -> Bytes { var result: Bytes; var i: i64 = 0; - while (i < 128) { result.data[i] = 0; i += 1; } + while (i < 128) { + result.data[i] = 0; + i += 1; + } result.length = 0; return result; } + fun pack(source: Bytes) -> Bytes { var result: Bytes = blank(); result.data[0] = 165; @@ -37,10 +48,15 @@ fun pack(source: Bytes) -> Bytes { result.length += 1; return result; } + fun unpack(packet: Bytes) -> Decoded { - if (packet.length < 3 || packet.data[0] != 165) { return Decoded::Invalid(1); } + if (packet.length < 3 || packet.data[0] != 165) { + return Decoded::Invalid(1); + } var payload_end: i64 = packet.length - 1; - if ((payload_end - 2) % 2 != 0) { return Decoded::Invalid(2); } + if ((payload_end - 2) % 2 != 0) { + return Decoded::Invalid(2); + } var result: Bytes = blank(); var checksum: u64 = 0; var i: i64 = 2; @@ -49,8 +65,12 @@ fun unpack(packet: Bytes) -> Decoded { i += 1; var value: u8 = packet.data[i]; i += 1; - if (count == 0) { return Decoded::Invalid(3); } - if (result.length + count > 128) { return Decoded::Invalid(4); } + if (count == 0) { + return Decoded::Invalid(3); + } + if (result.length + count > 128) { + return Decoded::Invalid(4); + } var n: i64 = 0; while (n < count) { result.data[result.length] = value; @@ -59,114 +79,183 @@ fun unpack(packet: Bytes) -> Decoded { n += 1; } } - if (result.length != packet.data[1] as i64) { return Decoded::Invalid(5); } - if (checksum != packet.data[payload_end] as u64) { return Decoded::Invalid(6); } + if (result.length != packet.data[1] as i64) { + return Decoded::Invalid(5); + } + if (checksum != packet.data[payload_end] as u64) { + return Decoded::Invalid(6); + } return Decoded::Packet(result); } + fun rejected(packet: Bytes, expected: i32) -> bool { match unpack(packet) { - Decoded::Packet(_) => { return false; } - Decoded::Invalid(code) => { return code == expected; } + Decoded::Packet(_) => { + return false; + } + Decoded::Invalid(code) => { + return code == expected; + } } } + fun exercise_language() -> i32 { var source: Bytes = blank(); source.length = 48; var i: i64 = 0; - while (i < 48) { source.data[i] = ((i / 6) * 17) as u8; i += 1; } + while (i < 48) { + source.data[i] = ((i / 6) * 17) as u8; + i += 1; + } var packet: Bytes = pack(source); - if (packet.length != 19 || packet.data[0] != 165 || packet.data[1] != 48) { return 1; } + if (packet.length != 19 || packet.data[0] != 165 || packet.data[1] != 48) { + return 1; + } i = 0; while (i < 8) { var offset: i64 = 2 + i * 2; var value_offset: i64 = offset + 1; - if (packet.data[offset] != 6 || packet.data[value_offset] != (i * 17) as u8) { return 2; } + if (packet.data[offset] != 6 || packet.data[value_offset] != (i * 17) as u8) { + return 2; + } i += 1; } - if (packet.data[18] != 95) { return 3; } + if (packet.data[18] != 95) { + return 3; + } match unpack(packet) { - Decoded::Invalid(_) => { return 4; } + Decoded::Invalid(_) => { + return 4; + } Decoded::Packet(decoded) => { - if (decoded.length != 48) { return 5; } + if (decoded.length != 48) { + return 5; + } i = 0; while (i < 48) { - if (decoded.data[i] != source.data[i]) { return 6; } + if (decoded.data[i] != source.data[i]) { + return 6; + } i += 1; } } } var broken: Bytes = packet; broken.length = 18; - if (!rejected(broken, 2)) { return 7; } + if (!rejected(broken, 2)) { + return 7; + } broken = packet; broken.data[2] = 0; - if (!rejected(broken, 3)) { return 8; } + if (!rejected(broken, 3)) { + return 8; + } broken.data[2] = 129; - if (!rejected(broken, 4)) { return 9; } + if (!rejected(broken, 4)) { + return 9; + } broken = packet; broken.data[18] = 0; - if (!rejected(broken, 6)) { return 10; } + if (!rejected(broken, 6)) { + return 10; + } match unpack(pack(blank())) { - Decoded::Invalid(_) => { return 11; } - Decoded::Packet(empty) => { if (empty.length != 0) { return 12; } } + Decoded::Invalid(_) => { + return 11; + } + Decoded::Packet(empty) => { + if (empty.length != 0) { + return 12; + } + } + } + if (packet.data[2] != 6 || source.data[47] != 119) { + return 13; } - if (packet.data[2] != 6 || source.data[47] != 119) { return 13; } return 0; } -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli.d $t0, $a0, 5" - "add.d $a0, $a0, $t0" - "xor $a0, $a0, $a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; +// Nine-byte and seventeen-byte aggregates straddle the two-register boundary. +// Transport encoded packet bytes, including the last partial register, not padding. + +struct Tag9 { + bytes: array; +} + +struct Chunk17 { + bytes: array; } -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; +export(c, "case_loong_tag") fun tag_boundary(chunk: Chunk17) -> Tag9 { + var tag: Tag9; var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; + while (i < 9) { + var other: i64 = 16 - i; + tag.bytes[i] = chunk.bytes[i] ^ chunk.bytes[other]; i += 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; + return tag; } + +export(c, "case_loong_chunk") fun chunk_boundary(tag: Tag9, chunk: Chunk17, salt: u8) -> Chunk17 { + var result: Chunk17; + var i: i64 = 0; + while (i < 17) { + var reverse: i64 = 16 - i; + result.bytes[i] = chunk.bytes[reverse] ^ tag.bytes[i % 9] ^ salt; + i += 1; + } + return result; +} + fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } + var source: Bytes = blank(); + source.length = 48; + var i: i64 = 0; + while (i < 48) { + source.data[i] = ((i / 6) * 17) as u8; + i += 1; + } + var packet: Bytes = pack(source); + var chunk: Chunk17; + i = 0; + while (i < 17) { + chunk.bytes[i] = packet.data[i]; + i += 1; + } + var tag: Tag9 = tag_boundary(chunk); + i = 0; + while (i < 9) { + var reverse: i64 = 16 - i; + if (tag.bytes[i] != (packet.data[i] ^ packet.data[reverse])) { + return 201; + } + i += 1; + } + var salt: i64 = 128; + while (salt < 192) { + var result: Chunk17 = chunk_boundary(tag, chunk, salt as u8); + i = 0; + while (i < 17) { + var reverse: i64 = 16 - i; + var expected: u8 = packet.data[reverse] ^ tag.bytes[i % 9] ^ (salt as u8); + if (result.bytes[i] != expected || chunk.bytes[i] != packet.data[i]) { + return 202; + } i += 1; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; + salt += 1; + } + if (packet.length != 19 || packet.data[18] != 95) { + return 203; } return 0; } fun main() -> i32 { var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + if (status != 0) { + return status; + } + return target_workload(); } diff --git a/tests/cases/shared/riscv64/test7.wave b/tests/cases/shared/riscv64/test7.wave index 89430a1f..8771c918 100644 --- a/tests/cases/shared/riscv64/test7.wave +++ b/tests/cases/shared/riscv64/test7.wave @@ -1,19 +1,35 @@ -// Generated by tools/populate_case_matrix.py. -// shared/riscv64: functional workload 7 and target data transport. // A bounded arena with generation-tagged handles. Reuse must invalidate stale // handles, preserve neighboring allocations, and clear newly allocated bytes. -struct Handle { index: i64; generation: i64; } -struct Pool { live: array; generation: array; values: array; } -struct Allocation { pool: Pool; handle: Handle; ok: bool; } + +struct Handle { + index: i64; + generation: i64; +} + +struct Pool { + live: array; + generation: array; + values: array; +} + +struct Allocation { + pool: Pool; + handle: Handle; + ok: bool; +} + fun empty_pool() -> Pool { var pool: Pool; var i: i64 = 0; while (i < 16) { - pool.live[i] = false; pool.generation[i] = 0; pool.values[i] = 0; + pool.live[i] = false; + pool.generation[i] = 0; + pool.values[i] = 0; i += 1; } return pool; } + fun allocate(pool: Pool, value: i64) -> Allocation { var result: Pool = pool; var i: i64 = 0; @@ -22,17 +38,34 @@ fun allocate(pool: Pool, value: i64) -> Allocation { result.live[i] = true; result.generation[i] += 1; result.values[i] = value; - return Allocation { pool: result, - handle: Handle { index: i, generation: result.generation[i] }, ok: true }; + return Allocation { + pool: result, + handle: Handle { + index: i, + generation: result.generation[i] + }, + ok: true + }; } i += 1; } - return Allocation { pool: result, handle: Handle { index: -1, generation: 0 }, ok: false }; + return Allocation { + pool: result, + handle: Handle { + index: -1, + generation: 0 + }, + ok: false + }; } + fun valid(pool: Pool, handle: Handle) -> bool { - if (handle.index < 0 || handle.index >= 16) { return false; } + if (handle.index < 0 || handle.index >= 16) { + return false; + } return pool.live[handle.index] && pool.generation[handle.index] == handle.generation; } + fun release(pool: Pool, handle: Handle) -> Pool { var result: Pool = pool; if (valid(result, handle)) { @@ -41,91 +74,121 @@ fun release(pool: Pool, handle: Handle) -> Pool { } return result; } + fun exercise_language() -> i32 { var pool: Pool = empty_pool(); var handles: array; var i: i64 = 0; while (i < 16) { var allocation: Allocation = allocate(pool, 9000000000 + i); - if (!allocation.ok || allocation.handle.index != i) { return 1; } + if (!allocation.ok || allocation.handle.index != i) { + return 1; + } pool = allocation.pool; handles[i] = allocation.handle; i += 1; } var full: Allocation = allocate(pool, -1); - if (full.ok || full.handle.index != -1) { return 2; } + if (full.ok || full.handle.index != -1) { + return 2; + } var snapshot: Pool = pool; i = 0; while (i < 16) { pool = release(pool, handles[i]); - if (pool.values[i] != 0 || valid(pool, handles[i])) { return 3; } + if (pool.values[i] != 0 || valid(pool, handles[i])) { + return 3; + } i += 2; } i = 0; while (i < 16) { var reused: Allocation = allocate(pool, 700 + i); - if (!reused.ok || reused.handle.index != i || reused.handle.generation != 2) { return 4; } + if (!reused.ok || reused.handle.index != i || reused.handle.generation != 2) { + return 4; + } pool = reused.pool; // A stale release must not free the replacement object. pool = release(pool, handles[i]); - if (!valid(pool, reused.handle) || pool.values[i] != 700 + i) { return 5; } + if (!valid(pool, reused.handle) || pool.values[i] != 700 + i) { + return 5; + } i += 2; } i = 1; while (i < 16) { - if (!valid(pool, handles[i]) || pool.values[i] != 9000000000 + i) { return 6; } + if (!valid(pool, handles[i]) || pool.values[i] != 9000000000 + i) { + return 6; + } i += 2; } - if (valid(pool, Handle { index: -1, generation: 1 })) { return 7; } - if (valid(pool, Handle { index: 16, generation: 1 })) { return 8; } - if (!valid(snapshot, handles[0]) || snapshot.values[0] != 9000000000) { return 9; } + if (valid(pool, Handle { + index: -1, generation: 1 + })) { + return 7; + } + if (valid(pool, Handle { + index: 16, generation: 1 + })) { + return 8; + } + if (!valid(snapshot, handles[0]) || snapshot.values[0] != 9000000000) { + return 9; + } return 0; } -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "slli t0, a0, 5" - "add a0, a0, t0" - "xor a0, a0, a1" - in("a0") state - in("a1") word - out("a0") result - clobber("t0") - }; - return result % 65521; -} +// Seven integer arguments leave one argument register for a two-word handle. +// Handle generations cross that split boundary and return as a two-word result. -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; - i += 1; +export(c, "case_riscv_generation") fun generation_boundary( + a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, g: i64, + handle: Handle, delta: i8, cookie: u32 +) -> Handle { + if ( + a != 9000000001 || b != -9000000002 || c != 3 || d != -4 || + e != 5 || f != -6 || g != 7 || delta != -7 || cookie != 4045620583 + ) { + return Handle { + index: -1, + generation: -1 + }; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; + return Handle { + index: handle.index, + generation: handle.generation - (delta as i64) + }; } + fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + var pool: Pool = empty_pool(); + var round: i64 = 0; + while (round < 32) { + var allocation: Allocation = allocate(pool, round * 17); + if (!allocation.ok) { + return 201; + } + pool = allocation.pool; + var original: Handle = allocation.handle; + var replacement: Handle = generation_boundary( + 9000000001, -9000000002, 3, -4, 5, -6, 7, + original, -7, 4045620583 + ); + if (replacement.index != original.index || replacement.generation != original.generation + 7) { + return 202; + } + pool.generation[replacement.index] = replacement.generation; + if (valid(pool, original) || !valid(pool, replacement)) { + return 203; + } + pool = release(pool, original); + if (pool.values[replacement.index] != round * 17) { + return 204; + } + pool = release(pool, replacement); + if (valid(pool, replacement) || pool.values[replacement.index] != 0) { + return 205; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } round += 1; } return 0; @@ -133,8 +196,8 @@ fun target_workload() -> i32 { fun main() -> i32 { var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + if (status != 0) { + return status; + } + return target_workload(); } diff --git a/tests/cases/shared/test114.wave b/tests/cases/shared/test114.wave new file mode 100644 index 00000000..1bcaeec5 --- /dev/null +++ b/tests/cases/shared/test114.wave @@ -0,0 +1,110 @@ +// Search an 8 KiB stream with seven bytes of input storage. Overlapping KMP +// prefixes must survive arbitrary chunk boundaries without retaining input. +struct Search { + prefix: array; + matched: i64; + consumed: i64; + hits: i64; + first: i64; + last: i64; +} + +fun pattern(index: i64) -> u8 { + var bytes: array = [65, 66, 65, 66, 65, 67]; + return bytes[index]; +} + +fun start() -> Search { + return Search { + prefix: [0, 0, 1, 2, 3, 0], + matched: 0, + consumed: 0, + hits: 0, + first: -1, + last: -1 + }; +} + +fun feed(state: Search, bytes: ptr, count: i64) -> Search { + var result: Search = state; + var i: i64 = 0; + while (i < count) { + var byte: u8 = bytes[i]; + while (result.matched > 0 && byte != pattern(result.matched)) { + var previous: i64 = result.matched - 1; + result.matched = result.prefix[previous]; + } + if (byte == pattern(result.matched)) { + result.matched += 1; + } + result.consumed += 1; + if (result.matched == 6) { + result.last = result.consumed - 6; + if (result.hits == 0) { + result.first = result.last; + } + result.hits += 1; + result.matched = result.prefix[5]; + } + i += 1; + } + return result; +} + +fun source_byte(index: i64) -> u8 { + var motif: array = [65, 66, 65, 66, 65, 66, 65, 67]; + return motif[index % 8]; +} + +fun main() -> i32 { + var width: i64 = 1; + while (width <= 7) { + var state: Search = start(); + var chunk: array; + chunk[0] = 219; + chunk[8] = 173; + while (state.consumed < 8192) { + var count: i64 = width; + if (count > 8192 - state.consumed) { + count = 8192 - state.consumed; + } + var i: i64 = 0; + while (i < count) { + var destination: i64 = i + 1; + chunk[destination] = source_byte(state.consumed + i); + i += 1; + } + var before: Search = state; + state = feed(state, &chunk[1], count); + // The source has exactly one match ending at each eighth byte. + if (state.hits != state.consumed / 8) { + return 1; + } + if (before.consumed + count != state.consumed) { + return 2; + } + if (chunk[0] != 219 || chunk[8] != 173) { + return 3; + } + var empty: Search = feed(state, null, 0); + if (empty.hits != state.hits || empty.matched != state.matched) { + return 4; + } + } + if (state.hits != 1024 || state.first != 2 || state.last != 8186) { + return 5; + } + width += 1; + } + var partial: array = [65, 66, 65, 66, 65]; + var pending: Search = feed(start(), &partial[0], 5); + if (pending.hits != 0 || pending.matched != 5) { + return 6; + } + var last: u8 = 67; + var finished: Search = feed(pending, &last, 1); + if (finished.hits != 1 || finished.first != 0 || pending.hits != 0) { + return 7; + } + return 0; +} diff --git a/tests/cases/shared/test115.wave b/tests/cases/shared/test115.wave new file mode 100644 index 00000000..d5b33283 --- /dev/null +++ b/tests/cases/shared/test115.wave @@ -0,0 +1,119 @@ +// Layout is the subject here: K&R, Allman, split calls, comments between +// tokens, and deeply grouped expressions must preserve the same semantics. +fun euclid(a: i64, b: i64) -> i64 { + var left: i64 = a; + var right: i64 = b; + while (right != 0) { + var remainder: i64 = left % right; + left = right; + right = remainder; + } + return left; +} + +fun subtraction +( + a: i64, + b: i64 +) +-> i64 +{ + var left: i64 = a; + var right: i64 = b; + if (left == 0) + { + return right; + } + else if (right == 0) + { + return left; + } + else + { + while + ( + left != right + ) + { + if (left > right) + { + left -= right; + } + else + { + right -= left; + } + } + } + return left; +} + +variant Answer { + Value(T), + Invalid +} + +fun checked(a: i64, b: i64) -> Answer { + if ( + a < 0 || // A newline after the operator must not end the expression. + b < 0 + ) { + return Answer::Invalid; + } + return Answer::Value( + euclid + ( + a, + b + ) + ); +} + +fun main() -> i32 { + var sum: i64 = 0; + for ( + var scale: i64 = 1; + scale <= 192; + scale += 1 + ) { + var expected: i64 = ((((((((((((scale)))))))))))); + var result: i64 = subtraction(7 * scale, 11 * scale); + if (result != expected) { + return 1; + } + match checked( + 7 * scale, + 11 * scale + ) { + Answer::Value(value) => + { + if (value != result) { + return 2; + } + sum += value; + } + Answer::Invalid => + { + return 3; + } + } + } + if (sum != 18528) { + return 4; + } + if (subtraction(0, 97) != 97 || euclid(97, 0) != 97) { + return 5; + } + match checked(-1, 5) + { + Answer::Value(_) => + { + return 6; + } + Answer::Invalid => + { + sum = 0; + } + } + return sum as i32; +} diff --git a/tests/cases/windows/amd64/test6.wave b/tests/cases/windows/amd64/test6.wave index 42924a41..bf03da6d 100644 --- a/tests/cases/windows/amd64/test6.wave +++ b/tests/cases/windows/amd64/test6.wave @@ -1,125 +1,167 @@ -// Generated by tools/populate_case_matrix.py. -// windows/amd64: functional workload 6 and target data transport. -// Base-10000 multi-precision multiplication, with carries crossing every limb. -// Exact independently computed limbs exercise wide intermediates and copies. -struct Big { limb: array; } -fun zero() -> Big { - var result: Big; - var i: i64 = 0; - while (i < 16) { result.limb[i] = 0; i += 1; } - return result; +// Windows page-backed storage grows across page boundaries and then shrinks. +// Counting sort uses a fixed 256-bin workspace, regardless of allocation size. +import("std::mem::alloc")::{mem_alloc_zeroed, mem_realloc, mem_free, mem_page_size}; + +struct Allocation { + data: ptr; + size: i64; } -fun multiply(a: Big, b: Big) -> Big { - var result: Big = zero(); + +fun exercise(allocation: ptr, page: i64) -> i32 { + var length: i64 = page + 19; + var memory: ptr = allocation.data; var i: i64 = 0; + while (i < allocation.size) { + if (memory[i] != 0) { + return 2; + } + i += 1; + } + i = 0; while (i < 8) { - var carry: u64 = 0; - var j: i64 = 0; - while (j < 8) { - var index: i64 = i + j; - var total: u64 = result.limb[index] + a.limb[i] * b.limb[j] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - j += 1; + memory[i] = 173; + var tail: i64 = 8 + length + i; + memory[tail] = 219; + i += 1; + } + i = 0; + while (i < length) { + var offset: i64 = i + 8; + memory[offset] = ((i * 73 + 19) % 256) as u8; + i += 1; + } + var grown_size: i64 = page * 2 + 35; + var grown: ptr = mem_realloc(memory, allocation.size, grown_size); + if (grown == null) { + return 3; + } + allocation.data = grown; + allocation.size = grown_size; + i = 0; + while (i < length) { + var offset: i64 = i + 8; + if (grown[offset] as i64 != (i * 73 + 19) % 256) { + return 4; } - var index: i64 = i + 8; - while (carry != 0 && index < 16) { - var total: u64 = result.limb[index] + carry; - result.limb[index] = total % 10000; - carry = total / 10000; - index += 1; + i += 1; + } + i = 0; + while (i < 8) { + var tail: i64 = 8 + length + i; + if (grown[i] != 173 || grown[tail] != 219) { + return 5; } i += 1; } - return result; -} -fun equal(a: Big, b: Big) -> bool { - var i: i64 = 0; - while (i < 16) { - if (a.limb[i] != b.limb[i]) { return false; } + // Newly allocated bytes have no assumed value; initialize every byte read below. + var old_length: i64 = length; + length = page * 2 + 19; + i = old_length; + while (i < length) { + var offset: i64 = i + 8; + grown[offset] = ((i * 73 + 19) % 256) as u8; i += 1; } - return true; -} -fun exercise_language() -> i32 { - var a: Big = zero(); - var b: Big = zero(); - a.limb[0] = 7890; a.limb[1] = 3456; a.limb[2] = 12; - b.limb[0] = 4321; b.limb[1] = 8765; b.limb[2] = 9; - var product: Big = multiply(a, b); - // 1234567890 * 987654321 = 1219326311126352690. - var expected: Big = zero(); - expected.limb[0] = 2690; expected.limb[1] = 2635; - expected.limb[2] = 3111; expected.limb[3] = 9326; expected.limb[4] = 121; - if (!equal(product, expected)) { return 1; } - if (!equal(multiply(b, a), product)) { return 2; } - if (!equal(multiply(a, zero()), zero())) { return 3; } - var one: Big = zero(); one.limb[0] = 1; - if (!equal(multiply(a, one), a)) { return 4; } - var maximal: Big = zero(); - var i: i64 = 0; - while (i < 8) { maximal.limb[i] = 9999; i += 1; } - var square: Big = multiply(maximal, maximal); - if (square.limb[0] != 1 || square.limb[8] != 9998) { return 5; } - i = 1; - while (i < 8) { if (square.limb[i] != 0) { return 6; } i += 1; } - i = 9; - while (i < 16) { if (square.limb[i] != 9999) { return 7; } i += 1; } - if (maximal.limb[0] != 9999 || a.limb[1] != 3456) { return 8; } - return 0; -} - -fun target_mix(state: u64, word: u64) -> u64 { - var result: u64 = asm { - "imul rax, rax, 33" - "xor rax, rcx" - in("rax") state - in("rcx") word - out("rax") result - }; - return result % 65521; -} - -// Feed actual records through the target's register calling convention, then -// compare each checkpoint to Wave arithmetic. Live copies and guard words must -// survive every call; the low and high halves of inputs both carry data. -struct TargetBlock { words: array; digest: u64; guard: u64; } -fun target_block(seed: u64) -> TargetBlock { - var block: TargetBlock; - var i: i64 = 0; - while (i < 16) { - block.words[i] = 4294967296 + seed * 97 + (i as u64) * 65537; + i = 0; + while (i < 8) { + var tail: i64 = 8 + length + i; + grown[tail] = 219; i += 1; } - block.digest = seed; - block.guard = 0xFEDCBA9876543210; - return block; -} -fun target_workload() -> i32 { - var round: u64 = 0; - while (round < 65) { - var block: TargetBlock = target_block(round); - var snapshot: TargetBlock = block; - var reference: u64 = round; - var i: i64 = 0; - while (i < 16) { - var word: u64 = block.words[i]; - block.digest = target_mix(block.digest, word); - reference = ((reference * 33) ^ word) % 65521; - if (block.digest != reference) { return 201; } - if (snapshot.words[i] != word || snapshot.digest != round) { return 202; } - i += 1; + var bins: array; + i = 0; + while (i < 256) { + bins[i] = 0; + i += 1; + } + i = 0; + while (i < length) { + var offset: i64 = i + 8; + var value: i64 = grown[offset] as i64; + bins[value] += 1; + i += 1; + } + var written: i64 = 0; + var value: i64 = 0; + while (value < 256) { + // 73 has inverse 249 modulo 256; compute each bucket independently. + var residue: i64 = ((value + 256 - 19) * 249) % 256; + var expected: i64 = length / 256; + if (residue < length % 256) { + expected += 1; + } + if (bins[value] != expected) { + return 6; + } + var count: i64 = 0; + while (count < bins[value]) { + var offset: i64 = written + 8; + grown[offset] = value as u8; + written += 1; + count += 1; } - if (block.guard != 0xFEDCBA9876543210 || snapshot.guard != block.guard) { return 203; } - round += 1; + value += 1; + } + if (written != length) { + return 7; + } + i = 0; + while (i < 8) { + var tail: i64 = 8 + length + i; + if (grown[i] != 173 || grown[tail] != 219) { + return 8; + } + i += 1; + } + var retained: i64 = page + 19; + var shrunk: ptr = mem_realloc(grown, grown_size, retained + 8); + if (shrunk == null) { + return 9; + } + allocation.data = shrunk; + allocation.size = retained + 8; + i = 0; + while (i < 8) { + if (shrunk[i] != 173) { + return 10; + } + i += 1; + } + var position: i64 = 0; + value = 0; + while (value < 256 && position < retained) { + var count: i64 = 0; + while (count < bins[value] && position < retained) { + var offset: i64 = position + 8; + if (shrunk[offset] as i64 != value) { + return 11; + } + position += 1; + count += 1; + } + value += 1; + } + if (position != retained) { + return 12; } return 0; } fun main() -> i32 { - var status: i32 = exercise_language(); - if (status != 0) { return status; } - status = target_workload(); - if (status != 0) { return status; } - return 0; + var page: i64 = mem_page_size(); + if (page <= 0 || page > 65536) { + return 1; + } + var allocation: Allocation = Allocation { + data: mem_alloc_zeroed(page + 35), + size: page + 35 + }; + if (allocation.data == null) { + return 1; + } + var status: i32 = exercise(&allocation, page); + if (mem_free(allocation.data, allocation.size) < 0 && status == 0) { + status = 13; + } + return status; } diff --git a/tests/cases/windows/riscv64/README.md b/tests/cases/windows/riscv64/README.md deleted file mode 100644 index 47ea604f..00000000 --- a/tests/cases/windows/riscv64/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Windows riscv64 cases - -This suite is reserved for the requested OS/architecture layout. Wave does not currently -advertise a Windows riscv64 target, so its Wave compatibility cases are syntax-checked but -remain excluded from execution and artifact generation by `cases.toml`. diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index d61cfe02..cddfbce2 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -417,12 +417,7 @@ fn retired_let_declarations_are_rejected() { let source = write_wave(&dir, file_name, source); let error = run_wavec_expect_failure([OsStr::new("check"), source.as_os_str()]); assert!(error.contains("error[E2001]"), "{}: {}", file_name, error); - assert!( - error.contains("failed to parse function declaration"), - "{}: {}", - file_name, - error - ); + assert!(error.contains("Let (`let`)"), "{}: {}", file_name, error); } } @@ -4337,6 +4332,9 @@ fn odd_sized_aggregate_transport_matches_clang_ir_contracts() { "aarch64-apple-darwin", "riscv64-unknown-linux-gnu", "aarch64-w64-windows-gnu", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))] "loongarch64-unknown-linux-gnu", ] { @@ -4367,6 +4365,52 @@ fn odd_sized_aggregate_transport_matches_clang_ir_contracts() { ]); let clang_ir = fs::read_to_string(clang_ir_path).unwrap(); let wave_ir = fs::read_to_string(wave_dir.join("interop.ll")).unwrap(); + if matches!(target, "x86_64-pc-windows-msvc" | "x86_64-pc-windows-gnu") { + // Win64 passes only 1/2/4/8-byte aggregates as integers. All other + // sizes use an sret pointer and a caller-owned indirect argument. + for (name, size) in [ + ("bytes1", 1), + ("bytes2", 2), + ("bytes3", 3), + ("bytes4", 4), + ("bytes5", 5), + ("bytes6", 6), + ("bytes7", 7), + ("bytes8", 8), + ("bytes9", 9), + ("bytes12", 12), + ("bytes16", 16), + ("nested", 8), + ("array_member", 6), + ("pointer_member", 8), + ] { + for (ir, prefix) in [(&clang_ir, "c"), (&wave_ir, "wave")] { + let line = ir + .lines() + .find(|line| { + line.starts_with("define ") + && line.contains(&format!("@{prefix}_{name}(")) + }) + .unwrap(); + if matches!(size, 1 | 2 | 4 | 8) { + assert!( + line.contains(&format!("i{} @{prefix}_{name}(i{}", size * 8, size * 8)), + "{target}: {line}" + ); + } else { + assert!( + line.contains(&format!("void @{prefix}_{name}(ptr")) + && line.contains("sret(") + && line.contains(", ptr") + && !line.contains("byval"), + "{target}: {line}" + ); + } + } + } + assert!(wave_ir.contains("alloca %Bytes3, align 16"), "{wave_ir}"); + continue; + } for size in [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 16] { let object_integer = format!("i{}", size * 8); let (result, argument) = match (tag, size <= 8) { @@ -4531,6 +4575,8 @@ fun main() -> i32 { return c_i8(-1) as i32 + c_u8(1) as i32 + c_i16(-1) as i32 + "x86_64-pc-windows-gnu", "riscv64-unknown-linux-gnu", "aarch64-w64-windows-gnu", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))] "loongarch64-unknown-linux-gnu", ] { @@ -5792,3 +5838,179 @@ fn run_shared_language_workloads(flag: &str, target: &str, runner: &str) { ); } } + +#[test] +fn explicit_msvc_targets_emit_coff_and_link_without_mingw() { + let dir = temp_case_dir("msvc-foundation"); + let source = write_wave( + &dir, + "entry.wave", + r#" +export(c) fun entry() -> i32 { return 7; } +extern(system) fun native_api(value: i32) -> i32; +fun main() -> i32 { return 0; } +"#, + ); + for (target, machine) in [ + ("x86_64-pc-windows-msvc", 0x8664u16), + ("aarch64-pc-windows-msvc", 0xaa64u16), + ] { + if llvm::codegen::target::target_spec_for_triple(target).is_none() { + continue; + } + let output_dir = dir.join(target); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + output_dir.as_os_str(), + ]); + let object = output_dir.join("entry.o"); + let data = fs::read(&object).unwrap(); + assert_eq!(u16::from_le_bytes([data[0], data[1]]), machine); + let executable = output_dir.join("entry.exe"); + let (plan, _) = run_wavec_capture([ + OsStr::new("build"), + object.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--dry-run"), + OsStr::new("--entry=entry"), + OsStr::new("-Cno-default-libs"), + OsStr::new("-o"), + executable.as_os_str(), + ]); + assert!( + plan.contains("lld-link") + && plan.contains("/NODEFAULTLIB") + && plan.contains("/ENTRY:entry"), + "{plan}" + ); + assert!( + !plan.contains("mingw") && !plan.contains("-lmsvcrt"), + "{plan}" + ); + // This verifies a PE image, not execution on a Windows host. + if Command::new("lld-link").arg("--version").output().is_ok() { + run_wavec([ + OsStr::new("build"), + object.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--entry=entry"), + OsStr::new("-Cno-default-libs"), + OsStr::new("-o"), + executable.as_os_str(), + ]); + let image = fs::read(&executable).unwrap(); + assert_eq!(&image[..2], b"MZ"); + let pe = u32::from_le_bytes(image[0x3c..0x40].try_into().unwrap()) as usize; + assert_eq!(&image[pe..pe + 4], b"PE\0\0"); + assert_eq!( + u16::from_le_bytes(image[pe + 4..pe + 6].try_into().unwrap()), + machine + ); + } + } +} + +#[cfg(target_os = "linux")] +#[test] +fn async_tasks_suspend_resume_cancel_and_exchange_tcp_data() { + let available = run_wavec_capture(["print", "target-list"]).0; + let arch = std::env::consts::ARCH; + if !available.lines().any(|t| t.starts_with(arch)) { + return; + } + let dir = temp_case_dir("async-runtime"); + let home = dir.join("home"); + copy_tree( + &PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("std"), + &home.join(".wave/lib/wave/std"), + ); + for name in [ + "lazy_nested", + "control_flow", + "timer_fairness", + "tcp_roundtrip", + ] { + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(format!("tests/fixtures/async/{name}.wave")); + let output = dir.join(name); + let compiled = wavec_command() + .env("HOME", &home) + .arg("build") + .arg(source) + .arg("-o") + .arg(&output) + .output() + .unwrap(); + assert!( + compiled.status.success(), + "{name}: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + let mut child = Command::new(&output).spawn().unwrap(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success(), "{name}: {status}"); + break; + } + if std::time::Instant::now() >= deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("{name} did not complete"); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn async_frames_emit_for_enabled_native_targets() { + let dir = temp_case_dir("async-targets"); + let home = dir.join("home"); + copy_tree( + &PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("std"), + &home.join(".wave/lib/wave/std"), + ); + let source = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/async/control_flow.wave"); + let available = run_wavec_capture(["print", "target-list"]).0; + for target in [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "riscv64-unknown-linux-gnu", + "loongarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-unknown-freebsd", + ] { + if !available.lines().any(|t| t == target) { + continue; + } + let output = dir.join(format!("{target}.o")); + let compiled = wavec_command() + .env("HOME", &home) + .arg("build") + .arg(&source) + .args(["--target", target, "--emit=obj", "-o"]) + .arg(&output) + .output() + .unwrap(); + assert!( + compiled.status.success(), + "{target}: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + assert!(output.metadata().unwrap().len() > 0); + } + fs::remove_dir_all(dir).unwrap(); +} diff --git a/tests/fixtures/async/control_flow.wave b/tests/fixtures/async/control_flow.wave new file mode 100644 index 00000000..31bbef75 --- /dev/null +++ b/tests/fixtures/async/control_flow.wave @@ -0,0 +1,43 @@ +import("std::task" as task); +static hits: i64 = 0; +variant Answer { Value(i64), Empty } +async fun value(v:T) -> T { await task::yield_now(); return v; } +async fun yes() -> bool { hits += 1; await task::yield_now(); return true; } +async fun change(p:ptr) -> i64 { await task::yield_now(); deref p += 3; return 4; } +async fun worker(n:i64) -> i64 { + var sum:i64 = 0; + var i:i64 = 0; + while (i < n) { + i += 1; + if (i == 2) { continue; } + sum += await value(i); + if (i == 4) { break; } + } + for (var j:i64 = 0; j < 3; j += 1) { sum += await value(j); } + var answer:Answer = Answer::Value(sum); + match answer { + Answer::Value(v) => { sum = await value(v + 1); } + Answer::Empty => { sum = -1; } + } + return sum; +} +async fun all() -> i32 { + var a:Future = task::spawn(worker(9)); + var b:Future = task::spawn(worker(9)); + var total:i64 = (await a) + (await b); + if (total != 24) { return 1; } + if (false && await yes()) { return 2; } + if (!(true || await yes())) { return 3; } + if (hits != 0) { return 4; } + var x:i64 = 10; + var v:i64 = x + await change(&x); + if (v != 14 || x != 13) { return 5; } + return 0; +} +fun main() -> i32 { + var cancelled:Future = value(1); + if (!task::cancel(cancelled)) { return 6; } + var result:i32 = task::block_on(all()); + task::shutdown(); + return result; +} diff --git a/tests/fixtures/async/lazy_nested.wave b/tests/fixtures/async/lazy_nested.wave new file mode 100644 index 00000000..02452ff2 --- /dev/null +++ b/tests/fixtures/async/lazy_nested.wave @@ -0,0 +1,19 @@ +import("std::task" as task); +static entered: i32 = 0; +async fun calculate(value: i64) -> i64 { + entered += 1; + await task::yield_now(); + return value * 2; +} +async fun process() -> i64 { + var value: i64 = await calculate(21); + return value; +} +fun main() -> i32 { + var future: Future = process(); + if (entered != 0) { return 10; } + var result: i64 = task::block_on(future); + task::shutdown(); + if (result != 42 || entered != 1) { return 11; } + return 0; +} diff --git a/tests/fixtures/async/tcp_roundtrip.wave b/tests/fixtures/async/tcp_roundtrip.wave new file mode 100644 index 00000000..1b28ccdc --- /dev/null +++ b/tests/fixtures/async/tcp_roundtrip.wave @@ -0,0 +1,62 @@ +import("std::task" as task); +import("std::task::net" as net); +import("std::net::tcp")::{TcpListener, tcp_bind_loopback, tcp_listener_local_addr_v4}; +import("std::net::address")::{SocketAddrV4}; +import("std::net::error")::{NetResult, NetIoResult}; +import("std::net::socket_base")::{net_socket_tcp_v4}; +async fun serve(listener:i64) -> i32 { + var fd:i64 = await net::accept(listener, 2000); + if (fd < 0) { return 20; } + var packet:array; + var total:i64 = 0; + while (total < 16) { + var received:NetIoResult = await net::read(fd, &packet[0] + total, 16 - total, 2000); + if (received.error.kind != 0 || received.eof) { net::close(fd); return 21; } + total += received.count; + } + var sent:NetIoResult = await net::write_all(fd, &packet[0], 16, 2000); + net::close(fd); + if (sent.error.kind != 0 || sent.count != 16) { return 22; } + return 0; +} +async fun client(address:SocketAddrV4) -> i32 { + var fd:i64 = net_socket_tcp_v4(); + if (fd < 0) { return 30; } + var connected:i64 = await net::connect(fd,address,2000); + if (connected < 0) { net::close(fd); return 31; } + var packet:array; + var i:i64 = 0; + while (i < 16) { packet[i] = (i * 7 + 3) as u8; i += 1; } + var sent:NetIoResult = await net::write_all(fd,&packet[0],16,2000); + if (sent.error.kind != 0) { net::close(fd); return 32; } + var total:i64 = 0; + while (total < 16) { + var got:NetIoResult = await net::read(fd,&packet[0] + total,16 - total,2000); + if (got.error.kind != 0 || got.eof) { net::close(fd); return 33; } + total += got.count; + } + i = 0; + while (i < 16) { if (packet[i] != (i * 7 + 3) as u8) { net::close(fd); return 34; } i += 1; } + var eof:NetIoResult = await net::read(fd,&packet[0],16,2000); + net::close(fd); + if (!eof.eof) { return 35; } + return 0; +} +async fun run(listener:i64,address:SocketAddrV4) -> i32 { + var server:Future = task::spawn(serve(listener)); + var peer:Future = task::spawn(client(address)); + var a:i32 = await server; + var b:i32 = await peer; + if (a != 0) { return a; } + return b; +} +fun main() -> i32 { + var listener:NetResult = tcp_bind_loopback(0); + if (!listener.ok) { return 10; } + var address:NetResult = tcp_listener_local_addr_v4(listener.value); + if (!address.ok) { return 11; } + var result:i32 = task::block_on(run(listener.value.fd,address.value)); + net::close(listener.value.fd); + task::shutdown(); + return result; +} diff --git a/tests/fixtures/async/timer_fairness.wave b/tests/fixtures/async/timer_fairness.wave new file mode 100644 index 00000000..656ddf5e --- /dev/null +++ b/tests/fixtures/async/timer_fairness.wave @@ -0,0 +1,9 @@ +import("std::task" as task); +static done: i32 = 0; +async fun sleeper() { await task::sleep_ms(2); done = 1; } +async fun main_work() { + var child: Future = task::spawn(sleeper()); + while (done == 0) { await task::yield_now(); } + await child; +} +fun main() -> i32 { task::block_on(main_work()); task::shutdown(); return 0; } diff --git a/tests/frontend_regressions.rs b/tests/frontend_regressions.rs index b0e15594..95c75c92 100644 --- a/tests/frontend_regressions.rs +++ b/tests/frontend_regressions.rs @@ -299,3 +299,470 @@ fn windows_std_import_and_print_use_userprofile_without_home() { successful(&imported); std::fs::remove_dir_all(profile).unwrap(); } + +#[test] +fn control_header_diagnostics_agree_in_human_and_json_output() { + let dir = directory(); + let target = frontend_target(); + let library = dir.join("broken.wave"); + let source = dir.join("main.wave"); + std::fs::write(&source, "import(\"./broken\"); fun main() {}\n").unwrap(); + for (body, context, expected, unexpected) in [ + ("if (1 {}", "if header", "')'", "{"), + ("if (1) {} else if (0 {}", "else if header", "')'", "{"), + ("if (1) {} else return;", "else header", "'{'", "return"), + ("while (1 {}", "while header", "')'", "{"), + ( + "for (i = 0 i < 2; i += 1) {}", + "for initializer", + "';'", + "i <", + ), + ] { + let text = format!("fun broken() {{\n {body}\n}}\n"); + std::fs::write(&library, &text).unwrap(); + let human = check(&source, &target); + let json = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(&target), + ]); + for output in [&human, &json] { + assert!(!output.status.success()); + assert!( + output.stdout.is_empty(), + "legacy parser output: {:?}", + output.stdout + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("E2001"), "{stderr}"); + assert!(stderr.contains("broken.wave"), "{stderr}"); + assert!( + stderr.contains(&format!("expected {expected} in {context}")), + "{stderr}" + ); + assert!( + !stderr.contains("failed to parse function declaration"), + "{stderr}" + ); + } + let json = String::from_utf8_lossy(&json.stderr); + let start = text.find(body).unwrap() + body.rfind(unexpected).unwrap(); + assert_eq!(json.lines().count(), 1, "{json}"); + assert!(json.contains(&format!("\"start\":{start},")), "{json}"); + assert!( + json.contains(&format!("\"expected\":[\"{expected}\"]")), + "{json}" + ); + assert!( + json.contains(&format!("\"context\":\"{context}\"")), + "{json}" + ); + } +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn nested_control_flow_retains_truthy_conditions_and_for_initializers() { + let dir = directory(); + let source = dir.join("control.wave"); + std::fs::write( + &source, + r#" +fun main() -> i32 { + var total: i32 = 0; + for (var i: i32 = 0; i < 4; i += 1) { + if (i == 0) { continue; } + else if (i == 1) { total += 2; } + else { var n: i32 = i; while (n) { total += n; n -= 1; } } + } + for (j: i32 = 0; j < 3; j += 1) { total += j; } + var k: i32 = 0; + for (k = 0; k < 9; k += 1) { if (k == 2) { break; } total += 1; } + if (total != 16) { return 1; } + return 0; +} +"#, + ) + .unwrap(); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--run"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ])); +} + +#[test] +fn lexer_escape_diagnostics_keep_labels_and_point_at_the_escape() { + let dir = directory(); + let source = dir.join("escape.wave"); + let target = frontend_target(); + for (escape, label) in [ + ("\\q", "unsupported escape sequence"), + ( + "\\xGG", + "hex escapes must be exactly two hexadecimal digits", + ), + ] { + let text = format!("// 한글\r\nfun main() {{ \"앞{escape}\"; }}\r\n"); + std::fs::write(&source, &text).unwrap(); + let human = check(&source, &target); + assert!(!human.status.success()); + let human = String::from_utf8_lossy(&human.stderr); + assert!(human.contains(label), "{human}"); + let output = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(&target), + ]); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + let json = String::from_utf8_lossy(&output.stderr); + assert!(json.contains(&format!("\"label\":\"{label}\"")), "{json}"); + let start = text.find(escape).unwrap(); + assert!( + json.contains(&format!( + "\"start\":{start},\"end\":{}", + start + escape.len() + )), + "{json}" + ); + assert!(json.contains("\"code\":\"E1004\""), "{json}"); + } +} + +#[test] +fn truncated_blocks_show_the_eof_source_line_and_caret() { + let dir = directory(); + let source = dir.join("eof.wave"); + let target = frontend_target(); + for newline in ["\n", "\r\n"] { + std::fs::write(&source, format!("fun main() {{{newline}")).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_wavec")) + .args(["check"]) + .arg(&source) + .args(["--target", &target]) + .env("NO_COLOR", "1") + .output() + .unwrap(); + assert!(!output.status.success()); + let human = String::from_utf8(output.stderr).unwrap(); + assert!(human.contains("eof.wave:2:1"), "{human}"); + assert!(human.contains(" 2 | \n | ^"), "{human}"); + assert!(!human.contains('\r'), "{human}"); + } +} + +#[test] +fn imported_expression_errors_keep_their_location_in_both_output_formats() { + let dir = directory(); + let source = dir.join("main.wave"); + let library = dir.join("broken.wave"); + let target = frontend_target(); + std::fs::write(&source, "import(\"./broken\"); fun main() {}\n").unwrap(); + for (marked, expected, context) in [ + ("item.@;", "identifier", "member access"), + ("pkg::@;", "identifier", "qualified name"), + ("call(1 @;", "')'", "function call"), + ("item.method(1 @;", "')'", "method call"), + ("items[0 @;", "']'", "index expression"), + ("(1 @;", "')'", "grouped expression"), + ("[1 @;", "']'", "array literal"), + ("Point { x: 1 @y: 2 };", "',' or '}'", "struct literal"), + ] { + let marked = format!("// 한글\r\nfun broken() {{\r\n {marked}\r\n}}"); + let start = marked.find('@').unwrap(); + std::fs::write(&library, marked.replace('@', "")).unwrap(); + let human = check(&source, &target); + let json = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(&target), + ]); + for output in [&human, &json] { + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains(&format!("expected {expected} in {context}")), + "{stderr}" + ); + assert!(stderr.contains("broken.wave"), "{stderr}"); + assert!(stderr.contains("E2001"), "{stderr}"); + } + let stderr = String::from_utf8_lossy(&json.stderr); + assert!(stderr.contains(&format!("\"start\":{start},")), "{stderr}"); + assert!( + stderr.contains(&format!("\"context\":\"{context}\"")), + "{stderr}" + ); + } +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn generic_aggregates_and_postfix_chains_preserve_runtime_values() { + let dir = directory(); + let source = dir.join("expressions.wave"); + std::fs::write( + &source, + r#" +struct Box { value: T; fun get(self: ptr>) -> T { return self.value; } } +struct Values { + items: array; + fun at(self: ptr, index: i32) -> i32 { return self.items[index]; } + fun first(self: ptr) -> Box { return Box { value: self.items[0] }; } +} +fun boxed(value: T) -> Box { return Box { value: value }; } +fun values() -> Values { return Values { items: [2, 5, 9] }; } +fun counted(count: ptr) -> Box { deref count += 1; return boxed(7); } +fun main() -> i32 { + var holder: Box = boxed(values()); + var left: i32 = 0; + var right: i32 = 0; + left = right = (&holder.value).at(1) + boxed(3).value * 2; + if (left != 11 || right != 11) { return 1; } + var old: i32 = holder.value.items[1]++; + var next: i32 = ++holder.value.items[2]; + if (old != 5 || next != 10 || (&holder.value).at(1) != 6) { return 2; } + var count: i32 = 0; + var result: i32 = counted(&count).value; + var number: Box = boxed(3); + if (result != 7 || count != 1 || (&number).get() != 3 || (&holder.value).first().value != 2) { return 3; } + return 0; +} +"#, + ) + .unwrap(); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--run"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ])); +} + +#[test] +fn eof_diagnostics_link_the_unmatched_opener_in_main_and_imported_sources() { + let dir = directory(); + let source = dir.join("main.wave"); + let library = dir.join("unclosed.wave"); + let target = frontend_target(); + for (marked, closer) in [ + ("fun f@(", ")"), + ("fun f() @{\r\n", "}"), + ("fun f() { call(@[1,", "]"), + ] { + let marked = format!("// 한글\r\n{marked}"); + let opener = marked.find('@').unwrap(); + let text = marked.replace('@', ""); + std::fs::write(&library, &text).unwrap(); + std::fs::write(&source, "import(\"./unclosed\"); fun main() {}").unwrap(); + for entry in [&source, &library] { + let output = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + entry.as_os_str(), + OsStr::new("--target"), + OsStr::new(&target), + ]); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + let json = utils::json::parse(&String::from_utf8(output.stderr).unwrap()).unwrap(); + let error = json.get("error").unwrap(); + assert_eq!(error.get_str("code"), Some("E2001")); + assert_eq!( + error.get("span").unwrap().get_num("start"), + Some(text.len() as f64) + ); + let related = error.get_arr("related").unwrap(); + assert_eq!(related.len(), 1); + assert!(related[0] + .get_str("message") + .unwrap() + .contains(&format!("expected '{closer}'"))); + assert_eq!( + related[0].get("span").unwrap().get_num("start"), + Some(opener as f64) + ); + assert!(related[0] + .get("span") + .unwrap() + .get_str("file") + .unwrap() + .ends_with("unclosed.wave")); + let human = check(entry, &target); + assert!(!human.status.success()); + let human = String::from_utf8_lossy(&human.stderr); + assert!(human.contains(&format!("expected '{closer}'")), "{human}"); + assert!(human.contains("opened here"), "{human}"); + assert!(human.matches('^').count() >= 2, "{human}"); + } + } +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn generic_methods_and_nested_method_receivers_execute_once() { + let dir = directory(); + let source = dir.join("methods.wave"); + std::fs::write(&source, r#" +struct Box { + value: T; + fun pick(self: ptr>, other: U) -> U { return other; } + fun get(self: ptr>) -> T { return self.value; } +} +struct Counter { value: i32; } +proto Counter { + fun pick(self: ptr, other: T) -> T { return other; } + fun recur(self: ptr, value: T, n: i32) -> T { + if (n == 0) { return value; } + return self.recur(value, n - 1); + } +} +fun counted(calls: ptr) -> i32 { deref calls += 1; return 4; } +fun add(value: i32, other: i32) -> i32 { return value + other; } +fun receiver(calls: ptr, value: ptr) -> ptr { deref calls += 1; return value; } +fun main() -> i32 { + var calls: i32 = 0; + if (counted(&calls).add(3).add(2) != 9 || calls != 1) { return 1; } + var box: Box = Box { value: 11 }; + var counter: Counter = Counter { value: 7 }; + var picked: Box = receiver(&calls, &counter).pick>(box); + if (calls != 2 || (&picked).get() != 11) { return 2; } + if ((&box).pick>(&counter).recur(17, 5) != 17) { return 3; } + var wide: i64 = 99; + if ((&box).pick(wide) != 99 || (&counter).pick(5) != 5) { return 4; } + return 0; +} +"#).unwrap(); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--run"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ])); +} + +#[test] +fn malformed_declarations_and_asm_are_clean_json_in_imported_sources() { + let dir = directory(); + let library = dir.join("broken.wave"); + let source = dir.join("main.wave"); + std::fs::write(&source, "import(\"./broken\"); fun main() {}\n").unwrap(); + for marked in [ + "pub type Item @i32;", + "enum E -> @{}", + "pub variant V { X(i32 @i64) }", + "fun f() { asm { in(@123) value } }", + "fun f() { var x: i32 = asm { out(rax) @123 }; }", + ] { + let start = marked.find('@').unwrap(); + std::fs::write(&library, marked.replace('@', "")).unwrap(); + let out = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(&frontend_target()), + ]); + assert!(!out.status.success()); + assert!(out.stdout.is_empty(), "{:?}", out.stdout); + let stderr = String::from_utf8(out.stderr).unwrap(); + assert_eq!(stderr.lines().count(), 1, "{stderr}"); + assert!( + stderr.contains("E2001") + && stderr.contains("broken.wave") + && stderr.contains(&format!("\"start\":{start},")), + "{stderr}" + ); + } +} + +#[cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))] +#[test] +fn backend_errors_keep_imported_spans_without_panic_message_guessing() { + let dir = directory(); + let library = dir.join("assembly.wave"); + let source = dir.join("main.wave"); + let text = "pub fun helper() { asm { in(\"invalid_register\") 1 } }"; + std::fs::write(&library, text).unwrap(); + std::fs::write( + &source, + "import(\"./assembly\")::{helper}; fun main() { helper(); }", + ) + .unwrap(); + for format in ["human", "json"] { + let output = wave(&[ + OsStr::new(&format!("--error-format={format}")), + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target=x86_64-unknown-linux-gnu"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + let diagnostic = String::from_utf8(output.stderr).unwrap(); + assert!( + diagnostic.contains("E3401") + && diagnostic.contains("assembly.wave") + && diagnostic.contains("lowering-validation"), + "{diagnostic}" + ); + assert!( + !diagnostic.contains("panic") && !diagnostic.contains("inferred"), + "{diagnostic}" + ); + if format == "json" { + assert_eq!(diagnostic.lines().count(), 1); + } + assert!(!dir.join("main.o").exists()); + } +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn missing_linker_keeps_phase_and_previous_artifact() { + let dir = directory(); + let source = dir.join("entry.wave"); + std::fs::write(&source, "fun main() -> i32 { return 0; }").unwrap(); + let executable = dir.join("preserved.exe"); + std::fs::write(&executable, b"previous artifact").unwrap(); + let linker = format!("-Clinker={}", dir.join("missing-linker").display()); + let output = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("build"), + source.as_os_str(), + OsStr::new(&linker), + OsStr::new("-o"), + executable.as_os_str(), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]); + assert_eq!(output.status.code(), Some(3)); + assert!(output.stdout.is_empty()); + let diagnostic = String::from_utf8(output.stderr).unwrap(); + assert!( + diagnostic.contains("\"kind\":\"external-tool-missing\"") + && diagnostic.contains("\"phase\":\"linking\""), + "{diagnostic}" + ); + assert_eq!(std::fs::read(&executable).unwrap(), b"previous artifact"); + assert!(!std::fs::read_dir(&dir).unwrap().any(|entry| entry + .unwrap() + .file_name() + .to_string_lossy() + .starts_with(".wave-output-"))); +} diff --git a/tools/test_case_manifest.py b/tools/test_case_manifest.py index 10add074..ee12a5e3 100644 --- a/tools/test_case_manifest.py +++ b/tools/test_case_manifest.py @@ -11,6 +11,7 @@ # AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. import unittest +from pathlib import Path from tools.case_manifest import ( MIN_CASES_PER_SUITE, @@ -54,7 +55,7 @@ def test_disabled_roadmap_targets_do_not_enter_ci(self): "openbsd-amd64", "netbsd-arm64", "fuchsia-riscv64", - "windows-riscv64", + "dragonfly-amd64", "wasi-wasm64", ): target = self.manifest.target(target_id) @@ -62,9 +63,21 @@ def test_disabled_roadmap_targets_do_not_enter_ci(self): self.assertFalse(target.ci) self.assertNotIn(target_id, ci_ids) - def test_named_roadmap_operating_systems_reserve_all_architectures(self): - for os_name in ("openbsd", "netbsd", "fuchsia", "android", "ios"): - for arch in ("amd64", "arm64", "riscv64"): + def test_planned_operating_systems_reserve_only_existing_or_announced_ports(self): + planned = { + "openbsd": {"amd64", "arm64", "riscv64"}, + "netbsd": {"amd64", "arm64", "riscv64"}, + "fuchsia": {"amd64", "arm64", "riscv64"}, + "android": {"amd64", "arm64", "riscv64"}, + "ios": {"amd64", "arm64"}, # amd64 is simulator-only. + "dragonfly": {"amd64"}, + } + for os_name, arches in planned.items(): + self.assertEqual( + {target.arch for target in self.manifest.targets if target.os == os_name}, + arches, + ) + for arch in arches: with self.subTest(os=os_name, arch=arch): target = self.manifest.target(f"{os_name}-{arch}") self.assertEqual(target.suite, f"{os_name}/{arch}") @@ -72,6 +85,16 @@ def test_named_roadmap_operating_systems_reserve_all_architectures(self): self.assertFalse(target.enabled) self.assertFalse(target.ci) + def test_unannounced_riscv_os_ports_are_not_reserved_or_discovered(self): + cases_root = Path(__file__).resolve().parents[1] / "tests" / "cases" + for os_name in ("macos", "windows", "ios"): + with self.subTest(os=os_name): + with self.assertRaises(CaseManifestError): + self.manifest.target(f"{os_name}-riscv64") + self.assertFalse((cases_root / os_name / "riscv64").exists()) + self.assertTrue(self.manifest.target("linux-riscv64").enabled) + self.assertTrue(self.manifest.target("freebsd-riscv64").enabled) + def test_freebsd_lp64_targets_compile_platform_provider_cases(self): matrix = self.manifest.github_matrices()["cross"]["include"] for arch, isa in (("amd64", "x86_64"), ("arm64", "aarch64"), ("riscv64", "riscv64")):