From 377bf67ca292490500939038fc512e18e6e7d650 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Wed, 12 Aug 2026 13:20:35 +0900 Subject: [PATCH 1/3] Complete RV64 psABI interoperability contracts Implement RISC-V C ABI lowering for required integer sign and zero extensions, empty aggregates, one- and two-register aggregates, indirect large parameters, structure returns, and call-site attributes. Add fixed-parameter C variadic declarations, frontend validation for scalar variadic arguments, and default argument promotions for narrow integers and f32 values. Fix aggregate staging and reconstruction so partial register chunks cannot read beyond the source object, and correct AArch64 AAPCS64 handling for homogeneous floating aggregates, integer-class aggregates, and indirect parameters larger than sixteen bytes. Add executable C-to-Wave and Wave-to-C ABI fixtures for RISC-V LP64, LP64F, and LP64D, x86_64 SysV, and AArch64 AAPCS64. Cover scalar extensions, floating-point registers, pointers, nested and padded aggregates, stack arguments, variadics, and large by-reference values. Lock down RISC-V PIC and static relocations, A-extension atomic lowering and fallback libcalls, compressed-instruction ELF flags and instruction encodings, IR and bitcode target metadata, and inline assembly register and control-flow contracts. Move the legacy Wave corpus from test/ into tests/cases/, update the runner and ownership documentation, exclude test sources from GitHub language detection, and expand architecture-named CI jobs with native and cross ABI contract execution. --- .gitattributes | 3 +- .github/workflows/rust.yml | 169 ++++++- CONTRIBUTING.md | 2 +- MAINTAINERS | 2 +- front/parser/src/ast.rs | 1 + front/parser/src/parser/decl.rs | 29 ++ front/parser/src/verification.rs | 40 +- llvm/src/codegen/abi_c.rs | 317 ++++++++++++- llvm/src/codegen/ir.rs | 52 ++- llvm/src/expression/rvalue/calls.rs | 140 ++++-- {test => tests/cases}/test.wave | 0 {test => tests/cases}/test10.wave | 0 {test => tests/cases}/test100.wave | 0 {test => tests/cases}/test101.wave | 0 {test => tests/cases}/test102.wave | 0 {test => tests/cases}/test103.wave | 0 {test => tests/cases}/test104.wave | 0 {test => tests/cases}/test105.wave | 0 {test => tests/cases}/test106.wave | 0 {test => tests/cases}/test107.wave | 0 {test => tests/cases}/test108.wave | 0 {test => tests/cases}/test11.wave | 0 {test => tests/cases}/test12.wave | 0 {test => tests/cases}/test13.wave | 0 {test => tests/cases}/test14.wave | 0 {test => tests/cases}/test15.wave | 0 {test => tests/cases}/test16.wave | 0 {test => tests/cases}/test17.wave | 0 {test => tests/cases}/test18.wave | 0 {test => tests/cases}/test19.wave | 0 {test => tests/cases}/test2.wave | 0 {test => tests/cases}/test20.wave | 0 {test => tests/cases}/test21.wave | 0 {test => tests/cases}/test22.wave | 0 {test => tests/cases}/test23.wave | 0 {test => tests/cases}/test24.wave | 0 {test => tests/cases}/test25.wave | 0 {test => tests/cases}/test26.wave | 0 {test => tests/cases}/test27.wave | 0 {test => tests/cases}/test28/main.wave | 0 {test => tests/cases}/test28/math.wave | 0 {test => tests/cases}/test29.wave | 0 {test => tests/cases}/test3.wave | 0 {test => tests/cases}/test30.wave | 0 {test => tests/cases}/test31.wave | 0 {test => tests/cases}/test32.wave | 0 {test => tests/cases}/test33.wave | 0 {test => tests/cases}/test34.wave | 0 {test => tests/cases}/test35.wave | 0 {test => tests/cases}/test36.wave | 0 {test => tests/cases}/test37.wave | 0 {test => tests/cases}/test38.wave | 0 {test => tests/cases}/test39.wave | 0 {test => tests/cases}/test4.wave | 0 {test => tests/cases}/test40.wave | 0 {test => tests/cases}/test41.wave | 0 {test => tests/cases}/test42.wave | 0 {test => tests/cases}/test43.wave | 0 {test => tests/cases}/test44.wave | 0 {test => tests/cases}/test45.wave | 0 {test => tests/cases}/test46.wave | 0 {test => tests/cases}/test47.wave | 0 {test => tests/cases}/test48.wave | 0 {test => tests/cases}/test49.wave | 0 {test => tests/cases}/test5.wave | 0 {test => tests/cases}/test50.wave | 0 {test => tests/cases}/test51.wave | 0 {test => tests/cases}/test52.wave | 0 {test => tests/cases}/test53.wave | 0 {test => tests/cases}/test54.wave | 0 {test => tests/cases}/test55.wave | 0 {test => tests/cases}/test56.wave | 0 {test => tests/cases}/test57.wave | 0 {test => tests/cases}/test58.wave | 0 {test => tests/cases}/test59.wave | 0 {test => tests/cases}/test6.wave | 0 {test => tests/cases}/test60.wave | 0 {test => tests/cases}/test61.wave | 0 {test => tests/cases}/test62.wave | 0 {test => tests/cases}/test63.wave | 0 {test => tests/cases}/test64.wave | 0 {test => tests/cases}/test65.wave | 0 {test => tests/cases}/test66.wave | 0 {test => tests/cases}/test67.wave | 0 {test => tests/cases}/test68.wave | 0 {test => tests/cases}/test69.wave | 0 {test => tests/cases}/test7.wave | 0 {test => tests/cases}/test70.wave | 0 {test => tests/cases}/test71.wave | 0 {test => tests/cases}/test72.wave | 0 {test => tests/cases}/test73.wave | 0 {test => tests/cases}/test74.wave | 0 {test => tests/cases}/test75.wave | 0 {test => tests/cases}/test76.wave | 0 {test => tests/cases}/test77.wave | 0 {test => tests/cases}/test78.wave | 0 {test => tests/cases}/test79.wave | 0 {test => tests/cases}/test8.wave | 0 {test => tests/cases}/test80.wave | 0 {test => tests/cases}/test81.wave | 0 {test => tests/cases}/test82.wave | 0 {test => tests/cases}/test83.wave | 0 {test => tests/cases}/test84.wave | 0 {test => tests/cases}/test85.wave | 0 {test => tests/cases}/test86.wave | 0 {test => tests/cases}/test87.wave | 0 {test => tests/cases}/test88.wave | 0 {test => tests/cases}/test89.wave | 0 {test => tests/cases}/test9.wave | 0 {test => tests/cases}/test90.wave | 0 {test => tests/cases}/test91.wave | 0 {test => tests/cases}/test92.wave | 0 {test => tests/cases}/test93.wave | 0 {test => tests/cases}/test94.wave | 0 {test => tests/cases}/test95.wave | 0 {test => tests/cases}/test96.wave | 0 {test => tests/cases}/test97.wave | 0 {test => tests/cases}/test98.wave | 0 {test => tests/cases}/test99.wave | 0 tests/codegen_regressions.rs | 478 +++++++++++++++++++- tests/fixtures/aarch64_aapcs64/interop.c | 87 ++++ tests/fixtures/aarch64_aapcs64/interop.wave | 53 +++ tests/fixtures/riscv64_psabi/abi_modes.c | 42 ++ tests/fixtures/riscv64_psabi/abi_modes.wave | 19 + tests/fixtures/riscv64_psabi/interop.c | 236 ++++++++++ tests/fixtures/riscv64_psabi/interop.wave | 141 ++++++ tests/fixtures/x86_64_sysv/interop.c | 87 ++++ tests/fixtures/x86_64_sysv/interop.wave | 53 +++ tools/run_tests.py | 2 +- 129 files changed, 1875 insertions(+), 78 deletions(-) rename {test => tests/cases}/test.wave (100%) rename {test => tests/cases}/test10.wave (100%) rename {test => tests/cases}/test100.wave (100%) rename {test => tests/cases}/test101.wave (100%) rename {test => tests/cases}/test102.wave (100%) rename {test => tests/cases}/test103.wave (100%) rename {test => tests/cases}/test104.wave (100%) rename {test => tests/cases}/test105.wave (100%) rename {test => tests/cases}/test106.wave (100%) rename {test => tests/cases}/test107.wave (100%) rename {test => tests/cases}/test108.wave (100%) rename {test => tests/cases}/test11.wave (100%) rename {test => tests/cases}/test12.wave (100%) rename {test => tests/cases}/test13.wave (100%) rename {test => tests/cases}/test14.wave (100%) rename {test => tests/cases}/test15.wave (100%) rename {test => tests/cases}/test16.wave (100%) rename {test => tests/cases}/test17.wave (100%) rename {test => tests/cases}/test18.wave (100%) rename {test => tests/cases}/test19.wave (100%) rename {test => tests/cases}/test2.wave (100%) rename {test => tests/cases}/test20.wave (100%) rename {test => tests/cases}/test21.wave (100%) rename {test => tests/cases}/test22.wave (100%) rename {test => tests/cases}/test23.wave (100%) rename {test => tests/cases}/test24.wave (100%) rename {test => tests/cases}/test25.wave (100%) rename {test => tests/cases}/test26.wave (100%) rename {test => tests/cases}/test27.wave (100%) rename {test => tests/cases}/test28/main.wave (100%) rename {test => tests/cases}/test28/math.wave (100%) rename {test => tests/cases}/test29.wave (100%) rename {test => tests/cases}/test3.wave (100%) rename {test => tests/cases}/test30.wave (100%) rename {test => tests/cases}/test31.wave (100%) rename {test => tests/cases}/test32.wave (100%) rename {test => tests/cases}/test33.wave (100%) rename {test => tests/cases}/test34.wave (100%) rename {test => tests/cases}/test35.wave (100%) rename {test => tests/cases}/test36.wave (100%) rename {test => tests/cases}/test37.wave (100%) rename {test => tests/cases}/test38.wave (100%) rename {test => tests/cases}/test39.wave (100%) rename {test => tests/cases}/test4.wave (100%) rename {test => tests/cases}/test40.wave (100%) rename {test => tests/cases}/test41.wave (100%) rename {test => tests/cases}/test42.wave (100%) rename {test => tests/cases}/test43.wave (100%) rename {test => tests/cases}/test44.wave (100%) rename {test => tests/cases}/test45.wave (100%) rename {test => tests/cases}/test46.wave (100%) rename {test => tests/cases}/test47.wave (100%) rename {test => tests/cases}/test48.wave (100%) rename {test => tests/cases}/test49.wave (100%) rename {test => tests/cases}/test5.wave (100%) rename {test => tests/cases}/test50.wave (100%) rename {test => tests/cases}/test51.wave (100%) rename {test => tests/cases}/test52.wave (100%) rename {test => tests/cases}/test53.wave (100%) rename {test => tests/cases}/test54.wave (100%) rename {test => tests/cases}/test55.wave (100%) rename {test => tests/cases}/test56.wave (100%) rename {test => tests/cases}/test57.wave (100%) rename {test => tests/cases}/test58.wave (100%) rename {test => tests/cases}/test59.wave (100%) rename {test => tests/cases}/test6.wave (100%) rename {test => tests/cases}/test60.wave (100%) rename {test => tests/cases}/test61.wave (100%) rename {test => tests/cases}/test62.wave (100%) rename {test => tests/cases}/test63.wave (100%) rename {test => tests/cases}/test64.wave (100%) rename {test => tests/cases}/test65.wave (100%) rename {test => tests/cases}/test66.wave (100%) rename {test => tests/cases}/test67.wave (100%) rename {test => tests/cases}/test68.wave (100%) rename {test => tests/cases}/test69.wave (100%) rename {test => tests/cases}/test7.wave (100%) rename {test => tests/cases}/test70.wave (100%) rename {test => tests/cases}/test71.wave (100%) rename {test => tests/cases}/test72.wave (100%) rename {test => tests/cases}/test73.wave (100%) rename {test => tests/cases}/test74.wave (100%) rename {test => tests/cases}/test75.wave (100%) rename {test => tests/cases}/test76.wave (100%) rename {test => tests/cases}/test77.wave (100%) rename {test => tests/cases}/test78.wave (100%) rename {test => tests/cases}/test79.wave (100%) rename {test => tests/cases}/test8.wave (100%) rename {test => tests/cases}/test80.wave (100%) rename {test => tests/cases}/test81.wave (100%) rename {test => tests/cases}/test82.wave (100%) rename {test => tests/cases}/test83.wave (100%) rename {test => tests/cases}/test84.wave (100%) rename {test => tests/cases}/test85.wave (100%) rename {test => tests/cases}/test86.wave (100%) rename {test => tests/cases}/test87.wave (100%) rename {test => tests/cases}/test88.wave (100%) rename {test => tests/cases}/test89.wave (100%) rename {test => tests/cases}/test9.wave (100%) rename {test => tests/cases}/test90.wave (100%) rename {test => tests/cases}/test91.wave (100%) rename {test => tests/cases}/test92.wave (100%) rename {test => tests/cases}/test93.wave (100%) rename {test => tests/cases}/test94.wave (100%) rename {test => tests/cases}/test95.wave (100%) rename {test => tests/cases}/test96.wave (100%) rename {test => tests/cases}/test97.wave (100%) rename {test => tests/cases}/test98.wave (100%) rename {test => tests/cases}/test99.wave (100%) create mode 100644 tests/fixtures/aarch64_aapcs64/interop.c create mode 100644 tests/fixtures/aarch64_aapcs64/interop.wave create mode 100644 tests/fixtures/riscv64_psabi/abi_modes.c create mode 100644 tests/fixtures/riscv64_psabi/abi_modes.wave create mode 100644 tests/fixtures/riscv64_psabi/interop.c create mode 100644 tests/fixtures/riscv64_psabi/interop.wave create mode 100644 tests/fixtures/x86_64_sysv/interop.c create mode 100644 tests/fixtures/x86_64_sysv/interop.wave diff --git a/.gitattributes b/.gitattributes index 25b2af52..f7f2f848 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -*.wave linguist-language=Wave \ No newline at end of file +*.wave linguist-language=Wave +tests/** linguist-detectable=false diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f245205a..b22e8de8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -67,12 +67,80 @@ jobs: - name: Run Rust tests run: cargo test --locked --all-targets --verbose + - name: Run x86_64 SysV ABI contract tests + env: + WAVE_RUN_X86_64_INTEROP_TESTS: "1" + run: >- + cargo test --locked --test codegen_regressions + x86_64_c_abi_interoperates_with_c --verbose + + - name: Run Wave end-to-end tests + run: python3 tools/run_tests.py + + build-linux-arm64: + name: Build Linux arm64 + runs-on: ubuntu-24.04-arm + continue-on-error: true + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.89.0 + with: + components: rustfmt, clippy + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install LLVM 21 + run: | + sudo apt-get update + sudo apt-get install -y wget software-properties-common + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 + echo "LLVM_SYS_211_PREFIX=/usr/lib/llvm-21" >> "$GITHUB_ENV" + echo "LLVM_CONFIG_PATH=/usr/lib/llvm-21/bin/llvm-config" >> "$GITHUB_ENV" + echo "/usr/lib/llvm-21/bin" >> "$GITHUB_PATH" + + - name: Install Wave stdlib + run: | + mkdir -p "$HOME/.wave/lib/wave" + rm -rf "$HOME/.wave/lib/wave/std" + cp -R std "$HOME/.wave/lib/wave/std" + + - name: Check Rust formatting + run: cargo fmt --all --check + + - name: Run Clippy + run: cargo clippy --locked --all-targets -- -D warnings + + - name: Validate Python tooling + run: python3 -m py_compile x.py tools/run_tests.py + + - name: Build release compiler + run: cargo build --locked --release --verbose + + - name: Run Rust tests + run: cargo test --locked --all-targets --verbose + + - name: Run AArch64 AAPCS64 contract tests + env: + WAVE_RUN_AARCH64_INTEROP_TESTS: "1" + run: >- + cargo test --locked --test codegen_regressions + aarch64_c_abi_interoperates_with_c --verbose + - name: Run Wave end-to-end tests run: python3 tools/run_tests.py build-linux-riscv64: name: Build Linux riscv64 runs-on: ubuntu-24.04 + continue-on-error: true timeout-minutes: 45 steps: @@ -80,6 +148,12 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@1.89.0 + with: + components: rustfmt, clippy + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install LLVM 21 and RISC-V runtime tools run: | @@ -114,13 +188,36 @@ jobs: test -f /usr/riscv64-linux-gnu/lib/libc.so test -f /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 + - name: Install Wave stdlib + run: | + mkdir -p "$HOME/.wave/lib/wave" + rm -rf "$HOME/.wave/lib/wave/std" + cp -R std "$HOME/.wave/lib/wave/std" + + - name: Check Rust formatting + run: cargo fmt --all --check + + - name: Run Clippy + run: cargo clippy --locked --all-targets -- -D warnings + + - name: Validate Python tooling + run: python3 -m py_compile x.py tools/run_tests.py + + - name: Build release compiler + run: cargo build --locked --release --verbose + + - name: Run Rust tests + run: cargo test --locked --all-targets --verbose + + - name: Run Wave end-to-end tests + run: python3 tools/run_tests.py + - name: Run RISC-V contract tests + env: + WAVE_RUN_RISCV64_INTEROP_TESTS: "1" run: >- cargo test --locked --test codegen_regressions riscv64_ --verbose - - name: Build release compiler with RISC-V backend - run: cargo build --locked --release --verbose - - name: Link and run Linux riscv64 binary run: | set -euo pipefail @@ -130,7 +227,7 @@ jobs: binary="$output_dir/test2" mkdir -p "$output_dir" - target/release/wavec build test/test2.wave \ + target/release/wavec build tests/cases/test2.wave \ --target riscv64-unknown-linux-gnu \ --sysroot "$riscv_sysroot" \ --out-dir "$output_dir" @@ -209,8 +306,70 @@ jobs: - name: Run Wave end-to-end tests run: python3 tools/run_tests.py + build-macos-amd64: + name: Build macOS amd64 + runs-on: macos-15-intel + continue-on-error: true + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.89.0 + with: + components: rustfmt, clippy + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install LLVM 21 and LLD + run: | + brew install llvm@21 lld + + - name: Set LLVM env + run: | + LLVM_PREFIX="$(brew --prefix llvm@21)" + LLD_PREFIX="$(brew --prefix lld)" + echo "LLVM_SYS_211_PREFIX=$LLVM_PREFIX" >> "$GITHUB_ENV" + echo "LLVM_CONFIG_PATH=$LLVM_PREFIX/bin/llvm-config" >> "$GITHUB_ENV" + echo "$LLVM_PREFIX/bin" >> "$GITHUB_PATH" + echo "$LLD_PREFIX/bin" >> "$GITHUB_PATH" + echo "WAVE_LD64_LLD=$LLD_PREFIX/bin/ld64.lld" >> "$GITHUB_ENV" + + - name: Verify LLVM tools + run: | + llvm-config --version + command -v ld64.lld + ld64.lld --version || true + + - name: Install Wave stdlib + run: | + mkdir -p "$HOME/.wave/lib/wave" + rm -rf "$HOME/.wave/lib/wave/std" + cp -R std "$HOME/.wave/lib/wave/std" + + - name: Check Rust formatting + run: cargo fmt --all --check + + - name: Run Clippy + run: cargo clippy --locked --all-targets -- -D warnings + + - name: Validate Python tooling + run: python3 -m py_compile x.py tools/run_tests.py + + - name: Build release compiler + run: cargo build --locked --release --verbose + + - name: Run Rust tests + run: cargo test --locked --all-targets --verbose + + - name: Run Wave end-to-end tests + run: python3 tools/run_tests.py + build-windows-amd64: - name: Build Windows amd64 + name: Build Windows GNU amd64 runs-on: windows-latest timeout-minutes: 60 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd13164f..f163a136 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,7 +149,7 @@ All additional functionality should be provided through external libraries (e.g. Wave uses: - Rust unit tests (`cargo test`) -- Manually executed `.wave` examples in `test/` (not automated) +- Automated `.wave` language cases in `tests/cases/` Contributors should: diff --git a/MAINTAINERS b/MAINTAINERS index 03bc8e09..eb459054 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20,4 +20,4 @@ F: src/ [Test Suite] M: luna@lunastev.org -F: test/ +F: tests/cases/ diff --git a/front/parser/src/ast.rs b/front/parser/src/ast.rs index ad23412e..3e49a3a0 100644 --- a/front/parser/src/ast.rs +++ b/front/parser/src/ast.rs @@ -117,6 +117,7 @@ pub struct ExternFunctionNode { pub abi: String, pub symbol: Option, pub params: Vec<(String, WaveType)>, + pub variadic: bool, pub return_type: WaveType, } diff --git a/front/parser/src/parser/decl.rs b/front/parser/src/parser/decl.rs index e2c5a598..f88caf24 100644 --- a/front/parser/src/parser/decl.rs +++ b/front/parser/src/parser/decl.rs @@ -500,6 +500,7 @@ fn parse_extern_fun_decl( // params let mut params: Vec<(String, WaveType)> = Vec::new(); let mut idx: usize = 0; + let mut variadic = false; loop { skip_ws(tokens); @@ -509,6 +510,33 @@ fn parse_extern_fun_decl( break; } + let mut lookahead = tokens.clone(); + let is_variadic = (0..3).all(|_| { + matches!( + lookahead.next().map(|token| &token.token_type), + Some(TokenType::Dot) + ) + }); + if is_variadic { + if params.is_empty() { + println!("Error: C variadic extern function requires a fixed parameter"); + return None; + } + for _ in 0..3 { + tokens.next(); + } + skip_ws(tokens); + if !expect( + tokens, + TokenType::Rparen, + "Expected ')' immediately after '...' in extern function", + ) { + return None; + } + variadic = true; + break; + } + // named param? (Identifier ... :) let is_named = match tokens.peek() { Some(Token { @@ -658,6 +686,7 @@ fn parse_extern_fun_decl( abi, symbol, params, + variadic, return_type, }) } diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index 14bd8ae1..aa9c3b3f 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -63,6 +63,7 @@ struct FunctionType { required_params: usize, return_type: WaveType, generic_params: Vec, + variadic: bool, } #[derive(Clone, Debug)] @@ -160,6 +161,7 @@ impl ProgramTypes { required_params: function.params.len(), return_type: function.return_type.clone(), generic_params: Vec::new(), + variadic: function.variadic, }, ) .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; @@ -528,6 +530,7 @@ fn function_type(function: &FunctionNode) -> FunctionType { .count(), return_type: function.return_type.clone().unwrap_or(WaveType::Void), generic_params: function.generic_params.clone(), + variadic: false, } } @@ -586,6 +589,7 @@ fn substitute_function_type( required_params: signature.required_params, return_type: substitute_wave_type(&signature.return_type, substitutions), generic_params: signature.generic_params.clone(), + variadic: signature.variadic, } } @@ -1515,6 +1519,7 @@ impl<'a> Validator<'a> { args, &signature.params, signature.required_params, + signature.variadic, )?; Ok(ExpressionType::Known(signature.return_type)) } @@ -1551,6 +1556,7 @@ impl<'a> Validator<'a> { args, params, signature.required_params.saturating_sub(1), + false, )?; return Ok(ExpressionType::Known(signature.return_type)); } @@ -1569,6 +1575,7 @@ impl<'a> Validator<'a> { args, &signature.params[1..], signature.required_params.saturating_sub(1), + false, )?; return Ok(ExpressionType::Known(signature.return_type)); } @@ -1655,8 +1662,9 @@ impl<'a> Validator<'a> { args: &[Expression], params: &[WaveType], required_params: usize, + variadic: bool, ) -> Result<(), String> { - if args.len() < required_params || args.len() > params.len() { + if args.len() < required_params || (!variadic && args.len() > params.len()) { let expectation = if required_params == params.len() { params.len().to_string() } else { @@ -1679,6 +1687,36 @@ impl<'a> Validator<'a> { &format!("argument {} of {} `{}`", index + 1, kind, name), )?; } + + if variadic { + for (index, argument) in args.iter().enumerate().skip(params.len()) { + let actual = self.validate_expr(argument)?; + let actual = canonical_expression_type(self.program, &actual).ok_or_else(|| { + format!( + "variadic argument {} of function `{}` has no scalar type", + index + 1, + name + ) + })?; + if !matches!( + actual, + WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Float(_) + | WaveType::Bool + | WaveType::Char + | WaveType::Byte + | WaveType::String + | WaveType::Pointer(_) + ) { + return Err(format!( + "variadic argument {} of function `{}` must be a scalar value", + index + 1, + name + )); + } + } + } Ok(()) } diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index ea790b13..87d025ec 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -15,7 +15,7 @@ use inkwell::attributes::{Attribute, AttributeLoc}; use inkwell::context::Context; use inkwell::targets::TargetData; use inkwell::types::{AnyType, AnyTypeEnum, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; -use inkwell::values::FunctionValue; +use inkwell::values::{BasicValueEnum, CallSiteValue, FunctionValue}; use inkwell::AddressSpace; use std::collections::HashMap; @@ -26,8 +26,10 @@ use super::types::{wave_type_to_llvm_type, TypeFlavor}; #[derive(Clone)] pub enum ParamLowering<'ctx> { + Ignore, Direct(BasicTypeEnum<'ctx>), // pass as this llvm type Split(Vec>), // pass as multiple params + Indirect { ty: AnyTypeEnum<'ctx> }, // pass a pointer without byval ByVal { ty: AnyTypeEnum<'ctx>, align: u32 }, // pass ptr + byval + align } @@ -38,13 +40,44 @@ pub enum RetLowering<'ctx> { SRet { ty: AnyTypeEnum<'ctx>, align: u32 }, // hidden first param } +#[derive(Clone, Copy)] +pub enum IntegerExtension { + Sign, + Zero, +} + #[derive(Clone)] pub struct ExternCInfo<'ctx> { pub llvm_name: String, // actual LLVM symbol name pub wave_ret: WaveType, // Wave-level return type (needed when sret => llvm void) pub ret: RetLowering<'ctx>, + pub ret_extension: Option, pub params: Vec>, // per-wave param + pub param_extensions: Vec>, // per-wave param pub llvm_param_types: Vec>, // final lowered param list (including sret ptr, split, byval ptr) + pub variadic: bool, + pub variadic_integer_extension: Option, +} + +fn riscv64_integer_extension(ty: &WaveType) -> Option { + match ty { + WaveType::Int(bits) if *bits <= 32 => Some(IntegerExtension::Sign), + WaveType::Uint(bits) if *bits < 32 => Some(IntegerExtension::Zero), + // RV64 widens unsigned 32-bit values to 32 bits and then sign-extends + // them to XLEN, just like signed 32-bit values. + WaveType::Uint(32) => Some(IntegerExtension::Sign), + WaveType::Bool | WaveType::Byte | WaveType::Char => Some(IntegerExtension::Zero), + _ => None, + } +} + +fn integer_extension_for_target(target: CodegenTarget, ty: &WaveType) -> Option { + match target { + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { + riscv64_integer_extension(ty) + } + _ => None, + } } pub struct LoweredExtern<'ctx> { @@ -112,6 +145,14 @@ fn classify_param_x86_64_sysv<'ctx>( }; } + if matches!( + t, + BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_) + ) && size == 0 + { + return ParamLowering::Ignore; + } + // small aggregates: try integer-only or homogeneous float if matches!( t, @@ -237,6 +278,10 @@ fn classify_ret_x86_64_sysv<'ctx>( }; } + if is_agg && size == 0 { + return RetLowering::Void; + } + if is_agg && size <= 16 { // integer-only ret => i{size*8} let mut leaves = vec![]; @@ -360,6 +405,7 @@ fn classify_param_x86_64_windows<'ctx>( match t { BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_) => match size { + 0 => ParamLowering::Ignore, 1 | 2 | 4 | 8 => ParamLowering::Direct( context .custom_width_int_type((size * 8) as u32) @@ -386,6 +432,7 @@ fn classify_ret_x86_64_windows<'ctx>( match t { BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_) => match size { + 0 => RetLowering::Void, 1 | 2 | 4 | 8 => RetLowering::Direct( context .custom_width_int_type((size * 8) as u32) @@ -400,7 +447,23 @@ fn classify_ret_x86_64_windows<'ctx>( } } -fn classify_param_arm64_darwin<'ctx>( +fn is_homogeneous_float_aggregate<'ctx>(td: &TargetData, t: BasicTypeEnum<'ctx>) -> bool { + let mut leaves = Vec::new(); + flatten_leaf_types(t, &mut leaves); + if leaves.is_empty() || leaves.len() > 4 { + return false; + } + + let Some(first_size) = is_float_ty(td, leaves[0]) else { + return false; + }; + leaves + .iter() + .all(|leaf| is_float_ty(td, *leaf) == Some(first_size)) +} + +fn classify_param_arm64<'ctx>( + context: &'ctx Context, td: &TargetData, t: BasicTypeEnum<'ctx>, ) -> ParamLowering<'ctx> { @@ -411,17 +474,38 @@ fn classify_param_arm64_darwin<'ctx>( ); if is_agg && size > 16 { - let align = td.get_abi_alignment(&t) as u32; - return ParamLowering::ByVal { + return ParamLowering::Indirect { ty: t.as_any_type_enum(), - align, }; } + if is_agg && size == 0 { + return ParamLowering::Ignore; + } + + if is_agg && !is_homogeneous_float_aggregate(td, t) { + if size <= 8 { + return ParamLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ); + } + + return ParamLowering::Split(vec![ + context.i64_type().as_basic_type_enum(), + context.i64_type().as_basic_type_enum(), + ]); + } + ParamLowering::Direct(t) } -fn classify_param_riscv64<'ctx>(td: &TargetData, t: BasicTypeEnum<'ctx>) -> ParamLowering<'ctx> { +fn classify_param_riscv64<'ctx>( + context: &'ctx Context, + td: &TargetData, + t: BasicTypeEnum<'ctx>, +) -> ParamLowering<'ctx> { let size = td.get_store_size(&t) as u64; let is_agg = matches!( t, @@ -436,10 +520,41 @@ fn classify_param_riscv64<'ctx>(td: &TargetData, t: BasicTypeEnum<'ctx>) -> Para }; } + if is_agg && size == 0 { + return ParamLowering::Ignore; + } + + if is_agg { + let mut leaves = Vec::new(); + flatten_leaf_types(t, &mut leaves); + let integer_only = leaves.iter().all(|leaf| { + matches!( + leaf, + BasicTypeEnum::IntType(_) | BasicTypeEnum::PointerType(_) + ) + }); + + if integer_only { + if size <= 8 { + return ParamLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ); + } + + return ParamLowering::Split(vec![ + context.i64_type().as_basic_type_enum(), + context.i64_type().as_basic_type_enum(), + ]); + } + } + ParamLowering::Direct(t) } -fn classify_ret_arm64_darwin<'ctx>( +fn classify_ret_arm64<'ctx>( + context: &'ctx Context, td: &TargetData, t: Option>, ) -> RetLowering<'ctx> { @@ -460,10 +575,27 @@ fn classify_ret_arm64_darwin<'ctx>( }; } + if is_agg && size == 0 { + return RetLowering::Void; + } + + if is_agg && !is_homogeneous_float_aggregate(td, t) { + if size <= 8 { + return RetLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ); + } + + return RetLowering::Direct(context.i64_type().array_type(2).as_basic_type_enum()); + } + RetLowering::Direct(t) } fn classify_ret_riscv64<'ctx>( + context: &'ctx Context, td: &TargetData, t: Option>, ) -> RetLowering<'ctx> { @@ -484,6 +616,33 @@ fn classify_ret_riscv64<'ctx>( }; } + if is_agg && size == 0 { + return RetLowering::Void; + } + + if is_agg { + let mut leaves = Vec::new(); + flatten_leaf_types(t, &mut leaves); + let integer_only = leaves.iter().all(|leaf| { + matches!( + leaf, + BasicTypeEnum::IntType(_) | BasicTypeEnum::PointerType(_) + ) + }); + + if integer_only { + if size <= 8 { + return RetLowering::Direct( + context + .custom_width_int_type((size * 8) as u32) + .as_basic_type_enum(), + ); + } + + return RetLowering::Direct(context.i64_type().array_type(2).as_basic_type_enum()); + } + } + RetLowering::Direct(t) } @@ -500,9 +659,9 @@ fn classify_param<'ctx>( CodegenTarget::WindowsX86_64Gnu => classify_param_x86_64_windows(context, td, t), CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => classify_param_arm64_darwin(td, t), + | CodegenTarget::FreestandingArm64 => classify_param_arm64(context, td, t), CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { - classify_param_riscv64(td, t) + classify_param_riscv64(context, td, t) } } } @@ -520,9 +679,9 @@ fn classify_ret<'ctx>( CodegenTarget::WindowsX86_64Gnu => classify_ret_x86_64_windows(context, td, t), CodegenTarget::LinuxArm64 | CodegenTarget::DarwinArm64 - | CodegenTarget::FreestandingArm64 => classify_ret_arm64_darwin(td, t), + | CodegenTarget::FreestandingArm64 => classify_ret_arm64(context, td, t), CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { - classify_ret_riscv64(td, t) + classify_ret_riscv64(context, td, t) } } } @@ -559,10 +718,16 @@ pub fn lower_extern_c<'ctx>( }; let ret = classify_ret(context, td, target, wave_ret_layout); + let ret_extension = integer_extension_for_target(target, &ext.return_type); let mut params: Vec> = vec![]; for p in wave_param_layout { params.push(classify_param(context, td, target, p)); } + let param_extensions = ext + .params + .iter() + .map(|(_, ty)| integer_extension_for_target(target, ty)) + .collect(); // build lowered param list (sret first, then params possibly split) let mut llvm_param_types: Vec> = vec![]; @@ -575,13 +740,14 @@ pub fn lower_extern_c<'ctx>( for p in ¶ms { match p { + ParamLowering::Ignore => {} ParamLowering::Direct(t) => llvm_param_types.push((*t).into()), ParamLowering::Split(parts) => { for pt in parts { llvm_param_types.push((*pt).into()); } } - ParamLowering::ByVal { ty, .. } => { + ParamLowering::Indirect { ty } | ParamLowering::ByVal { ty, .. } => { let ptr = any_ptr_basic(context, ty.clone()); llvm_param_types.push(ptr.into()); } @@ -590,9 +756,9 @@ pub fn lower_extern_c<'ctx>( let fn_type = match &ret { RetLowering::Void | RetLowering::SRet { .. } => { - context.void_type().fn_type(&llvm_param_types, false) + context.void_type().fn_type(&llvm_param_types, ext.variadic) } - RetLowering::Direct(t) => t.fn_type(&llvm_param_types, false), + RetLowering::Direct(t) => t.fn_type(&llvm_param_types, ext.variadic), }; LoweredExtern { @@ -602,12 +768,28 @@ pub fn lower_extern_c<'ctx>( llvm_name: info_llvm_name, wave_ret: ext.return_type.clone(), ret, + ret_extension, params, + param_extensions, llvm_param_types, + variadic: ext.variadic, + variadic_integer_extension: matches!( + target, + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 + ) + .then_some(IntegerExtension::Sign), }, } } +fn integer_extension_attr<'ctx>(context: &'ctx Context, extension: IntegerExtension) -> Attribute { + let name = match extension { + IntegerExtension::Sign => "signext", + IntegerExtension::Zero => "zeroext", + }; + context.create_enum_attribute(Attribute::get_named_enum_kind_id(name), 0) +} + pub fn apply_extern_c_attrs<'ctx>( context: &'ctx Context, f: FunctionValue<'ctx>, @@ -615,6 +797,13 @@ pub fn apply_extern_c_attrs<'ctx>( ) { let mut llvm_param_index: u32 = 0; + if let Some(extension) = info.ret_extension { + f.add_attribute( + AttributeLoc::Return, + integer_extension_attr(context, extension), + ); + } + // sret first param if let RetLowering::SRet { ty, align } = &info.ret { let sret_kind = Attribute::get_named_enum_kind_id("sret"); @@ -628,14 +817,24 @@ pub fn apply_extern_c_attrs<'ctx>( llvm_param_index += 1; } - for p in &info.params { + for (p, extension) in info.params.iter().zip(info.param_extensions.iter()) { match p { + ParamLowering::Ignore => {} ParamLowering::Direct(_) => { + if let Some(extension) = extension { + f.add_attribute( + AttributeLoc::Param(llvm_param_index), + integer_extension_attr(context, *extension), + ); + } llvm_param_index += 1; } ParamLowering::Split(parts) => { llvm_param_index += parts.len() as u32; } + ParamLowering::Indirect { .. } => { + llvm_param_index += 1; + } ParamLowering::ByVal { ty, align } => { let byval_kind = Attribute::get_named_enum_kind_id("byval"); let byval_attr = context.create_type_attribute(byval_kind, *ty); @@ -650,3 +849,91 @@ pub fn apply_extern_c_attrs<'ctx>( } } } + +pub fn apply_extern_c_callsite_attrs<'ctx>( + context: &'ctx Context, + call: CallSiteValue<'ctx>, + info: &ExternCInfo<'ctx>, +) { + let mut llvm_param_index: u32 = 0; + + if let Some(extension) = info.ret_extension { + call.add_attribute( + AttributeLoc::Return, + integer_extension_attr(context, extension), + ); + } + + if let RetLowering::SRet { ty, align } = &info.ret { + let sret_kind = Attribute::get_named_enum_kind_id("sret"); + call.add_attribute( + AttributeLoc::Param(0), + context.create_type_attribute(sret_kind, *ty), + ); + + let align_kind = Attribute::get_named_enum_kind_id("align"); + call.add_attribute( + AttributeLoc::Param(0), + context.create_enum_attribute(align_kind, *align as u64), + ); + + llvm_param_index += 1; + } + + for (p, extension) in info.params.iter().zip(info.param_extensions.iter()) { + match p { + ParamLowering::Ignore => {} + ParamLowering::Direct(_) => { + if let Some(extension) = extension { + call.add_attribute( + AttributeLoc::Param(llvm_param_index), + integer_extension_attr(context, *extension), + ); + } + llvm_param_index += 1; + } + ParamLowering::Split(parts) => { + llvm_param_index += parts.len() as u32; + } + ParamLowering::Indirect { .. } => { + llvm_param_index += 1; + } + ParamLowering::ByVal { ty, align } => { + let byval_kind = Attribute::get_named_enum_kind_id("byval"); + call.add_attribute( + AttributeLoc::Param(llvm_param_index), + context.create_type_attribute(byval_kind, *ty), + ); + + let align_kind = Attribute::get_named_enum_kind_id("align"); + call.add_attribute( + AttributeLoc::Param(llvm_param_index), + context.create_enum_attribute(align_kind, *align as u64), + ); + + llvm_param_index += 1; + } + } + } +} + +pub fn apply_extern_c_variadic_callsite_attrs<'ctx>( + context: &'ctx Context, + call: CallSiteValue<'ctx>, + info: &ExternCInfo<'ctx>, + arguments: &[BasicValueEnum<'ctx>], +) { + let Some(extension) = info.variadic_integer_extension else { + return; + }; + let first_index = info.llvm_param_types.len() as u32; + for (index, argument) in arguments.iter().enumerate() { + if matches!(argument, BasicValueEnum::IntValue(value) if value.get_type().get_bit_width() == 32) + { + call.add_attribute( + AttributeLoc::Param(first_index + index as u32), + integer_extension_attr(context, extension), + ); + } + } +} diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index b28bde64..82f8f3e2 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -77,7 +77,15 @@ fn reinterpret_abi_value<'ctx>( let source = value.get_type(); let source_size = td.get_store_size(&source); let target_size = td.get_store_size(&target); - if source_size != target_size { + let source_is_aggregate = matches!( + source, + BasicTypeEnum::ArrayType(_) | BasicTypeEnum::StructType(_) + ); + let target_is_aggregate = matches!( + target, + BasicTypeEnum::ArrayType(_) | BasicTypeEnum::StructType(_) + ); + if source_size != target_size && !(source_is_aggregate && target_is_aggregate) { panic!( "cannot reinterpret C ABI value '{}' from {} bytes to {} bytes", tag, source_size, target_size @@ -91,7 +99,12 @@ fn reinterpret_abi_value<'ctx>( let target_ptr = builder .build_alloca(target, &format!("{}_target", tag)) .unwrap(); - let size = context.i64_type().const_int(source_size, false); + builder + .build_store(target_ptr, target.const_zero()) + .unwrap(); + let size = context + .i64_type() + .const_int(source_size.min(target_size), false); builder .build_memcpy( target_ptr, @@ -118,6 +131,10 @@ fn rebuild_split_abi_value<'ctx>( let target_ptr = builder .build_alloca(target, &format!("{}_target", tag)) .unwrap(); + let target_size = td.get_store_size(&target); + builder + .build_store(target_ptr, target.const_zero()) + .unwrap(); let mut offset = 0u64; for (index, part) in parts.iter().enumerate() { @@ -138,21 +155,20 @@ fn rebuild_split_abi_value<'ctx>( ) .unwrap() }; - builder - .build_memcpy( - destination, - 1, - part_ptr, - td.get_abi_alignment(&part_type), - context.i64_type().const_int(part_size, false), - ) - .unwrap(); + let copy_size = part_size.min(target_size.saturating_sub(offset)); + if copy_size > 0 { + builder + .build_memcpy( + destination, + 1, + part_ptr, + td.get_abi_alignment(&part_type), + context.i64_type().const_int(copy_size, false), + ) + .unwrap(); + } offset += part_size; } - - if offset > td.get_store_size(&target) { - panic!("split C ABI value '{}' exceeds its Wave aggregate", tag); - } builder .build_load(target, target_ptr, &format!("{}_load", tag)) .unwrap() @@ -191,6 +207,7 @@ fn build_export_c_wrapper<'ctx>( .enumerate() { let value = match lowering { + ParamLowering::Ignore => wave_type.const_zero(), ParamLowering::Direct(_) => { let incoming = export .wrapper @@ -206,11 +223,11 @@ fn build_export_c_wrapper<'ctx>( &format!("export_arg_{}", wave_index), ) } - ParamLowering::ByVal { .. } => { + ParamLowering::Indirect { .. } | ParamLowering::ByVal { .. } => { let pointer = export .wrapper .get_nth_param(llvm_index) - .expect("missing byval C ABI wrapper argument") + .expect("missing indirect C ABI wrapper argument") .into_pointer_value(); llvm_index += 1; builder @@ -810,6 +827,7 @@ fn build_module( .iter() .map(|parameter| (parameter.name.clone(), parameter.param_type.clone())) .collect(), + variadic: false, return_type: return_type.clone().unwrap_or(WaveType::Void), }; let lowered = lower_extern_c(context, td, abi_target, &export_decl, &struct_types); diff --git a/llvm/src/expression/rvalue/calls.rs b/llvm/src/expression/rvalue/calls.rs index d8b36d2a..cb904b8f 100644 --- a/llvm/src/expression/rvalue/calls.rs +++ b/llvm/src/expression/rvalue/calls.rs @@ -11,13 +11,16 @@ // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. use super::ExprGenEnv; -use crate::codegen::abi_c::{ParamLowering, RetLowering}; +use crate::codegen::abi_c::{ + apply_extern_c_callsite_attrs, apply_extern_c_variadic_callsite_attrs, ParamLowering, + RetLowering, +}; use crate::statement::variable::{coerce_basic_value, CoercionMode}; use inkwell::types::{AnyTypeEnum, AsTypeRef, BasicMetadataTypeEnum, BasicType, BasicTypeEnum}; use inkwell::values::{ BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; -use parser::ast::{Expression, WaveType}; +use parser::ast::{Expression, Literal, WaveType}; fn meta_to_basic<'ctx>(m: BasicMetadataTypeEnum<'ctx>) -> BasicTypeEnum<'ctx> { match m { @@ -149,8 +152,19 @@ fn resolve_struct_key<'ctx>( fn wave_type_of_expr<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> Option { match e { + Expression::Literal(literal) => Some(match literal { + Literal::Int(_) => WaveType::Int(32), + Literal::Float(_) => WaveType::Float(32), + Literal::String(_) => WaveType::String, + Literal::Bool(_) => WaveType::Bool, + Literal::Char(_) => WaveType::Char, + Literal::Byte(_) => WaveType::Byte, + }), + Expression::Null => Some(WaveType::Pointer(Box::new(WaveType::Byte))), Expression::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()), Expression::Grouped(inner) => wave_type_of_expr(env, inner), + Expression::Cast { target_type, .. } => Some(target_type.clone()), + Expression::Unary { expr, .. } => wave_type_of_expr(env, expr), Expression::AddressOf(inner) => { wave_type_of_expr(env, inner).map(|t| WaveType::Pointer(Box::new(t))) } @@ -171,6 +185,44 @@ fn wave_type_of_expr<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> Op } } +fn lower_c_variadic_argument<'ctx, 'a>( + env: &mut ExprGenEnv<'ctx, 'a>, + expression: &Expression, + index: usize, +) -> BasicValueEnum<'ctx> { + let value = env.gen(expression, None); + match value { + BasicValueEnum::FloatValue(float) if float.get_type() == env.context.f32_type() => env + .builder + .build_float_ext( + float, + env.context.f64_type(), + &format!("vararg{}_f64", index), + ) + .unwrap() + .as_basic_value_enum(), + BasicValueEnum::IntValue(integer) if integer.get_type().get_bit_width() < 32 => { + let target = env.context.i32_type(); + let signed = matches!(wave_type_of_expr(env, expression), Some(WaveType::Int(_))); + if signed { + env.builder + .build_int_s_extend(integer, target, &format!("vararg{}_sext", index)) + .unwrap() + .as_basic_value_enum() + } else { + env.builder + .build_int_z_extend(integer, target, &format!("vararg{}_zext", index)) + .unwrap() + .as_basic_value_enum() + } + } + BasicValueEnum::IntValue(_) + | BasicValueEnum::FloatValue(_) + | BasicValueEnum::PointerValue(_) => value, + _ => panic!("C variadic argument {} must be a scalar value", index + 1), + } +} + fn infer_struct_name_for_method<'ctx, 'a>( env: &ExprGenEnv<'ctx, 'a>, object: &Expression, @@ -377,11 +429,14 @@ pub(crate) fn gen_function_call<'ctx, 'a>( ) }); - if args.len() != info.params.len() { + if (!info.variadic && args.len() != info.params.len()) + || (info.variadic && args.len() < info.params.len()) + { panic!( - "Extern `{}` expects {} arguments (wave-level), got {}", + "Extern `{}` expects {}{} arguments (wave-level), got {}", name, info.params.len(), + if info.variadic { " or more" } else { "" }, args.len() ); } @@ -390,6 +445,7 @@ pub(crate) fn gen_function_call<'ctx, 'a>( let llvm_param_types = fn_type.get_param_types(); let mut lowered_args: Vec> = Vec::new(); + let mut variadic_args: Vec> = Vec::new(); let mut llvm_pi: usize = 0; // 1) sret hidden param @@ -412,6 +468,9 @@ pub(crate) fn gen_function_call<'ctx, 'a>( // 2) wave params for (i, (arg_expr, p)) in args.iter().zip(info.params.iter()).enumerate() { match p { + ParamLowering::Ignore => { + env.gen(arg_expr, None); + } ParamLowering::Direct(t) => { let mut v = env.gen(arg_expr, Some(*t)); v = coerce_to_expected(env, v, *t, name, i); @@ -419,7 +478,7 @@ pub(crate) fn gen_function_call<'ctx, 'a>( llvm_pi += 1; } - ParamLowering::ByVal { ty, .. } => { + ParamLowering::Indirect { ty } | ParamLowering::ByVal { ty, .. } => { let agg = any_agg_to_basic(*ty); let v = env.gen(arg_expr, Some(agg)); let tmp = env @@ -465,6 +524,12 @@ pub(crate) fn gen_function_call<'ctx, 'a>( } } + for (index, expression) in args.iter().enumerate().skip(info.params.len()) { + let value = lower_c_variadic_argument(env, expression, index); + lowered_args.push(value.into()); + variadic_args.push(value); + } + let call_name = match info.ret { RetLowering::Void | RetLowering::SRet { .. } => String::new(), _ => format!("call_{}", name), @@ -474,10 +539,17 @@ pub(crate) fn gen_function_call<'ctx, 'a>( .builder .build_call(function, &lowered_args, &call_name) .unwrap(); + apply_extern_c_callsite_attrs(env.context, call_site, info); + apply_extern_c_variadic_callsite_attrs(env.context, call_site, info, &variadic_args); // 3) return match &info.ret { RetLowering::Void => { + if info.wave_ret != WaveType::Void { + let expected = expected_type + .expect("ignored C ABI aggregate return requires an expected type"); + return expected.const_zero(); + } if expected_type.is_some() { panic!( "Extern '{}' returns void and cannot be used as a value", @@ -815,30 +887,33 @@ fn split_agg_parts_from_agg<'ctx, 'a>( panic!("Split lowering got zero-sized parts"); } - let i8_ptr_ty = env - .context - .ptr_type(inkwell::AddressSpace::default()) - .as_basic_type_enum(); + let aggregate_type = agg_val.get_type(); + let aggregate_size = env.target_data.get_store_size(&aggregate_type); + let staging_type = env.context.i8_type().array_type(total_bytes as u32); + let staging = env + .builder + .build_alloca(staging_type, &format!("{tag}_staging")) + .unwrap(); + env.builder + .build_store(staging, staging_type.const_zero()) + .unwrap(); - let src_i8_ptr = match agg_val { - BasicValueEnum::PointerValue(pv) => env - .builder - .build_bit_cast(pv, i8_ptr_ty, &format!("{tag}_src_ptrcast")) - .unwrap() - .into_pointer_value(), - _ => { - let agg_ty = agg_val.get_type(); - let tmp = env - .builder - .build_alloca(agg_ty, &format!("{tag}_src_tmp")) - .unwrap(); - env.builder.build_store(tmp, agg_val).unwrap(); - env.builder - .build_bit_cast(tmp, i8_ptr_ty, &format!("{tag}_src_i8")) - .unwrap() - .into_pointer_value() - } - }; + let aggregate = env + .builder + .build_alloca(aggregate_type, &format!("{tag}_aggregate")) + .unwrap(); + env.builder.build_store(aggregate, agg_val).unwrap(); + env.builder + .build_memcpy( + staging, + 1, + aggregate, + env.target_data.get_abi_alignment(&aggregate_type), + env.context + .i64_type() + .const_int(aggregate_size.min(total_bytes), false), + ) + .unwrap(); let mut offset: u64 = 0; for (pi, part_ty) in parts.iter().enumerate() { @@ -847,18 +922,13 @@ fn split_agg_parts_from_agg<'ctx, 'a>( .builder .build_alloca(*part_ty, &format!("{tag}_part_dst_{pi}")) .unwrap(); - let dst_i8 = env - .builder - .build_bit_cast(dst, i8_ptr_ty, &format!("{tag}_dst_i8_{pi}")) - .unwrap() - .into_pointer_value(); let off = env.context.i64_type().const_int(offset, false); let src_off = unsafe { env.builder .build_gep( env.context.i8_type(), - src_i8_ptr, + staging, &[off], &format!("{tag}_src_gep_{pi}"), ) @@ -866,7 +936,7 @@ fn split_agg_parts_from_agg<'ctx, 'a>( }; let sz = env.context.i64_type().const_int(part_size, false); - env.builder.build_memcpy(dst_i8, 1, src_off, 1, sz).unwrap(); + env.builder.build_memcpy(dst, 1, src_off, 1, sz).unwrap(); let part_val = env .builder diff --git a/test/test.wave b/tests/cases/test.wave similarity index 100% rename from test/test.wave rename to tests/cases/test.wave diff --git a/test/test10.wave b/tests/cases/test10.wave similarity index 100% rename from test/test10.wave rename to tests/cases/test10.wave diff --git a/test/test100.wave b/tests/cases/test100.wave similarity index 100% rename from test/test100.wave rename to tests/cases/test100.wave diff --git a/test/test101.wave b/tests/cases/test101.wave similarity index 100% rename from test/test101.wave rename to tests/cases/test101.wave diff --git a/test/test102.wave b/tests/cases/test102.wave similarity index 100% rename from test/test102.wave rename to tests/cases/test102.wave diff --git a/test/test103.wave b/tests/cases/test103.wave similarity index 100% rename from test/test103.wave rename to tests/cases/test103.wave diff --git a/test/test104.wave b/tests/cases/test104.wave similarity index 100% rename from test/test104.wave rename to tests/cases/test104.wave diff --git a/test/test105.wave b/tests/cases/test105.wave similarity index 100% rename from test/test105.wave rename to tests/cases/test105.wave diff --git a/test/test106.wave b/tests/cases/test106.wave similarity index 100% rename from test/test106.wave rename to tests/cases/test106.wave diff --git a/test/test107.wave b/tests/cases/test107.wave similarity index 100% rename from test/test107.wave rename to tests/cases/test107.wave diff --git a/test/test108.wave b/tests/cases/test108.wave similarity index 100% rename from test/test108.wave rename to tests/cases/test108.wave diff --git a/test/test11.wave b/tests/cases/test11.wave similarity index 100% rename from test/test11.wave rename to tests/cases/test11.wave diff --git a/test/test12.wave b/tests/cases/test12.wave similarity index 100% rename from test/test12.wave rename to tests/cases/test12.wave diff --git a/test/test13.wave b/tests/cases/test13.wave similarity index 100% rename from test/test13.wave rename to tests/cases/test13.wave diff --git a/test/test14.wave b/tests/cases/test14.wave similarity index 100% rename from test/test14.wave rename to tests/cases/test14.wave diff --git a/test/test15.wave b/tests/cases/test15.wave similarity index 100% rename from test/test15.wave rename to tests/cases/test15.wave diff --git a/test/test16.wave b/tests/cases/test16.wave similarity index 100% rename from test/test16.wave rename to tests/cases/test16.wave diff --git a/test/test17.wave b/tests/cases/test17.wave similarity index 100% rename from test/test17.wave rename to tests/cases/test17.wave diff --git a/test/test18.wave b/tests/cases/test18.wave similarity index 100% rename from test/test18.wave rename to tests/cases/test18.wave diff --git a/test/test19.wave b/tests/cases/test19.wave similarity index 100% rename from test/test19.wave rename to tests/cases/test19.wave diff --git a/test/test2.wave b/tests/cases/test2.wave similarity index 100% rename from test/test2.wave rename to tests/cases/test2.wave diff --git a/test/test20.wave b/tests/cases/test20.wave similarity index 100% rename from test/test20.wave rename to tests/cases/test20.wave diff --git a/test/test21.wave b/tests/cases/test21.wave similarity index 100% rename from test/test21.wave rename to tests/cases/test21.wave diff --git a/test/test22.wave b/tests/cases/test22.wave similarity index 100% rename from test/test22.wave rename to tests/cases/test22.wave diff --git a/test/test23.wave b/tests/cases/test23.wave similarity index 100% rename from test/test23.wave rename to tests/cases/test23.wave diff --git a/test/test24.wave b/tests/cases/test24.wave similarity index 100% rename from test/test24.wave rename to tests/cases/test24.wave diff --git a/test/test25.wave b/tests/cases/test25.wave similarity index 100% rename from test/test25.wave rename to tests/cases/test25.wave diff --git a/test/test26.wave b/tests/cases/test26.wave similarity index 100% rename from test/test26.wave rename to tests/cases/test26.wave diff --git a/test/test27.wave b/tests/cases/test27.wave similarity index 100% rename from test/test27.wave rename to tests/cases/test27.wave diff --git a/test/test28/main.wave b/tests/cases/test28/main.wave similarity index 100% rename from test/test28/main.wave rename to tests/cases/test28/main.wave diff --git a/test/test28/math.wave b/tests/cases/test28/math.wave similarity index 100% rename from test/test28/math.wave rename to tests/cases/test28/math.wave diff --git a/test/test29.wave b/tests/cases/test29.wave similarity index 100% rename from test/test29.wave rename to tests/cases/test29.wave diff --git a/test/test3.wave b/tests/cases/test3.wave similarity index 100% rename from test/test3.wave rename to tests/cases/test3.wave diff --git a/test/test30.wave b/tests/cases/test30.wave similarity index 100% rename from test/test30.wave rename to tests/cases/test30.wave diff --git a/test/test31.wave b/tests/cases/test31.wave similarity index 100% rename from test/test31.wave rename to tests/cases/test31.wave diff --git a/test/test32.wave b/tests/cases/test32.wave similarity index 100% rename from test/test32.wave rename to tests/cases/test32.wave diff --git a/test/test33.wave b/tests/cases/test33.wave similarity index 100% rename from test/test33.wave rename to tests/cases/test33.wave diff --git a/test/test34.wave b/tests/cases/test34.wave similarity index 100% rename from test/test34.wave rename to tests/cases/test34.wave diff --git a/test/test35.wave b/tests/cases/test35.wave similarity index 100% rename from test/test35.wave rename to tests/cases/test35.wave diff --git a/test/test36.wave b/tests/cases/test36.wave similarity index 100% rename from test/test36.wave rename to tests/cases/test36.wave diff --git a/test/test37.wave b/tests/cases/test37.wave similarity index 100% rename from test/test37.wave rename to tests/cases/test37.wave diff --git a/test/test38.wave b/tests/cases/test38.wave similarity index 100% rename from test/test38.wave rename to tests/cases/test38.wave diff --git a/test/test39.wave b/tests/cases/test39.wave similarity index 100% rename from test/test39.wave rename to tests/cases/test39.wave diff --git a/test/test4.wave b/tests/cases/test4.wave similarity index 100% rename from test/test4.wave rename to tests/cases/test4.wave diff --git a/test/test40.wave b/tests/cases/test40.wave similarity index 100% rename from test/test40.wave rename to tests/cases/test40.wave diff --git a/test/test41.wave b/tests/cases/test41.wave similarity index 100% rename from test/test41.wave rename to tests/cases/test41.wave diff --git a/test/test42.wave b/tests/cases/test42.wave similarity index 100% rename from test/test42.wave rename to tests/cases/test42.wave diff --git a/test/test43.wave b/tests/cases/test43.wave similarity index 100% rename from test/test43.wave rename to tests/cases/test43.wave diff --git a/test/test44.wave b/tests/cases/test44.wave similarity index 100% rename from test/test44.wave rename to tests/cases/test44.wave diff --git a/test/test45.wave b/tests/cases/test45.wave similarity index 100% rename from test/test45.wave rename to tests/cases/test45.wave diff --git a/test/test46.wave b/tests/cases/test46.wave similarity index 100% rename from test/test46.wave rename to tests/cases/test46.wave diff --git a/test/test47.wave b/tests/cases/test47.wave similarity index 100% rename from test/test47.wave rename to tests/cases/test47.wave diff --git a/test/test48.wave b/tests/cases/test48.wave similarity index 100% rename from test/test48.wave rename to tests/cases/test48.wave diff --git a/test/test49.wave b/tests/cases/test49.wave similarity index 100% rename from test/test49.wave rename to tests/cases/test49.wave diff --git a/test/test5.wave b/tests/cases/test5.wave similarity index 100% rename from test/test5.wave rename to tests/cases/test5.wave diff --git a/test/test50.wave b/tests/cases/test50.wave similarity index 100% rename from test/test50.wave rename to tests/cases/test50.wave diff --git a/test/test51.wave b/tests/cases/test51.wave similarity index 100% rename from test/test51.wave rename to tests/cases/test51.wave diff --git a/test/test52.wave b/tests/cases/test52.wave similarity index 100% rename from test/test52.wave rename to tests/cases/test52.wave diff --git a/test/test53.wave b/tests/cases/test53.wave similarity index 100% rename from test/test53.wave rename to tests/cases/test53.wave diff --git a/test/test54.wave b/tests/cases/test54.wave similarity index 100% rename from test/test54.wave rename to tests/cases/test54.wave diff --git a/test/test55.wave b/tests/cases/test55.wave similarity index 100% rename from test/test55.wave rename to tests/cases/test55.wave diff --git a/test/test56.wave b/tests/cases/test56.wave similarity index 100% rename from test/test56.wave rename to tests/cases/test56.wave diff --git a/test/test57.wave b/tests/cases/test57.wave similarity index 100% rename from test/test57.wave rename to tests/cases/test57.wave diff --git a/test/test58.wave b/tests/cases/test58.wave similarity index 100% rename from test/test58.wave rename to tests/cases/test58.wave diff --git a/test/test59.wave b/tests/cases/test59.wave similarity index 100% rename from test/test59.wave rename to tests/cases/test59.wave diff --git a/test/test6.wave b/tests/cases/test6.wave similarity index 100% rename from test/test6.wave rename to tests/cases/test6.wave diff --git a/test/test60.wave b/tests/cases/test60.wave similarity index 100% rename from test/test60.wave rename to tests/cases/test60.wave diff --git a/test/test61.wave b/tests/cases/test61.wave similarity index 100% rename from test/test61.wave rename to tests/cases/test61.wave diff --git a/test/test62.wave b/tests/cases/test62.wave similarity index 100% rename from test/test62.wave rename to tests/cases/test62.wave diff --git a/test/test63.wave b/tests/cases/test63.wave similarity index 100% rename from test/test63.wave rename to tests/cases/test63.wave diff --git a/test/test64.wave b/tests/cases/test64.wave similarity index 100% rename from test/test64.wave rename to tests/cases/test64.wave diff --git a/test/test65.wave b/tests/cases/test65.wave similarity index 100% rename from test/test65.wave rename to tests/cases/test65.wave diff --git a/test/test66.wave b/tests/cases/test66.wave similarity index 100% rename from test/test66.wave rename to tests/cases/test66.wave diff --git a/test/test67.wave b/tests/cases/test67.wave similarity index 100% rename from test/test67.wave rename to tests/cases/test67.wave diff --git a/test/test68.wave b/tests/cases/test68.wave similarity index 100% rename from test/test68.wave rename to tests/cases/test68.wave diff --git a/test/test69.wave b/tests/cases/test69.wave similarity index 100% rename from test/test69.wave rename to tests/cases/test69.wave diff --git a/test/test7.wave b/tests/cases/test7.wave similarity index 100% rename from test/test7.wave rename to tests/cases/test7.wave diff --git a/test/test70.wave b/tests/cases/test70.wave similarity index 100% rename from test/test70.wave rename to tests/cases/test70.wave diff --git a/test/test71.wave b/tests/cases/test71.wave similarity index 100% rename from test/test71.wave rename to tests/cases/test71.wave diff --git a/test/test72.wave b/tests/cases/test72.wave similarity index 100% rename from test/test72.wave rename to tests/cases/test72.wave diff --git a/test/test73.wave b/tests/cases/test73.wave similarity index 100% rename from test/test73.wave rename to tests/cases/test73.wave diff --git a/test/test74.wave b/tests/cases/test74.wave similarity index 100% rename from test/test74.wave rename to tests/cases/test74.wave diff --git a/test/test75.wave b/tests/cases/test75.wave similarity index 100% rename from test/test75.wave rename to tests/cases/test75.wave diff --git a/test/test76.wave b/tests/cases/test76.wave similarity index 100% rename from test/test76.wave rename to tests/cases/test76.wave diff --git a/test/test77.wave b/tests/cases/test77.wave similarity index 100% rename from test/test77.wave rename to tests/cases/test77.wave diff --git a/test/test78.wave b/tests/cases/test78.wave similarity index 100% rename from test/test78.wave rename to tests/cases/test78.wave diff --git a/test/test79.wave b/tests/cases/test79.wave similarity index 100% rename from test/test79.wave rename to tests/cases/test79.wave diff --git a/test/test8.wave b/tests/cases/test8.wave similarity index 100% rename from test/test8.wave rename to tests/cases/test8.wave diff --git a/test/test80.wave b/tests/cases/test80.wave similarity index 100% rename from test/test80.wave rename to tests/cases/test80.wave diff --git a/test/test81.wave b/tests/cases/test81.wave similarity index 100% rename from test/test81.wave rename to tests/cases/test81.wave diff --git a/test/test82.wave b/tests/cases/test82.wave similarity index 100% rename from test/test82.wave rename to tests/cases/test82.wave diff --git a/test/test83.wave b/tests/cases/test83.wave similarity index 100% rename from test/test83.wave rename to tests/cases/test83.wave diff --git a/test/test84.wave b/tests/cases/test84.wave similarity index 100% rename from test/test84.wave rename to tests/cases/test84.wave diff --git a/test/test85.wave b/tests/cases/test85.wave similarity index 100% rename from test/test85.wave rename to tests/cases/test85.wave diff --git a/test/test86.wave b/tests/cases/test86.wave similarity index 100% rename from test/test86.wave rename to tests/cases/test86.wave diff --git a/test/test87.wave b/tests/cases/test87.wave similarity index 100% rename from test/test87.wave rename to tests/cases/test87.wave diff --git a/test/test88.wave b/tests/cases/test88.wave similarity index 100% rename from test/test88.wave rename to tests/cases/test88.wave diff --git a/test/test89.wave b/tests/cases/test89.wave similarity index 100% rename from test/test89.wave rename to tests/cases/test89.wave diff --git a/test/test9.wave b/tests/cases/test9.wave similarity index 100% rename from test/test9.wave rename to tests/cases/test9.wave diff --git a/test/test90.wave b/tests/cases/test90.wave similarity index 100% rename from test/test90.wave rename to tests/cases/test90.wave diff --git a/test/test91.wave b/tests/cases/test91.wave similarity index 100% rename from test/test91.wave rename to tests/cases/test91.wave diff --git a/test/test92.wave b/tests/cases/test92.wave similarity index 100% rename from test/test92.wave rename to tests/cases/test92.wave diff --git a/test/test93.wave b/tests/cases/test93.wave similarity index 100% rename from test/test93.wave rename to tests/cases/test93.wave diff --git a/test/test94.wave b/tests/cases/test94.wave similarity index 100% rename from test/test94.wave rename to tests/cases/test94.wave diff --git a/test/test95.wave b/tests/cases/test95.wave similarity index 100% rename from test/test95.wave rename to tests/cases/test95.wave diff --git a/test/test96.wave b/tests/cases/test96.wave similarity index 100% rename from test/test96.wave rename to tests/cases/test96.wave diff --git a/test/test97.wave b/tests/cases/test97.wave similarity index 100% rename from test/test97.wave rename to tests/cases/test97.wave diff --git a/test/test98.wave b/tests/cases/test98.wave similarity index 100% rename from test/test98.wave rename to tests/cases/test98.wave diff --git a/test/test99.wave b/tests/cases/test99.wave similarity index 100% rename from test/test99.wave rename to tests/cases/test99.wave diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 81ff7841..5b10521b 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -2653,10 +2653,486 @@ fun main() -> i32 { assert!(ir.contains("@__wave_export_impl_wave_make"), "{}", ir); } +#[test] +fn riscv64_c_abi_marks_required_integer_extensions() { + let dir = temp_case_dir("riscv64-c-abi-integer-extensions"); + let source = write_wave( + &dir, + "integer_extensions.wave", + r#" +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_u8(value: u8) -> u8; +extern(c) fun c_u32(value: u32) -> u32; +extern(c) fun c_variadic(count: i32, ...) -> i64; + +export(c) fun wave_i8(value: i8) -> i8 { return value; } +export(c) fun wave_u8(value: u8) -> u8 { return value; } +export(c) fun wave_u32(value: u32) -> u32 { return value; } + +fun main() -> i32 { + if (c_i8(-1) != -1) { return 1; } + if (c_u8(255) != 255) { return 2; } + if (c_u32(4294967295) != 4294967295) { return 3; } + let narrow: i8 = -1; + let single: f32 = 1.5; + if (c_variadic(2, narrow, single) != 0) { return 4; } + return 0; +} +"#, + ); + let out = dir.join("out"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + let ir = fs::read_to_string(out.join("integer_extensions.ll")).unwrap(); + + for expected in [ + "define signext i8 @wave_i8(i8 signext", + "define zeroext i8 @wave_u8(i8 zeroext", + "define signext i32 @wave_u32(i32 signext", + "call signext i8 @c_i8(i8 signext", + "call zeroext i8 @c_u8(i8 zeroext", + "call signext i32 @c_u32(i32 signext", + "declare signext i8 @c_i8(i8 signext)", + "declare zeroext i8 @c_u8(i8 zeroext)", + "declare signext i32 @c_u32(i32 signext)", + "call i64 (i32, ...) @c_variadic(i32 signext 2, i32 signext %vararg1_sext, double %vararg2_f64)", + "declare i64 @c_variadic(i32 signext, ...)", + ] { + assert!(ir.contains(expected), "missing `{expected}` in:\n{ir}"); + } +} + +fn run_linux_c_abi_fixture( + fixture_name: &str, + target: &str, + c_compiler: &str, + runner: Option<&str>, +) { + let dir = temp_case_dir(&format!("{fixture_name}-c-abi-interop")); + let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join(fixture_name); + let c_object = dir.join("interop-c.o"); + let wave_out = dir.join("wave"); + let binary = dir.join("interop"); + + let c_compile = Command::new(c_compiler) + .args([ + "-O2", + "-ffreestanding", + "-fno-builtin", + "-fno-stack-protector", + "-c", + ]) + .arg(fixture_dir.join("interop.c")) + .arg("-o") + .arg(&c_object) + .output() + .unwrap_or_else(|error| panic!("failed to start {c_compiler}: {error}")); + assert!( + c_compile.status.success(), + "{fixture_name} C fixture compile failed:\n{}", + String::from_utf8_lossy(&c_compile.stderr) + ); + + run_wavec([ + OsStr::new("build"), + fixture_dir.join("interop.wave").as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + wave_out.as_os_str(), + ]); + + let link = Command::new(c_compiler) + .args(["-nostdlib", "-static", "-Wl,-e,_start"]) + .arg(wave_out.join("interop.o")) + .arg(&c_object) + .arg("-o") + .arg(&binary) + .output() + .unwrap_or_else(|error| panic!("failed to start {c_compiler} linker: {error}")); + assert!( + link.status.success(), + "{fixture_name} fixture link failed:\n{}", + String::from_utf8_lossy(&link.stderr) + ); + + let mut command = if let Some(runner) = runner { + let mut command = Command::new(runner); + command.arg(&binary); + command + } else { + Command::new(&binary) + }; + let run = command + .output() + .unwrap_or_else(|error| panic!("failed to run {fixture_name} fixture: {error}")); + assert!( + run.status.success(), + "{fixture_name} C/Wave ABI fixture failed with status {}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); +} + +#[test] +fn x86_64_c_abi_interoperates_with_c() { + if std::env::var_os("WAVE_RUN_X86_64_INTEROP_TESTS").is_none() { + eprintln!("skipped: set WAVE_RUN_X86_64_INTEROP_TESTS=1 to run native ABI test"); + return; + } + + assert_eq!(std::env::consts::ARCH, "x86_64"); + assert_eq!(std::env::consts::OS, "linux"); + run_linux_c_abi_fixture("x86_64_sysv", "x86_64-unknown-linux-gnu", "gcc", None); +} + +#[test] +fn aarch64_c_abi_interoperates_with_c() { + if std::env::var_os("WAVE_RUN_AARCH64_INTEROP_TESTS").is_none() { + eprintln!("skipped: set WAVE_RUN_AARCH64_INTEROP_TESTS=1 to run ABI test"); + return; + } + + assert_eq!(std::env::consts::OS, "linux"); + let native = std::env::consts::ARCH == "aarch64"; + run_linux_c_abi_fixture( + "aarch64_aapcs64", + "aarch64-unknown-linux-gnu", + if native { + "gcc" + } else { + "aarch64-linux-gnu-gcc" + }, + if native { None } else { Some("qemu-aarch64") }, + ); +} + +#[test] +fn riscv64_c_abi_interoperates_with_c_under_qemu() { + if std::env::var_os("WAVE_RUN_RISCV64_INTEROP_TESTS").is_none() { + eprintln!("skipped: set WAVE_RUN_RISCV64_INTEROP_TESTS=1 to run cross-toolchain test"); + return; + } + + let dir = temp_case_dir("riscv64-c-abi-interop"); + let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("riscv64_psabi"); + let fixture_wave = fixture_dir.join("interop.wave"); + let c_object = dir.join("interop-c.o"); + let wave_out = dir.join("wave"); + let binary = dir.join("interop"); + + let c_compile = Command::new("riscv64-linux-gnu-gcc") + .args([ + "-march=rv64gc", + "-mabi=lp64d", + "-msmall-data-limit=0", + "-O2", + "-ffreestanding", + "-fno-builtin", + "-fno-stack-protector", + "-c", + ]) + .arg(fixture_dir.join("interop.c")) + .arg("-o") + .arg(&c_object) + .output() + .expect("failed to start riscv64-linux-gnu-gcc"); + assert!( + c_compile.status.success(), + "C fixture compile failed:\n{}", + String::from_utf8_lossy(&c_compile.stderr) + ); + + run_wavec([ + OsStr::new("build"), + fixture_wave.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + wave_out.as_os_str(), + ]); + + let link = Command::new("riscv64-linux-gnu-gcc") + .args([ + "-march=rv64gc", + "-mabi=lp64d", + "-nostdlib", + "-static", + "-Wl,-e,_start", + ]) + .arg(wave_out.join("interop.o")) + .arg(&c_object) + .arg("-o") + .arg(&binary) + .output() + .expect("failed to start riscv64-linux-gnu-gcc linker"); + assert!( + link.status.success(), + "RISC-V fixture link failed:\n{}", + String::from_utf8_lossy(&link.stderr) + ); + + let run = Command::new("qemu-riscv64") + .arg(&binary) + .output() + .expect("failed to start qemu-riscv64"); + assert!( + run.status.success(), + "C/Wave psABI fixture failed with status {}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); +} + +#[test] +fn riscv64_lp64_abi_modes_interoperate_with_c_under_qemu() { + if std::env::var_os("WAVE_RUN_RISCV64_INTEROP_TESTS").is_none() { + eprintln!("skipped: set WAVE_RUN_RISCV64_INTEROP_TESTS=1 to run cross-toolchain test"); + return; + } + + let fixture_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("riscv64_psabi"); + + for (abi, march, features) in [ + ("lp64", "rv64imac", "+m,+a,-f,-d,+c"), + ("lp64f", "rv64imafc", "+m,+a,+f,-d,+c"), + ("lp64d", "rv64gc", "+m,+a,+f,+d,+c"), + ] { + let dir = temp_case_dir(&format!("riscv64-{abi}-c-abi-interop")); + let c_object = dir.join("abi-modes-c.o"); + let wave_out = dir.join("wave"); + let binary = dir.join("abi-modes"); + + let c_compile = Command::new("riscv64-linux-gnu-gcc") + .arg(format!("-march={march}")) + .arg(format!("-mabi={abi}")) + .args([ + "-msmall-data-limit=0", + "-O2", + "-ffreestanding", + "-fno-builtin", + "-fno-stack-protector", + "-c", + ]) + .arg(fixture_dir.join("abi_modes.c")) + .arg("-o") + .arg(&c_object) + .output() + .expect("failed to start riscv64-linux-gnu-gcc"); + assert!( + c_compile.status.success(), + "{abi} C fixture compile failed:\n{}", + String::from_utf8_lossy(&c_compile.stderr) + ); + + run_wavec([ + OsStr::new("build"), + fixture_dir.join("abi_modes.wave").as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--features"), + OsStr::new(features), + OsStr::new("--abi"), + OsStr::new(abi), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + wave_out.as_os_str(), + ]); + + let link = Command::new("riscv64-linux-gnu-gcc") + .arg(format!("-march={march}")) + .arg(format!("-mabi={abi}")) + .args(["-nostdlib", "-static", "-Wl,-e,_start"]) + .arg(wave_out.join("abi_modes.o")) + .arg(&c_object) + .arg("-o") + .arg(&binary) + .output() + .expect("failed to start riscv64-linux-gnu-gcc linker"); + assert!( + link.status.success(), + "{abi} RISC-V fixture link failed:\n{}", + String::from_utf8_lossy(&link.stderr) + ); + + let run = Command::new("qemu-riscv64") + .arg(&binary) + .output() + .expect("failed to start qemu-riscv64"); + assert!( + run.status.success(), + "{abi} C/Wave psABI fixture failed with status {}", + run.status + ); + } +} + +#[test] +fn riscv64_relocation_atomic_and_compressed_feature_contracts() { + if std::env::var_os("WAVE_RUN_RISCV64_INTEROP_TESTS").is_none() { + eprintln!("skipped: set WAVE_RUN_RISCV64_INTEROP_TESTS=1 to run toolchain test"); + return; + } + + let dir = temp_case_dir("riscv64-object-contracts"); + let relocation_source = write_wave( + &dir, + "relocation.ll", + r#" +@local_value = global i64 7, align 8 +@external_value = external global i64 + +define ptr @local_address() { +entry: + ret ptr @local_value +} + +define ptr @external_address() { +entry: + ret ptr @external_value +} +"#, + ); + let pic_out = dir.join("pic"); + let static_out = dir.join("static"); + run_wavec([ + OsStr::new("build"), + relocation_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("-Crelocation-model=pic"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + pic_out.as_os_str(), + ]); + run_wavec([ + OsStr::new("build"), + relocation_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("-Crelocation-model=static"), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + static_out.as_os_str(), + ]); + + let read_relocations = |path: &Path| { + let output = Command::new("llvm-readobj") + .arg("--relocations") + .arg(path) + .output() + .expect("failed to start llvm-readobj"); + assert!( + output.status.success(), + "llvm-readobj failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() + }; + let pic_relocations = read_relocations(&pic_out.join("relocation.o")); + assert!(pic_relocations.contains("R_RISCV_GOT_HI20 local_value")); + assert!(pic_relocations.contains("R_RISCV_GOT_HI20 external_value")); + assert!(pic_relocations.contains("R_RISCV_PCREL_LO12_I")); + assert!(!pic_relocations.contains("R_RISCV_HI20 local_value")); + + let static_relocations = read_relocations(&static_out.join("relocation.o")); + assert!(static_relocations.contains("R_RISCV_HI20 local_value")); + assert!(static_relocations.contains("R_RISCV_LO12_I local_value")); + assert!(static_relocations.contains("R_RISCV_HI20 external_value")); + assert!(!static_relocations.contains("R_RISCV_GOT_HI20")); + + let atomic_source = write_wave( + &dir, + "atomic.ll", + r#" +define i64 @atomic_add(ptr %address) { +entry: + %previous = atomicrmw add ptr %address, i64 1 seq_cst + ret i64 %previous +} +"#, + ); + let atomic_a_out = dir.join("atomic-a"); + let atomic_no_a_out = dir.join("atomic-no-a"); + let compressed_off_out = dir.join("compressed-off"); + for (features, out) in [ + ("+m,+a,-f,-d,+c", &atomic_a_out), + ("+m,-a,-f,-d,+c", &atomic_no_a_out), + ("+m,+a,-f,-d,-c", &compressed_off_out), + ] { + run_wavec([ + OsStr::new("build"), + atomic_source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--features"), + OsStr::new(features), + OsStr::new("--abi=lp64"), + OsStr::new("--emit=obj,asm"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + } + + let atomic_a_assembly = fs::read_to_string(atomic_a_out.join("atomic.s")).unwrap(); + assert!(atomic_a_assembly.contains("amoadd.d.aqrl")); + assert!(!atomic_a_assembly.contains("__atomic_fetch_add_8")); + let atomic_no_a_assembly = fs::read_to_string(atomic_no_a_out.join("atomic.s")).unwrap(); + assert!(!atomic_no_a_assembly.contains("amoadd")); + assert!(atomic_no_a_assembly.contains("__atomic_fetch_add_8")); + + let compressed_object = atomic_a_out.join("atomic.o"); + let uncompressed_object = compressed_off_out.join("atomic.o"); + assert_eq!(riscv64_elf_flags(&compressed_object) & 1, 1); + assert_eq!(riscv64_elf_flags(&uncompressed_object) & 1, 0); + let compressed_bytes = fs::read(&compressed_object).unwrap(); + let uncompressed_bytes = fs::read(&uncompressed_object).unwrap(); + assert!(bytes_contains(&compressed_bytes, b"c2p0")); + assert!(!bytes_contains(&uncompressed_bytes, b"c2p0")); + + let disassemble = |path: &Path| { + let output = Command::new("llvm-objdump") + .arg("--disassemble") + .arg(path) + .output() + .expect("failed to start llvm-objdump"); + assert!( + output.status.success(), + "llvm-objdump failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).into_owned() + }; + let compressed_disassembly = disassemble(&compressed_object); + let uncompressed_disassembly = disassemble(&uncompressed_object); + assert!(compressed_disassembly.contains("8082")); + assert!(uncompressed_disassembly.contains("00008067")); +} + #[test] fn waveos_boot_smoke_builds_windows_freestanding_coff_object() { let dir = temp_case_dir("waveos-boot-smoke-coff"); - let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/test108.wave"); + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/cases/test108.wave"); let object = dir.join("waveos_boot_smoke.obj"); run_wavec([ diff --git a/tests/fixtures/aarch64_aapcs64/interop.c b/tests/fixtures/aarch64_aapcs64/interop.c new file mode 100644 index 00000000..827dace4 --- /dev/null +++ b/tests/fixtures/aarch64_aapcs64/interop.c @@ -0,0 +1,87 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed char i8; +typedef unsigned int u32; +typedef signed int i32; +typedef signed long i64; +typedef unsigned long u64; + +void *memcpy(void *destination, const void *source, u64 count) { + unsigned char *out = (unsigned char *)destination; + const unsigned char *in = (const unsigned char *)source; + for (u64 index = 0; index < count; ++index) + out[index] = in[index]; + return destination; +} + +struct pair { u64 first; u64 second; }; +struct floats { double first; double second; }; +struct mixed { double floating; i64 integer; }; +struct triple { u64 first; u64 second; u64 third; }; + +_Static_assert(sizeof(void *) == 8, "AArch64 pointers must be 8 bytes"); +_Static_assert(sizeof(struct pair) == 16, "unexpected pair layout"); +_Static_assert(sizeof(struct mixed) == 16, "unexpected mixed layout"); +_Static_assert(sizeof(struct triple) == 24, "unexpected triple layout"); + +extern i8 wave_i8(i8); +extern u32 wave_u32(u32); +extern float wave_f32(float); +extern double wave_f64(double); +extern struct pair wave_pair(struct pair); +extern struct floats wave_floats(struct floats); +extern struct mixed wave_mixed(struct mixed); +extern struct triple wave_triple(struct triple); +extern i64 wave_stack_ten(i64, i64, i64, i64, i64, i64, i64, i64, i64, i64); + +i8 c_i8(i8 value) { return value; } +u32 c_u32(u32 value) { return value; } +float c_f32(float value) { return value; } +double c_f64(double value) { return value; } +struct pair c_pair(struct pair value) { return value; } +struct floats c_floats(struct floats value) { return value; } +struct mixed c_mixed(struct mixed value) { return value; } +struct triple c_triple(struct triple value) { return value; } + +i64 c_stack_ten(i64 a0, i64 a1, i64 a2, i64 a3, i64 a4, + i64 a5, i64 a6, i64 a7, i64 a8, i64 a9) { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +i32 c_check_wave_exports(void) { + struct pair pair = wave_pair((struct pair){11, 22}); + struct floats floats = wave_floats((struct floats){3.5, 4.5}); + struct mixed mixed = wave_mixed((struct mixed){5.5, -6}); + struct triple triple = wave_triple((struct triple){31, 32, 33}); + + if (wave_i8(-128) != -128 || wave_u32(4294967295U) != 4294967295U) + return 1; + if (wave_f32(1.5f) != 1.5f || wave_f64(2.5) != 2.5) + return 2; + if (pair.first != 11 || pair.second != 22) + return 3; + if (floats.first != 3.5 || floats.second != 4.5) + return 4; + if (mixed.floating != 5.5 || mixed.integer != -6) + return 5; + if (triple.first != 31 || triple.second != 32 || triple.third != 33) + return 6; + if (wave_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) + return 7; + return 0; +} + +extern i32 main(void); + +__attribute__((noreturn)) void _start(void) { + register long exit_code __asm__("x0") = main(); + register long syscall_number __asm__("x8") = 93; + __asm__ volatile("svc #0" + : + : "r"(exit_code), "r"(syscall_number) + : "memory"); + __builtin_unreachable(); +} diff --git a/tests/fixtures/aarch64_aapcs64/interop.wave b/tests/fixtures/aarch64_aapcs64/interop.wave new file mode 100644 index 00000000..bbb3186e --- /dev/null +++ b/tests/fixtures/aarch64_aapcs64/interop.wave @@ -0,0 +1,53 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +struct Pair { first: u64; second: u64; } +struct Floats { first: f64; second: f64; } +struct Mixed { floating: f64; integer: i64; } +struct Triple { first: u64; second: u64; third: u64; } + +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_u32(value: u32) -> u32; +extern(c) fun c_f32(value: f32) -> f32; +extern(c) fun c_f64(value: f64) -> f64; +extern(c) fun c_pair(value: Pair) -> Pair; +extern(c) fun c_floats(value: Floats) -> Floats; +extern(c) fun c_mixed(value: Mixed) -> Mixed; +extern(c) fun c_triple(value: Triple) -> Triple; +extern(c) fun c_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64; +extern(c) fun c_check_wave_exports() -> i32; + +export(c) fun wave_i8(value: i8) -> i8 { return value; } +export(c) fun wave_u32(value: u32) -> u32 { return value; } +export(c) fun wave_f32(value: f32) -> f32 { return value; } +export(c) fun wave_f64(value: f64) -> f64 { return value; } +export(c) fun wave_pair(value: Pair) -> Pair { return value; } +export(c) fun wave_floats(value: Floats) -> Floats { return value; } +export(c) fun wave_mixed(value: Mixed) -> Mixed { return value; } +export(c) fun wave_triple(value: Triple) -> Triple { return value; } +export(c) fun wave_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64 { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +fun main() -> i32 { + if (c_i8(-128) != -128) { return 1; } + if (c_u32(4294967295) != 4294967295) { return 2; } + if (c_f32(1.5) != 1.5) { return 3; } + if (c_f64(2.5) != 2.5) { return 4; } + + let pair: Pair = c_pair(Pair { first: 11, second: 22 }); + if (pair.first != 11 || pair.second != 22) { return 5; } + let floats: Floats = c_floats(Floats { first: 3.5, second: 4.5 }); + if (floats.first != 3.5 || floats.second != 4.5) { return 6; } + let mixed: Mixed = c_mixed(Mixed { floating: 5.5, integer: -6 }); + if (mixed.floating != 5.5 || mixed.integer != -6) { return 7; } + let triple: Triple = c_triple(Triple { first: 31, second: 32, third: 33 }); + if (triple.first != 31 || triple.second != 32 || triple.third != 33) { return 8; } + if (c_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) { return 9; } + + let export_status: i32 = c_check_wave_exports(); + if (export_status != 0) { return 20 + export_status; } + return 0; +} diff --git a/tests/fixtures/riscv64_psabi/abi_modes.c b/tests/fixtures/riscv64_psabi/abi_modes.c new file mode 100644 index 00000000..449e3998 --- /dev/null +++ b/tests/fixtures/riscv64_psabi/abi_modes.c @@ -0,0 +1,42 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed int i32; +typedef unsigned int u32; +typedef unsigned long u64; + +union f32_bits { + float value; + u32 bits; +}; + +union f64_bits { + double value; + u64 bits; +}; + +extern float wave_roundtrip_f32(float value); +extern double wave_roundtrip_f64(double value); + +float c_identity_f32(float value) { return value; } +double c_identity_f64(double value) { return value; } + +__attribute__((noreturn)) void _start(void) { + union f32_bits f32 = {.bits = 0x3fc00000U}; + union f64_bits f64 = {.bits = 0x4004000000000000UL}; + f32.value = wave_roundtrip_f32(f32.value); + f64.value = wave_roundtrip_f64(f64.value); + + i32 status = 0; + if (f32.bits != 0x3fc00000U) + status = 1; + if (f64.bits != 0x4004000000000000UL) + status = 2; + + register long exit_code __asm__("a0") = status; + register long syscall_number __asm__("a7") = 93; + __asm__ volatile("ecall" : : "r"(exit_code), "r"(syscall_number) : "memory"); + __builtin_unreachable(); +} diff --git a/tests/fixtures/riscv64_psabi/abi_modes.wave b/tests/fixtures/riscv64_psabi/abi_modes.wave new file mode 100644 index 00000000..85075aaa --- /dev/null +++ b/tests/fixtures/riscv64_psabi/abi_modes.wave @@ -0,0 +1,19 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +extern(c) fun c_identity_f32(value: f32) -> f32; +extern(c) fun c_identity_f64(value: f64) -> f64; + +export(c) fun wave_roundtrip_f32(value: f32) -> f32 { + return c_identity_f32(value); +} + +export(c) fun wave_roundtrip_f64(value: f64) -> f64 { + return c_identity_f64(value); +} + +fun main() -> i32 { + return 0; +} diff --git a/tests/fixtures/riscv64_psabi/interop.c b/tests/fixtures/riscv64_psabi/interop.c new file mode 100644 index 00000000..bbe766ea --- /dev/null +++ b/tests/fixtures/riscv64_psabi/interop.c @@ -0,0 +1,236 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed char i8; +typedef unsigned char u8; +typedef signed short i16; +typedef unsigned short u16; +typedef signed int i32; +typedef unsigned int u32; +typedef signed long i64; +typedef unsigned long u64; + +#define U32_MAX 4294967295U + +void *memcpy(void *destination, const void *source, u64 count) { + u8 *destination_bytes = (u8 *)destination; + const u8 *source_bytes = (const u8 *)source; + for (u64 index = 0; index < count; ++index) + destination_bytes[index] = source_bytes[index]; + return destination; +} + +struct empty {}; + +struct one { + u64 value; +}; + +struct pair { + u64 first; + u64 second; +}; + +struct floats { + double first; + double second; +}; + +struct float_array { + float values[2]; +}; + +struct mixed { + double floating; + i64 integer; +}; + +struct mixed_reverse { + i64 integer; + double floating; +}; + +struct triple { + u64 first; + u64 second; + u64 third; +}; + +struct padded { + u8 small; + u64 wide; +}; + +struct nested { + u16 head; + struct padded body; + u32 tail; +}; + +struct arrayed { + u16 values[3]; + u64 tail; +}; + +struct nine { + u8 values[9]; +}; + +struct twelve { + u32 first; + u32 second; + u32 third; +}; + +_Static_assert(sizeof(void *) == 8, "RV64 pointers must be 8 bytes"); +_Static_assert(_Alignof(void *) == 8, "RV64 pointers must be 8-byte aligned"); +_Static_assert(sizeof(struct padded) == 16, "unexpected padded layout"); +_Static_assert(sizeof(struct nested) == 32, "unexpected nested layout"); +_Static_assert(sizeof(struct arrayed) == 16, "unexpected array layout"); +_Static_assert(sizeof(struct nine) == 9, "unexpected 9-byte layout"); +_Static_assert(sizeof(struct twelve) == 12, "unexpected 12-byte layout"); + +extern i8 wave_i8(i8 value); +extern u8 wave_u8(u8 value); +extern i16 wave_i16(i16 value); +extern u16 wave_u16(u16 value); +extern i32 wave_i32(i32 value); +extern u32 wave_u32(u32 value); +extern struct one wave_one(struct one value); +extern struct pair wave_pair(struct pair value); +extern struct floats wave_floats(struct floats value); +extern struct float_array wave_float_array(struct float_array value); +extern struct mixed wave_mixed(struct mixed value); +extern struct mixed_reverse wave_mixed_reverse(struct mixed_reverse value); +extern struct triple wave_triple(struct triple value); +extern u8 *wave_pointer(u8 *value); +extern struct nested wave_nested(struct nested value); +extern struct arrayed wave_arrayed(struct arrayed value); +extern struct nine wave_nine(struct nine value); +extern struct twelve wave_twelve(struct twelve value); +extern i64 wave_stack_ten(i64, i64, i64, i64, i64, i64, i64, i64, i64, i64); +extern struct empty wave_empty(struct empty value); + +i8 c_i8(i8 value) { return value; } +u8 c_u8(u8 value) { return value; } +i16 c_i16(i16 value) { return value; } +u16 c_u16(u16 value) { return value; } +i32 c_i32(i32 value) { return value; } +u32 c_u32(u32 value) { return value; } +struct one c_one(struct one value) { return value; } +struct pair c_pair(struct pair value) { return value; } +struct floats c_floats(struct floats value) { return value; } +struct float_array c_float_array(struct float_array value) { return value; } +struct mixed c_mixed(struct mixed value) { return value; } +struct mixed_reverse c_mixed_reverse(struct mixed_reverse value) { return value; } +struct triple c_triple(struct triple value) { return value; } +u8 *c_pointer(u8 *value) { return value; } +struct nested c_nested(struct nested value) { return value; } +struct arrayed c_arrayed(struct arrayed value) { return value; } +struct nine c_nine(struct nine value) { return value; } +struct twelve c_twelve(struct twelve value) { return value; } +struct empty c_empty(struct empty value) { return value; } + +i64 c_stack_ten(i64 a0, i64 a1, i64 a2, i64 a3, i64 a4, + i64 a5, i64 a6, i64 a7, i64 a8, i64 a9) { + register u64 stack_pointer __asm__("sp"); + if ((stack_pointer & 15) != 0) + return -1; + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +i64 c_variadic_sum(i32 count, ...) { + __builtin_va_list arguments; + __builtin_va_start(arguments, count); + i64 sum = 0; + for (i32 index = 0; index < count; ++index) + sum += __builtin_va_arg(arguments, i64); + __builtin_va_end(arguments); + return sum; +} + +double c_variadic_f64(i32 count, ...) { + __builtin_va_list arguments; + __builtin_va_start(arguments, count); + double sum = 0.0; + for (i32 index = 0; index < count; ++index) + sum += __builtin_va_arg(arguments, double); + __builtin_va_end(arguments); + return sum; +} + +i64 c_variadic_promotions(i32 count, ...) { + __builtin_va_list arguments; + __builtin_va_start(arguments, count); + i64 sum = 0; + for (i32 index = 0; index < count; ++index) + sum += __builtin_va_arg(arguments, i32); + __builtin_va_end(arguments); + return sum; +} + +i32 c_check_wave_exports(void) { + wave_empty((struct empty){}); + struct one one = wave_one((struct one){7}); + struct pair pair = wave_pair((struct pair){11, 22}); + struct floats floats = wave_floats((struct floats){1.5, 2.5}); + struct float_array float_array = wave_float_array((struct float_array){{1.25f, 2.75f}}); + struct mixed mixed = wave_mixed((struct mixed){3.5, -7}); + struct mixed_reverse mixed_reverse = wave_mixed_reverse((struct mixed_reverse){-8, 4.5}); + struct triple triple = wave_triple((struct triple){31, 32, 33}); + struct nested nested = wave_nested( + (struct nested){41, {42, 43}, 44}); + struct arrayed arrayed = wave_arrayed( + (struct arrayed){{51, 52, 53}, 54}); + struct nine nine = wave_nine((struct nine){{61, 62, 63, 64, 65, 66, 67, 68, 69}}); + struct twelve twelve = wave_twelve((struct twelve){71, 72, 73}); + + if (wave_i8(-128) != -128 || wave_u8(255) != 255) + return 1; + if (wave_i16(-32768) != -32768 || wave_u16(65535) != 65535) + return 2; + if (wave_i32(-2147483647 - 1) != (-2147483647 - 1)) + return 3; + if (wave_u32(U32_MAX) != U32_MAX) + return 4; + if (one.value != 7) + return 5; + if (pair.first != 11 || pair.second != 22) + return 6; + if (floats.first != 1.5 || floats.second != 2.5) + return 7; + if (float_array.values[0] != 1.25f || float_array.values[1] != 2.75f) + return 16; + if (mixed.floating != 3.5 || mixed.integer != -7) + return 8; + if (mixed_reverse.integer != -8 || mixed_reverse.floating != 4.5) + return 17; + if (triple.first != 31 || triple.second != 32 || triple.third != 33) + return 9; + if (wave_pointer((u8 *)0) != (u8 *)0) + return 10; + if (nested.head != 41 || nested.body.small != 42 || + nested.body.wide != 43 || nested.tail != 44) + return 11; + if (arrayed.values[0] != 51 || arrayed.values[1] != 52 || + arrayed.values[2] != 53 || arrayed.tail != 54) + return 12; + if (wave_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) + return 13; + if (nine.values[0] != 61 || nine.values[8] != 69) + return 14; + if (twelve.first != 71 || twelve.second != 72 || twelve.third != 73) + return 15; + return 0; +} + +extern i32 main(void); + +__attribute__((noreturn)) void _start(void) { + register long exit_code __asm__("a0") = main(); + register long syscall_number __asm__("a7") = 93; + __asm__ volatile("ecall" : : "r"(exit_code), "r"(syscall_number) : "memory"); + __builtin_unreachable(); +} diff --git a/tests/fixtures/riscv64_psabi/interop.wave b/tests/fixtures/riscv64_psabi/interop.wave new file mode 100644 index 00000000..1153b67f --- /dev/null +++ b/tests/fixtures/riscv64_psabi/interop.wave @@ -0,0 +1,141 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +struct Empty {} +struct One { value: u64; } +struct Pair { first: u64; second: u64; } +struct Floats { first: f64; second: f64; } +struct FloatArray { values: array; } +struct Mixed { floating: f64; integer: i64; } +struct MixedReverse { integer: i64; floating: f64; } +struct Triple { first: u64; second: u64; third: u64; } +struct Padded { small: u8; wide: u64; } +struct Nested { head: u16; body: Padded; tail: u32; } +struct Arrayed { values: array; tail: u64; } +struct Nine { values: array; } +struct Twelve { first: u32; second: u32; third: u32; } + +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_u8(value: u8) -> u8; +extern(c) fun c_i16(value: i16) -> i16; +extern(c) fun c_u16(value: u16) -> u16; +extern(c) fun c_i32(value: i32) -> i32; +extern(c) fun c_u32(value: u32) -> u32; +extern(c) fun c_one(value: One) -> One; +extern(c) fun c_pair(value: Pair) -> Pair; +extern(c) fun c_floats(value: Floats) -> Floats; +extern(c) fun c_float_array(value: FloatArray) -> FloatArray; +extern(c) fun c_mixed(value: Mixed) -> Mixed; +extern(c) fun c_mixed_reverse(value: MixedReverse) -> MixedReverse; +extern(c) fun c_triple(value: Triple) -> Triple; +extern(c) fun c_pointer(value: ptr) -> ptr; +extern(c) fun c_nested(value: Nested) -> Nested; +extern(c) fun c_arrayed(value: Arrayed) -> Arrayed; +extern(c) fun c_nine(value: Nine) -> Nine; +extern(c) fun c_twelve(value: Twelve) -> Twelve; +extern(c) fun c_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64; +extern(c) fun c_variadic_sum(count: i32, ...) -> i64; +extern(c) fun c_variadic_f64(count: i32, ...) -> f64; +extern(c) fun c_variadic_promotions(count: i32, ...) -> i64; +extern(c) fun c_check_wave_exports() -> i32; +extern(c) fun c_empty(value: Empty) -> Empty; + +export(c) fun wave_i8(value: i8) -> i8 { return value; } +export(c) fun wave_u8(value: u8) -> u8 { return value; } +export(c) fun wave_i16(value: i16) -> i16 { return value; } +export(c) fun wave_u16(value: u16) -> u16 { return value; } +export(c) fun wave_i32(value: i32) -> i32 { return value; } +export(c) fun wave_u32(value: u32) -> u32 { return value; } +export(c) fun wave_one(value: One) -> One { return value; } +export(c) fun wave_pair(value: Pair) -> Pair { return value; } +export(c) fun wave_floats(value: Floats) -> Floats { return value; } +export(c) fun wave_float_array(value: FloatArray) -> FloatArray { return value; } +export(c) fun wave_mixed(value: Mixed) -> Mixed { return value; } +export(c) fun wave_mixed_reverse(value: MixedReverse) -> MixedReverse { return value; } +export(c) fun wave_triple(value: Triple) -> Triple { return value; } +export(c) fun wave_pointer(value: ptr) -> ptr { return value; } +export(c) fun wave_nested(value: Nested) -> Nested { return value; } +export(c) fun wave_arrayed(value: Arrayed) -> Arrayed { return value; } +export(c) fun wave_nine(value: Nine) -> Nine { return value; } +export(c) fun wave_twelve(value: Twelve) -> Twelve { return value; } +export(c) fun wave_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64 { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} +export(c) fun wave_empty(value: Empty) -> Empty { return value; } + +fun main() -> i32 { + let empty: Empty = c_empty(Empty {}); + if (c_i8(-128) != -128) { return 1; } + if (c_u8(255) != 255) { return 2; } + if (c_i16(-32768) != -32768) { return 3; } + if (c_u16(65535) != 65535) { return 4; } + if (c_i32(-2147483648) != -2147483648) { return 5; } + if (c_u32(4294967295) != 4294967295) { return 6; } + + let one: One = c_one(One { value: 7 }); + if (one.value != 7) { return 7; } + + let pair: Pair = c_pair(Pair { first: 11, second: 22 }); + if (pair.first != 11 || pair.second != 22) { return 8; } + + let floats: Floats = c_floats(Floats { first: 1.5, second: 2.5 }); + if (floats.first != 1.5 || floats.second != 2.5) { return 9; } + + let float_array: FloatArray = c_float_array(FloatArray { values: [1.25, 2.75] }); + if (float_array.values[0] != 1.25 || float_array.values[1] != 2.75) { return 24; } + + let mixed: Mixed = c_mixed(Mixed { floating: 3.5, integer: -7 }); + if (mixed.floating != 3.5 || mixed.integer != -7) { return 10; } + + let mixed_reverse: MixedReverse = c_mixed_reverse(MixedReverse { integer: -8, floating: 4.5 }); + if (mixed_reverse.integer != -8 || mixed_reverse.floating != 4.5) { return 25; } + + let triple: Triple = c_triple(Triple { first: 31, second: 32, third: 33 }); + if (triple.first != 31 || triple.second != 32 || triple.third != 33) { return 11; } + + if (c_pointer(null) != null) { return 12; } + + let nested: Nested = c_nested(Nested { + head: 41, + body: Padded { small: 42, wide: 43 }, + tail: 44 + }); + if (nested.head != 41 || nested.body.small != 42 || nested.body.wide != 43 || nested.tail != 44) { + return 13; + } + + let arrayed: Arrayed = c_arrayed(Arrayed { values: [51, 52, 53], tail: 54 }); + if (arrayed.values[0] != 51) { return 14; } + if (arrayed.values[1] != 52) { return 15; } + if (arrayed.values[2] != 53) { return 16; } + if (arrayed.tail != 54) { return 17; } + + let nine: Nine = c_nine(Nine { values: [61, 62, 63, 64, 65, 66, 67, 68, 69] }); + if (nine.values[0] != 61 || nine.values[8] != 69) { return 18; } + + let twelve: Twelve = c_twelve(Twelve { first: 71, second: 72, third: 73 }); + if (twelve.first != 71 || twelve.second != 72 || twelve.third != 73) { return 19; } + + if (c_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) { return 20; } + if (c_variadic_sum(10, 1 as i64, 2 as i64, 3 as i64, 4 as i64, 5 as i64, 6 as i64, 7 as i64, 8 as i64, 9 as i64, 10 as i64) != 55) { + return 21; + } + let first_float: f64 = 1.25; + let second_float: f64 = 2.5; + let third_float: f64 = 3.75; + let promoted_float: f32 = 0.5; + if (c_variadic_f64(4, first_float, second_float, third_float, promoted_float) != 8.0) { return 22; } + let signed_small: i8 = -3; + let unsigned_small: u8 = 250; + let signed_short: i16 = -4; + let unsigned_short: u16 = 65000; + if (c_variadic_promotions(4, signed_small, unsigned_small, signed_short, unsigned_short) != 65243) { + return 23; + } + + let export_status: i32 = c_check_wave_exports(); + if (export_status != 0) { return 30 + export_status; } + return 0; +} diff --git a/tests/fixtures/x86_64_sysv/interop.c b/tests/fixtures/x86_64_sysv/interop.c new file mode 100644 index 00000000..5fcd33fb --- /dev/null +++ b/tests/fixtures/x86_64_sysv/interop.c @@ -0,0 +1,87 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed char i8; +typedef unsigned int u32; +typedef signed int i32; +typedef signed long i64; +typedef unsigned long u64; + +void *memcpy(void *destination, const void *source, u64 count) { + unsigned char *out = (unsigned char *)destination; + const unsigned char *in = (const unsigned char *)source; + for (u64 index = 0; index < count; ++index) + out[index] = in[index]; + return destination; +} + +struct pair { u64 first; u64 second; }; +struct floats { double first; double second; }; +struct mixed { double floating; i64 integer; }; +struct triple { u64 first; u64 second; u64 third; }; + +_Static_assert(sizeof(void *) == 8, "x86_64 pointers must be 8 bytes"); +_Static_assert(sizeof(struct pair) == 16, "unexpected pair layout"); +_Static_assert(sizeof(struct mixed) == 16, "unexpected mixed layout"); +_Static_assert(sizeof(struct triple) == 24, "unexpected triple layout"); + +extern i8 wave_i8(i8); +extern u32 wave_u32(u32); +extern float wave_f32(float); +extern double wave_f64(double); +extern struct pair wave_pair(struct pair); +extern struct floats wave_floats(struct floats); +extern struct mixed wave_mixed(struct mixed); +extern struct triple wave_triple(struct triple); +extern i64 wave_stack_ten(i64, i64, i64, i64, i64, i64, i64, i64, i64, i64); + +i8 c_i8(i8 value) { return value; } +u32 c_u32(u32 value) { return value; } +float c_f32(float value) { return value; } +double c_f64(double value) { return value; } +struct pair c_pair(struct pair value) { return value; } +struct floats c_floats(struct floats value) { return value; } +struct mixed c_mixed(struct mixed value) { return value; } +struct triple c_triple(struct triple value) { return value; } + +i64 c_stack_ten(i64 a0, i64 a1, i64 a2, i64 a3, i64 a4, + i64 a5, i64 a6, i64 a7, i64 a8, i64 a9) { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +i32 c_check_wave_exports(void) { + struct pair pair = wave_pair((struct pair){11, 22}); + struct floats floats = wave_floats((struct floats){3.5, 4.5}); + struct mixed mixed = wave_mixed((struct mixed){5.5, -6}); + struct triple triple = wave_triple((struct triple){31, 32, 33}); + + if (wave_i8(-128) != -128 || wave_u32(4294967295U) != 4294967295U) + return 1; + if (wave_f32(1.5f) != 1.5f || wave_f64(2.5) != 2.5) + return 2; + if (pair.first != 11 || pair.second != 22) + return 3; + if (floats.first != 3.5 || floats.second != 4.5) + return 4; + if (mixed.floating != 5.5 || mixed.integer != -6) + return 5; + if (triple.first != 31 || triple.second != 32 || triple.third != 33) + return 6; + if (wave_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) + return 7; + return 0; +} + +extern i32 main(void); + +__attribute__((force_align_arg_pointer, noreturn)) void _start(void) { + register long exit_code __asm__("rdi") = main(); + register long syscall_number __asm__("rax") = 60; + __asm__ volatile("syscall" + : + : "r"(exit_code), "r"(syscall_number) + : "rcx", "r11", "memory"); + __builtin_unreachable(); +} diff --git a/tests/fixtures/x86_64_sysv/interop.wave b/tests/fixtures/x86_64_sysv/interop.wave new file mode 100644 index 00000000..bbb3186e --- /dev/null +++ b/tests/fixtures/x86_64_sysv/interop.wave @@ -0,0 +1,53 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +struct Pair { first: u64; second: u64; } +struct Floats { first: f64; second: f64; } +struct Mixed { floating: f64; integer: i64; } +struct Triple { first: u64; second: u64; third: u64; } + +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_u32(value: u32) -> u32; +extern(c) fun c_f32(value: f32) -> f32; +extern(c) fun c_f64(value: f64) -> f64; +extern(c) fun c_pair(value: Pair) -> Pair; +extern(c) fun c_floats(value: Floats) -> Floats; +extern(c) fun c_mixed(value: Mixed) -> Mixed; +extern(c) fun c_triple(value: Triple) -> Triple; +extern(c) fun c_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64; +extern(c) fun c_check_wave_exports() -> i32; + +export(c) fun wave_i8(value: i8) -> i8 { return value; } +export(c) fun wave_u32(value: u32) -> u32 { return value; } +export(c) fun wave_f32(value: f32) -> f32 { return value; } +export(c) fun wave_f64(value: f64) -> f64 { return value; } +export(c) fun wave_pair(value: Pair) -> Pair { return value; } +export(c) fun wave_floats(value: Floats) -> Floats { return value; } +export(c) fun wave_mixed(value: Mixed) -> Mixed { return value; } +export(c) fun wave_triple(value: Triple) -> Triple { return value; } +export(c) fun wave_stack_ten(a0: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64, a7: i64, a8: i64, a9: i64) -> i64 { + return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9; +} + +fun main() -> i32 { + if (c_i8(-128) != -128) { return 1; } + if (c_u32(4294967295) != 4294967295) { return 2; } + if (c_f32(1.5) != 1.5) { return 3; } + if (c_f64(2.5) != 2.5) { return 4; } + + let pair: Pair = c_pair(Pair { first: 11, second: 22 }); + if (pair.first != 11 || pair.second != 22) { return 5; } + let floats: Floats = c_floats(Floats { first: 3.5, second: 4.5 }); + if (floats.first != 3.5 || floats.second != 4.5) { return 6; } + let mixed: Mixed = c_mixed(Mixed { floating: 5.5, integer: -6 }); + if (mixed.floating != 5.5 || mixed.integer != -6) { return 7; } + let triple: Triple = c_triple(Triple { first: 31, second: 32, third: 33 }); + if (triple.first != 31 || triple.second != 32 || triple.third != 33) { return 8; } + if (c_stack_ten(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) != 55) { return 9; } + + let export_status: i32 = c_check_wave_exports(); + if (export_status != 0) { return 20 + export_status; } + return 0; +} diff --git a/tools/run_tests.py b/tools/run_tests.py index 776cc716..5e533626 100644 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -25,7 +25,7 @@ import errno ROOT = Path(__file__).resolve().parent.parent -TEST_DIR = ROOT / "test" +TEST_DIR = ROOT / "tests" / "cases" TIMEOUT_SEC = 5 From b38fb922e57247139723f3bed041b96a4f02b770 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Wed, 12 Aug 2026 17:18:06 +0900 Subject: [PATCH 2/3] Complete RISC-V psABI audits and bundle Linux CRTs Correct C ABI lowering for odd-sized aggregates by separating object layout from architecture-specific transport types for parameters and returns. Drive C variadic default promotions from semantic expression types, preserve signed and unsigned extension rules, reject untyped null arguments, allow explicitly typed null pointers, and safely discard scalar and aggregate call results. Apply narrow integer ABI attributes consistently across x86_64 SysV, Darwin, AArch64 Darwin, Windows GNU, and RISC-V targets, with Clang IR contract comparisons and bidirectional C interoperability fixtures. Add pre-link RISC-V ELF and archive ABI validation for LP64, LP64F, and LP64D inputs, including archive member diagnostics, before invoking the external linker. Bundle Wave-owned Linux crt1, Scrt1, rcrt1, crti, and crtn objects for x86_64 and AArch64 and for every supported RISC-V floating-point ABI. Assemble them from repository sources at Cargo build and package time, select them before any system CRT, and preserve the target ELF machine and RISC-V e_flags contracts. Expand CI and regression coverage for CRT packaging, irregular aggregate sizes, variadic expressions, discarded returns, cross-target runtime discovery, ARM64 UDP execution, and x86_64/AArch64/RISC-V C interop. Make completed architecture jobs fail visibly and isolate parallel test temporary directories. --- .github/workflows/rust.yml | 40 +- front/parser/src/verification.rs | 21 +- llvm/build.rs | 178 ++++++++ llvm/crt/linux/aarch64/crt1.s | 61 +++ llvm/crt/linux/aarch64/crti.s | 29 ++ llvm/crt/linux/aarch64/crtn.s | 15 + llvm/crt/linux/riscv64/crt1.s | 70 +++ llvm/crt/linux/riscv64/crti.s | 29 ++ llvm/crt/linux/riscv64/crtn.s | 15 + llvm/crt/linux/x86_64/crt1.s | 7 + llvm/crt/linux/x86_64/crti.s | 29 ++ llvm/crt/linux/x86_64/crtn.s | 15 + llvm/src/codegen/abi_c.rs | 86 ++-- llvm/src/codegen/ir.rs | 43 +- llvm/src/codegen/mod.rs | 1 + llvm/src/codegen/semantic.rs | 28 ++ llvm/src/expression/rvalue/calls.rs | 141 ++++-- llvm/src/toolchain.rs | 99 ++++- src/cli.rs | 166 ++++--- src/lib.rs | 1 + src/link_validation/elf.rs | 224 ++++++++++ src/link_validation/mod.rs | 17 + src/link_validation/riscv.rs | 122 ++++++ tests/cases/test100.wave | 2 +- tests/cases/test61.wave | 2 +- tests/cases/test62.wave | 2 +- tests/codegen_regressions.rs | 554 +++++++++++++++++++++++- tests/fixtures/c_abi_edges/interop.c | 131 ++++++ tests/fixtures/c_abi_edges/interop.wave | 103 +++++ tests/fixtures/c_abi_edges/narrow.c | 18 + tools/run_tests.py | 26 +- x.py | 93 ++-- 32 files changed, 2145 insertions(+), 223 deletions(-) create mode 100644 llvm/build.rs create mode 100644 llvm/crt/linux/aarch64/crt1.s create mode 100644 llvm/crt/linux/aarch64/crti.s create mode 100644 llvm/crt/linux/aarch64/crtn.s create mode 100644 llvm/crt/linux/riscv64/crt1.s create mode 100644 llvm/crt/linux/riscv64/crti.s create mode 100644 llvm/crt/linux/riscv64/crtn.s create mode 100644 llvm/crt/linux/x86_64/crti.s create mode 100644 llvm/crt/linux/x86_64/crtn.s create mode 100644 llvm/src/codegen/semantic.rs create mode 100644 src/link_validation/elf.rs create mode 100644 src/link_validation/mod.rs create mode 100644 src/link_validation/riscv.rs create mode 100644 tests/fixtures/c_abi_edges/interop.c create mode 100644 tests/fixtures/c_abi_edges/interop.wave create mode 100644 tests/fixtures/c_abi_edges/narrow.c diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b22e8de8..fdd0c3c3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -68,6 +68,7 @@ jobs: run: cargo test --locked --all-targets --verbose - name: Run x86_64 SysV ABI contract tests + if: ${{ always() }} env: WAVE_RUN_X86_64_INTEROP_TESTS: "1" run: >- @@ -80,7 +81,6 @@ jobs: build-linux-arm64: name: Build Linux arm64 runs-on: ubuntu-24.04-arm - continue-on-error: true timeout-minutes: 45 steps: @@ -128,6 +128,7 @@ jobs: run: cargo test --locked --all-targets --verbose - name: Run AArch64 AAPCS64 contract tests + if: ${{ always() }} env: WAVE_RUN_AARCH64_INTEROP_TESTS: "1" run: >- @@ -135,12 +136,12 @@ jobs: aarch64_c_abi_interoperates_with_c --verbose - name: Run Wave end-to-end tests + if: ${{ always() }} run: python3 tools/run_tests.py build-linux-riscv64: name: Build Linux riscv64 runs-on: ubuntu-24.04 - continue-on-error: true timeout-minutes: 45 steps: @@ -184,9 +185,6 @@ jobs: riscv64-linux-gnu-gcc --version riscv64-linux-gnu-readelf --version qemu-riscv64 --version - test -f /usr/riscv64-linux-gnu/lib/crt1.o - test -f /usr/riscv64-linux-gnu/lib/libc.so - test -f /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1 - name: Install Wave stdlib run: | @@ -209,16 +207,48 @@ jobs: - name: Run Rust tests run: cargo test --locked --all-targets --verbose + - name: Verify bundled Linux CRT matrix + if: ${{ always() }} + run: | + set -euo pipefail + + crt_root="$(find target/release/build -type d -path '*/out/crt' -print -quit)" + test -n "$crt_root" + + for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do + for crt_name in crt1.o Scrt1.o rcrt1.o crti.o crtn.o; do + test -f "$crt_root/$target/$crt_name" + done + done + + for abi in lp64 lp64f lp64d; do + for crt_name in crt1.o Scrt1.o rcrt1.o crti.o crtn.o; do + crt_path="$crt_root/riscv64-unknown-linux-gnu/$abi/$crt_name" + test -f "$crt_path" + llvm-readelf -h "$crt_path" | grep -Eq 'Machine:[[:space:]]+RISC-V' + done + done + + llvm-readelf -h "$crt_root/riscv64-unknown-linux-gnu/lp64/crt1.o" \ + | grep -Eq 'Flags:[[:space:]]+0x1.*RVC' + llvm-readelf -h "$crt_root/riscv64-unknown-linux-gnu/lp64f/crt1.o" \ + | grep -Eq 'Flags:[[:space:]]+0x3.*single-float ABI' + llvm-readelf -h "$crt_root/riscv64-unknown-linux-gnu/lp64d/crt1.o" \ + | grep -Eq 'Flags:[[:space:]]+0x5.*double-float ABI' + - name: Run Wave end-to-end tests + if: ${{ always() }} run: python3 tools/run_tests.py - name: Run RISC-V contract tests + if: ${{ always() }} env: WAVE_RUN_RISCV64_INTEROP_TESTS: "1" run: >- cargo test --locked --test codegen_regressions riscv64_ --verbose - name: Link and run Linux riscv64 binary + if: ${{ always() }} run: | set -euo pipefail diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index aa9c3b3f..30415a6a 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -604,6 +604,7 @@ struct Validator<'a> { span_counts: HashMap<(SemanticSpanKind, String), usize>, primary_span: Option, diagnostic_help: Option, + expression_types: HashMap, } impl<'a> Validator<'a> { @@ -619,6 +620,7 @@ impl<'a> Validator<'a> { span_counts: HashMap::new(), primary_span: None, diagnostic_help: None, + expression_types: HashMap::new(), } } @@ -1157,6 +1159,17 @@ impl<'a> Validator<'a> { } fn validate_expr(&mut self, expression: &Expression) -> Result { + let result = self.validate_expr_inner(expression); + if let Ok(expression_type) = &result { + if let Some(ty) = canonical_expression_type(self.program, expression_type) { + self.expression_types + .insert(expression as *const Expression as usize, ty); + } + } + result + } + + fn validate_expr_inner(&mut self, expression: &Expression) -> Result { match expression { Expression::Literal(literal) => Ok(match literal { Literal::Int(raw) => ExpressionType::IntLiteral(raw.clone()), @@ -2532,6 +2545,12 @@ pub fn validate_program(nodes: &Vec) -> Result<(), String> { } pub fn validate_program_detailed(nodes: &[ASTNode]) -> Result<(), SemanticDiagnostic> { + analyze_expression_types(nodes).map(|_| ()) +} + +pub fn analyze_expression_types( + nodes: &[ASTNode], +) -> Result, SemanticDiagnostic> { let program = ProgramTypes::collect(nodes).map_err(|(index, message, primary)| { semantic_diagnostic_for_top_level(nodes, index, message, primary) })?; @@ -2607,7 +2626,7 @@ pub fn validate_program_detailed(nodes: &[ASTNode]) -> Result<(), SemanticDiagno } } - Ok(()) + Ok(validator.expression_types) } fn top_level_span_hint(node: &ASTNode) -> SemanticSpanHint { diff --git a/llvm/build.rs b/llvm/build.rs new file mode 100644 index 00000000..a4c82d73 --- /dev/null +++ b/llvm/build.rs @@ -0,0 +1,178 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const CRT_OBJECTS: &[(&str, &str)] = &[ + ("crt1.o", "crt1.s"), + ("Scrt1.o", "crt1.s"), + ("rcrt1.o", "crt1.s"), + ("crti.o", "crti.s"), + ("crtn.o", "crtn.s"), +]; + +struct CrtSpec { + feature: &'static str, + target: &'static str, + source: &'static str, + abi: Option<&'static str>, + attributes: Option<&'static str>, +} + +fn main() { + println!("cargo:rerun-if-env-changed=WAVE_LLVM_MC"); + println!("cargo:rerun-if-env-changed=LLVM_CONFIG_PATH"); + println!("cargo:rerun-if-env-changed=LLVM_SYS_211_PREFIX"); + for architecture in ["x86_64", "aarch64", "riscv64"] { + for source in ["crt1.s", "crti.s", "crtn.s"] { + println!("cargo:rerun-if-changed=crt/linux/{architecture}/{source}"); + } + } + + let output_root = + PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")).join("crt"); + let llvm_mc = find_llvm_mc(); + let build_all = env::var_os("CARGO_FEATURE_LLVM_TARGET_ALL").is_some(); + + let specs = [ + CrtSpec { + feature: "CARGO_FEATURE_LLVM_TARGET_X86", + target: "x86_64-unknown-linux-gnu", + source: "crt/linux/x86_64/crt1.s", + abi: None, + attributes: None, + }, + CrtSpec { + feature: "CARGO_FEATURE_LLVM_TARGET_AARCH64", + target: "aarch64-unknown-linux-gnu", + source: "crt/linux/aarch64/crt1.s", + abi: None, + attributes: None, + }, + CrtSpec { + feature: "CARGO_FEATURE_LLVM_TARGET_RISCV", + target: "riscv64-unknown-linux-gnu", + source: "crt/linux/riscv64/crt1.s", + abi: Some("lp64"), + attributes: Some("+m,+a,+c,+zicsr,+zifencei"), + }, + CrtSpec { + feature: "CARGO_FEATURE_LLVM_TARGET_RISCV", + target: "riscv64-unknown-linux-gnu", + source: "crt/linux/riscv64/crt1.s", + abi: Some("lp64f"), + attributes: Some("+m,+a,+f,+c,+zicsr,+zifencei"), + }, + CrtSpec { + feature: "CARGO_FEATURE_LLVM_TARGET_RISCV", + target: "riscv64-unknown-linux-gnu", + source: "crt/linux/riscv64/crt1.s", + abi: Some("lp64d"), + attributes: Some("+m,+a,+f,+d,+c,+zicsr,+zifencei"), + }, + ]; + + for spec in specs { + if build_all || env::var_os(spec.feature).is_some() { + build_crt(&llvm_mc, &output_root, &spec); + } + } + + println!( + "cargo:rustc-env=WAVE_BUILD_CRT_DIR={}", + output_root.display() + ); +} + +fn build_crt(llvm_mc: &OsString, output_root: &Path, spec: &CrtSpec) { + let source_dir = Path::new(spec.source) + .parent() + .expect("Linux CRT source has an architecture directory"); + let mut output_dir = output_root.join(spec.target); + if let Some(abi) = spec.abi { + output_dir.push(abi); + } + fs::create_dir_all(&output_dir).unwrap_or_else(|error| { + panic!( + "failed to create Linux CRT output directory '{}': {}", + output_dir.display(), + error + ) + }); + + for (object_name, source_name) in CRT_OBJECTS { + let source = source_dir.join(source_name); + let output = output_dir.join(object_name); + let mut command = Command::new(llvm_mc); + command + .arg(format!("-triple={}", spec.target)) + .arg("-filetype=obj"); + if let Some(attributes) = spec.attributes { + command.arg(format!("-mattr={attributes}")); + } + let result = command.arg(&source).arg("-o").arg(&output).output(); + match result { + Ok(result) if result.status.success() => {} + Ok(result) => panic!( + "failed to assemble Linux CRT '{}' for '{}': {}", + source.display(), + spec.target, + String::from_utf8_lossy(&result.stderr).trim() + ), + Err(error) => panic!( + "failed to execute llvm-mc while assembling Linux CRT for '{}': {}", + spec.target, error + ), + } + } +} + +fn find_llvm_mc() -> OsString { + if let Some(path) = env::var_os("WAVE_LLVM_MC") { + return path; + } + + if let Some(prefix) = env::var_os("LLVM_SYS_211_PREFIX") { + let candidate = llvm_tool_in(Path::new(&prefix).join("bin"), "llvm-mc"); + if candidate.is_file() { + return candidate.into_os_string(); + } + } + + if let Some(config) = env::var_os("LLVM_CONFIG_PATH") { + let config = PathBuf::from(config); + if let Some(bin_dir) = config.parent() { + let candidate = llvm_tool_in(bin_dir, "llvm-mc"); + if candidate.is_file() { + return candidate.into_os_string(); + } + } + } + + OsString::from(if cfg!(windows) { + "llvm-mc.exe" + } else { + "llvm-mc" + }) +} + +fn llvm_tool_in(directory: impl AsRef, name: &str) -> PathBuf { + directory.as_ref().join(if cfg!(windows) { + format!("{name}.exe") + } else { + name.to_string() + }) +} diff --git a/llvm/crt/linux/aarch64/crt1.s b/llvm/crt/linux/aarch64/crt1.s new file mode 100644 index 00000000..5ed3e41e --- /dev/null +++ b/llvm/crt/linux/aarch64/crt1.s @@ -0,0 +1,61 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + .text + .globl _start + .type _start,%function +_start: + .cfi_startproc + .cfi_undefined x30 + mov x29, xzr + mov x30, xzr + + # Linux AArch64 enters with x0 holding the dynamic loader finalizer and + # sp pointing at argc followed by argv, envp, and the auxiliary vector. + mov x5, x0 + ldr x1, [sp] + add x2, sp, #8 + mov x6, sp + mov x3, xzr + mov x4, xzr + + adrp x0, __wave_main_trampoline + add x0, x0, :lo12:__wave_main_trampoline + bl __libc_start_main + brk #0 + .cfi_endproc + .size _start, .-_start + + .type __wave_main_trampoline,%function +__wave_main_trampoline: + .cfi_startproc + stp x29, x30, [sp, #-16]! + .cfi_def_cfa_offset 16 + .cfi_offset x29, -16 + .cfi_offset x30, -8 + mov x29, sp + bl main + mov w0, wzr + ldp x29, x30, [sp], #16 + .cfi_def_cfa_offset 0 + ret + .cfi_endproc + .size __wave_main_trampoline, .-__wave_main_trampoline + + .data + .globl __data_start +__data_start: + .xword 0 + .weak data_start + .set data_start, __data_start + + .section .note.GNU-stack,"",%progbits diff --git a/llvm/crt/linux/aarch64/crti.s b/llvm/crt/linux/aarch64/crti.s new file mode 100644 index 00000000..20993666 --- /dev/null +++ b/llvm/crt/linux/aarch64/crti.s @@ -0,0 +1,29 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + .section .init,"ax",%progbits + .globl _init + .hidden _init + .type _init,%function +_init: + ret + .size _init, .-_init + + .section .fini,"ax",%progbits + .globl _fini + .hidden _fini + .type _fini,%function +_fini: + ret + .size _fini, .-_fini + + .section .note.GNU-stack,"",%progbits diff --git a/llvm/crt/linux/aarch64/crtn.s b/llvm/crt/linux/aarch64/crtn.s new file mode 100644 index 00000000..8e9278ba --- /dev/null +++ b/llvm/crt/linux/aarch64/crtn.s @@ -0,0 +1,15 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + # End marker for the Wave-owned Linux CRT sequence. Wave's crti.o + # provides complete _init and _fini functions, so no epilogue is needed. + .section .note.GNU-stack,"",%progbits diff --git a/llvm/crt/linux/riscv64/crt1.s b/llvm/crt/linux/riscv64/crt1.s new file mode 100644 index 00000000..b5348726 --- /dev/null +++ b/llvm/crt/linux/riscv64/crt1.s @@ -0,0 +1,70 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + .text + .globl _start + .type _start,@function +_start: + .cfi_startproc + .cfi_undefined ra + call .Lwave_load_gp + + # Linux RISC-V enters with a0 holding the dynamic loader finalizer and + # sp pointing at argc followed by argv, envp, and the auxiliary vector. + mv a5, a0 + lla a0, __wave_main_trampoline + ld a1, 0(sp) + addi a2, sp, 8 + andi sp, sp, -16 + li a3, 0 + li a4, 0 + mv a6, sp + call __libc_start_main@plt + ebreak + .cfi_endproc + .size _start, .-_start + + .type __wave_main_trampoline,@function +__wave_main_trampoline: + .cfi_startproc + addi sp, sp, -16 + .cfi_def_cfa_offset 16 + sd ra, 8(sp) + .cfi_offset ra, -8 + call main + ld ra, 8(sp) + addi sp, sp, 16 + .cfi_def_cfa_offset 0 + li a0, 0 + ret + .cfi_endproc + .size __wave_main_trampoline, .-__wave_main_trampoline + +.Lwave_load_gp: + .option push + .option norelax + lla gp, __global_pointer$ + .option pop + ret + + .section .preinit_array,"aw",@preinit_array + .p2align 3 + .dword .Lwave_load_gp + + .data + .globl __data_start +__data_start: + .dword 0 + .weak data_start + .set data_start, __data_start + + .section .note.GNU-stack,"",@progbits diff --git a/llvm/crt/linux/riscv64/crti.s b/llvm/crt/linux/riscv64/crti.s new file mode 100644 index 00000000..21f52bd5 --- /dev/null +++ b/llvm/crt/linux/riscv64/crti.s @@ -0,0 +1,29 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + .section .init,"ax",@progbits + .globl _init + .hidden _init + .type _init,@function +_init: + ret + .size _init, .-_init + + .section .fini,"ax",@progbits + .globl _fini + .hidden _fini + .type _fini,@function +_fini: + ret + .size _fini, .-_fini + + .section .note.GNU-stack,"",@progbits diff --git a/llvm/crt/linux/riscv64/crtn.s b/llvm/crt/linux/riscv64/crtn.s new file mode 100644 index 00000000..82ff6664 --- /dev/null +++ b/llvm/crt/linux/riscv64/crtn.s @@ -0,0 +1,15 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + # End marker for the Wave-owned Linux CRT sequence. Wave's crti.o + # provides complete _init and _fini functions, so no epilogue is needed. + .section .note.GNU-stack,"",@progbits diff --git a/llvm/crt/linux/x86_64/crt1.s b/llvm/crt/linux/x86_64/crt1.s index 5b129683..d926586e 100644 --- a/llvm/crt/linux/x86_64/crt1.s +++ b/llvm/crt/linux/x86_64/crt1.s @@ -54,4 +54,11 @@ __wave_main_trampoline: .size __wave_main_trampoline, .-__wave_main_trampoline + .data + .globl __data_start +__data_start: + .quad 0 + .weak data_start + .set data_start, __data_start + .section .note.GNU-stack,"",@progbits diff --git a/llvm/crt/linux/x86_64/crti.s b/llvm/crt/linux/x86_64/crti.s new file mode 100644 index 00000000..21f52bd5 --- /dev/null +++ b/llvm/crt/linux/x86_64/crti.s @@ -0,0 +1,29 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + .section .init,"ax",@progbits + .globl _init + .hidden _init + .type _init,@function +_init: + ret + .size _init, .-_init + + .section .fini,"ax",@progbits + .globl _fini + .hidden _fini + .type _fini,@function +_fini: + ret + .size _fini, .-_fini + + .section .note.GNU-stack,"",@progbits diff --git a/llvm/crt/linux/x86_64/crtn.s b/llvm/crt/linux/x86_64/crtn.s new file mode 100644 index 00000000..82ff6664 --- /dev/null +++ b/llvm/crt/linux/x86_64/crtn.s @@ -0,0 +1,15 @@ +# This file is part of the Wave language project. +# Copyright (c) 2024-2026 Wave Foundation +# Copyright (c) 2024-2026 LunaStev and contributors +# +# This Source Code Form is subject to the terms of the +# Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + + # End marker for the Wave-owned Linux CRT sequence. Wave's crti.o + # provides complete _init and _fini functions, so no epilogue is needed. + .section .note.GNU-stack,"",@progbits diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index 87d025ec..c7f858bb 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -59,24 +59,30 @@ pub struct ExternCInfo<'ctx> { pub variadic_integer_extension: Option, } -fn riscv64_integer_extension(ty: &WaveType) -> Option { - match ty { - WaveType::Int(bits) if *bits <= 32 => Some(IntegerExtension::Sign), +fn integer_extension_for_target(target: CodegenTarget, ty: &WaveType) -> Option { + let narrow_extension = || match ty { + WaveType::Int(bits) if *bits < 32 => Some(IntegerExtension::Sign), WaveType::Uint(bits) if *bits < 32 => Some(IntegerExtension::Zero), - // RV64 widens unsigned 32-bit values to 32 bits and then sign-extends - // them to XLEN, just like signed 32-bit values. - WaveType::Uint(32) => Some(IntegerExtension::Sign), WaveType::Bool | WaveType::Byte | WaveType::Char => Some(IntegerExtension::Zero), _ => None, - } -} + }; -fn integer_extension_for_target(target: CodegenTarget, ty: &WaveType) -> Option { match target { - CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => { - riscv64_integer_extension(ty) - } - _ => None, + CodegenTarget::LinuxX86_64 + | CodegenTarget::DarwinX86_64 + | CodegenTarget::FreestandingX86_64 + | CodegenTarget::DarwinArm64 => narrow_extension(), + CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64 => match ty { + WaveType::Int(bits) if *bits <= 32 => Some(IntegerExtension::Sign), + WaveType::Uint(bits) if *bits < 32 => Some(IntegerExtension::Zero), + // RV64 widens u32 to 32 bits and then sign-extends it to XLEN. + WaveType::Uint(32) => Some(IntegerExtension::Sign), + WaveType::Bool | WaveType::Byte | WaveType::Char => Some(IntegerExtension::Zero), + _ => None, + }, + CodegenTarget::LinuxArm64 + | CodegenTarget::FreestandingArm64 + | CodegenTarget::WindowsX86_64Gnu => None, } } @@ -162,6 +168,12 @@ fn classify_param_x86_64_sysv<'ctx>( let mut leaves = vec![]; flatten_leaf_types(t, &mut leaves); + if size <= 8 && leaves.len() == 1 { + if let BasicTypeEnum::PointerType(pointer) = leaves[0] { + return ParamLowering::Direct(pointer.as_basic_type_enum()); + } + } + // homogeneous float aggregate let mut float_kind: Option = None; let mut all_float = true; @@ -287,6 +299,12 @@ fn classify_ret_x86_64_sysv<'ctx>( let mut leaves = vec![]; flatten_leaf_types(t, &mut leaves); + if size <= 8 && leaves.len() == 1 { + if let BasicTypeEnum::PointerType(pointer) = leaves[0] { + return RetLowering::Direct(pointer.as_basic_type_enum()); + } + } + let mut all_intlike = true; for lt in &leaves { match lt { @@ -484,18 +502,21 @@ fn classify_param_arm64<'ctx>( } if is_agg && !is_homogeneous_float_aggregate(td, t) { + let mut leaves = Vec::new(); + flatten_leaf_types(t, &mut leaves); + if size <= 8 && leaves.len() == 1 { + if let BasicTypeEnum::PointerType(pointer) = leaves[0] { + return ParamLowering::Direct(pointer.as_basic_type_enum()); + } + } if size <= 8 { - return ParamLowering::Direct( - context - .custom_width_int_type((size * 8) as u32) - .as_basic_type_enum(), - ); + // AAPCS64 transports a non-HFA aggregate occupying at most one + // general-purpose register in a full 64-bit ABI slot. The object + // representation remains its original (possibly odd) byte size. + return ParamLowering::Direct(context.i64_type().as_basic_type_enum()); } - return ParamLowering::Split(vec![ - context.i64_type().as_basic_type_enum(), - context.i64_type().as_basic_type_enum(), - ]); + return ParamLowering::Direct(context.i64_type().array_type(2).as_basic_type_enum()); } ParamLowering::Direct(t) @@ -536,17 +557,12 @@ fn classify_param_riscv64<'ctx>( if integer_only { if size <= 8 { - return ParamLowering::Direct( - context - .custom_width_int_type((size * 8) as u32) - .as_basic_type_enum(), - ); + // The RV64 psABI uses an XLEN-sized transport slot for an + // integer aggregate that fits in one argument register. + return ParamLowering::Direct(context.i64_type().as_basic_type_enum()); } - return ParamLowering::Split(vec![ - context.i64_type().as_basic_type_enum(), - context.i64_type().as_basic_type_enum(), - ]); + return ParamLowering::Direct(context.i64_type().array_type(2).as_basic_type_enum()); } } @@ -632,11 +648,9 @@ fn classify_ret_riscv64<'ctx>( if integer_only { if size <= 8 { - return RetLowering::Direct( - context - .custom_width_int_type((size * 8) as u32) - .as_basic_type_enum(), - ); + // Keep the aggregate object size separate from its XLEN-sized + // ABI return transport representation. + return RetLowering::Direct(context.i64_type().as_basic_type_enum()); } return RetLowering::Direct(context.i64_type().array_type(2).as_basic_type_enum()); diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 82f8f3e2..27fba0cb 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -85,7 +85,7 @@ fn reinterpret_abi_value<'ctx>( target, BasicTypeEnum::ArrayType(_) | BasicTypeEnum::StructType(_) ); - if source_size != target_size && !(source_is_aggregate && target_is_aggregate) { + if source_size != target_size && !(source_is_aggregate || target_is_aggregate) { panic!( "cannot reinterpret C ABI value '{}' from {} bytes to {} bytes", tag, source_size, target_size @@ -519,10 +519,30 @@ fn build_module( codegen_trace("resolve named types"); let named_types = collect_named_types(ast_nodes); - let ast_nodes: Vec = ast_nodes + let mut ast_nodes: Vec = ast_nodes .iter() .map(|n| resolve_ast_node(n, &named_types)) .collect(); + let proto_functions: Vec = ast_nodes + .iter() + .filter_map(|node| match node { + ASTNode::ProtoImpl(implementation) => Some(implementation), + _ => None, + }) + .flat_map(|implementation| { + implementation.methods.iter().map(|method| { + let mut function = method.clone(); + function.name = format!("{}_{}", implementation.target, method.name); + ASTNode::Function(function) + }) + }) + .collect(); + ast_nodes.extend(proto_functions); + let semantic_types = + parser::verification::analyze_expression_types(&ast_nodes).unwrap_or_else(|diagnostic| { + panic!("semantic analysis before codegen failed: {diagnostic}") + }); + super::semantic::install_expression_types(semantic_types); codegen_trace("resolve target triple"); let triple = if let Some(raw) = &backend.target { @@ -715,31 +735,18 @@ fn build_module( ); } - let mut proto_functions: Vec<(String, FunctionNode)> = Vec::new(); - for ast in &ast_nodes { - if let ASTNode::ProtoImpl(proto_impl) = ast { - for method in &proto_impl.methods { - let new_name = format!("{}_{}", proto_impl.target, method.name); - let mut new_fn = method.clone(); - new_fn.name = new_name.clone(); - proto_functions.push((new_name, new_fn)); - } - } - } - let mut functions: HashMap = HashMap::new(); let mut export_wrappers: Vec = Vec::new(); - let function_nodes: Vec = ast_nodes + let function_nodes: Vec<&FunctionNode> = ast_nodes .iter() .filter_map(|ast| { if let ASTNode::Function(f) = ast { - Some(f.clone()) + Some(f) } else { None } }) - .chain(proto_functions.iter().map(|(_, f)| f.clone())) .collect(); let extern_functions: Vec<&ExternFunctionNode> = ast_nodes @@ -759,7 +766,7 @@ fn build_module( return_type, export, .. - } in &function_nodes + } in function_nodes.iter().copied() { if let Some(export) = export { if !is_supported_extern_abi(&export.abi) { diff --git a/llvm/src/codegen/mod.rs b/llvm/src/codegen/mod.rs index 386d2cec..d16f794a 100644 --- a/llvm/src/codegen/mod.rs +++ b/llvm/src/codegen/mod.rs @@ -18,6 +18,7 @@ pub mod format; pub mod ir; pub mod legacy; pub mod plan; +pub(crate) mod semantic; pub mod target; pub mod types; diff --git a/llvm/src/codegen/semantic.rs b/llvm/src/codegen/semantic.rs new file mode 100644 index 00000000..3a1f9663 --- /dev/null +++ b/llvm/src/codegen/semantic.rs @@ -0,0 +1,28 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +use parser::ast::{Expression, WaveType}; +use std::cell::RefCell; +use std::collections::HashMap; + +thread_local! { + static EXPRESSION_TYPES: RefCell> = RefCell::new(HashMap::new()); +} + +pub(super) fn install_expression_types(types: HashMap) { + EXPRESSION_TYPES.with(|current| *current.borrow_mut() = types); +} + +pub(crate) fn expression_type(expression: &Expression) -> Option { + let key = expression as *const Expression as usize; + EXPRESSION_TYPES.with(|types| types.borrow().get(&key).cloned()) +} diff --git a/llvm/src/expression/rvalue/calls.rs b/llvm/src/expression/rvalue/calls.rs index cb904b8f..be0b712d 100644 --- a/llvm/src/expression/rvalue/calls.rs +++ b/llvm/src/expression/rvalue/calls.rs @@ -20,7 +20,7 @@ use inkwell::types::{AnyTypeEnum, AsTypeRef, BasicMetadataTypeEnum, BasicType, B use inkwell::values::{ BasicMetadataValueEnum, BasicValue, BasicValueEnum, PointerValue, ValueKind, }; -use parser::ast::{Expression, Literal, WaveType}; +use parser::ast::{Expression, WaveType}; fn meta_to_basic<'ctx>(m: BasicMetadataTypeEnum<'ctx>) -> BasicTypeEnum<'ctx> { match m { @@ -83,6 +83,11 @@ fn pack_agg_to_int<'ctx, 'a>( .build_alloca(dst, &format!("{}_int_tmp", tag)) .unwrap(); + // The ABI transport slot may be wider than the aggregate object (for + // example a 3-byte RV64 aggregate transported in i64). Keep padding bits + // deterministic instead of loading uninitialized stack bytes. + env.builder.build_store(int_tmp, dst.const_zero()).unwrap(); + let bytes = env.target_data.get_store_size(&agg_ty) as u64; let size_v = env.context.i64_type().const_int(bytes, false); @@ -150,39 +155,8 @@ fn resolve_struct_key<'ctx>( panic!("LLVM struct type has no name and cannot be matched to struct_types"); } -fn wave_type_of_expr<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> Option { - match e { - Expression::Literal(literal) => Some(match literal { - Literal::Int(_) => WaveType::Int(32), - Literal::Float(_) => WaveType::Float(32), - Literal::String(_) => WaveType::String, - Literal::Bool(_) => WaveType::Bool, - Literal::Char(_) => WaveType::Char, - Literal::Byte(_) => WaveType::Byte, - }), - Expression::Null => Some(WaveType::Pointer(Box::new(WaveType::Byte))), - Expression::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()), - Expression::Grouped(inner) => wave_type_of_expr(env, inner), - Expression::Cast { target_type, .. } => Some(target_type.clone()), - Expression::Unary { expr, .. } => wave_type_of_expr(env, expr), - Expression::AddressOf(inner) => { - wave_type_of_expr(env, inner).map(|t| WaveType::Pointer(Box::new(t))) - } - Expression::Deref(inner) => { - // *p -> T (p: ptr) - if let Expression::Variable(name) = &**inner { - let vi = env.variables.get(name)?; - match &vi.ty { - WaveType::Pointer(inner_ty) => Some((**inner_ty).clone()), - WaveType::String => Some(WaveType::Byte), - _ => None, - } - } else { - None - } - } - _ => None, - } +fn semantic_wave_type_of_expr(expression: &Expression) -> Option { + crate::codegen::semantic::expression_type(expression) } fn lower_c_variadic_argument<'ctx, 'a>( @@ -190,6 +164,10 @@ fn lower_c_variadic_argument<'ctx, 'a>( expression: &Expression, index: usize, ) -> BasicValueEnum<'ctx> { + if matches!(expression, Expression::Null) { + panic!("untyped null reached C variadic codegen after semantic validation"); + } + let semantic_type = semantic_wave_type_of_expr(expression); let value = env.gen(expression, None); match value { BasicValueEnum::FloatValue(float) if float.get_type() == env.context.f32_type() => env @@ -203,7 +181,7 @@ fn lower_c_variadic_argument<'ctx, 'a>( .as_basic_value_enum(), BasicValueEnum::IntValue(integer) if integer.get_type().get_bit_width() < 32 => { let target = env.context.i32_type(); - let signed = matches!(wave_type_of_expr(env, expression), Some(WaveType::Int(_))); + let signed = matches!(semantic_type, Some(WaveType::Int(_))); if signed { env.builder .build_int_s_extend(integer, target, &format!("vararg{}_sext", index)) @@ -233,7 +211,7 @@ fn infer_struct_name_for_method<'ctx, 'a>( _ => {} } - let wt = wave_type_of_expr(env, object)?; + let wt = semantic_wave_type_of_expr(object)?; match wt { WaveType::Struct(name) => Some(name), WaveType::Pointer(inner) => match *inner { @@ -546,9 +524,10 @@ pub(crate) fn gen_function_call<'ctx, 'a>( match &info.ret { RetLowering::Void => { if info.wave_ret != WaveType::Void { - let expected = expected_type - .expect("ignored C ABI aggregate return requires an expected type"); - return expected.const_zero(); + return expected_type.map_or_else( + || env.context.i32_type().const_zero().as_basic_value_enum(), + BasicTypeEnum::const_zero, + ); } if expected_type.is_some() { panic!( @@ -719,6 +698,23 @@ fn coerce_to_expected<'ctx, 'a>( .as_basic_value_enum() } + // 3.1) a single-pointer aggregate transported as the pointer value + // itself by x86_64 SysV and AArch64 argument lowering. + ( + got_agg @ (BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_)), + BasicTypeEnum::PointerType(dst), + ) => { + let source = env + .builder + .build_alloca(got_agg, &format!("arg{}_ptr_agg", arg_index)) + .unwrap(); + env.builder.build_store(source, val).unwrap(); + env.builder + .build_load(dst, source, &format!("arg{}_ptr_transport", arg_index)) + .unwrap() + .as_basic_value_enum() + } + // 4) ptr -> ptr (bitcast) (BasicTypeEnum::PointerType(_), BasicTypeEnum::PointerType(dst)) => env .builder @@ -734,12 +730,12 @@ fn coerce_to_expected<'ctx, 'a>( let sz = env.target_data.get_store_size(&got_agg) as u64; let bits = (sz * 8) as u32; - if bits == dst.get_bit_width() { + if bits <= dst.get_bit_width() { return pack_agg_to_int(env, val, dst, &format!("arg{}_pack", arg_index)); } panic!( - "Cannot pack aggregate to int: agg bits {} != dst bits {} (arg {} of {})", + "Cannot pack aggregate to int: agg bits {} > dst bits {} (arg {} of {})", bits, dst.get_bit_width(), arg_index, @@ -747,6 +743,55 @@ fn coerce_to_expected<'ctx, 'a>( ); } + // 4.45) object aggregate -> aggregate-shaped ABI transport slot. + // AArch64 and RV64 represent 9..16-byte integer aggregates as a + // two-XLEN array in LLVM IR. Copy only the object bytes and keep the + // transport padding deterministic. + ( + got_agg @ (BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_)), + dst_agg @ (BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_)), + ) => { + let got_size = env.target_data.get_store_size(&got_agg); + let dst_size = env.target_data.get_store_size(&dst_agg); + if got_size > dst_size { + panic!( + "Cannot pack aggregate transport: object size {} > slot size {} (arg {} of {})", + got_size, dst_size, arg_index, name + ); + } + + let source = env + .builder + .build_alloca(got_agg, &format!("arg{}_agg_source", arg_index)) + .unwrap(); + env.builder.build_store(source, val).unwrap(); + let transport = env + .builder + .build_alloca(dst_agg, &format!("arg{}_agg_transport", arg_index)) + .unwrap(); + env.builder + .build_store(transport, dst_agg.const_zero()) + .unwrap(); + let bytes = env.context.i64_type().const_int(got_size, false); + env.builder + .build_memcpy( + transport, + env.target_data.get_abi_alignment(&dst_agg), + source, + env.target_data.get_abi_alignment(&got_agg), + bytes, + ) + .unwrap(); + env.builder + .build_load( + dst_agg, + transport, + &format!("arg{}_agg_transport_load", arg_index), + ) + .unwrap() + .as_basic_value_enum() + } + // 4.5) agg(struct/array) -> vector (HFA/ABI: e.g. Vector2 passed as <2 x float>) ( got_agg @ (BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_)), @@ -961,6 +1006,20 @@ fn coerce_lowered_ret_to_expected<'ctx, 'a>( } match (lowered_ret.get_type(), expected) { + ( + BasicTypeEnum::PointerType(_), + BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_), + ) => { + let destination = env + .builder + .build_alloca(expected, &format!("{tag}_ptr_agg")) + .unwrap(); + env.builder.build_store(destination, lowered_ret).unwrap(); + env.builder + .build_load(expected, destination, &format!("{tag}_ptr_agg_load")) + .unwrap() + .as_basic_value_enum() + } (BasicTypeEnum::IntType(_), BasicTypeEnum::StructType(_) | BasicTypeEnum::ArrayType(_)) => { let iv = lowered_ret.into_int_value(); unpack_int_to_agg(env, iv, expected, tag) diff --git a/llvm/src/toolchain.rs b/llvm/src/toolchain.rs index 97db5835..93e8c1ac 100644 --- a/llvm/src/toolchain.rs +++ b/llvm/src/toolchain.rs @@ -11,44 +11,109 @@ // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -pub fn find_bundled_linux_crt1(target: &str) -> Option { - bundled_linux_crt1_candidates(target) +pub fn find_bundled_linux_crt(target: &str, abi: Option<&str>, name: &str) -> Option { + bundled_linux_crt_candidates(target, abi, name) .into_iter() .find(|path| path.is_file()) } -pub fn expected_bundled_linux_crt1(target: &str) -> PathBuf { - bundled_linux_crt1_candidates(target) +pub fn expected_bundled_linux_crt(target: &str, abi: Option<&str>, name: &str) -> PathBuf { + bundled_linux_crt_candidates(target, abi, name) .into_iter() .next() - .unwrap_or_else(|| PathBuf::from(format!("crt/{}/crt1.o", target))) + .unwrap_or_else(|| PathBuf::from("crt").join(crt_relative_path(target, abi, name))) } -fn bundled_linux_crt1_candidates(target: &str) -> Vec { +fn bundled_linux_crt_candidates(target: &str, abi: Option<&str>, name: &str) -> Vec { let mut paths = Vec::new(); + let relative = crt_relative_path(target, abi, name); - if let Ok(path) = env::var("WAVE_LINUX_CRT1_OBJECT") { + if name == "crt1.o" { + if let Ok(path) = env::var("WAVE_LINUX_CRT1_OBJECT") { + if !path.trim().is_empty() { + paths.push(PathBuf::from(path)); + } + } + } + + if let Ok(path) = env::var("WAVE_LINUX_CRT_DIR") { if !path.trim().is_empty() { - paths.push(PathBuf::from(path)); + paths.push(PathBuf::from(path).join(&relative)); } } if let Ok(exe) = env::current_exe() { if let Some(dir) = exe.parent() { - paths.push(dir.join("crt").join(target).join("crt1.o")); + paths.push(dir.join("crt").join(&relative)); if let Some(root) = dir.parent() { - paths.push( - root.join("lib") - .join("wave") - .join("crt") - .join(target) - .join("crt1.o"), - ); + paths.push(root.join("lib").join("wave").join("crt").join(&relative)); } } } + paths.push(PathBuf::from(env!("WAVE_BUILD_CRT_DIR")).join(relative)); paths } + +fn crt_relative_path(target: &str, abi: Option<&str>, name: &str) -> PathBuf { + let mut path = PathBuf::from(target); + if target == "riscv64-unknown-linux-gnu" { + path.push(abi.unwrap_or("lp64d")); + } + path.push(Path::new(name).file_name().unwrap_or_default()); + path +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bundled_linux_crt_covers_every_hosted_linux_target() { + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-x86"))] + for target in ["x86_64-unknown-linux-gnu"] { + for name in ["crt1.o", "Scrt1.o", "rcrt1.o", "crti.o", "crtn.o"] { + let path = find_bundled_linux_crt(target, None, name) + .unwrap_or_else(|| panic!("missing bundled {name} for {target}")); + assert_elf_machine(&path, target); + } + } + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-aarch64"))] + for target in ["aarch64-unknown-linux-gnu"] { + for name in ["crt1.o", "Scrt1.o", "rcrt1.o", "crti.o", "crtn.o"] { + let path = find_bundled_linux_crt(target, None, name) + .unwrap_or_else(|| panic!("missing bundled {name} for {target}")); + assert_elf_machine(&path, target); + } + } + + #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-riscv"))] + for (abi, expected_float_flags) in [("lp64", 0), ("lp64f", 2), ("lp64d", 4)] { + for name in ["crt1.o", "Scrt1.o", "rcrt1.o", "crti.o", "crtn.o"] { + let path = find_bundled_linux_crt("riscv64-unknown-linux-gnu", Some(abi), name) + .unwrap_or_else(|| panic!("missing bundled RISC-V {abi} {name}")); + let bytes = assert_elf_machine(&path, "riscv64-unknown-linux-gnu"); + let flags = u32::from_le_bytes(bytes[48..52].try_into().unwrap()); + assert_eq!(flags & 0x6, expected_float_flags, "{}", path.display()); + } + } + } + + fn assert_elf_machine(path: &Path, target: &str) -> Vec { + let bytes = std::fs::read(path).unwrap(); + assert!(bytes.len() >= 52, "{} is too short", path.display()); + assert_eq!(&bytes[..4], b"\x7fELF", "{}", path.display()); + let machine = u16::from_le_bytes([bytes[18], bytes[19]]); + let expected = match target { + "x86_64-unknown-linux-gnu" => 62, + "aarch64-unknown-linux-gnu" => 183, + "riscv64-unknown-linux-gnu" => 243, + _ => unreachable!(), + }; + assert_eq!(machine, expected, "{}", path.display()); + bytes + } +} diff --git a/src/cli.rs b/src/cli.rs index 932dbe81..8719c765 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,7 @@ use crate::errors::CliError; use crate::flags::{ validate_opt_flag, DebugFlags, DepFlags, DepPackage, LinkFlags, LlvmFlags, WhaleFlags, }; +use crate::link_validation::{validate_riscv_link_inputs, RiscvFloatAbi}; use crate::{runner, std as wave_std, version}; use crate::version::get_os_pretty_name; @@ -2206,6 +2207,19 @@ fn link_objects( } let (bin, args) = build_linker_args(global, build, objects, output); + let target = target_triple_for_global(global); + if matches!( + target_spec_for_triple(&target).map(|spec| spec.codegen), + Some(CodegenTarget::LinuxRISCV64 | CodegenTarget::FreestandingRISCV64) + ) { + let abi = global.llvm.abi.as_deref().unwrap_or("lp64d"); + let target_abi = RiscvFloatAbi::from_target_abi(abi).ok_or_else(|| { + CliError::CommandFailed(format!("unsupported RISC-V target ABI '{}'", abi)) + })?; + let validation_inputs = collect_linker_input_paths(objects, &args, build.static_link); + validate_riscv_link_inputs(target_abi, &validation_inputs) + .map_err(|error| CliError::CommandFailed(error.to_string()))?; + } let mut command = ProcessCommand::new(&bin); configure_bundled_llvm_tool_env(&mut command, &bin); @@ -2239,12 +2253,12 @@ fn validate_default_elf_runtime(global: &Global, build: &BuildRequest) -> Result let mut missing = Vec::new(); if !build.shared && !build.no_start_files { let start_name = elf_start_file_name(build); - let has_system_start = find_elf_runtime_file(&target, global, start_name).is_some() - && find_elf_runtime_file(&target, global, "crti.o").is_some(); - let has_bundled_start = - start_name == "crt1.o" && llvm::toolchain::find_bundled_linux_crt1(&target).is_some(); - if !has_system_start && !has_bundled_start { - missing.push(format!("{} and crti.o", start_name)); + let has_bundled_crt = [start_name, "crti.o", "crtn.o"].into_iter().all(|name| { + llvm::toolchain::find_bundled_linux_crt(&target, global.llvm.abi.as_deref(), name) + .is_some() + }); + if !has_bundled_crt { + missing.push(format!("Wave CRT ({}, crti.o, crtn.o)", start_name)); } } let libc_names: &[&str] = if build.static_link { @@ -2522,6 +2536,74 @@ fn append_link_search_and_libs(args: &mut Vec, global: &Global) { } } +fn collect_linker_input_paths( + objects: &[String], + args: &[String], + static_link: bool, +) -> Vec { + let mut inputs = objects.to_vec(); + let mut search_paths = Vec::new(); + let mut libraries = Vec::new(); + let mut index = 0; + while index < args.len() { + let argument = &args[index]; + if matches!(argument.as_str(), "-o" | "--output") { + index += 2; + continue; + } + if argument == "-L" { + if let Some(path) = args.get(index + 1) { + search_paths.push(PathBuf::from(path)); + } + index += 2; + continue; + } + if let Some(path) = argument.strip_prefix("-L") { + if !path.is_empty() { + search_paths.push(PathBuf::from(path)); + } + index += 1; + continue; + } + if let Some(library) = argument.strip_prefix("-l") { + if !library.is_empty() { + libraries.push(library.to_string()); + } + index += 1; + continue; + } + if argument.starts_with('-') { + index += 1; + continue; + } + if Path::new(argument).is_file() { + inputs.push(argument.clone()); + } + index += 1; + } + + for library in libraries { + let candidates = if let Some(exact) = library.strip_prefix(':') { + vec![exact.to_string()] + } else if static_link { + vec![format!("lib{library}.a")] + } else { + vec![format!("lib{library}.so"), format!("lib{library}.a")] + }; + if let Some(path) = search_paths + .iter() + .flat_map(|directory| candidates.iter().map(move |name| directory.join(name))) + .find(|path| path.is_file()) + { + inputs.push(path.to_string_lossy().to_string()); + } + } + + inputs.sort(); + inputs.dedup(); + inputs +} + fn append_lld_link_args(args: &mut Vec, link_args: &[String]) { for arg in link_args { if arg == "-nostartfiles" { @@ -2607,21 +2689,16 @@ fn append_elf_start_files( let start_name = elf_start_file_name(build); - let start_file = find_elf_runtime_file(target, global, start_name); - let init_file = find_elf_runtime_file(target, global, "crti.o"); - if let (Some(start_file), Some(init_file)) = (start_file, init_file) { - args.push(start_file); - args.push(init_file); - return true; - } - - if start_name != "crt1.o" { - args.push(start_name.to_string()); - return false; - } - - append_bundled_linux_crt1(args, target); - false + append_bundled_linux_crt( + args, + bundled_linux_crt_path(target, global.llvm.abi.as_deref(), start_name), + ); + args.push( + bundled_linux_crt_path(target, global.llvm.abi.as_deref(), "crti.o") + .to_string_lossy() + .to_string(), + ); + true } fn elf_start_file_name(build: &BuildRequest) -> &'static str { @@ -2633,9 +2710,11 @@ fn elf_start_file_name(build: &BuildRequest) -> &'static str { } fn append_elf_end_files(args: &mut Vec, target: &str, global: &Global) { - if let Some(path) = find_elf_runtime_file(target, global, "crtn.o") { - args.push(path); - } + args.push( + bundled_linux_crt_path(target, global.llvm.abi.as_deref(), "crtn.o") + .to_string_lossy() + .to_string(), + ); } fn append_elf_default_libs(args: &mut Vec, target: &str, global: &Global) { @@ -2678,15 +2757,15 @@ fn append_elf_default_lib( args.push(format!("-l{}", link_name)); } -fn append_bundled_linux_crt1(args: &mut Vec, target: &str) { +fn append_bundled_linux_crt(args: &mut Vec, path: PathBuf) { args.push("-e".to_string()); args.push("_start".to_string()); - args.push( - llvm::toolchain::find_bundled_linux_crt1(target) - .unwrap_or_else(|| llvm::toolchain::expected_bundled_linux_crt1(target)) - .to_string_lossy() - .to_string(), - ); + args.push(path.to_string_lossy().to_string()); +} + +fn bundled_linux_crt_path(target: &str, abi: Option<&str>, name: &str) -> PathBuf { + llvm::toolchain::find_bundled_linux_crt(target, abi, name) + .unwrap_or_else(|| llvm::toolchain::expected_bundled_linux_crt(target, abi, name)) } fn append_elf_search_paths(args: &mut Vec, target: &str, global: &Global) { @@ -2737,9 +2816,7 @@ fn find_elf_runtime_file(target: &str, global: &Global, name: &str) -> Option) -> Option<&'static str> } } -fn elf_object_matches_target(path: &Path, target: &str) -> bool { - let expected_machine = match target_spec_for_triple(target).map(|spec| spec.architecture) { - Some(llvm::codegen::arch::Architecture::X86_64) => 62, - Some(llvm::codegen::arch::Architecture::Aarch64) => 183, - Some(llvm::codegen::arch::Architecture::Riscv64) => 243, - None => return false, - }; - let Ok(header) = fs::read(path) else { - return false; - }; - if header.len() < 20 || &header[..4] != b"\x7fELF" { - return false; - } - let machine = match header[5] { - 1 => u16::from_le_bytes([header[18], header[19]]), - 2 => u16::from_be_bytes([header[18], header[19]]), - _ => return false, - }; - machine == expected_machine -} - fn sysroot_path(sysroot: &str, suffix: &str) -> PathBuf { if sysroot.is_empty() { PathBuf::from("/").join(suffix) diff --git a/src/lib.rs b/src/lib.rs index 1e7f318f..f4131fff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub mod cli; pub mod errors; pub mod flags; +pub mod link_validation; pub mod runner; pub mod std; pub mod version; diff --git a/src/link_validation/elf.rs b/src/link_validation/elf.rs new file mode 100644 index 00000000..00e3dcc4 --- /dev/null +++ b/src/link_validation/elf.rs @@ -0,0 +1,224 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; + +const ELF_MAGIC: &[u8; 4] = b"\x7fELF"; +const AR_MAGIC: &[u8; 8] = b"!\n"; + +#[derive(Debug)] +pub(super) struct ElfMetadata { + pub input: String, + pub machine: u16, + pub flags: u32, +} + +#[derive(Debug)] +pub enum LinkInputInspectionError { + Read { + input: PathBuf, + source: std::io::Error, + }, + Malformed { + input: String, + reason: String, + }, +} + +impl fmt::Display for LinkInputInspectionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Read { input, source } => write!( + formatter, + "failed to inspect linker input '{}': {}", + input.display(), + source + ), + Self::Malformed { input, reason } => { + write!( + formatter, + "invalid ELF linker input '{}': {}", + input, reason + ) + } + } + } +} + +impl std::error::Error for LinkInputInspectionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Read { source, .. } => Some(source), + Self::Malformed { .. } => None, + } + } +} + +pub(super) fn inspect_link_inputs( + inputs: &[String], +) -> Result, LinkInputInspectionError> { + let mut metadata = Vec::new(); + for input in inputs { + let path = Path::new(input); + let bytes = fs::read(path).map_err(|source| LinkInputInspectionError::Read { + input: path.to_path_buf(), + source, + })?; + inspect_input(path, &bytes, &mut metadata)?; + } + Ok(metadata) +} + +fn inspect_input( + path: &Path, + bytes: &[u8], + metadata: &mut Vec, +) -> Result<(), LinkInputInspectionError> { + if bytes.starts_with(ELF_MAGIC) { + metadata.push(read_elf(&path.display().to_string(), bytes)?); + } else if bytes.starts_with(AR_MAGIC) { + inspect_archive(path, bytes, metadata)?; + } + // LLVM bitcode and linker scripts do not carry ELF e_flags. + Ok(()) +} + +fn read_elf(display: &str, bytes: &[u8]) -> Result { + if bytes.len() < 20 || &bytes[..4] != ELF_MAGIC { + return Err(malformed(display, "truncated ELF header")); + } + let little_endian = match bytes[5] { + 1 => true, + 2 => false, + _ => return Err(malformed(display, "invalid ELF data encoding")), + }; + let machine = read_u16(&bytes[18..20], little_endian); + let flags_offset = match bytes[4] { + 1 => 36, + 2 => 48, + _ => return Err(malformed(display, "invalid ELF class")), + }; + let Some(raw_flags) = bytes.get(flags_offset..flags_offset + 4) else { + return Err(malformed(display, "truncated ELF e_flags")); + }; + Ok(ElfMetadata { + input: display.to_string(), + machine, + flags: read_u32(raw_flags, little_endian), + }) +} + +fn inspect_archive( + path: &Path, + bytes: &[u8], + metadata: &mut Vec, +) -> Result<(), LinkInputInspectionError> { + let display = path.display().to_string(); + let mut offset = AR_MAGIC.len(); + let mut long_names: Option<&[u8]> = None; + while offset < bytes.len() { + let Some(header) = bytes.get(offset..offset + 60) else { + return Err(malformed(&display, "truncated archive header")); + }; + if &header[58..60] != b"`\n" { + return Err(malformed(&display, "invalid archive member header")); + } + let size_text = std::str::from_utf8(&header[48..58]) + .map_err(|_| malformed(&display, "invalid archive member size"))?; + let size = size_text + .trim() + .parse::() + .map_err(|_| malformed(&display, "invalid archive member size"))?; + let data_start = offset + 60; + let Some(member_data) = bytes.get(data_start..data_start + size) else { + return Err(malformed(&display, "truncated archive member")); + }; + let raw_name = std::str::from_utf8(&header[..16]) + .map_err(|_| malformed(&display, "invalid archive member name"))? + .trim(); + + let (name, payload) = if raw_name == "//" { + long_names = Some(member_data); + (None, member_data) + } else if raw_name == "/" || raw_name == "/SYM64/" { + (None, member_data) + } else if let Some(length) = raw_name.strip_prefix("#1/") { + let length = length + .parse::() + .map_err(|_| malformed(&display, "invalid BSD archive member name"))?; + let Some(name_bytes) = member_data.get(..length) else { + return Err(malformed(&display, "truncated BSD archive member name")); + }; + ( + Some(String::from_utf8_lossy(name_bytes).into_owned()), + &member_data[length..], + ) + } else if let Some(name_offset) = raw_name.strip_prefix('/') { + let name_offset = name_offset + .parse::() + .map_err(|_| malformed(&display, "invalid GNU archive name offset"))?; + let table = long_names + .ok_or_else(|| malformed(&display, "archive long-name table is missing"))?; + let tail = table + .get(name_offset..) + .ok_or_else(|| malformed(&display, "archive long-name offset is out of range"))?; + let end = tail + .windows(2) + .position(|window| window == b"/\n") + .unwrap_or(tail.len()); + ( + Some(String::from_utf8_lossy(&tail[..end]).into_owned()), + member_data, + ) + } else { + ( + Some(raw_name.trim_end_matches('/').to_string()), + member_data, + ) + }; + + if let Some(name) = name { + if payload.starts_with(ELF_MAGIC) { + metadata.push(read_elf(&format!("{}({})", path.display(), name), payload)?); + } + } + offset = data_start + size + (size & 1); + } + Ok(()) +} + +fn malformed(input: &str, reason: &str) -> LinkInputInspectionError { + LinkInputInspectionError::Malformed { + input: input.to_string(), + reason: reason.to_string(), + } +} + +fn read_u16(bytes: &[u8], little_endian: bool) -> u16 { + let bytes = [bytes[0], bytes[1]]; + if little_endian { + u16::from_le_bytes(bytes) + } else { + u16::from_be_bytes(bytes) + } +} + +fn read_u32(bytes: &[u8], little_endian: bool) -> u32 { + let bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; + if little_endian { + u32::from_le_bytes(bytes) + } else { + u32::from_be_bytes(bytes) + } +} diff --git a/src/link_validation/mod.rs b/src/link_validation/mod.rs new file mode 100644 index 00000000..97205d3f --- /dev/null +++ b/src/link_validation/mod.rs @@ -0,0 +1,17 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +mod elf; +mod riscv; + +pub use elf::LinkInputInspectionError; +pub use riscv::{validate_riscv_link_inputs, AbiValidationError, RiscvFloatAbi}; diff --git a/src/link_validation/riscv.rs b/src/link_validation/riscv.rs new file mode 100644 index 00000000..34e7add1 --- /dev/null +++ b/src/link_validation/riscv.rs @@ -0,0 +1,122 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +use super::elf::{inspect_link_inputs, LinkInputInspectionError}; +use std::fmt; + +const EM_RISCV: u16 = 243; +const EF_RISCV_FLOAT_ABI_MASK: u32 = 0x6; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RiscvFloatAbi { + Lp64, + Lp64f, + Lp64d, +} + +impl RiscvFloatAbi { + pub fn from_target_abi(value: &str) -> Option { + match value.to_ascii_lowercase().as_str() { + "lp64" => Some(Self::Lp64), + "lp64f" => Some(Self::Lp64f), + "lp64d" => Some(Self::Lp64d), + _ => None, + } + } + + fn from_elf_flags(flags: u32) -> Option { + match flags & EF_RISCV_FLOAT_ABI_MASK { + 0x0 => Some(Self::Lp64), + 0x2 => Some(Self::Lp64f), + 0x4 => Some(Self::Lp64d), + _ => None, + } + } +} + +impl fmt::Display for RiscvFloatAbi { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::Lp64 => "LP64", + Self::Lp64f => "LP64F", + Self::Lp64d => "LP64D", + }) + } +} + +#[derive(Debug)] +pub enum AbiValidationError { + Inspection(LinkInputInspectionError), + Unsupported { + input: String, + }, + Mismatch { + target: RiscvFloatAbi, + input: String, + input_abi: RiscvFloatAbi, + }, +} + +impl fmt::Display for AbiValidationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Inspection(error) => error.fmt(formatter), + Self::Unsupported { input } => write!( + formatter, + "invalid RISC-V linker input '{}': unsupported floating-point ABI flag", + input + ), + Self::Mismatch { + target, + input, + input_abi, + } => write!( + formatter, + "RISC-V floating-point ABI mismatch before linking\ntarget ABI: {}\ninput: {}\ninput ABI: {}", + target, input, input_abi + ), + } + } +} + +impl std::error::Error for AbiValidationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Inspection(error) => Some(error), + Self::Unsupported { .. } | Self::Mismatch { .. } => None, + } + } +} + +pub fn validate_riscv_link_inputs( + target_abi: RiscvFloatAbi, + inputs: &[String], +) -> Result<(), AbiValidationError> { + for metadata in inspect_link_inputs(inputs).map_err(AbiValidationError::Inspection)? { + if metadata.machine != EM_RISCV { + continue; + } + let input_abi = RiscvFloatAbi::from_elf_flags(metadata.flags).ok_or_else(|| { + AbiValidationError::Unsupported { + input: metadata.input.clone(), + } + })?; + if input_abi != target_abi { + return Err(AbiValidationError::Mismatch { + target: target_abi, + input: metadata.input, + input_abi, + }); + } + } + Ok(()) +} diff --git a/tests/cases/test100.wave b/tests/cases/test100.wave index b3521448..3d97827f 100644 --- a/tests/cases/test100.wave +++ b/tests/cases/test100.wave @@ -1,4 +1,4 @@ -// wave-test: host-os=linux, host-arch=aarch64 +// wave-test: host-os=linux, host-arch=aarch64, udp-input=true fun main() { var sockfd: i64; asm { diff --git a/tests/cases/test61.wave b/tests/cases/test61.wave index 37906dde..f8d41eda 100644 --- a/tests/cases/test61.wave +++ b/tests/cases/test61.wave @@ -1,4 +1,4 @@ -// wave-test: host-os=linux, host-arch=x86_64 +// wave-test: host-os=linux, host-arch=x86_64, udp-input=true const AF_INET: i32 = 2; const SOCK_DGRAM: i32 = 2; const SYS_SOCKET: i64 = 41; diff --git a/tests/cases/test62.wave b/tests/cases/test62.wave index 29522c90..a75cbbda 100644 --- a/tests/cases/test62.wave +++ b/tests/cases/test62.wave @@ -1,4 +1,4 @@ -// wave-test: host-os=linux, host-arch=x86_64 +// wave-test: host-os=linux, host-arch=x86_64, udp-input=true const AF_INET: i32 = 2; const SOCK_DGRAM: i32 = 2; const SYS_SOCKET: i64 = 41; diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 5b10521b..380cc260 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -14,6 +14,10 @@ use std::ffi::{OsStr, OsString}; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use wavec::link_validation::{validate_riscv_link_inputs, RiscvFloatAbi}; + +static NEXT_TEMP_CASE: AtomicU64 = AtomicU64::new(0); fn wavec_bin() -> PathBuf { if let Some(path) = option_env!("CARGO_BIN_EXE_wavec") { @@ -24,7 +28,13 @@ fn wavec_bin() -> PathBuf { } fn temp_case_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!("wavec-{}-{}", name, std::process::id())); + let sequence = NEXT_TEMP_CASE.fetch_add(1, Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!( + "wavec-{}-{}-{}", + name, + std::process::id(), + sequence + )); let _ = fs::remove_dir_all(&dir); fs::create_dir_all(&dir).unwrap(); dir @@ -1727,6 +1737,14 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { "{}", stdout ); + assert!( + json_contains_path_components( + &stdout, + &["crt", "riscv64-unknown-linux-gnu", "lp64", "crt1.o",], + ), + "LP64 link plan must select Wave's ABI-matched CRT:\n{}", + stdout + ); for host_path in [ "/usr/lib64/crt1.o", "/usr/lib64/crti.o", @@ -1762,8 +1780,11 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { stdout ); assert!( - json_contains_path_components(&stdout, &["crt", "riscv64-unknown-linux-gnu", "crt1.o"]), - "static link plan must retain a CRT entry point:\n{}", + json_contains_path_components( + &stdout, + &["crt", "riscv64-unknown-linux-gnu", "lp64d", "crt1.o",], + ), + "static link plan must retain Wave's LP64D CRT entry point:\n{}", stdout ); @@ -1786,6 +1807,71 @@ fn target_configuration_is_rejected_before_frontend_or_backend_work() { ); } +#[test] +fn hosted_linux_link_plans_use_wave_crt_for_every_architecture_and_mode() { + let dir = temp_case_dir("bundled-linux-crt-matrix"); + let source = write_wave(&dir, "main.wave", "fun main() -> i32 { return 0; }\n"); + + for (target, abi) in [ + ("x86_64-unknown-linux-gnu", None), + ("aarch64-unknown-linux-gnu", None), + ("riscv64-unknown-linux-gnu", Some("lp64")), + ("riscv64-unknown-linux-gnu", Some("lp64f")), + ("riscv64-unknown-linux-gnu", Some("lp64d")), + ] { + for (options, object_name) in [ + (Vec::<&str>::new(), "crt1.o"), + (vec!["--pie"], "Scrt1.o"), + (vec!["--static", "--pie"], "rcrt1.o"), + ] { + let mut args = vec![ + OsString::from("--error-format=json"), + OsString::from("build"), + source.as_os_str().to_os_string(), + OsString::from("--target"), + OsString::from(target), + ]; + if let Some(abi) = abi { + args.push(OsString::from(format!("--abi={abi}"))); + } + args.extend(options.into_iter().map(OsString::from)); + args.push(OsString::from("--emit=bin")); + args.push(OsString::from("--dry-run")); + + let output = run_wavec_raw(args); + assert!( + output.status.success(), + "{target} {object_name} dry-run failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + let mut components = vec!["crt", target]; + if let Some(abi) = abi { + components.push(abi); + } + components.push(object_name); + assert!( + json_contains_path_components(&stdout, &components), + "{target} must select Wave's {object_name}:\n{stdout}" + ); + let mut crti_components = vec!["crt", target]; + let mut crtn_components = vec!["crt", target]; + if let Some(abi) = abi { + crti_components.push(abi); + crtn_components.push(abi); + } + crti_components.push("crti.o"); + crtn_components.push("crtn.o"); + assert!( + json_contains_path_components(&stdout, &crti_components) + && json_contains_path_components(&stdout, &crtn_components), + "{target} must select Wave's crti.o and crtn.o:\n{stdout}" + ); + } + } +} + #[test] fn riscv64_debian_cross_prefix_does_not_double_apply_linker_sysroot() { let dir = temp_case_dir("riscv64-debian-cross-prefix"); @@ -2709,6 +2795,450 @@ fun main() -> i32 { } } +fn clang_for_contract_tests() -> Option { + if let Some(value) = std::env::var_os("CLANG") { + let path = PathBuf::from(value); + if Command::new(&path).arg("--version").output().is_ok() { + return Some(path); + } + } + ["clang-21", "clang"] + .into_iter() + .map(PathBuf::from) + .find(|path| Command::new(path).arg("--version").output().is_ok()) +} + +#[test] +fn odd_sized_aggregate_transport_matches_clang_ir_contracts() { + let Some(clang) = clang_for_contract_tests() else { + eprintln!("skipped: clang is unavailable for ABI IR comparison"); + return; + }; + let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/c_abi_edges"); + let dir = temp_case_dir("odd-aggregate-clang-contract"); + + for target in [ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "riscv64-unknown-linux-gnu", + ] { + let tag = target.split('-').next().unwrap(); + let target_label = target.replace('-', "_"); + let clang_ir_path = dir.join(format!("{target_label}-clang.ll")); + let clang_output = Command::new(&clang) + .args(["-target", target, "-S", "-emit-llvm", "-ffreestanding"]) + .arg(fixture.join("interop.c")) + .arg("-o") + .arg(&clang_ir_path) + .output() + .unwrap(); + assert!( + clang_output.status.success(), + "clang ABI probe failed for {target}:\n{}", + String::from_utf8_lossy(&clang_output.stderr) + ); + let wave_dir = dir.join(format!("{target_label}-wave")); + run_wavec([ + OsStr::new("build"), + fixture.join("interop.wave").as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + wave_dir.as_os_str(), + ]); + let clang_ir = fs::read_to_string(clang_ir_path).unwrap(); + let wave_ir = fs::read_to_string(wave_dir.join("interop.ll")).unwrap(); + 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) { + ("x86_64", true) => (object_integer.clone(), object_integer), + ("x86_64", false) => { + let remainder = (size - 8) * 8; + ( + format!("{{ i64, i{remainder} }}"), + format!("i64, i{remainder}"), + ) + } + ("aarch64", true) => (object_integer, "i64".to_string()), + ("aarch64", false) => ("[2 x i64]".to_string(), "[2 x i64]".to_string()), + ("riscv64", true) => ("i64".to_string(), "i64".to_string()), + ("riscv64", false) => ("[2 x i64]".to_string(), "[2 x i64]".to_string()), + _ => unreachable!(), + }; + let c_contract = format!("{result} @c_bytes{size}({argument}"); + let wave_contract = format!("{result} @wave_bytes{size}({argument}"); + let clang_c_definition = clang_ir + .lines() + .find(|line| line.contains(&format!("@c_bytes{size}("))) + .unwrap_or_else(|| panic!("missing c_bytes{size} definition:\n{clang_ir}")) + .replace(" %0", "") + .replace(" %1", ""); + assert!( + clang_c_definition.contains(&c_contract), + "missing `{c_contract}` in `{clang_c_definition}`" + ); + assert!( + wave_ir.contains(&format!("declare {c_contract}")), + "{wave_ir}" + ); + assert!( + clang_ir.contains(&wave_contract), + "missing `{wave_contract}`:\n{clang_ir}" + ); + let wave_definition = wave_ir + .lines() + .find(|line| line.contains(&format!("@wave_bytes{size}("))) + .unwrap_or_else(|| panic!("missing wave_bytes{size} definition:\n{wave_ir}")) + .replace(" %0", "") + .replace(" %1", ""); + assert!( + wave_definition.contains(&format!("define {wave_contract}")), + "missing `{wave_contract}` in `{wave_definition}`" + ); + } + for contract in ["void @c_empty()", "void @wave_empty()"] { + assert!( + clang_ir.contains(contract), + "missing `{contract}`:\n{clang_ir}" + ); + } + assert!(wave_ir.contains("declare void @c_empty()"), "{wave_ir}"); + assert!(wave_ir.contains("define void @wave_empty()"), "{wave_ir}"); + + let aggregate_contracts: &[(&str, &str, &str)] = match tag { + "x86_64" => &[ + ("nested", "i64", "i64"), + ("array_member", "i48", "i48"), + ("pointer_member", "ptr", "ptr"), + ], + "aarch64" => &[ + ("nested", "i64", "i64"), + ("array_member", "i48", "i64"), + ("pointer_member", "i64", "ptr"), + ], + "riscv64" => &[ + ("nested", "i64", "i64"), + ("array_member", "i64", "i64"), + ("pointer_member", "i64", "i64"), + ], + _ => unreachable!(), + }; + for (name, result, argument) in aggregate_contracts { + let c_contract = format!("{result} @c_{name}({argument}"); + let wave_contract = format!("{result} @wave_{name}({argument}"); + let clang_definition = clang_ir + .lines() + .find(|line| line.contains(&format!("@c_{name}("))) + .unwrap_or_else(|| panic!("missing c_{name} definition:\n{clang_ir}")) + .replace(" %0", ""); + assert!(clang_definition.contains(&c_contract), "{clang_definition}"); + assert!( + wave_ir.contains(&format!("declare {c_contract}")), + "{wave_ir}" + ); + assert!(clang_ir.contains(&wave_contract), "{clang_ir}"); + let wave_definition = wave_ir + .lines() + .find(|line| line.contains(&format!("@wave_{name}("))) + .unwrap_or_else(|| panic!("missing wave_{name} definition:\n{wave_ir}")) + .replace(" %0", ""); + assert!( + wave_definition.contains(&format!("define {wave_contract}")), + "{wave_definition}" + ); + } + } +} + +#[test] +fn narrow_integer_c_abi_attributes_match_clang_targets() { + let Some(clang) = clang_for_contract_tests() else { + eprintln!("skipped: clang is unavailable for ABI IR comparison"); + return; + }; + let dir = temp_case_dir("narrow-integer-c-abi-contract"); + let fixture = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/c_abi_edges/narrow.c"); + let source = write_wave( + &dir, + "narrow.wave", + r#" +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_u8(value: u8) -> u8; +extern(c) fun c_i16(value: i16) -> i16; +extern(c) fun c_u16(value: u16) -> u16; +extern(c) fun c_i32(value: i32) -> i32; +extern(c) fun c_u32(value: u32) -> u32; +export(c) fun wave_i8(value: i8) -> i8 { return value; } +export(c) fun wave_u8(value: u8) -> u8 { return value; } +export(c) fun wave_i16(value: i16) -> i16 { return value; } +export(c) fun wave_u16(value: u16) -> u16 { return value; } +export(c) fun wave_i32(value: i32) -> i32 { return value; } +export(c) fun wave_u32(value: u32) -> u32 { return value; } +fun main() -> i32 { return c_i8(-1) as i32 + c_u8(1) as i32 + c_i16(-1) as i32 + c_u16(1) as i32 + c_i32(-1) + c_u32(1) as i32; } +"#, + ); + for target in [ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-gnu", + "riscv64-unknown-linux-gnu", + ] { + let out = dir.join(target); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + let ir = fs::read_to_string(out.join("narrow.ll")).unwrap(); + let clang_ir_path = dir.join(format!("{}-clang.ll", target)); + let clang_output = Command::new(&clang) + .args(["-target", target, "-S", "-emit-llvm", "-ffreestanding"]) + .arg(&fixture) + .arg("-o") + .arg(&clang_ir_path) + .output() + .unwrap(); + assert!( + clang_output.status.success(), + "clang ABI probe failed for {target}:\n{}", + String::from_utf8_lossy(&clang_output.stderr) + ); + let clang_ir = fs::read_to_string(clang_ir_path).unwrap(); + + for name in ["i8", "u8", "i16", "u16", "i32", "u32"] { + let clang_line = clang_ir + .lines() + .find(|line| line.starts_with("define") && line.contains(&format!("@c_{name}("))) + .unwrap_or_else(|| panic!("missing Clang definition for {name}:\n{clang_ir}")); + let wave_definition = ir + .lines() + .find(|line| line.starts_with("define") && line.contains(&format!("@wave_{name}("))) + .unwrap_or_else(|| panic!("missing Wave definition for {name}:\n{ir}")); + let wave_declaration = ir + .lines() + .find(|line| line.starts_with("declare") && line.contains(&format!("@c_{name}("))) + .unwrap_or_else(|| panic!("missing Wave declaration for {name}:\n{ir}")); + let extension = if clang_line.contains("signext") { + Some("signext") + } else if clang_line.contains("zeroext") { + Some("zeroext") + } else { + None + }; + let clang_count = extension.map_or(0, |value| clang_line.matches(value).count()); + let wave_definition_count = + extension.map_or(0, |value| wave_definition.matches(value).count()); + let wave_declaration_count = + extension.map_or(0, |value| wave_declaration.matches(value).count()); + assert_eq!( + wave_definition_count, clang_count, + "target {target}, type {name}: Clang `{clang_line}`, Wave `{wave_definition}`" + ); + assert_eq!( + wave_declaration_count, clang_count, + "target {target}, type {name}: Clang `{clang_line}`, Wave `{wave_declaration}`" + ); + if extension.is_none() { + assert!(!wave_definition.contains("signext")); + assert!(!wave_definition.contains("zeroext")); + assert!(!wave_declaration.contains("signext")); + assert!(!wave_declaration.contains("zeroext")); + } + } + } +} + +#[test] +fn c_variadic_promotions_use_semantic_expression_types() { + let dir = temp_case_dir("c-variadic-semantic-promotions"); + let source = write_wave( + &dir, + "promotions.wave", + r#" +extern(c) fun consume(count: i32, ...) -> i64; +fun signed_result() -> i8 { return -1; } +fun main() -> i32 { + let signed: i8 = -128; + let unsigned: u8 = 255; + let zero: i8 = 0; + let one: i8 = 1; + consume(10, signed, unsigned, signed + 127, zero - one, signed * zero - one, signed < zero, !zero, (signed + 127) * one, signed_result(), 255 as u8); + consume(2, null as ptr, null as ptr); + return 0; +} +"#, + ); + let out = dir.join("out"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + let ir = fs::read_to_string(out.join("promotions.ll")).unwrap(); + for expected in [ + "%vararg1_sext = sext i8", + "%vararg2_zext = zext i8", + "%vararg3_sext = sext i8", + "%vararg4_sext = sext i8", + "%vararg5_sext = sext i8", + "%vararg6_zext = zext i1", + "%vararg7_zext = zext i1", + "%vararg8_sext = sext i8", + "%vararg9_sext = sext i8", + ] { + assert!(ir.contains(expected), "missing `{expected}`:\n{ir}"); + } + assert!(ir.contains("i32 signext 255"), "{ir}"); + assert!(ir.contains("ptr null, ptr null"), "{ir}"); + + let invalid = write_wave( + &dir, + "untyped_null.wave", + "extern(c) fun consume(count: i32, ...) -> i64; fun main() { consume(1, null); }\n", + ); + let error = run_wavec_expect_failure([OsStr::new("check"), invalid.as_os_str()]); + assert!( + error.contains("variadic argument 2") && error.contains("no scalar type"), + "{error}" + ); +} + +#[test] +fn discarded_c_return_values_do_not_require_an_expected_type() { + let dir = temp_case_dir("discarded-c-return-values"); + let source = write_wave( + &dir, + "discard.wave", + r#" +struct Empty {} +struct Pair { first: i64; second: i64; } +extern(c) fun c_empty(value: Empty) -> Empty; +extern(c) fun c_pair(value: Pair) -> Pair; +extern(c) fun c_integer() -> i64; +extern(c) fun c_float() -> f64; +extern(c) fun c_pointer() -> ptr; +fun main() { + c_empty(Empty {}); + c_pair(Pair { first: 1, second: 2 }); + c_integer(); + c_float(); + c_pointer(); +} +"#, + ); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + dir.join("out").as_os_str(), + ]); +} + +#[test] +fn riscv_link_input_abi_is_validated_before_linking() { + let dir = temp_case_dir("riscv-pre-link-abi"); + let source = write_wave(&dir, "main.wave", "fun main() -> i32 { return 0; }\n"); + let mut objects = Vec::new(); + for abi in ["lp64", "lp64f", "lp64d"] { + let out = dir.join(abi); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--abi"), + OsStr::new(abi), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + out.as_os_str(), + ]); + objects.push((abi, out.join("main.o"))); + } + for (abi, object) in &objects { + let expected = RiscvFloatAbi::from_target_abi(abi).unwrap(); + validate_riscv_link_inputs(expected, &[object.display().to_string()]).unwrap(); + } + let error = + validate_riscv_link_inputs(RiscvFloatAbi::Lp64d, &[objects[0].1.display().to_string()]) + .unwrap_err() + .to_string(); + assert!(error.contains("target ABI: LP64D"), "{error}"); + assert!(error.contains("input ABI: LP64"), "{error}"); + + let archive = dir.join("libmixed.a"); + let archive_output = Command::new("ar") + .arg("rcs") + .arg(&archive) + .arg(&objects[1].1) + .output() + .expect("failed to start ar"); + assert!( + archive_output.status.success(), + "{}", + String::from_utf8_lossy(&archive_output.stderr) + ); + let archive_error = + validate_riscv_link_inputs(RiscvFloatAbi::Lp64d, &[archive.display().to_string()]) + .unwrap_err() + .to_string(); + assert!( + archive_error.contains("libmixed.a(main.o)"), + "{archive_error}" + ); + assert!( + archive_error.contains("input ABI: LP64F"), + "{archive_error}" + ); + + for (input, expected_input_abi) in [(&objects[0].1, "LP64"), (&archive, "LP64F")] { + let output = run_wavec_raw([ + OsStr::new("build"), + source.as_os_str(), + input.as_os_str(), + OsStr::new("--target"), + OsStr::new("riscv64-unknown-linux-gnu"), + OsStr::new("--abi=lp64d"), + OsStr::new("--no-start-files"), + OsStr::new("-Cno-default-libs"), + OsStr::new("-Clinker=/bin/false"), + OsStr::new("--out-dir"), + dir.join("link-attempt").as_os_str(), + ]); + assert!(!output.status.success()); + let error = String::from_utf8_lossy(&output.stderr); + assert!( + error.contains("RISC-V floating-point ABI mismatch before linking"), + "{error}" + ); + assert!(error.contains("target ABI: LP64D"), "{error}"); + assert!( + error.contains(&format!("input ABI: {expected_input_abi}")), + "{error}" + ); + assert!( + !error.contains("link failed"), + "external linker ran before ABI validation: {error}" + ); + } +} + fn run_linux_c_abi_fixture( fixture_name: &str, target: &str, @@ -2796,6 +3326,7 @@ fn x86_64_c_abi_interoperates_with_c() { assert_eq!(std::env::consts::ARCH, "x86_64"); assert_eq!(std::env::consts::OS, "linux"); run_linux_c_abi_fixture("x86_64_sysv", "x86_64-unknown-linux-gnu", "gcc", None); + run_linux_c_abi_fixture("c_abi_edges", "x86_64-unknown-linux-gnu", "gcc", None); } #[test] @@ -2817,6 +3348,16 @@ fn aarch64_c_abi_interoperates_with_c() { }, if native { None } else { Some("qemu-aarch64") }, ); + run_linux_c_abi_fixture( + "c_abi_edges", + "aarch64-unknown-linux-gnu", + if native { + "gcc" + } else { + "aarch64-linux-gnu-gcc" + }, + if native { None } else { Some("qemu-aarch64") }, + ); } #[test] @@ -2899,6 +3440,13 @@ fn riscv64_c_abi_interoperates_with_c_under_qemu() { String::from_utf8_lossy(&run.stdout), String::from_utf8_lossy(&run.stderr) ); + + run_linux_c_abi_fixture( + "c_abi_edges", + "riscv64-unknown-linux-gnu", + "riscv64-linux-gnu-gcc", + Some("qemu-riscv64"), + ); } #[test] diff --git a/tests/fixtures/c_abi_edges/interop.c b/tests/fixtures/c_abi_edges/interop.c new file mode 100644 index 00000000..73288094 --- /dev/null +++ b/tests/fixtures/c_abi_edges/interop.c @@ -0,0 +1,131 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed char i8; +typedef unsigned char u8; +typedef signed int i32; +typedef signed long i64; +typedef unsigned long u64; + +void *memcpy(void *destination, const void *source, u64 count) { + u8 *out = (u8 *)destination; + const u8 *in = (const u8 *)source; + for (u64 index = 0; index < count; ++index) out[index] = in[index]; + return destination; +} + +struct empty {}; +struct bytes1 { u8 values[1]; }; +struct bytes2 { u8 values[2]; }; +struct bytes3 { u8 values[3]; }; +struct bytes4 { u8 values[4]; }; +struct bytes5 { u8 values[5]; }; +struct bytes6 { u8 values[6]; }; +struct bytes7 { u8 values[7]; }; +struct bytes8 { u8 values[8]; }; +struct bytes9 { u8 values[9]; }; +struct bytes12 { u8 values[12]; }; +struct bytes16 { u8 values[16]; }; +struct nested { struct bytes3 head; struct bytes5 tail; }; +struct array_member { unsigned short values[3]; }; +struct pointer_member { u8 *value; }; + +#define CHECK_SIZE(name, size) _Static_assert(sizeof(struct name) == size, "bad " #name " size") +CHECK_SIZE(bytes1, 1); CHECK_SIZE(bytes2, 2); CHECK_SIZE(bytes3, 3); +CHECK_SIZE(bytes4, 4); CHECK_SIZE(bytes5, 5); CHECK_SIZE(bytes6, 6); +CHECK_SIZE(bytes7, 7); CHECK_SIZE(bytes8, 8); CHECK_SIZE(bytes9, 9); +CHECK_SIZE(bytes12, 12); CHECK_SIZE(bytes16, 16); CHECK_SIZE(nested, 8); +CHECK_SIZE(array_member, 6); CHECK_SIZE(pointer_member, 8); + +#define DECLARE_WAVE(name) extern struct name wave_##name(struct name) +DECLARE_WAVE(empty); DECLARE_WAVE(bytes1); DECLARE_WAVE(bytes2); DECLARE_WAVE(bytes3); +DECLARE_WAVE(bytes4); DECLARE_WAVE(bytes5); DECLARE_WAVE(bytes6); DECLARE_WAVE(bytes7); +DECLARE_WAVE(bytes8); DECLARE_WAVE(bytes9); DECLARE_WAVE(bytes12); DECLARE_WAVE(bytes16); +DECLARE_WAVE(nested); DECLARE_WAVE(array_member); DECLARE_WAVE(pointer_member); + +#define DEFINE_ECHO(name) struct name c_##name(struct name value) { return value; } +DEFINE_ECHO(empty) DEFINE_ECHO(bytes1) DEFINE_ECHO(bytes2) DEFINE_ECHO(bytes3) +DEFINE_ECHO(bytes4) DEFINE_ECHO(bytes5) DEFINE_ECHO(bytes6) DEFINE_ECHO(bytes7) +DEFINE_ECHO(bytes8) DEFINE_ECHO(bytes9) DEFINE_ECHO(bytes12) DEFINE_ECHO(bytes16) +DEFINE_ECHO(nested) DEFINE_ECHO(array_member) DEFINE_ECHO(pointer_member) + +i8 c_i8(i8 value) { return value; } +double c_f64(double value) { return value; } +u8 *c_pointer(u8 *value) { return value; } + +i64 c_promotions(i32 count, ...) { + __builtin_va_list arguments; + __builtin_va_start(arguments, count); + i64 sum = 0; + for (i32 index = 0; index < count; ++index) sum += __builtin_va_arg(arguments, i32); + __builtin_va_end(arguments); + return sum; +} + +i64 c_pointer_variadic(i32 count, ...) { + __builtin_va_list arguments; + __builtin_va_start(arguments, count); + i64 nonnull = 0; + for (i32 index = 0; index < count; ++index) + nonnull += __builtin_va_arg(arguments, void *) != 0; + __builtin_va_end(arguments); + return nonnull; +} + +i32 c_check_wave_exports(void) { + wave_empty((struct empty){}); + struct bytes1 b1 = wave_bytes1((struct bytes1){{1}}); + struct bytes2 b2 = wave_bytes2((struct bytes2){{2, 3}}); + struct bytes3 b3 = wave_bytes3((struct bytes3){{3, 4, 5}}); + struct bytes4 b4 = wave_bytes4((struct bytes4){{4, 5, 6, 7}}); + struct bytes5 b5 = wave_bytes5((struct bytes5){{5, 6, 7, 8, 9}}); + struct bytes6 b6 = wave_bytes6((struct bytes6){{6, 7, 8, 9, 10, 11}}); + struct bytes7 b7 = wave_bytes7((struct bytes7){{7, 8, 9, 10, 11, 12, 13}}); + struct bytes8 b8 = wave_bytes8((struct bytes8){{8, 9, 10, 11, 12, 13, 14, 15}}); + struct bytes9 b9 = wave_bytes9((struct bytes9){{9, 10, 11, 12, 13, 14, 15, 16, 17}}); + struct bytes12 b12 = wave_bytes12((struct bytes12){{12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}}); + struct bytes16 b16 = wave_bytes16((struct bytes16){{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}}); + struct nested nested = wave_nested((struct nested){{{1, 2, 3}}, {{4, 5, 6, 7, 8}}}); + struct array_member array = wave_array_member((struct array_member){{101, 102, 103}}); + struct pointer_member pointer = wave_pointer_member((struct pointer_member){0}); + if (b1.values[0] != 1 || b2.values[1] != 3 || b3.values[2] != 5) return 1; + if (b4.values[3] != 7 || b5.values[4] != 9 || b6.values[5] != 11) return 2; + if (b7.values[6] != 13 || b8.values[7] != 15 || b9.values[8] != 17) return 3; + if (b12.values[11] != 23 || b16.values[15] != 31) return 4; + if (nested.head.values[2] != 3 || nested.tail.values[4] != 8) return 5; + if (array.values[2] != 103 || pointer.value != 0) return 6; + return 0; +} + +#if defined(__x86_64__) +__asm__( + ".global _start\n" + "_start:\n" + "andq $-16, %rsp\n" + "call main\n" + "movslq %eax, %rdi\n" + "movq $60, %rax\n" + "syscall\n" +); +#elif defined(__aarch64__) +__asm__( + ".global _start\n" + "_start:\n" + "bl main\n" + "mov x8, #93\n" + "svc #0\n" +); +#elif defined(__riscv) +__asm__( + ".global _start\n" + "_start:\n" + "andi sp, sp, -16\n" + "call main\n" + "li a7, 93\n" + "ecall\n" +); +#else +#error unsupported fixture architecture +#endif diff --git a/tests/fixtures/c_abi_edges/interop.wave b/tests/fixtures/c_abi_edges/interop.wave new file mode 100644 index 00000000..2ddbb7d1 --- /dev/null +++ b/tests/fixtures/c_abi_edges/interop.wave @@ -0,0 +1,103 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +struct Empty {} +struct Bytes1 { values: array; } +struct Bytes2 { values: array; } +struct Bytes3 { values: array; } +struct Bytes4 { values: array; } +struct Bytes5 { values: array; } +struct Bytes6 { values: array; } +struct Bytes7 { values: array; } +struct Bytes8 { values: array; } +struct Bytes9 { values: array; } +struct Bytes12 { values: array; } +struct Bytes16 { values: array; } +struct Nested { head: Bytes3; tail: Bytes5; } +struct ArrayMember { values: array; } +struct PointerMember { value: ptr; } + +extern(c) fun c_empty(value: Empty) -> Empty; +extern(c) fun c_bytes1(value: Bytes1) -> Bytes1; +extern(c) fun c_bytes2(value: Bytes2) -> Bytes2; +extern(c) fun c_bytes3(value: Bytes3) -> Bytes3; +extern(c) fun c_bytes4(value: Bytes4) -> Bytes4; +extern(c) fun c_bytes5(value: Bytes5) -> Bytes5; +extern(c) fun c_bytes6(value: Bytes6) -> Bytes6; +extern(c) fun c_bytes7(value: Bytes7) -> Bytes7; +extern(c) fun c_bytes8(value: Bytes8) -> Bytes8; +extern(c) fun c_bytes9(value: Bytes9) -> Bytes9; +extern(c) fun c_bytes12(value: Bytes12) -> Bytes12; +extern(c) fun c_bytes16(value: Bytes16) -> Bytes16; +extern(c) fun c_nested(value: Nested) -> Nested; +extern(c) fun c_array_member(value: ArrayMember) -> ArrayMember; +extern(c) fun c_pointer_member(value: PointerMember) -> PointerMember; +extern(c) fun c_i8(value: i8) -> i8; +extern(c) fun c_f64(value: f64) -> f64; +extern(c) fun c_pointer(value: ptr) -> ptr; +extern(c) fun c_promotions(count: i32, ...) -> i64; +extern(c) fun c_pointer_variadic(count: i32, ...) -> i64; +extern(c) fun c_check_wave_exports() -> i32; + +export(c) fun wave_empty(value: Empty) -> Empty { return value; } +export(c) fun wave_bytes1(value: Bytes1) -> Bytes1 { return value; } +export(c) fun wave_bytes2(value: Bytes2) -> Bytes2 { return value; } +export(c) fun wave_bytes3(value: Bytes3) -> Bytes3 { return value; } +export(c) fun wave_bytes4(value: Bytes4) -> Bytes4 { return value; } +export(c) fun wave_bytes5(value: Bytes5) -> Bytes5 { return value; } +export(c) fun wave_bytes6(value: Bytes6) -> Bytes6 { return value; } +export(c) fun wave_bytes7(value: Bytes7) -> Bytes7 { return value; } +export(c) fun wave_bytes8(value: Bytes8) -> Bytes8 { return value; } +export(c) fun wave_bytes9(value: Bytes9) -> Bytes9 { return value; } +export(c) fun wave_bytes12(value: Bytes12) -> Bytes12 { return value; } +export(c) fun wave_bytes16(value: Bytes16) -> Bytes16 { return value; } +export(c) fun wave_nested(value: Nested) -> Nested { return value; } +export(c) fun wave_array_member(value: ArrayMember) -> ArrayMember { return value; } +export(c) fun wave_pointer_member(value: PointerMember) -> PointerMember { return value; } + +fun signed_result() -> i8 { return -1; } + +fun main() -> i32 { + c_empty(Empty {}); + c_bytes3(Bytes3 { values: [3, 4, 5] }); + c_i8(-1); + c_f64(1.5); + c_pointer(null); + + let b1: Bytes1 = c_bytes1(Bytes1 { values: [1] }); + let b2: Bytes2 = c_bytes2(Bytes2 { values: [2, 3] }); + let b3: Bytes3 = c_bytes3(Bytes3 { values: [3, 4, 5] }); + let b4: Bytes4 = c_bytes4(Bytes4 { values: [4, 5, 6, 7] }); + let b5: Bytes5 = c_bytes5(Bytes5 { values: [5, 6, 7, 8, 9] }); + let b6: Bytes6 = c_bytes6(Bytes6 { values: [6, 7, 8, 9, 10, 11] }); + let b7: Bytes7 = c_bytes7(Bytes7 { values: [7, 8, 9, 10, 11, 12, 13] }); + let b8: Bytes8 = c_bytes8(Bytes8 { values: [8, 9, 10, 11, 12, 13, 14, 15] }); + let b9: Bytes9 = c_bytes9(Bytes9 { values: [9, 10, 11, 12, 13, 14, 15, 16, 17] }); + let b12: Bytes12 = c_bytes12(Bytes12 { values: [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] }); + let b16: Bytes16 = c_bytes16(Bytes16 { values: [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] }); + if (b1.values[0] != 1 || b2.values[1] != 3 || b3.values[2] != 5) { return 1; } + if (b4.values[3] != 7 || b5.values[4] != 9 || b6.values[5] != 11) { return 2; } + if (b7.values[6] != 13 || b8.values[7] != 15 || b9.values[8] != 17) { return 3; } + if (b12.values[11] != 23 || b16.values[15] != 31) { return 4; } + + let nested: Nested = c_nested(Nested { head: Bytes3 { values: [1, 2, 3] }, tail: Bytes5 { values: [4, 5, 6, 7, 8] } }); + let array_member: ArrayMember = c_array_member(ArrayMember { values: [101, 102, 103] }); + let pointer_member: PointerMember = c_pointer_member(PointerMember { value: null }); + if (nested.head.values[2] != 3 || nested.tail.values[4] != 8) { return 5; } + if (array_member.values[2] != 103 || pointer_member.value != null) { return 6; } + + let signed: i8 = -128; + let unsigned: u8 = 255; + let zero: i8 = 0; + let one: i8 = 1; + if (c_promotions(10, signed, unsigned, signed + 127, zero - one, signed * zero - one, signed < zero, !zero, (signed + 127) * one, signed_result(), 255 as u8) != 379) { + return 7; + } + if (c_pointer_variadic(2, null as ptr, null as ptr) != 0) { return 8; } + + let export_status: i32 = c_check_wave_exports(); + if (export_status != 0) { return 20 + export_status; } + return 0; +} diff --git a/tests/fixtures/c_abi_edges/narrow.c b/tests/fixtures/c_abi_edges/narrow.c new file mode 100644 index 00000000..2924067b --- /dev/null +++ b/tests/fixtures/c_abi_edges/narrow.c @@ -0,0 +1,18 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// SPDX-License-Identifier: MPL-2.0 + +typedef signed char i8; +typedef unsigned char u8; +typedef signed short i16; +typedef unsigned short u16; +typedef signed int i32; +typedef unsigned int u32; + +i8 c_i8(i8 value) { return value; } +u8 c_u8(u8 value) { return value; } +i16 c_i16(i16 value) { return value; } +u16 c_u16(u16 value) { return value; } +i32 c_i32(i32 value) { return value; } +u32 c_u32(u32 value) { return value; } diff --git a/tools/run_tests.py b/tools/run_tests.py index 5e533626..a277405c 100644 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -137,6 +137,7 @@ def parse_test_metadata(rel_path: str): "emit": "obj", "freestanding": False, "expected_exit": 0, + "udp_input": False, } try: @@ -173,6 +174,8 @@ def parse_test_metadata(rel_path: str): meta["freestanding"] = value.lower() in {"1", "true", "yes"} elif key == "expected-exit": meta["expected_exit"] = int(value) + elif key == "udp-input": + meta["udp_input"] = value.lower() in {"1", "true", "yes"} except OSError: pass @@ -224,12 +227,15 @@ def command_for_test(name: str, rel_path: str): raise ValueError(f"unsupported wave-test mode '{mode}' in {rel_path}") -def send_udp_for_test61(): - time.sleep(0.5) +def send_udp_test_input(): try: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.sendto(b"hello from python\n", ("127.0.0.1", 8080)) - sock.close() + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + # `wavec run` compiles before starting the receiver. Repeat the + # datagram during that startup window instead of racing a single + # send against compilation on slower CI hosts. + for _ in range(50): + time.sleep(0.1) + sock.sendto(b"hello from python\n", ("127.0.0.1", 8080)) except OSError: # Some CI/sandbox environments block local sockets. pass @@ -325,15 +331,9 @@ def run_and_classify(name, rel_path, cmd): return run_test56_server(cmd) try: - if name == "test61.wave": + if parse_test_metadata(rel_path)["udp_input"]: threading.Thread( - target=send_udp_for_test61, - daemon=True - ).start() - - if name == "test62.wave": - threading.Thread( - target=send_udp_for_test61, + target=send_udp_test_input, daemon=True ).start() diff --git a/x.py b/x.py index a878809a..7e19f248 100644 --- a/x.py +++ b/x.py @@ -660,44 +660,75 @@ def llvm_tool_run_env(tool): env["LD_LIBRARY_PATH"] = f"{lib_dir}:{current}" if current else str(lib_dir) return env -def linux_crt_source_for_target(target): - if target == "x86_64-unknown-linux-gnu": - return ROOT / "llvm" / "crt" / "linux" / "x86_64" / "crt1.s" - return None +def linux_crt_specs(): + return [ + ("x86_64-unknown-linux-gnu", None, "x86_64", None), + ("aarch64-unknown-linux-gnu", None, "aarch64", None), + ( + "riscv64-unknown-linux-gnu", + "lp64", + "riscv64", + "+m,+a,+c,+zicsr,+zifencei", + ), + ( + "riscv64-unknown-linux-gnu", + "lp64f", + "riscv64", + "+m,+a,+f,+c,+zicsr,+zifencei", + ), + ( + "riscv64-unknown-linux-gnu", + "lp64d", + "riscv64", + "+m,+a,+f,+d,+c,+zicsr,+zifencei", + ), + ] -def write_linux_crt_objects(stage_dir, target): - if not is_linux_target(target): +def write_linux_crt_objects(stage_dir, package_target): + if not is_linux_target(package_target): return [] - source = linux_crt_source_for_target(target) - if source is None: - return [] - if not source.exists(): - print(f"[!] Missing Linux CRT source for {target}: {source}") - sys.exit(1) - - llvm_mc = find_release_tool("llvm-mc", target) + llvm_mc = find_release_tool("llvm-mc", package_target) if llvm_mc is None: print("[!] Missing LLVM tool for Linux CRT object: llvm-mc") sys.exit(1) - dst_dir = stage_dir / "crt" / target - dst_dir.mkdir(parents=True, exist_ok=True) - dst = dst_dir / "crt1.o" - - subprocess.run( - [ - str(llvm_mc), - f"-triple={target}", - "-filetype=obj", - str(source), - "-o", - str(dst), - ], - env=llvm_tool_run_env(llvm_mc), - check=True, - ) - return [dst] + objects = [] + for target, abi, architecture, attributes in linux_crt_specs(): + source_dir = ROOT / "llvm" / "crt" / "linux" / architecture + + dst_dir = stage_dir / "crt" / target + if abi is not None: + dst_dir /= abi + dst_dir.mkdir(parents=True, exist_ok=True) + + for object_name, source_name in [ + ("crt1.o", "crt1.s"), + ("Scrt1.o", "crt1.s"), + ("rcrt1.o", "crt1.s"), + ("crti.o", "crti.s"), + ("crtn.o", "crtn.s"), + ]: + source = source_dir / source_name + if not source.exists(): + print(f"[!] Missing Linux CRT source for {target}: {source}") + sys.exit(1) + dst = dst_dir / object_name + command = [ + str(llvm_mc), + f"-triple={target}", + "-filetype=obj", + ] + if attributes is not None: + command.append(f"-mattr={attributes}") + command.extend([str(source), "-o", str(dst)]) + subprocess.run( + command, + env=llvm_tool_run_env(llvm_mc), + check=True, + ) + objects.append(dst) + return objects def ldd_shared_libs(binary): if shutil.which("ldd") is None: From 58b2f7382624f86ec58bfd7a98638a247d6df9d9 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Wed, 12 Aug 2026 19:30:57 +0900 Subject: [PATCH 3/3] Fix cross-platform ABI regressions exposed by CI Analyze resolved proto implementations before lowering their methods into backend function nodes. Move method bodies instead of cloning them so semantic expression-type identities remain valid, while preventing lowered proto symbols from being registered twice during code generation. Normalize NUL padding in BSD extended archive member names so RISC-V ELF metadata inside Darwin archives is inspected and reported as archive(member). Add a focused archive parser regression covering the LP64F flags used by pre-link ABI validation. Accept both opaque-pointer and integer spellings for the single-register x86_64 SysV pointer aggregate in Clang IR contract tests. These Clang host-build representations use the same INTEGER-class GPR transport while Wave keeps its stable pointer representation. This restores the proto E2E cases on Linux and RISC-V, the RISC-V archive mismatch test on macOS, and the aggregate ABI contract test on Windows without weakening the actual C interop checks. --- llvm/src/codegen/ir.rs | 35 +++++++++++++++-------------- src/link_validation/elf.rs | 43 ++++++++++++++++++++++++++++++++---- tests/codegen_regressions.rs | 30 +++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 22 deletions(-) diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 27fba0cb..5d61080f 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -519,29 +519,32 @@ fn build_module( codegen_trace("resolve named types"); let named_types = collect_named_types(ast_nodes); - let mut ast_nodes: Vec = ast_nodes + let ast_nodes: Vec = ast_nodes .iter() .map(|n| resolve_ast_node(n, &named_types)) .collect(); - let proto_functions: Vec = ast_nodes - .iter() - .filter_map(|node| match node { - ASTNode::ProtoImpl(implementation) => Some(implementation), - _ => None, - }) - .flat_map(|implementation| { - implementation.methods.iter().map(|method| { - let mut function = method.clone(); - function.name = format!("{}_{}", implementation.target, method.name); - ASTNode::Function(function) - }) - }) - .collect(); - ast_nodes.extend(proto_functions); let semantic_types = parser::verification::analyze_expression_types(&ast_nodes).unwrap_or_else(|diagnostic| { panic!("semantic analysis before codegen failed: {diagnostic}") }); + + // Semantic analysis understands proto methods directly and registers their + // lowered names. Move those same method nodes into the function stream only + // after analysis so they are neither registered twice nor cloned away from + // the expression addresses recorded in `semantic_types`. + let mut lowered_nodes = Vec::with_capacity(ast_nodes.len()); + for node in ast_nodes { + match node { + ASTNode::ProtoImpl(mut implementation) => { + for mut method in implementation.methods.drain(..) { + method.name = format!("{}_{}", implementation.target, method.name); + lowered_nodes.push(ASTNode::Function(method)); + } + } + node => lowered_nodes.push(node), + } + } + let ast_nodes = lowered_nodes; super::semantic::install_expression_types(semantic_types); codegen_trace("resolve target triple"); diff --git a/src/link_validation/elf.rs b/src/link_validation/elf.rs index 00e3dcc4..97c8c521 100644 --- a/src/link_validation/elf.rs +++ b/src/link_validation/elf.rs @@ -160,10 +160,10 @@ fn inspect_archive( let Some(name_bytes) = member_data.get(..length) else { return Err(malformed(&display, "truncated BSD archive member name")); }; - ( - Some(String::from_utf8_lossy(name_bytes).into_owned()), - &member_data[length..], - ) + let name = String::from_utf8_lossy(name_bytes) + .trim_end_matches('\0') + .to_string(); + (Some(name), &member_data[length..]) } else if let Some(name_offset) = raw_name.strip_prefix('/') { let name_offset = name_offset .parse::() @@ -222,3 +222,38 @@ fn read_u32(bytes: &[u8], little_endian: bool) -> u32 { u32::from_be_bytes(bytes) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn archive_header(name: &str, size: usize) -> Vec { + let header = format!("{name:<16}{:<12}{:<6}{:<6}{:<8}{size:<10}`\n", 0, 0, 0, 0); + assert_eq!(header.len(), 60); + header.into_bytes() + } + + #[test] + fn reads_nul_padded_bsd_extended_member_names() { + let member_name = b"main.o\0\0\0\0\0\0"; + let mut elf = vec![0u8; 52]; + elf[..4].copy_from_slice(ELF_MAGIC); + elf[4] = 1; + elf[5] = 1; + elf[18..20].copy_from_slice(&243u16.to_le_bytes()); + elf[36..40].copy_from_slice(&2u32.to_le_bytes()); + + let size = member_name.len() + elf.len(); + let mut archive = AR_MAGIC.to_vec(); + archive.extend(archive_header("#1/12", size)); + archive.extend(member_name); + archive.extend(elf); + + let mut metadata = Vec::new(); + inspect_archive(Path::new("libmixed.a"), &archive, &mut metadata).unwrap(); + assert_eq!(metadata.len(), 1); + assert_eq!(metadata[0].input, "libmixed.a(main.o)"); + assert_eq!(metadata[0].machine, 243); + assert_eq!(metadata[0].flags, 2); + } +} diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 380cc260..9200617c 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -2929,17 +2929,43 @@ fn odd_sized_aggregate_transport_matches_clang_ir_contracts() { for (name, result, argument) in aggregate_contracts { let c_contract = format!("{result} @c_{name}({argument}"); let wave_contract = format!("{result} @wave_{name}({argument}"); + // Some Clang host builds spell the x86_64 SysV INTEGER class for + // a one-pointer aggregate as `i64`, while others preserve opaque + // `ptr` in IR. Both lower to the same single GPR transport slot. + let clang_contracts = if tag == "x86_64" && *name == "pointer_member" { + vec![c_contract.clone(), "i64 @c_pointer_member(i64".to_string()] + } else { + vec![c_contract.clone()] + }; let clang_definition = clang_ir .lines() .find(|line| line.contains(&format!("@c_{name}("))) .unwrap_or_else(|| panic!("missing c_{name} definition:\n{clang_ir}")) .replace(" %0", ""); - assert!(clang_definition.contains(&c_contract), "{clang_definition}"); + assert!( + clang_contracts + .iter() + .any(|contract| clang_definition.contains(contract)), + "{clang_definition}" + ); assert!( wave_ir.contains(&format!("declare {c_contract}")), "{wave_ir}" ); - assert!(clang_ir.contains(&wave_contract), "{clang_ir}"); + let clang_wave_contracts = if tag == "x86_64" && *name == "pointer_member" { + vec![ + wave_contract.clone(), + "i64 @wave_pointer_member(i64".to_string(), + ] + } else { + vec![wave_contract.clone()] + }; + assert!( + clang_wave_contracts + .iter() + .any(|contract| clang_ir.contains(contract)), + "{clang_ir}" + ); let wave_definition = wave_ir .lines() .find(|line| line.contains(&format!("@wave_{name}(")))