rlumod 1.0.0

Safe, no_std matrix updates and linear solves using the LUmod algorithm
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  quality:
    name: Quality
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install Rust stable
        run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
      - name: Install pre-commit
        run: python3 -m pip install -r requirements.txt
      - name: Run pre-commit
        run: pre-commit run --all-files
      - name: Lint
        run: cargo +stable clippy --all-targets --all-features -- -D warnings
      - name: Lint downstream staticlib fixture
        run: cargo +stable clippy --manifest-path tests/staticlib/Cargo.toml --lib -- -D warnings
      - name: Build documentation
        env:
          RUSTDOCFLAGS: -D warnings
        run: cargo +stable doc --all-features --no-deps
      - name: Verify package
        run: cargo +stable package
      - name: Verify C FFI header is included in the Rust package
        shell: bash
        run: |
          cargo +stable package --list | grep -Fx 'include/lumod-c/lumod_dense.h'
          cargo +stable package --list | grep -Fx 'include/rlumod/rlumod.h'

  native-tests:
    name: Test (${{ matrix.runner }})
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-22.04
            experimental: false
          - runner: ubuntu-24.04
            experimental: false
            ffi: true
          - runner: ubuntu-22.04-arm
            experimental: false
          - runner: ubuntu-24.04-arm
            experimental: false
          - runner: windows-2022
            experimental: false
            ffi: true
          - runner: windows-2025
            experimental: false
          - runner: windows-11-arm
            experimental: false
          - runner: macos-15-intel
            experimental: false
          - runner: macos-26-intel
            experimental: false
          - runner: macos-15
            experimental: false
            ffi: true
          - runner: macos-26
            experimental: false
          - runner: ubuntu-26.04
            experimental: true
          - runner: ubuntu-26.04-arm
            experimental: true
          - runner: windows-11-vs2026-arm
            experimental: true
          - runner: xcode-27
            experimental: true
    runs-on: ${{ matrix.runner }}
    continue-on-error: ${{ matrix.experimental }}
    timeout-minutes: 15
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install Rust stable
        run: rustup toolchain install stable --profile minimal
      - name: Run tests
        run: cargo +stable test --all-features
      - name: Build downstream C FFI static libraries
        if: matrix.ffi == true
        run: |
          cargo +stable build --manifest-path tests/staticlib/Cargo.toml --release --target-dir tests/staticlib/target/one --no-default-features --features one-based
          cargo +stable build --manifest-path tests/staticlib/Cargo.toml --release --target-dir tests/staticlib/target/zero --no-default-features --features zero-based
          cargo +stable build --manifest-path tests/staticlib/Cargo.toml --release --target-dir tests/staticlib/target/both --no-default-features --features one-based,zero-based
      - name: Compile and run C and C++ FFI smoke tests
        if: matrix.ffi == true && runner.os != 'Windows'
        shell: bash
        run: |
          compile_and_run() {
            variant="$1"
            source="$2"
            stem="$3"
            target="tests/staticlib/target/${variant}"
            archive="${target}/release/librlumod_staticlib_consumer.a"
            cc -std=c11 -Wall -Wextra -Werror -Iinclude \
              "tests/staticlib/${source}" "${archive}" -lm -o "${target}/${stem}-c"
            "${target}/${stem}-c"
            c++ -x c++ -std=c++17 -Wall -Wextra -Werror -Iinclude \
              "tests/staticlib/${source}" -x none "${archive}" -lm \
              -o "${target}/${stem}-cpp"
            "${target}/${stem}-cpp"
          }
          compile_and_run one smoke_one_based.c smoke-one
          compile_and_run zero smoke_zero_based.c smoke-zero
          compile_and_run both smoke_one_based.c smoke-one
          compile_and_run both smoke_zero_based.c smoke-zero
          compile_and_run zero abi_zero_based.c abi-zero
          compile_and_run both abi_zero_based.c abi-zero
      - name: Compile and run MSVC C and C++ FFI smoke tests
        if: matrix.ffi == true && runner.os == 'Windows'
        shell: cmd
        run: |
          call "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" -arch=amd64
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\one\smoke-one-c.obj tests\staticlib\smoke_one_based.c /Fe:tests\staticlib\target\one\smoke-one-c.exe /link tests\staticlib\target\one\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\one\smoke-one-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\one\smoke-one-cpp.obj tests\staticlib\smoke_one_based.c /Fe:tests\staticlib\target\one\smoke-one-cpp.exe /link tests\staticlib\target\one\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\one\smoke-one-cpp.exe
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\zero\smoke-zero-c.obj tests\staticlib\smoke_zero_based.c /Fe:tests\staticlib\target\zero\smoke-zero-c.exe /link tests\staticlib\target\zero\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\zero\smoke-zero-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\zero\smoke-zero-cpp.obj tests\staticlib\smoke_zero_based.c /Fe:tests\staticlib\target\zero\smoke-zero-cpp.exe /link tests\staticlib\target\zero\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\zero\smoke-zero-cpp.exe
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\both\smoke-one-c.obj tests\staticlib\smoke_one_based.c /Fe:tests\staticlib\target\both\smoke-one-c.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\smoke-one-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\both\smoke-one-cpp.obj tests\staticlib\smoke_one_based.c /Fe:tests\staticlib\target\both\smoke-one-cpp.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\smoke-one-cpp.exe
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\both\smoke-zero-c.obj tests\staticlib\smoke_zero_based.c /Fe:tests\staticlib\target\both\smoke-zero-c.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\smoke-zero-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\both\smoke-zero-cpp.obj tests\staticlib\smoke_zero_based.c /Fe:tests\staticlib\target\both\smoke-zero-cpp.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\smoke-zero-cpp.exe
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\zero\abi-zero-c.obj tests\staticlib\abi_zero_based.c /Fe:tests\staticlib\target\zero\abi-zero-c.exe /link tests\staticlib\target\zero\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\zero\abi-zero-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\zero\abi-zero-cpp.obj tests\staticlib\abi_zero_based.c /Fe:tests\staticlib\target\zero\abi-zero-cpp.exe /link tests\staticlib\target\zero\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\zero\abi-zero-cpp.exe
          cl /nologo /W4 /WX /std:c11 /Iinclude /Fotests\staticlib\target\both\abi-zero-c.obj tests\staticlib\abi_zero_based.c /Fe:tests\staticlib\target\both\abi-zero-c.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\abi-zero-c.exe
          cl /nologo /W4 /WX /TP /std:c++17 /Iinclude /Fotests\staticlib\target\both\abi-zero-cpp.obj tests\staticlib\abi_zero_based.c /Fe:tests\staticlib\target\both\abi-zero-cpp.exe /link tests\staticlib\target\both\release\rlumod_staticlib_consumer.lib
          tests\staticlib\target\both\abi-zero-cpp.exe
      - name: Verify exact C FFI export symbol sets
        if: matrix.runner == 'ubuntu-24.04'
        shell: bash
        run: |
          c_ffi_symbols() {
            grep -E '^(LUmod|Lprod|LUforw|LUback|Usolve|elm|elmgen|rlumod_[[:alnum:]_]+)$' "$1" |
              sort -u
          }
          expected_one_symbols() {
            printf '%s\n' LUmod Lprod Usolve | sort
          }
          expected_zero_symbols() {
            printf '%s\n' \
              rlumod_storage_lengths \
              rlumod_f32_factor_init \
              rlumod_f32_workspace_init \
              rlumod_f32_push \
              rlumod_f32_replace_row \
              rlumod_f32_replace_column \
              rlumod_f32_remove \
              rlumod_f32_solve_in_place \
              rlumod_f32_solve_transpose_in_place \
              rlumod_f64_factor_init \
              rlumod_f64_workspace_init \
              rlumod_f64_push \
              rlumod_f64_replace_row \
              rlumod_f64_replace_column \
              rlumod_f64_remove \
              rlumod_f64_solve_in_place \
              rlumod_f64_solve_transpose_in_place | sort
          }
          expected_both_symbols() {
            {
              expected_one_symbols
              expected_zero_symbols
            } | sort -u
          }

          CARGO_TARGET_DIR=target/feature-gate cargo +stable rustc --release \
            --no-default-features --features lumod-c --lib -- --emit=obj
          nm -g --defined-only target/feature-gate/release/deps/rlumod-*.o |
            awk '{print $3}' > target/feature-gate/symbols.txt
          test -z "$(c_ffi_symbols target/feature-gate/symbols.txt)"

          CARGO_TARGET_DIR=target/feature-gate-one cargo +stable rustc --release \
            --no-default-features --features c-ffi-one-based --lib -- --emit=obj
          nm -g --defined-only target/feature-gate-one/release/deps/rlumod-*.o | \
            awk '{print $3}' > target/feature-gate-one/symbols.txt

          CARGO_TARGET_DIR=target/feature-gate-zero cargo +stable rustc --release \
            --no-default-features --features c-ffi-zero-based --lib -- --emit=obj
          nm -g --defined-only target/feature-gate-zero/release/deps/rlumod-*.o | \
            awk '{print $3}' > target/feature-gate-zero/symbols.txt

          CARGO_TARGET_DIR=target/feature-gate-both cargo +stable rustc --release \
            --no-default-features \
            --features c-ffi-one-based,c-ffi-zero-based --lib -- --emit=obj
          nm -g --defined-only target/feature-gate-both/release/deps/rlumod-*.o | \
            awk '{print $3}' > target/feature-gate-both/symbols.txt

          archive_one="tests/staticlib/target/one/release/librlumod_staticlib_consumer.a"
          archive_zero="tests/staticlib/target/zero/release/librlumod_staticlib_consumer.a"
          archive_both="tests/staticlib/target/both/release/librlumod_staticlib_consumer.a"
          nm -g --defined-only "${archive_one}" | awk '{print $3}' \
            > tests/staticlib/target/one/symbols.txt
          nm -g --defined-only "${archive_zero}" | awk '{print $3}' \
            > tests/staticlib/target/zero/symbols.txt
          nm -g --defined-only "${archive_both}" | awk '{print $3}' \
            > tests/staticlib/target/both/symbols.txt

          diff -u <(expected_one_symbols) \
            <(c_ffi_symbols target/feature-gate-one/symbols.txt)
          diff -u <(expected_zero_symbols) \
            <(c_ffi_symbols target/feature-gate-zero/symbols.txt)
          diff -u <(expected_both_symbols) \
            <(c_ffi_symbols target/feature-gate-both/symbols.txt)
          diff -u <(expected_one_symbols) \
            <(c_ffi_symbols tests/staticlib/target/one/symbols.txt)
          diff -u <(expected_zero_symbols) \
            <(c_ffi_symbols tests/staticlib/target/zero/symbols.txt)
          diff -u <(expected_both_symbols) \
            <(c_ffi_symbols tests/staticlib/target/both/symbols.txt)

          ! grep -Eq '(^|[^[:alnum:]_])(LUforw|LUback|elm|elmgen)([^[:alnum:]_]|$)' \
            include/lumod-c/lumod_dense.h

  msrv:
    name: Test MSRV (Rust 1.85)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install Rust 1.85
        run: rustup toolchain install 1.85.0 --profile minimal --target thumbv7em-none-eabi
      - name: Run all targets on the MSRV
        run: cargo +1.85.0 test --all-targets --all-features
      - name: Build downstream C FFI staticlib on the MSRV
        run: cargo +1.85.0 build --manifest-path tests/staticlib/Cargo.toml --release
      - name: Check no_std C FFI target on the MSRV
        run: cargo +1.85.0 check --all-features --target thumbv7em-none-eabi --release

  wasi-tests:
    name: Test WebAssembly (WASI)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install Rust stable and WASI target
        run: rustup toolchain install stable --profile minimal --target wasm32-wasip1
      - name: Install Wasmtime
        uses: taiki-e/install-action@288e746965032cfcc232e09af2daf5f23c14d780 # v2
        with:
          tool: wasmtime@47.0.3
          fallback: none
      - name: Run tests in Wasmtime
        env:
          CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime
        run: cargo +stable test --target wasm32-wasip1 --all-features --lib --tests

  browser-tests:
    name: Test WebAssembly (Chrome)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - name: Check out repository
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install Rust stable and browser WebAssembly target
        run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown
      - name: Install wasm-pack
        uses: taiki-e/install-action@288e746965032cfcc232e09af2daf5f23c14d780 # v2
        with:
          tool: wasm-pack@0.15.0
          fallback: none
      - name: Run tests in headless Chrome
        run: wasm-pack test --headless --chrome -- --all-features