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..fdd0c3c3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -67,9 +67,78 @@ jobs: - name: Run Rust tests 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: >- + 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 + 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 + if: ${{ always() }} + 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 + if: ${{ always() }} + run: python3 tools/run_tests.py + build-linux-riscv64: name: Build Linux riscv64 runs-on: ubuntu-24.04 @@ -80,6 +149,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: | @@ -110,18 +185,70 @@ 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: | + 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: 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: Build release compiler with RISC-V backend - run: cargo build --locked --release --verbose - - name: Link and run Linux riscv64 binary + if: ${{ always() }} run: | set -euo pipefail @@ -130,7 +257,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 +336,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..30415a6a 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, } } @@ -600,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> { @@ -615,6 +620,7 @@ impl<'a> Validator<'a> { span_counts: HashMap::new(), primary_span: None, diagnostic_help: None, + expression_types: HashMap::new(), } } @@ -1153,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()), @@ -1515,6 +1532,7 @@ impl<'a> Validator<'a> { args, &signature.params, signature.required_params, + signature.variadic, )?; Ok(ExpressionType::Known(signature.return_type)) } @@ -1551,6 +1569,7 @@ impl<'a> Validator<'a> { args, params, signature.required_params.saturating_sub(1), + false, )?; return Ok(ExpressionType::Known(signature.return_type)); } @@ -1569,6 +1588,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 +1675,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 +1700,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(()) } @@ -2494,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) })?; @@ -2569,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 ea790b13..c7f858bb 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,50 @@ 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 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), + WaveType::Bool | WaveType::Byte | WaveType::Char => Some(IntegerExtension::Zero), + _ => None, + }; + + match target { + 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, + } } pub struct LoweredExtern<'ctx> { @@ -112,6 +151,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, @@ -121,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; @@ -237,11 +290,21 @@ 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![]; 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 { @@ -360,6 +423,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 +450,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 +465,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 +492,41 @@ 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) { + 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 { + // 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::Direct(context.i64_type().array_type(2).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 +541,36 @@ 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 { + // 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::Direct(context.i64_type().array_type(2).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 +591,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 +632,31 @@ 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 { + // 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()); + } + } + RetLowering::Direct(t) } @@ -500,9 +673,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 +693,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 +732,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 +754,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 +770,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 +782,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 +811,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 +831,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 +863,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..5d61080f 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 @@ -506,6 +523,29 @@ fn build_module( .iter() .map(|n| resolve_ast_node(n, &named_types)) .collect(); + 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"); let triple = if let Some(raw) = &backend.target { @@ -698,31 +738,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 @@ -742,7 +769,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) { @@ -810,6 +837,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/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 d8b36d2a..be0b712d 100644 --- a/llvm/src/expression/rvalue/calls.rs +++ b/llvm/src/expression/rvalue/calls.rs @@ -11,7 +11,10 @@ // 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::{ @@ -80,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); @@ -147,27 +155,49 @@ 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::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()), - Expression::Grouped(inner) => wave_type_of_expr(env, inner), - 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, - } +fn semantic_wave_type_of_expr(expression: &Expression) -> Option { + crate::codegen::semantic::expression_type(expression) +} + +fn lower_c_variadic_argument<'ctx, 'a>( + env: &mut ExprGenEnv<'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 + .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!(semantic_type, Some(WaveType::Int(_))); + if signed { + env.builder + .build_int_s_extend(integer, target, &format!("vararg{}_sext", index)) + .unwrap() + .as_basic_value_enum() } else { - None + env.builder + .build_int_z_extend(integer, target, &format!("vararg{}_zext", index)) + .unwrap() + .as_basic_value_enum() } } - _ => None, + BasicValueEnum::IntValue(_) + | BasicValueEnum::FloatValue(_) + | BasicValueEnum::PointerValue(_) => value, + _ => panic!("C variadic argument {} must be a scalar value", index + 1), } } @@ -181,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 { @@ -377,11 +407,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 +423,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 +446,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 +456,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 +502,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 +517,18 @@ 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 { + 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!( "Extern '{}' returns void and cannot be used as a value", @@ -647,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 @@ -662,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, @@ -675,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(_)), @@ -815,30 +932,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 +967,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 +981,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 @@ -891,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..97c8c521 --- /dev/null +++ b/src/link_validation/elf.rs @@ -0,0 +1,259 @@ +// 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")); + }; + 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::() + .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) + } +} + +#[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/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/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 92% rename from test/test100.wave rename to tests/cases/test100.wave index b3521448..3d97827f 100644 --- a/test/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/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 95% rename from test/test61.wave rename to tests/cases/test61.wave index 37906dde..f8d41eda 100644 --- a/test/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/test/test62.wave b/tests/cases/test62.wave similarity index 96% rename from test/test62.wave rename to tests/cases/test62.wave index 29522c90..a75cbbda 100644 --- a/test/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/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..9200617c 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"); @@ -2653,10 +2739,974 @@ 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 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}"); + // 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_contracts + .iter() + .any(|contract| clang_definition.contains(contract)), + "{clang_definition}" + ); + assert!( + wave_ir.contains(&format!("declare {c_contract}")), + "{wave_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}("))) + .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, + 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); + run_linux_c_abi_fixture("c_abi_edges", "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") }, + ); + 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] +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) + ); + + run_linux_c_abi_fixture( + "c_abi_edges", + "riscv64-unknown-linux-gnu", + "riscv64-linux-gnu-gcc", + Some("qemu-riscv64"), + ); +} + +#[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/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/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..a277405c 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 @@ -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: