mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 01:37:00 +00:00
Compare commits
80 Commits
tapanito/c
...
ximinez/nu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1295d1ce8 | ||
|
|
aa8888732e | ||
|
|
50c0d9f2b0 | ||
|
|
8abe82eefa | ||
|
|
5b8e6cd1dd | ||
|
|
b5574baa36 | ||
|
|
015d9a6cb9 | ||
|
|
12e81abef3 | ||
|
|
51902cd6b4 | ||
|
|
c84939ccea | ||
|
|
9e8c3caef4 | ||
|
|
35bee87909 | ||
|
|
261508a0ec | ||
|
|
9f872f2179 | ||
|
|
d1af39d9dd | ||
|
|
f6a26ca34f | ||
|
|
c0569037f8 | ||
|
|
be9ae88d48 | ||
|
|
cd21d74538 | ||
|
|
2fdfd2b420 | ||
|
|
06a3f76ccd | ||
|
|
dadf4d737d | ||
|
|
7b66b42713 | ||
|
|
18ac8a0583 | ||
|
|
de2efa5cb9 | ||
|
|
8dcd88e83c | ||
|
|
5333422402 | ||
|
|
4ec049e727 | ||
|
|
ae9c72bb7c | ||
|
|
5abecb9fcb | ||
|
|
12670b0c3f | ||
|
|
1e7876a03c | ||
|
|
e851e80de0 | ||
|
|
a963035f76 | ||
|
|
8ab904de57 | ||
|
|
100ec464d9 | ||
|
|
e89e6f50e8 | ||
|
|
27456fa439 | ||
|
|
d6844311c0 | ||
|
|
fbee0349f5 | ||
|
|
84ca271d95 | ||
|
|
75dfc65f5f | ||
|
|
48b1716e6f | ||
|
|
4ab886bcbc | ||
|
|
7f64c337d8 | ||
|
|
61bdd6fb78 | ||
|
|
8e06e78f11 | ||
|
|
42fda85fbc | ||
|
|
3a4b92b050 | ||
|
|
aea19df3c1 | ||
|
|
8b56749ca3 | ||
|
|
71cf996fc6 | ||
|
|
4c7ea64b6c | ||
|
|
c8947c6f75 | ||
|
|
09ae5b719f | ||
|
|
09f2d06dd4 | ||
|
|
6964013941 | ||
|
|
70c6e01d7e | ||
|
|
ddfb7ee69c | ||
|
|
b69b9242e2 | ||
|
|
cd2fcf0a5e | ||
|
|
69656d6b67 | ||
|
|
46b946b22e | ||
|
|
ae03b30f29 | ||
|
|
4c7c019add | ||
|
|
47f30c913d | ||
|
|
d7d5b83f6d | ||
|
|
e22938d69f | ||
|
|
7c9a56ff24 | ||
|
|
5a40416673 | ||
|
|
30334cd1f4 | ||
|
|
5558e1b522 | ||
|
|
cd0f49a003 | ||
|
|
22d2703ce8 | ||
|
|
d03274b731 | ||
|
|
1b6047afe1 | ||
|
|
175a04160d | ||
|
|
b050c151f8 | ||
|
|
a2b21d75ce | ||
|
|
b40d2a8e7d |
3
.github/actions/build-deps/action.yml
vendored
3
.github/actions/build-deps/action.yml
vendored
@@ -35,9 +35,8 @@ runs:
|
||||
LOG_VERBOSITY: ${{ inputs.log_verbosity }}
|
||||
SANITIZERS: ${{ inputs.sanitizers }}
|
||||
run: |
|
||||
echo 'Installing dependencies.'
|
||||
conan install \
|
||||
--profile ci \
|
||||
--profile:all ci \
|
||||
--build="${BUILD_OPTION}" \
|
||||
--options:host='&:tests=True' \
|
||||
--options:host='&:xrpld=True' \
|
||||
|
||||
34
.github/actions/set-compiler-env/action.yml
vendored
Normal file
34
.github/actions/set-compiler-env/action.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Set compiler environment
|
||||
description: "Set CC and CXX environment variables for the given compiler."
|
||||
|
||||
inputs:
|
||||
compiler:
|
||||
description: 'The compiler to use ("gcc" or "clang").'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
steps:
|
||||
- name: Set CC and CXX for gcc
|
||||
if: ${{ inputs.compiler == 'gcc' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CC=gcc" >>"${GITHUB_ENV}"
|
||||
echo "CXX=g++" >>"${GITHUB_ENV}"
|
||||
|
||||
- name: Set CC and CXX for clang
|
||||
if: ${{ inputs.compiler == 'clang' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "CC=clang" >>"${GITHUB_ENV}"
|
||||
echo "CXX=clang++" >>"${GITHUB_ENV}"
|
||||
|
||||
- name: Fail on unknown compiler
|
||||
if: ${{ inputs.compiler != 'gcc' && inputs.compiler != 'clang' }}
|
||||
shell: bash
|
||||
env:
|
||||
COMPILER: ${{ inputs.compiler }}
|
||||
run: |
|
||||
echo "Unknown compiler: $COMPILER" >&2
|
||||
exit 1
|
||||
25
.github/actions/setup-conan/action.yml
vendored
25
.github/actions/setup-conan/action.yml
vendored
@@ -15,32 +15,35 @@ runs:
|
||||
using: composite
|
||||
|
||||
steps:
|
||||
- name: Set up Conan configuration
|
||||
- name: Apply custom configuration to global.conf
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'Installing configuration.'
|
||||
cat conan/global.conf ${{ runner.os == 'Linux' && '>>' || '>' }} $(conan config home)/global.conf
|
||||
|
||||
echo 'Conan configuration:'
|
||||
conan config show '*'
|
||||
|
||||
- name: Set up Conan profile
|
||||
- name: Show global configuration
|
||||
shell: bash
|
||||
run: |
|
||||
conan config show '*'
|
||||
|
||||
- name: Install profiles
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'Installing profile.'
|
||||
conan config install conan/profiles/ -tf $(conan config home)/profiles/
|
||||
|
||||
echo 'Conan profile:'
|
||||
- name: Show CI profile
|
||||
shell: bash
|
||||
run: |
|
||||
conan profile show --profile ci
|
||||
|
||||
- name: Set up Conan remote
|
||||
- name: Add a remote
|
||||
shell: bash
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
REMOTE_URL: ${{ inputs.remote_url }}
|
||||
run: |
|
||||
echo "Adding Conan remote '${REMOTE_NAME}' at '${REMOTE_URL}'."
|
||||
conan remote add --index 0 --force "${REMOTE_NAME}" "${REMOTE_URL}"
|
||||
|
||||
echo 'Listing Conan remotes.'
|
||||
- name: List remotes
|
||||
shell: bash
|
||||
run: |
|
||||
conan remote list
|
||||
|
||||
579
.github/scripts/strategy-matrix/generate.py
vendored
579
.github/scripts/strategy-matrix/generate.py
vendored
@@ -1,384 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import dataclasses
|
||||
import itertools
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
THIS_DIR = Path(__file__).parent.resolve()
|
||||
|
||||
_BASE_CMAKE_ARGS = ["-Dtests=ON", "-Dwerr=ON", "-Dxrpld=ON", "-Dwextra=ON"]
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
architecture: list[dict]
|
||||
os: list[dict]
|
||||
# Maps sanitizer names (as used in cmake) to short config-name suffixes.
|
||||
_SANITIZER_SUFFIX: dict[str, str] = {
|
||||
"address": "asan",
|
||||
"undefinedbehavior": "ubsan",
|
||||
"thread": "tsan",
|
||||
}
|
||||
|
||||
|
||||
def get_cmake_args(build_type: str, extra_args: str) -> str:
|
||||
"""Get the full list of CMake arguments for a config."""
|
||||
args = _BASE_CMAKE_ARGS.copy()
|
||||
if build_type == "Release":
|
||||
args.append("-Dassert=ON")
|
||||
if extra_args:
|
||||
args.extend(extra_args.split())
|
||||
return " ".join(args)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input types — shapes of the JSON config files
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LinuxConfig:
|
||||
"""One entry in linux.json's 'configs' or 'package_configs' arrays."""
|
||||
|
||||
compiler: list[str]
|
||||
build_type: list[str]
|
||||
cmake_args: list[str]
|
||||
arch: list[str]
|
||||
sanitizers: list[str] = dataclasses.field(default_factory=list)
|
||||
suffix: str = ""
|
||||
extra_cmake_args: str = ""
|
||||
image: str = "" # only used by package_configs entries
|
||||
|
||||
|
||||
"""
|
||||
Generate a strategy matrix for GitHub Actions CI.
|
||||
@dataclasses.dataclass
|
||||
class LinuxFile:
|
||||
"""Shape of linux.json."""
|
||||
|
||||
On each PR commit we will build a selection of Debian, RHEL, Ubuntu, MacOS, and
|
||||
Windows configurations, while upon merge into the develop or release branches,
|
||||
we will build all configurations, and test most of them.
|
||||
image_tag: str
|
||||
configs: dict[str, list[LinuxConfig]] # distro → configs
|
||||
package_configs: dict[str, list[LinuxConfig]] # distro → packaging configs
|
||||
|
||||
We will further set additional CMake arguments as follows:
|
||||
- All builds will have the `tests`, `werr`, and `xrpld` options.
|
||||
- All builds will have the `wextra` option except for GCC 12 and Clang 16.
|
||||
- All release builds will have the `assert` option.
|
||||
- Certain Debian Bookworm configurations will change the reference fee, enable
|
||||
codecov, and enable voidstar in PRs.
|
||||
"""
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "LinuxFile":
|
||||
data = json.loads(path.read_text())
|
||||
|
||||
def parse(section: dict) -> dict[str, list[LinuxConfig]]:
|
||||
return {
|
||||
distro: [LinuxConfig(**c) for c in cfgs]
|
||||
for distro, cfgs in section.items()
|
||||
}
|
||||
|
||||
return cls(
|
||||
image_tag=data["image_tag"],
|
||||
configs=parse(data["configs"]),
|
||||
package_configs=parse(data.get("package_configs", {})),
|
||||
)
|
||||
|
||||
|
||||
def build_config_name(os_entry: dict[str, str], platform: str, build_type: str) -> str:
|
||||
parts = [os_entry["distro_name"]]
|
||||
for key in ("distro_version", "compiler_name", "compiler_version"):
|
||||
if value := os_entry[key]:
|
||||
parts.append(value)
|
||||
parts.append("arm64" if "arm64" in platform else "amd64")
|
||||
parts.append(build_type.lower())
|
||||
return "-".join(parts)
|
||||
@dataclasses.dataclass
|
||||
class PlatformConfig:
|
||||
"""One entry in macos.json's or windows.json's 'configs' array."""
|
||||
|
||||
build_type: list[str]
|
||||
build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug)
|
||||
extra_cmake_args: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if isinstance(self.build_type, str):
|
||||
self.build_type = [self.build_type]
|
||||
|
||||
|
||||
def generate_packaging_matrix(config: Config) -> list[dict]:
|
||||
"""Emit one entry per os entry with `package: true`. Architecture is
|
||||
hardcoded to linux/amd64 here (and the runner is hardcoded at the
|
||||
workflow level) until arm64 packaging is ready.
|
||||
@dataclasses.dataclass
|
||||
class PlatformFile:
|
||||
"""Shape of macos.json and windows.json."""
|
||||
|
||||
platform: str # e.g. "macos/arm64" or "windows/amd64"
|
||||
runner: list[str] # GitHub Actions runner labels
|
||||
configs: list[PlatformConfig]
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "PlatformFile":
|
||||
data = json.loads(path.read_text())
|
||||
return cls(
|
||||
platform=data["platform"],
|
||||
runner=data["runner"],
|
||||
configs=[PlatformConfig(**c) for c in data["configs"]],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output types — shapes of the generated GitHub Actions matrix entries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Architecture:
|
||||
platform: str
|
||||
runner: list[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MatrixEntry:
|
||||
"""One entry in the generated build/test strategy matrix."""
|
||||
|
||||
config_name: str
|
||||
cmake_args: str
|
||||
cmake_target: str
|
||||
build_only: bool
|
||||
build_type: str
|
||||
architecture: Architecture
|
||||
sanitizers: str
|
||||
image: str = "" # container image; empty for macOS/Windows (runs natively)
|
||||
compiler: str = "" # compiler name ("gcc" or "clang"); empty for macOS/Windows
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PackagingEntry:
|
||||
"""One entry in the generated packaging strategy matrix."""
|
||||
|
||||
artifact_name: str
|
||||
image: str
|
||||
distro: str # e.g. "debian" or "rhel"; drives package-format-specific steps
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Matrix expansion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ARCHS: dict[str, Architecture] = {
|
||||
"amd64": Architecture(
|
||||
platform="linux/amd64", runner=["self-hosted", "Linux", "X64", "heavy"]
|
||||
),
|
||||
"arm64": Architecture(
|
||||
platform="linux/arm64",
|
||||
runner=["self-hosted", "Linux", "ARM64", "heavy-arm64"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def expand_linux_matrix(linux: LinuxFile) -> list[MatrixEntry]:
|
||||
"""Expand a LinuxFile into a flat list of matrix entries.
|
||||
|
||||
Each config entry is expanded over the cross-product of its
|
||||
compiler, build_type, sanitizers, and architecture lists.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"artifact_name": f"xrpld-{build_config_name(os, 'linux/amd64', 'Release')}",
|
||||
"os": os,
|
||||
}
|
||||
for os in config.os
|
||||
if os.get("package", False)
|
||||
]
|
||||
entries: list[MatrixEntry] = []
|
||||
|
||||
for distro, configs in linux.configs.items():
|
||||
for cfg in configs:
|
||||
# An empty sanitizers list means "one entry with no sanitizer".
|
||||
effective_sanitizers = cfg.sanitizers or [""]
|
||||
effective_archs = {arch: _ARCHS[arch] for arch in cfg.arch}
|
||||
|
||||
def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
|
||||
configurations = []
|
||||
for architecture, os, build_type, cmake_args in itertools.product(
|
||||
config.architecture, config.os, config.build_type, config.cmake_args
|
||||
):
|
||||
# The default CMake target is 'all' for Linux and MacOS and 'install'
|
||||
# for Windows, but it can get overridden for certain configurations.
|
||||
cmake_target = "install" if os["distro_name"] == "windows" else "all"
|
||||
|
||||
# We build and test all configurations by default, except for Windows in
|
||||
# Debug, because it is too slow, as well as when code coverage is
|
||||
# enabled as that mode already runs the tests.
|
||||
build_only = False
|
||||
if os["distro_name"] == "windows" and build_type == "Debug":
|
||||
build_only = True
|
||||
|
||||
# Only generate a subset of configurations in PRs.
|
||||
if not all:
|
||||
# Debian:
|
||||
# - Bookworm using GCC 13: Debug on linux/amd64, set the reference
|
||||
# fee to 500 and enable code coverage (which will be done below).
|
||||
# - Bookworm using GCC 15: Debug on linux/amd64, enable Address and
|
||||
# UB sanitizers (which will be done below).
|
||||
# - Bookworm using Clang 16: Debug on linux/amd64, enable voidstar.
|
||||
# - Bookworm using Clang 17: Release on linux/amd64, set the
|
||||
# reference fee to 1000.
|
||||
# - Bookworm using Clang 20: Debug on linux/amd64, enable Address
|
||||
# and UB sanitizers (which will be done below).
|
||||
if os["distro_name"] == "debian":
|
||||
skip = True
|
||||
if os["distro_version"] == "bookworm":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-13"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"-DUNIT_TEST_REFERENCE_FEE=500 {cmake_args}"
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-15"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-16"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"-Dvoidstar=ON {cmake_args}"
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-17"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"-DUNIT_TEST_REFERENCE_FEE=1000 {cmake_args}"
|
||||
skip = False
|
||||
elif os["distro_version"] == "trixie":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-22"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# RHEL:
|
||||
# - 9 using GCC 12: Debug and Release on linux/amd64
|
||||
# (Release is required for RPM packaging).
|
||||
# - 10 using Clang: Release on linux/amd64.
|
||||
if os["distro_name"] == "rhel":
|
||||
skip = True
|
||||
if os["distro_version"] == "9":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
|
||||
and build_type in ["Debug", "Release"]
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
elif os["distro_version"] == "10":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-any"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# Ubuntu:
|
||||
# - Jammy using GCC 12: Debug on linux/arm64, Release on
|
||||
# linux/amd64 (Release is required for DEB packaging).
|
||||
# - Noble using GCC 14: Release on linux/amd64.
|
||||
# - Noble using Clang 18: Debug on linux/amd64.
|
||||
# - Noble using Clang 19: Release on linux/arm64.
|
||||
if os["distro_name"] == "ubuntu":
|
||||
skip = True
|
||||
if os["distro_version"] == "jammy":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/arm64"
|
||||
):
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
elif os["distro_version"] == "noble":
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-14"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-18"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
skip = False
|
||||
if (
|
||||
f"{os['compiler_name']}-{os['compiler_version']}" == "clang-19"
|
||||
and build_type == "Release"
|
||||
and architecture["platform"] == "linux/arm64"
|
||||
):
|
||||
skip = False
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# MacOS:
|
||||
# - Debug on macos/arm64.
|
||||
if os["distro_name"] == "macos" and not (
|
||||
build_type == "Debug" and architecture["platform"] == "macos/arm64"
|
||||
for compiler, build_type, sanitizer, (arch, arch_info) in itertools.product(
|
||||
cfg.compiler,
|
||||
cfg.build_type,
|
||||
effective_sanitizers,
|
||||
effective_archs.items(),
|
||||
):
|
||||
continue
|
||||
name = f"{distro}-{compiler}-{build_type.lower()}-{arch}"
|
||||
suffix_parts = [
|
||||
s for s in [cfg.suffix, _SANITIZER_SUFFIX.get(sanitizer, "")] if s
|
||||
]
|
||||
if suffix_parts:
|
||||
name += "-" + "-".join(suffix_parts)
|
||||
|
||||
# Windows:
|
||||
# - Release on windows/amd64.
|
||||
if os["distro_name"] == "windows" and not (
|
||||
build_type == "Release" and architecture["platform"] == "windows/amd64"
|
||||
):
|
||||
continue
|
||||
|
||||
# Additional CMake arguments.
|
||||
cmake_args = f"{cmake_args} -Dtests=ON -Dwerr=ON -Dxrpld=ON"
|
||||
if not f"{os['compiler_name']}-{os['compiler_version']}" in [
|
||||
"gcc-12",
|
||||
"clang-16",
|
||||
]:
|
||||
cmake_args = f"{cmake_args} -Dwextra=ON"
|
||||
if build_type == "Release":
|
||||
cmake_args = f"{cmake_args} -Dassert=ON"
|
||||
|
||||
# We skip all RHEL on arm64 due to a build failure that needs further
|
||||
# investigation.
|
||||
if os["distro_name"] == "rhel" and architecture["platform"] == "linux/arm64":
|
||||
continue
|
||||
|
||||
# We skip all clang 20+ on arm64 due to Boost build error.
|
||||
if (
|
||||
os["compiler_name"] == "clang"
|
||||
and os["compiler_version"].isdigit()
|
||||
and int(os["compiler_version"]) >= 20
|
||||
and architecture["platform"] == "linux/arm64"
|
||||
):
|
||||
continue
|
||||
|
||||
# Enable code coverage for Debian Bookworm using GCC 13 in Debug on
|
||||
# linux/amd64.
|
||||
if (
|
||||
f"{os['distro_name']}-{os['distro_version']}" == "debian-bookworm"
|
||||
and f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-13"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"{cmake_args} -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0"
|
||||
|
||||
# Enable unity build for Ubuntu Jammy using GCC 12 in Debug on
|
||||
# linux/amd64.
|
||||
if (
|
||||
f"{os['distro_name']}-{os['distro_version']}" == "ubuntu-jammy"
|
||||
and f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
|
||||
and build_type == "Debug"
|
||||
and architecture["platform"] == "linux/amd64"
|
||||
):
|
||||
cmake_args = f"{cmake_args} -Dunity=ON"
|
||||
|
||||
# Generate a unique name for the configuration, e.g. macos-arm64-debug
|
||||
# or debian-bookworm-gcc-12-amd64-release.
|
||||
config_name = build_config_name(os, architecture["platform"], build_type)
|
||||
if "-Dcoverage=ON" in cmake_args:
|
||||
config_name += "-coverage"
|
||||
if "-Dunity=ON" in cmake_args:
|
||||
config_name += "-unity"
|
||||
|
||||
# Add the configuration to the list, with the most unique fields first,
|
||||
# so that they are easier to identify in the GitHub Actions UI, as long
|
||||
# names get truncated.
|
||||
# Add Address and UB sanitizers as separate configurations for specific
|
||||
# bookworm distros. Thread sanitizer is currently disabled (see below).
|
||||
# GCC-Asan xrpld-embedded tests are failing because of https://github.com/google/sanitizers/issues/856
|
||||
if (
|
||||
os["distro_version"] == "bookworm"
|
||||
and f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-15"
|
||||
) or (
|
||||
os["distro_version"] == "trixie"
|
||||
and f"{os['compiler_name']}-{os['compiler_version']}" == "clang-22"
|
||||
):
|
||||
# Add ASAN and UBSAN configurations for both gcc-15 and clang-22
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-asan",
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "address",
|
||||
}
|
||||
)
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-ubsan",
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "undefinedbehavior",
|
||||
}
|
||||
)
|
||||
# TSAN is deactivated due to seg faults with latest compilers.
|
||||
activate_tsan = False
|
||||
if activate_tsan:
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name + "-tsan-ubsan",
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "thread,undefinedbehavior",
|
||||
}
|
||||
entries.append(
|
||||
MatrixEntry(
|
||||
config_name=name,
|
||||
image=f"ghcr.io/xrplf/xrpld/nix-{distro}:{linux.image_tag}",
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="all",
|
||||
build_only=False,
|
||||
build_type=build_type,
|
||||
architecture=arch_info,
|
||||
sanitizers=sanitizer,
|
||||
compiler=compiler,
|
||||
)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
|
||||
"""Generate the packaging matrix from a LinuxFile's package_configs section.
|
||||
|
||||
Packaging uses vanilla distro images (debian:bookworm, ubi9, …) instead of
|
||||
the nix-based build images, because deb/rpm tooling (debhelper, rpm-build)
|
||||
is taken from the distro's archive rather than from nixpkgs. Each config
|
||||
entry carries its own 'image'.
|
||||
"""
|
||||
entries = []
|
||||
for distro, configs in linux.package_configs.items():
|
||||
for cfg in configs:
|
||||
for compiler, build_type in itertools.product(cfg.compiler, cfg.build_type):
|
||||
entries.append(
|
||||
PackagingEntry(
|
||||
artifact_name=f"xrpld-{distro}-{compiler}-{build_type.lower()}-amd64",
|
||||
image=cfg.image,
|
||||
distro=distro,
|
||||
)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def expand_platform_matrix(pf: PlatformFile) -> list[MatrixEntry]:
|
||||
"""Expand a PlatformFile (macOS or Windows) into matrix entries."""
|
||||
platform_name, arch = pf.platform.split("/")
|
||||
is_windows = platform_name == "windows"
|
||||
|
||||
entries: list[MatrixEntry] = []
|
||||
for cfg in pf.configs:
|
||||
for build_type in cfg.build_type:
|
||||
entries.append(
|
||||
MatrixEntry(
|
||||
config_name=f"{platform_name}-{arch}-{build_type.lower()}",
|
||||
cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args),
|
||||
cmake_target="install" if is_windows else "all",
|
||||
build_only=cfg.build_only,
|
||||
build_type=build_type,
|
||||
architecture=Architecture(platform=pf.platform, runner=pf.runner),
|
||||
sanitizers="",
|
||||
)
|
||||
else:
|
||||
configurations.append(
|
||||
{
|
||||
"config_name": config_name,
|
||||
"cmake_args": cmake_args,
|
||||
"cmake_target": cmake_target,
|
||||
"build_only": build_only,
|
||||
"build_type": build_type,
|
||||
"os": os,
|
||||
"architecture": architecture,
|
||||
"sanitizers": "",
|
||||
}
|
||||
)
|
||||
|
||||
return configurations
|
||||
return entries
|
||||
|
||||
|
||||
def read_config(file: Path) -> Config:
|
||||
config = json.loads(file.read_text())
|
||||
if (
|
||||
config["architecture"] is None
|
||||
or config["os"] is None
|
||||
or config["build_type"] is None
|
||||
or config["cmake_args"] is None
|
||||
):
|
||||
raise Exception("Invalid configuration file.")
|
||||
|
||||
return Config(**config)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
help="Set to generate all configurations (generally used when merging a PR) or leave unset to generate a subset of configurations (generally used when committing to a PR).",
|
||||
action="store_true",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a CI strategy matrix for all platforms or a specific one."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
help="Path to the JSON file containing the strategy matrix configurations.",
|
||||
required=False,
|
||||
type=Path,
|
||||
help="Platform to generate for ('linux', 'macos', or 'windows'). Defaults to all platforms.",
|
||||
choices=["linux", "macos", "windows"],
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--packaging",
|
||||
help="Emit the packaging matrix (derived from the 'package' field on os entries) instead of the build/test matrix.",
|
||||
help="Emit the Linux packaging matrix instead of the build/test matrix.",
|
||||
action="store_true",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
matrix = []
|
||||
if args.packaging:
|
||||
config_path = args.config if args.config else THIS_DIR / "linux.json"
|
||||
matrix += generate_packaging_matrix(read_config(config_path))
|
||||
elif args.config is None or args.config == "":
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "linux.json")
|
||||
)
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "macos.json")
|
||||
)
|
||||
matrix += generate_strategy_matrix(
|
||||
args.all, read_config(THIS_DIR / "windows.json")
|
||||
)
|
||||
else:
|
||||
matrix += generate_strategy_matrix(args.all, read_config(args.config))
|
||||
matrix: list[MatrixEntry] | list[PackagingEntry] = []
|
||||
|
||||
# Generate the strategy matrix.
|
||||
print(f"matrix={json.dumps({'include': matrix})}")
|
||||
if args.packaging:
|
||||
matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json"))
|
||||
else:
|
||||
if args.config in ("linux", None):
|
||||
matrix += expand_linux_matrix(LinuxFile.load(THIS_DIR / "linux.json"))
|
||||
if args.config in ("macos", None):
|
||||
matrix += expand_platform_matrix(PlatformFile.load(THIS_DIR / "macos.json"))
|
||||
if args.config in ("windows", None):
|
||||
matrix += expand_platform_matrix(
|
||||
PlatformFile.load(THIS_DIR / "windows.json")
|
||||
)
|
||||
|
||||
print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}")
|
||||
|
||||
300
.github/scripts/strategy-matrix/linux.json
vendored
300
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,221 +1,83 @@
|
||||
{
|
||||
"architecture": [
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"runner": ["self-hosted", "Linux", "X64", "heavy"]
|
||||
},
|
||||
{
|
||||
"platform": "linux/arm64",
|
||||
"runner": ["self-hosted", "Linux", "ARM64", "heavy-arm64"]
|
||||
}
|
||||
],
|
||||
"os": [
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "12",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "13",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "15",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "16",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "17",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "18",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "19",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "bookworm",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "20",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "trixie",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "trixie",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "15",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "trixie",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "20",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "trixie",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "21",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "debian",
|
||||
"distro_version": "trixie",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "22",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "8",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "8",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "any",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "9",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "12",
|
||||
"image_sha": "4c086b9",
|
||||
"package": true
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "9",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "13",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "9",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "9",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "any",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "10",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "rhel",
|
||||
"distro_version": "10",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "any",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "jammy",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "12",
|
||||
"image_sha": "4c086b9",
|
||||
"package": true
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "13",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "gcc",
|
||||
"compiler_version": "14",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "16",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "17",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "18",
|
||||
"image_sha": "4c086b9"
|
||||
},
|
||||
{
|
||||
"distro_name": "ubuntu",
|
||||
"distro_version": "noble",
|
||||
"compiler_name": "clang",
|
||||
"compiler_version": "19",
|
||||
"image_sha": "4c086b9"
|
||||
}
|
||||
],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"cmake_args": [""]
|
||||
"image_tag": "sha-6c54342",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"arch": ["amd64", "arm64"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc", "clang"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"sanitizers": ["address", "undefinedbehavior"]
|
||||
},
|
||||
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "coverage",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "voidstar",
|
||||
"extra_cmake_args": "-Dvoidstar=ON"
|
||||
},
|
||||
{
|
||||
"compiler": ["clang"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "reffee",
|
||||
"extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000"
|
||||
},
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Debug"],
|
||||
"arch": ["amd64"],
|
||||
"suffix": "unity",
|
||||
"extra_cmake_args": "-Dunity=ON"
|
||||
}
|
||||
],
|
||||
|
||||
"debian": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
}
|
||||
],
|
||||
|
||||
"rhel": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"package_configs": {
|
||||
"debian": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"image": "debian:bookworm"
|
||||
}
|
||||
],
|
||||
|
||||
"rhel": [
|
||||
{
|
||||
"compiler": ["gcc"],
|
||||
"build_type": ["Release"],
|
||||
"arch": ["amd64"],
|
||||
"image": "registry.access.redhat.com/ubi9/ubi:latest"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
24
.github/scripts/strategy-matrix/macos.json
vendored
24
.github/scripts/strategy-matrix/macos.json
vendored
@@ -1,19 +1,15 @@
|
||||
{
|
||||
"architecture": [
|
||||
"platform": "macos/arm64",
|
||||
"runner": ["self-hosted", "macOS", "ARM64", "mac-runner-m1"],
|
||||
"configs": [
|
||||
{
|
||||
"platform": "macos/arm64",
|
||||
"runner": ["self-hosted", "macOS", "ARM64", "mac-runner-m1"]
|
||||
}
|
||||
],
|
||||
"os": [
|
||||
"build_type": "Release",
|
||||
"extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
|
||||
},
|
||||
{
|
||||
"distro_name": "macos",
|
||||
"distro_version": "",
|
||||
"compiler_name": "",
|
||||
"compiler_version": "",
|
||||
"image_sha": ""
|
||||
"build_type": "Debug",
|
||||
"extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
|
||||
"build_only": true
|
||||
}
|
||||
],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"cmake_args": ["-DCMAKE_POLICY_VERSION_MINIMUM=3.5"]
|
||||
]
|
||||
}
|
||||
|
||||
23
.github/scripts/strategy-matrix/windows.json
vendored
23
.github/scripts/strategy-matrix/windows.json
vendored
@@ -1,19 +1,8 @@
|
||||
{
|
||||
"architecture": [
|
||||
{
|
||||
"platform": "windows/amd64",
|
||||
"runner": ["self-hosted", "Windows", "devbox"]
|
||||
}
|
||||
],
|
||||
"os": [
|
||||
{
|
||||
"distro_name": "windows",
|
||||
"distro_version": "",
|
||||
"compiler_name": "",
|
||||
"compiler_version": "",
|
||||
"image_sha": ""
|
||||
}
|
||||
],
|
||||
"build_type": ["Debug", "Release"],
|
||||
"cmake_args": [""]
|
||||
"platform": "windows/amd64",
|
||||
"runner": ["self-hosted", "Windows", "devbox"],
|
||||
"configs": [
|
||||
{ "build_type": "Release" },
|
||||
{ "build_type": "Debug", "build_only": true }
|
||||
]
|
||||
}
|
||||
|
||||
1
.github/workflows/on-tag.yml
vendored
1
.github/workflows/on-tag.yml
vendored
@@ -33,7 +33,6 @@ jobs:
|
||||
with:
|
||||
ccache_enabled: false
|
||||
os: ${{ matrix.os }}
|
||||
strategy_matrix: minimal
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
1
.github/workflows/on-trigger.yml
vendored
1
.github/workflows/on-trigger.yml
vendored
@@ -88,7 +88,6 @@ jobs:
|
||||
# not identical to a regular compilation.
|
||||
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
|
||||
os: ${{ matrix.os }}
|
||||
strategy_matrix: ${{ github.event_name == 'schedule' && 'all' || 'minimal' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
49
.github/workflows/reusable-build-test-config.yml
vendored
49
.github/workflows/reusable-build-test-config.yml
vendored
@@ -57,6 +57,12 @@ on:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
compiler:
|
||||
description: 'The compiler to use ("gcc" or "clang"). Leave empty for macOS/Windows (uses system default).'
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: "The Codecov token to use for uploading coverage reports."
|
||||
@@ -124,6 +130,12 @@ jobs:
|
||||
with:
|
||||
subtract: ${{ inputs.nproc_subtract }}
|
||||
|
||||
- name: Set compiler environment (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: ./.github/actions/set-compiler-env
|
||||
with:
|
||||
compiler: ${{ inputs.compiler }}
|
||||
|
||||
- name: Setup Conan
|
||||
env:
|
||||
SANITIZERS: ${{ inputs.sanitizers }}
|
||||
@@ -191,6 +203,21 @@ jobs:
|
||||
--parallel "${BUILD_NPROC}" \
|
||||
--target "${CMAKE_TARGET}"
|
||||
|
||||
# This step is needed to allow running in non-Nix environments
|
||||
- name: Patch binary to use default loader and remove rpath (Linux)
|
||||
if: ${{ runner.os == 'Linux' && env.SANITIZERS_ENABLED == 'false' }}
|
||||
run: |
|
||||
loader="$(/tmp/loader-path.sh)"
|
||||
patchelf --set-interpreter "${loader}" --remove-rpath "${{ env.BUILD_DIR }}/xrpld"
|
||||
|
||||
# We're only running aarch64 Linux builds in Ubuntu-based images, so this is kept simple
|
||||
- name: Install libatomic (Linux aarch64)
|
||||
if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }}
|
||||
run: |
|
||||
apt update --yes
|
||||
apt install -y --no-install-recommends \
|
||||
libatomic1
|
||||
|
||||
- name: Show ccache statistics
|
||||
if: ${{ inputs.ccache_enabled }}
|
||||
run: |
|
||||
@@ -217,7 +244,7 @@ jobs:
|
||||
./xrpld --definitions | python3 -m json.tool >server_definitions.json
|
||||
|
||||
- name: Upload server definitions
|
||||
if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-bookworm-gcc-13-amd64-release' }}
|
||||
if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-gcc-release-amd64' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: server-definitions
|
||||
@@ -279,7 +306,25 @@ jobs:
|
||||
set -o pipefail
|
||||
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
|
||||
[ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$((BUILD_NPROC - 2))
|
||||
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
|
||||
|
||||
# The resolver/preload workaround is only correct for the ASan build:
|
||||
# a regular build doesn't hit the __dn_expand interceptor bug, and must
|
||||
# NOT have libasan injected. So only preload when xrpld is ASan-built.
|
||||
#
|
||||
# libresolv hosts getaddrinfo's resolver helpers (dn_expand, res_*). Under ASan
|
||||
# these are intercepted via dlsym(RTLD_NEXT, ...), which yields a NULL pointer
|
||||
# and crashes DNS resolution if libresolv isn't loaded. Linking it guarantees
|
||||
# the symbols are present; it's a harmless no-op on glibc >= 2.34 (merged into
|
||||
# libc) and is what the compiler driver already does for sanitizer builds.
|
||||
# https://github.com/llvm/llvm-project/issues/59007
|
||||
# https://github.com/google/sanitizers/issues/1592
|
||||
if ldd ./xrpld | grep -q libasan; then
|
||||
PRELOAD="$(gcc -print-file-name=libasan.so):/usr/lib/x86_64-linux-gnu/libresolv.so.2"
|
||||
else
|
||||
PRELOAD=""
|
||||
fi
|
||||
|
||||
LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
|
||||
|
||||
- name: Show test failure summary
|
||||
if: ${{ failure() && !inputs.build_only }}
|
||||
|
||||
12
.github/workflows/reusable-build-test.yml
vendored
12
.github/workflows/reusable-build-test.yml
vendored
@@ -19,13 +19,6 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
strategy_matrix:
|
||||
# TODO: Support additional strategies, e.g. "ubuntu" for generating all Ubuntu configurations.
|
||||
description: 'The strategy matrix to use for generating the configurations ("minimal", "all").'
|
||||
required: false
|
||||
type: string
|
||||
default: "minimal"
|
||||
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
description: "The Codecov token to use for uploading coverage reports."
|
||||
@@ -37,7 +30,6 @@ jobs:
|
||||
uses: ./.github/workflows/reusable-strategy-matrix.yml
|
||||
with:
|
||||
os: ${{ inputs.os }}
|
||||
strategy_matrix: ${{ inputs.strategy_matrix }}
|
||||
|
||||
# Build and test the binary for each configuration.
|
||||
build-test-config:
|
||||
@@ -47,7 +39,6 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: ${{ github.event_name == 'merge_group' }}
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
max-parallel: 10
|
||||
with:
|
||||
build_only: ${{ matrix.build_only }}
|
||||
build_type: ${{ matrix.build_type }}
|
||||
@@ -55,8 +46,9 @@ jobs:
|
||||
cmake_args: ${{ matrix.cmake_args }}
|
||||
cmake_target: ${{ matrix.cmake_target }}
|
||||
runs_on: ${{ toJSON(matrix.architecture.runner) }}
|
||||
image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || '' }}
|
||||
image: ${{ matrix.image || '' }}
|
||||
config_name: ${{ matrix.config_name }}
|
||||
sanitizers: ${{ matrix.sanitizers }}
|
||||
compiler: ${{ matrix.compiler || '' }}
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
39
.github/workflows/reusable-package.yml
vendored
39
.github/workflows/reusable-package.yml
vendored
@@ -1,8 +1,7 @@
|
||||
# Build Linux packages (DEB and RPM) from pre-built binary artifacts.
|
||||
# Discovers which configurations to package from linux.json (os entries
|
||||
# with "package": true) and fans out one job per entry. Today only
|
||||
# linux/amd64 is emitted; the architecture is hardcoded both here
|
||||
# (runner) and in generate.py.
|
||||
# Discovers which configurations to package from linux.json (configs in
|
||||
# "package_configs") and fans out one job per distro. Only linux/amd64 is
|
||||
# supported; the runner is hardcoded in the job below.
|
||||
name: Package
|
||||
|
||||
on:
|
||||
@@ -33,13 +32,12 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Generate packaging matrix
|
||||
id: generate
|
||||
working-directory: .github/scripts/strategy-matrix
|
||||
run: |
|
||||
./generate.py --packaging --config=linux.json >>"${GITHUB_OUTPUT}"
|
||||
run: ./generate.py --packaging >>"${GITHUB_OUTPUT}"
|
||||
|
||||
generate-version:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -66,10 +64,35 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: ${{ format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) }}
|
||||
container: ${{ matrix.image }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# Packaging runs in a vanilla distro image, so the tooling has to come
|
||||
# from the distro's archive: debhelper for deb, rpm-build (and the
|
||||
# systemd / find-debuginfo macros it depends on) for rpm. Run this
|
||||
# before actions/checkout so the latter can use git (real history) for
|
||||
# build_pkg.sh's SOURCE_DATE_EPOCH; otherwise it falls back to a tarball
|
||||
# download and the timestamp comes from wall-clock time.
|
||||
- name: Install packaging tooling (deb)
|
||||
if: ${{ matrix.distro == 'debian' }}
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
debhelper \
|
||||
git
|
||||
|
||||
- name: Install packaging tooling (rpm)
|
||||
if: ${{ matrix.distro == 'rhel' }}
|
||||
run: |
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
git \
|
||||
rpm-build \
|
||||
redhat-rpm-config \
|
||||
systemd-rpm-macros
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
|
||||
15
.github/workflows/reusable-strategy-matrix.yml
vendored
15
.github/workflows/reusable-strategy-matrix.yml
vendored
@@ -4,15 +4,9 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
os:
|
||||
description: 'The operating system to use for the build ("linux", "macos", "windows").'
|
||||
description: 'The operating system to use for the build ("linux", "macos", "windows", or empty for all).'
|
||||
required: false
|
||||
type: string
|
||||
strategy_matrix:
|
||||
# TODO: Support additional strategies, e.g. "ubuntu" for generating all Ubuntu configurations.
|
||||
description: 'The strategy matrix to use for generating the configurations ("minimal", "all").'
|
||||
required: false
|
||||
type: string
|
||||
default: "minimal"
|
||||
outputs:
|
||||
matrix:
|
||||
description: "The generated strategy matrix."
|
||||
@@ -34,12 +28,11 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Generate strategy matrix
|
||||
working-directory: .github/scripts/strategy-matrix
|
||||
id: generate
|
||||
env:
|
||||
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }}
|
||||
GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }}
|
||||
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}"
|
||||
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }}
|
||||
run: ./generate.py ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}"
|
||||
|
||||
11
.github/workflows/upload-conan-deps.yml
vendored
11
.github/workflows/upload-conan-deps.yml
vendored
@@ -48,8 +48,6 @@ jobs:
|
||||
# Generate the strategy matrix to be used by the following job.
|
||||
generate-matrix:
|
||||
uses: ./.github/workflows/reusable-strategy-matrix.yml
|
||||
with:
|
||||
strategy_matrix: ${{ github.event_name == 'pull_request' && 'minimal' || 'all' }}
|
||||
|
||||
# Build and upload the dependencies for each configuration.
|
||||
run-upload-conan-deps:
|
||||
@@ -58,9 +56,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
max-parallel: 10
|
||||
runs-on: ${{ matrix.architecture.runner }}
|
||||
container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || null }}
|
||||
container: ${{ matrix.image || null }}
|
||||
steps:
|
||||
- name: Cleanup workspace (macOS and Windows)
|
||||
if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }}
|
||||
@@ -83,6 +80,12 @@ jobs:
|
||||
with:
|
||||
subtract: ${{ env.NPROC_SUBTRACT }}
|
||||
|
||||
- name: Set compiler environment (Linux)
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: ./.github/actions/set-compiler-env
|
||||
with:
|
||||
compiler: ${{ matrix.compiler }}
|
||||
|
||||
- name: Setup Conan
|
||||
env:
|
||||
SANITIZERS: ${{ matrix.sanitizers }}
|
||||
|
||||
@@ -145,13 +145,39 @@ else()
|
||||
INTERFACE
|
||||
-rdynamic
|
||||
$<$<BOOL:${is_linux}>:-Wl,-z,relro,-z,now,--build-id>
|
||||
# link to static libc/c++ iff: * static option set and * NOT APPLE (AppleClang does not support static
|
||||
# libc/c++) and * NOT SANITIZERS (sanitizers typically don't work with static libc/c++)
|
||||
$<$<AND:$<BOOL:${static}>,$<NOT:$<BOOL:${APPLE}>>,$<NOT:$<BOOL:${SANITIZERS_ENABLED}>>>:
|
||||
# link to static libc/c++ if:
|
||||
# * static option set and
|
||||
# * NOT APPLE (AppleClang does not support static libc/c++)
|
||||
$<$<AND:$<BOOL:${static}>,$<NOT:$<BOOL:${APPLE}>>>:
|
||||
-static-libstdc++
|
||||
-static-libgcc
|
||||
>
|
||||
)
|
||||
|
||||
# Keep -stdlib=libstdc++ off the compile commands, but preserve it for linking.
|
||||
#
|
||||
# Conan turns `compiler.libcxx=libstdc++` into `-stdlib=libstdc++` and puts it in
|
||||
# CMAKE_CXX_FLAGS, which CMake passes to BOTH compile and link steps. On a normal Clang
|
||||
# the compile step consumes it while choosing the C++ stdlib include paths. The Nixpkgs
|
||||
# Clang wrapper supplies those paths itself (via -nostdinc++), so at compile time the
|
||||
# flag is unused -> Clang errors under our -Werror. At link time the flag IS consumed
|
||||
# (it selects the C++ runtime), so we move it there instead of dropping it entirely.
|
||||
get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH)
|
||||
if(
|
||||
_cxx_real MATCHES "^/nix/store/"
|
||||
AND is_linux
|
||||
AND is_clang
|
||||
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
|
||||
)
|
||||
string(
|
||||
REPLACE "-stdlib=libstdc++"
|
||||
""
|
||||
CMAKE_CXX_FLAGS
|
||||
"${CMAKE_CXX_FLAGS}"
|
||||
)
|
||||
string(STRIP "${CMAKE_CXX_FLAGS}" CMAKE_CXX_FLAGS)
|
||||
add_link_options($<$<LINK_LANGUAGE:CXX>:-stdlib=libstdc++>)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Antithesis instrumentation will only be built and deployed using machines running Linux.
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
{% set os = detect_api.detect_os() %}
|
||||
include(sanitizers)
|
||||
|
||||
[conf]
|
||||
{% if os == "Linux" %}
|
||||
user.package:libc_version=2.31
|
||||
tools.info.package_id:confs+=["user.package:libc_version"]
|
||||
{% endif %}
|
||||
|
||||
@@ -50,6 +50,7 @@ words:
|
||||
- AMMXRP
|
||||
- amt
|
||||
- amts
|
||||
- archs
|
||||
- asnode
|
||||
- asynchrony
|
||||
- attestation
|
||||
|
||||
@@ -9,6 +9,7 @@ clang-format --version
|
||||
cmake --version
|
||||
conan --version
|
||||
curl --version
|
||||
doxygen --version
|
||||
g++ --version
|
||||
gcc --version
|
||||
gcov --version
|
||||
|
||||
@@ -27,63 +27,87 @@ fi
|
||||
echo "Detected OS: ${ID} ${VERSION_ID:-}"
|
||||
|
||||
case "${ID}" in
|
||||
debian)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan1
|
||||
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ubuntu | debian | rhel | centos | rocky | almalinux)
|
||||
echo "Supported OS detected: ${ID}"
|
||||
;;
|
||||
|
||||
ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
software-properties-common
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan1
|
||||
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
;;
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf install -y \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan
|
||||
|
||||
dnf clean -y all
|
||||
rm -rf /var/cache/dnf/*
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "ERROR: unsupported OS '${ID}'. Supported: debian, ubuntu, rhel-family" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify that every expected library is now resolvable by the dynamic linker.
|
||||
missing=0
|
||||
for lib in libasan.so.8 libtsan.so.2 libubsan.so.1; do
|
||||
if ldconfig -p | grep -q "${lib}"; then
|
||||
echo "OK: ${lib} found"
|
||||
else
|
||||
echo "ERROR: ${lib} not found after installation" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
function preinstall() {
|
||||
case "${ID}" in
|
||||
ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
software-properties-common
|
||||
add-apt-repository -y ppa:ubuntu-toolchain-r/test
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "${missing}" -ne 0 ]; then
|
||||
echo "ERROR: ${missing} library/libraries missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
function install() {
|
||||
case "${ID}" in
|
||||
debian | ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan1
|
||||
;;
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf install -y \
|
||||
libasan8 \
|
||||
libtsan2 \
|
||||
libubsan
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function postinstall() {
|
||||
# Don't clear cache in non-CI environments
|
||||
if [ -z "${CI:-}" ]; then
|
||||
echo "Not running in CI environment; skipping cache cleanup"
|
||||
return
|
||||
fi
|
||||
|
||||
case "${ID}" in
|
||||
debian | ubuntu)
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
;;
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf clean -y all
|
||||
rm -rf /var/cache/dnf/*
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function verify() {
|
||||
# Verify that every expected library is now resolvable by the dynamic linker.
|
||||
missing=0
|
||||
for lib in libasan.so.8 libtsan.so.2 libubsan.so.1; do
|
||||
if ldconfig -p | grep -q "${lib}"; then
|
||||
echo "OK: ${lib} found"
|
||||
else
|
||||
echo "ERROR: ${lib} not found after installation" >&2
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${missing}" -ne 0 ]; then
|
||||
echo "ERROR: ${missing} library/libraries missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
preinstall
|
||||
install
|
||||
postinstall
|
||||
verify
|
||||
|
||||
echo "All sanitizer runtime libraries installed successfully."
|
||||
|
||||
@@ -51,37 +51,43 @@ namespace detail {
|
||||
* compile time. Doing it at runtime would be pretty wasteful and
|
||||
* inefficient.
|
||||
*/
|
||||
constexpr std::size_t kInt64Digits = 20;
|
||||
consteval std::array<std::uint64_t, kInt64Digits>
|
||||
constexpr std::size_t kUint64Digits = 20;
|
||||
constexpr std::size_t kUint128Digits = 39;
|
||||
|
||||
template <typename T, std::size_t digits>
|
||||
consteval std::array<T, digits>
|
||||
buildPowersOfTen()
|
||||
{
|
||||
std::array<std::uint64_t, kInt64Digits> result{};
|
||||
std::array<T, digits> result{};
|
||||
|
||||
std::uint64_t power = 1;
|
||||
T power = 1;
|
||||
std::size_t exponent = 0;
|
||||
// end the loop early so it doesn't overflow;
|
||||
for (; exponent < result.size() - 1; ++exponent, power *= 10)
|
||||
{
|
||||
result[exponent] = power;
|
||||
if (power > std::numeric_limits<std::uint64_t>::max() / 10)
|
||||
if (power > std::numeric_limits<T>::max() / 10)
|
||||
throw std::logic_error("Power of 10 table is too big");
|
||||
}
|
||||
result[exponent] = power;
|
||||
if (power < std::numeric_limits<std::uint64_t>::max() / 10)
|
||||
throw std::logic_error("Power of 10 table is not big enough for the uint64_t type");
|
||||
if (power < std::numeric_limits<T>::max() / 10)
|
||||
throw std::logic_error("Power of 10 table is not big enough for the given type");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
constexpr std::array<std::uint64_t, detail::kInt64Digits> kPowerOfTen = detail::buildPowersOfTen();
|
||||
template <typename T = std::uint64_t, std::size_t digits = detail::kUint64Digits>
|
||||
constexpr std::array<T, digits> kPowerOfTenImpl = detail::buildPowersOfTen<T, digits>();
|
||||
|
||||
constexpr auto kPowerOfTen = kPowerOfTenImpl<std::uint64_t, detail::kUint64Digits>;
|
||||
|
||||
static_assert(kPowerOfTen[0] == 1);
|
||||
static_assert(kPowerOfTen[1] == 10);
|
||||
static_assert(kPowerOfTen[10] == 10'000'000'000);
|
||||
static_assert(
|
||||
isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1);
|
||||
isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1);
|
||||
|
||||
/** MantissaRange defines a range for the mantissa of a normalized Number.
|
||||
*
|
||||
@@ -141,7 +147,7 @@ struct MantissaRange final
|
||||
int const log{getExponent(scale)};
|
||||
rep const min{getMin(scale, log)};
|
||||
rep const max{(min * 10) - 1};
|
||||
CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)};
|
||||
CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)};
|
||||
|
||||
static MantissaRange const&
|
||||
getMantissaRange(MantissaScale scale);
|
||||
@@ -319,6 +325,8 @@ public:
|
||||
static constexpr internalrep kMaxRep = std::numeric_limits<rep>::max();
|
||||
static_assert(kMaxRep == 9'223'372'036'854'775'807);
|
||||
static_assert(-kMaxRep == std::numeric_limits<rep>::min() + 1);
|
||||
static constexpr internalrep kMaxRepUp = ((kMaxRep / 10) + 1) * 10;
|
||||
static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL);
|
||||
|
||||
// May need to make unchecked private
|
||||
struct Unchecked
|
||||
@@ -560,7 +568,7 @@ private:
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix);
|
||||
|
||||
template <class T>
|
||||
friend void
|
||||
@@ -570,7 +578,7 @@ private:
|
||||
int& exponent,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix,
|
||||
bool dropped);
|
||||
|
||||
[[nodiscard]] bool
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
// Macros below are copied from antithesis_sdk.h and slightly simplified
|
||||
// The duplication is because Visual Studio 2019 cannot compile that header
|
||||
// even with the option -Zc:__cplusplus added.
|
||||
// NOTE: cond must not contain bare commas outside () or []. Commas inside {}
|
||||
// are not protected by the preprocessor and would be parsed as extra arguments.
|
||||
#define ALWAYS(cond, message, ...) assert((message) && (cond))
|
||||
#define ALWAYS_OR_UNREACHABLE(cond, message) assert((message) && (cond))
|
||||
#define SOMETIMES(cond, message, ...)
|
||||
@@ -24,8 +22,6 @@
|
||||
#define XRPL_ASSERT_PARTS(cond, function, description, ...) \
|
||||
XRPL_ASSERT(cond, function " : " description)
|
||||
|
||||
#define XRPL_ASSERT_IF(guard, cond, message) XRPL_ASSERT(!(guard) || (cond), message)
|
||||
|
||||
// How to use the instrumentation macros:
|
||||
//
|
||||
// * XRPL_ASSERT if cond must be true but the line might not be reached during
|
||||
@@ -33,14 +29,6 @@
|
||||
// * XRPL_ASSERT_PARTS is for convenience, and works like XRPL_ASSERT, but
|
||||
// splits the message param into "function" and "description", then joins
|
||||
// them with " : " before passing to XRPL_ASSERT.
|
||||
// * XRPL_ASSERT_IF(guard, cond, message) asserts the implication
|
||||
// `guard => cond`: it can only fail when guard is true (e.g. an amendment
|
||||
// is enabled) and cond is false. Unlike `if (guard) XRPL_ASSERT(...)`, the
|
||||
// assertion site is always evaluated, so the fuzzer registers it
|
||||
// unconditionally; cond itself is short-circuited and only evaluated when
|
||||
// guard is true. NOTE: do not rely on side effects in guard — in release
|
||||
// builds the assertion body is stripped, and the compiler may optimize away
|
||||
// a side-effect-free guard entirely.
|
||||
// * ALWAYS if cond must be true _and_ the line must be reached during fuzzing.
|
||||
// Same like `assert` in normal use.
|
||||
// * REACHABLE if the line must be reached during fuzzing
|
||||
|
||||
@@ -12,6 +12,7 @@ in
|
||||
cmake
|
||||
conan
|
||||
curlMinimal # needed for codecov/codecov-action
|
||||
doxygen
|
||||
gcovr
|
||||
git
|
||||
gnumake
|
||||
|
||||
@@ -65,7 +65,7 @@ MantissaRange::getRanges()
|
||||
static_assert(kRange.log == 15);
|
||||
static_assert(kRange.min < Number::kMaxRep);
|
||||
static_assert(kRange.max < Number::kMaxRep);
|
||||
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
|
||||
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled);
|
||||
}
|
||||
{
|
||||
[[maybe_unused]]
|
||||
@@ -76,7 +76,7 @@ MantissaRange::getRanges()
|
||||
static_assert(kRange.log == 18);
|
||||
static_assert(kRange.min < Number::kMaxRep);
|
||||
static_assert(kRange.max > Number::kMaxRep);
|
||||
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
|
||||
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled);
|
||||
}
|
||||
{
|
||||
[[maybe_unused]]
|
||||
@@ -87,7 +87,7 @@ MantissaRange::getRanges()
|
||||
static_assert(kRange.log == 18);
|
||||
static_assert(kRange.min < Number::kMaxRep);
|
||||
static_assert(kRange.max > Number::kMaxRep);
|
||||
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled);
|
||||
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled);
|
||||
}
|
||||
return map;
|
||||
}();
|
||||
@@ -206,12 +206,6 @@ public:
|
||||
void
|
||||
doDropDigit(T& mantissa, int& exponent) noexcept;
|
||||
|
||||
// Indicate round direction: 1 is up, -1 is down, 0 is even
|
||||
// This enables the client to round towards nearest, and on
|
||||
// tie, round towards even.
|
||||
[[nodiscard]] int
|
||||
round() const noexcept;
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
@@ -221,25 +215,52 @@ public:
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix,
|
||||
std::string location);
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
|
||||
doRoundDown(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix);
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
void
|
||||
doRound(rep& drops, std::string location) const;
|
||||
doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location);
|
||||
|
||||
private:
|
||||
template <class T>
|
||||
void
|
||||
pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix);
|
||||
|
||||
enum class Round {
|
||||
Down = -1,
|
||||
Even = 0,
|
||||
Up = 1,
|
||||
};
|
||||
|
||||
// Indicate round direction: 1 is up, -1 is down, 0 is even
|
||||
// This enables the client to round towards nearest, and on
|
||||
// tie, round towards even.
|
||||
[[nodiscard]]
|
||||
Round
|
||||
round() const noexcept;
|
||||
|
||||
void
|
||||
doPush(unsigned d) noexcept;
|
||||
|
||||
template <UnsignedMantissa T>
|
||||
void
|
||||
bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
|
||||
bringIntoRange(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix);
|
||||
};
|
||||
|
||||
inline void
|
||||
@@ -310,45 +331,77 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
|
||||
++exponent;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa");
|
||||
if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep &&
|
||||
mantissa < kMaxRepUp)
|
||||
{
|
||||
// Special case rounding rules for the values between kMaxRep and kMaxRepUp.
|
||||
// Scale the spread between kMaxRep and kMaxRepUp from 1 to 9, and push it onto the guard as
|
||||
// if it was a digit that got removed, but don't remove it. This method is future-proof in
|
||||
// case the number of mantissa bits ever changes. Effects:
|
||||
// * For round to nearest
|
||||
// * if the mantissa is below the midpoint, it'll round "down" to kMaxRepUp
|
||||
// * if above the midpoint, it'll round "down" to kMaxRep
|
||||
// * if can never be exactly at the midpoint, because kMaxRepUp is always even, and
|
||||
// kMaxRep is always odd, so don't worry about it.
|
||||
// * For round upward, will round up to kMaxRepUp for positive values, down for negative.
|
||||
// * For round downward, does the opposite of upward.
|
||||
// * For round toward zero, always rounds down.
|
||||
auto constexpr spread = kMaxRepUp - kMaxRep;
|
||||
static_assert(spread < 10 && spread >= 0);
|
||||
|
||||
auto const diff = mantissa - kMaxRep;
|
||||
auto const digit = (diff * 10) / spread;
|
||||
XRPL_ASSERT(digit > 0 && digit < 10, "xrpld::Number::Guard::xxxx : valid overflow digit");
|
||||
|
||||
// Don't remove the digit from the mantissa, but add it to the guard as if it was.
|
||||
push(digit);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// -1 if Guard is less than half
|
||||
// 0 if Guard is exactly half
|
||||
// 1 if Guard is greater than half
|
||||
int
|
||||
// Down if Guard is less than half
|
||||
// Even if Guard is exactly half
|
||||
// Up if Guard is greater than half
|
||||
Number::Guard::Round
|
||||
Number::Guard::round() const noexcept
|
||||
{
|
||||
auto mode = Number::getround();
|
||||
|
||||
if (mode == RoundingMode::TowardsZero)
|
||||
return -1;
|
||||
return Round::Down;
|
||||
|
||||
if (mode == RoundingMode::Downward)
|
||||
{
|
||||
if (sbit_)
|
||||
{
|
||||
if (digits_ > 0 || xbit_)
|
||||
return 1;
|
||||
return Round::Up;
|
||||
}
|
||||
return -1;
|
||||
return Round::Down;
|
||||
}
|
||||
|
||||
if (mode == RoundingMode::Upward)
|
||||
{
|
||||
if (sbit_)
|
||||
return -1;
|
||||
return Round::Down;
|
||||
if (digits_ > 0 || xbit_)
|
||||
return 1;
|
||||
return -1;
|
||||
return Round::Up;
|
||||
return Round::Down;
|
||||
}
|
||||
|
||||
// assume round to nearest if mode is not one of the predefined values
|
||||
if (digits_ > 0x5000'0000'0000'0000)
|
||||
return 1;
|
||||
return Round::Up;
|
||||
if (digits_ < 0x5000'0000'0000'0000)
|
||||
return -1;
|
||||
return Round::Down;
|
||||
if (xbit_)
|
||||
return 1;
|
||||
return 0;
|
||||
return Round::Up;
|
||||
return Round::Even;
|
||||
}
|
||||
|
||||
template <UnsignedMantissa T>
|
||||
@@ -357,16 +410,19 @@ Number::Guard::bringIntoRange(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
internalrep const& minMantissa)
|
||||
internalrep const& minMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
// Bring mantissa back into the minMantissa / maxMantissa range AFTER
|
||||
// rounding
|
||||
if (mantissa < minMantissa)
|
||||
if (mantissa < minMantissa &&
|
||||
(cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled || mantissa != 0))
|
||||
{
|
||||
mantissa *= 10;
|
||||
--exponent;
|
||||
}
|
||||
if (exponent < kMinExponent)
|
||||
if (exponent < kMinExponent ||
|
||||
(cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa == 0))
|
||||
{
|
||||
static constexpr Number kZero = Number{};
|
||||
|
||||
@@ -384,16 +440,18 @@ Number::Guard::doRoundUp(
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix,
|
||||
std::string location)
|
||||
{
|
||||
auto r = round();
|
||||
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
|
||||
pushOverflow(mantissa, cuspRoundingFix);
|
||||
|
||||
auto const r = round();
|
||||
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
|
||||
{
|
||||
auto const safeToIncrement = [&maxMantissa](auto const& mantissa) {
|
||||
return mantissa < maxMantissa && mantissa < kMaxRep;
|
||||
};
|
||||
if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled)
|
||||
if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
|
||||
{
|
||||
// Ensure mantissa after incrementing fits within both the
|
||||
// min/maxMantissa range and is a valid "rep".
|
||||
@@ -409,20 +467,27 @@ Number::Guard::doRoundUp(
|
||||
// be impossible to recurse more than once, because once the mantissa is divided by
|
||||
// 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no
|
||||
// chance of bringing it back over.
|
||||
doDropDigit(mantissa, exponent);
|
||||
XRPL_ASSERT_PARTS(
|
||||
safeToIncrement(mantissa),
|
||||
"xrpl::Number::Guard::doRoundUp",
|
||||
"can't recurse more than once");
|
||||
doRoundUp(
|
||||
negative,
|
||||
mantissa,
|
||||
exponent,
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
cuspRoundingFixEnabled,
|
||||
location);
|
||||
return;
|
||||
if (mantissa > kMaxRep && mantissa < kMaxRepUp)
|
||||
{
|
||||
mantissa = kMaxRepUp;
|
||||
}
|
||||
else
|
||||
{
|
||||
doDropDigit(mantissa, exponent);
|
||||
XRPL_ASSERT_PARTS(
|
||||
safeToIncrement(mantissa),
|
||||
"xrpl::Number::Guard::doRoundUp",
|
||||
"can't recurse more than once");
|
||||
doRoundUp(
|
||||
negative,
|
||||
mantissa,
|
||||
exponent,
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
cuspRoundingFix,
|
||||
location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -440,7 +505,13 @@ Number::Guard::doRoundUp(
|
||||
}
|
||||
}
|
||||
}
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa);
|
||||
else if (
|
||||
cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep &&
|
||||
mantissa < kMaxRepUp)
|
||||
{
|
||||
mantissa = kMaxRep;
|
||||
}
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix);
|
||||
if (exponent > kMaxExponent)
|
||||
Throw<std::overflow_error>(std::string(location));
|
||||
}
|
||||
@@ -451,10 +522,13 @@ Number::Guard::doRoundDown(
|
||||
bool& negative,
|
||||
T& mantissa,
|
||||
int& exponent,
|
||||
internalrep const& minMantissa)
|
||||
internalrep const& minMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
// Do not pushOverflow here.
|
||||
|
||||
auto r = round();
|
||||
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
|
||||
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
|
||||
{
|
||||
--mantissa;
|
||||
if (mantissa < minMantissa)
|
||||
@@ -463,15 +537,20 @@ Number::Guard::doRoundDown(
|
||||
--exponent;
|
||||
}
|
||||
}
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa);
|
||||
bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix);
|
||||
}
|
||||
|
||||
// Modify the result to the correctly rounded value
|
||||
void
|
||||
Number::Guard::doRound(rep& drops, std::string location) const
|
||||
Number::Guard::doRound(
|
||||
rep& drops,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix,
|
||||
std::string location)
|
||||
{
|
||||
pushOverflow(drops, cuspRoundingFix);
|
||||
|
||||
auto r = round();
|
||||
if (r == 1 || (r == 0 && (drops & 1) == 1))
|
||||
if (r == Round::Up || (r == Round::Even && (drops & 1) == 1))
|
||||
{
|
||||
if (drops >= kMaxRep)
|
||||
{
|
||||
@@ -486,6 +565,14 @@ Number::Guard::doRound(rep& drops, std::string location) const
|
||||
}
|
||||
++drops;
|
||||
}
|
||||
else if (
|
||||
cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && drops > kMaxRep &&
|
||||
drops < kMaxRepUp)
|
||||
{
|
||||
// This will probably be impossible because this function is not called by mutating
|
||||
// functions, so the Number will already be normalized.
|
||||
drops = kMaxRep;
|
||||
}
|
||||
if (isNegative())
|
||||
drops = -drops;
|
||||
}
|
||||
@@ -530,12 +617,14 @@ doNormalize(
|
||||
int& exponent,
|
||||
MantissaRange::rep const& minMantissa,
|
||||
MantissaRange::rep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix,
|
||||
bool dropped)
|
||||
{
|
||||
static constexpr auto kMinExponent = Number::kMinExponent;
|
||||
static constexpr auto kMaxExponent = Number::kMaxExponent;
|
||||
static constexpr auto kMaxRep = Number::kMaxRep;
|
||||
auto const repLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled
|
||||
? Number::kMaxRep
|
||||
: Number::kMaxRepUp;
|
||||
|
||||
using Guard = Number::Guard;
|
||||
|
||||
@@ -585,7 +674,7 @@ doNormalize(
|
||||
// 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460.
|
||||
// mantissa() will return mantissa / 10, and exponent() will return
|
||||
// exponent + 1.
|
||||
if (m > kMaxRep)
|
||||
if (m > repLimit)
|
||||
{
|
||||
if (exponent >= kMaxExponent)
|
||||
throw std::overflow_error("Number::normalize 1.5");
|
||||
@@ -595,7 +684,7 @@ doNormalize(
|
||||
// modification, it must be less than kMaxRep. In other words, the original
|
||||
// value should have been no more than kMaxRep * 10.
|
||||
// (kMaxRep * 10 > maxMantissa)
|
||||
XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64");
|
||||
XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit");
|
||||
mantissa = m;
|
||||
|
||||
g.doRoundUp(
|
||||
@@ -604,7 +693,7 @@ doNormalize(
|
||||
exponent,
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
cuspRoundingFixEnabled,
|
||||
cuspRoundingFix,
|
||||
"Number::normalize 2");
|
||||
XRPL_ASSERT_PARTS(
|
||||
mantissa >= minMantissa && mantissa <= maxMantissa,
|
||||
@@ -620,13 +709,12 @@ Number::normalize<uint128_t>(
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
// Not used by every compiler version, and thus not necessarily
|
||||
// counted by coverage build
|
||||
// LCOV_EXCL_START
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -638,13 +726,12 @@ Number::normalize<unsigned long long>(
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
// Not used by every compiler version, and thus not necessarily
|
||||
// counted by coverage build
|
||||
// LCOV_EXCL_START
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
@@ -656,16 +743,15 @@ Number::normalize<unsigned long>(
|
||||
int& exponent,
|
||||
internalrep const& minMantissa,
|
||||
internalrep const& maxMantissa,
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
|
||||
MantissaRange::CuspRoundingFix cuspRoundingFix)
|
||||
{
|
||||
doNormalize(
|
||||
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
|
||||
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
|
||||
}
|
||||
|
||||
void
|
||||
Number::normalize(MantissaRange const& range)
|
||||
{
|
||||
normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled);
|
||||
normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix);
|
||||
}
|
||||
|
||||
// Copy the number, but set a new exponent. Because the mantissa doesn't change,
|
||||
@@ -720,45 +806,79 @@ Number::operator+=(Number const& y)
|
||||
uint128_t ym = y.mantissa_;
|
||||
auto ye = y.exponent_;
|
||||
Guard g;
|
||||
|
||||
auto const& range = kRange.get();
|
||||
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
auto const cuspRoundingFix = range.cuspRoundingFix;
|
||||
auto const repLimit =
|
||||
cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp;
|
||||
|
||||
// Bring the exponents of both values into agreement, so the mantissas are on the same scale
|
||||
// and can be added directly together
|
||||
// expandM / expandE: First try to expand the mantissa and bring the exponent down
|
||||
// shringM / shrinkE: Then shrink the mantissa and bring the exponent up, if necessary
|
||||
auto const adjust = [&g, &range](
|
||||
uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) {
|
||||
constexpr uint128_t kSafeLimit = kPowerOfTenImpl<uint128_t, detail::kUint128Digits>[37];
|
||||
|
||||
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
|
||||
{
|
||||
while (shrinkE < expandE && shrinkM % 10 == 0)
|
||||
{
|
||||
g.doDropDigit(shrinkM, shrinkE);
|
||||
}
|
||||
|
||||
// We've got 128 bits of mantissa to work with here. Don't throw away data unless we
|
||||
// have to
|
||||
while (shrinkE < expandE && expandE > kMinExponent && expandM < kSafeLimit)
|
||||
{
|
||||
expandM *= 10;
|
||||
--expandE;
|
||||
}
|
||||
}
|
||||
|
||||
while (shrinkE < expandE)
|
||||
{
|
||||
g.doDropDigit(shrinkM, shrinkE);
|
||||
}
|
||||
};
|
||||
|
||||
if (xe < ye)
|
||||
{
|
||||
if (xn)
|
||||
g.setNegative();
|
||||
do
|
||||
{
|
||||
g.doDropDigit(xm, xe);
|
||||
} while (xe < ye);
|
||||
|
||||
adjust(ym, ye, xm, xe);
|
||||
}
|
||||
else if (xe > ye)
|
||||
{
|
||||
if (yn)
|
||||
g.setNegative();
|
||||
do
|
||||
{
|
||||
g.doDropDigit(ym, ye);
|
||||
} while (xe > ye);
|
||||
}
|
||||
|
||||
auto const& range = kRange.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
|
||||
adjust(xm, xe, ym, ye);
|
||||
}
|
||||
|
||||
if (xn == yn)
|
||||
{
|
||||
xm += ym;
|
||||
if (xm > maxMantissa || xm > kMaxRep)
|
||||
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
|
||||
{
|
||||
g.doDropDigit(xm, xe);
|
||||
while (xm > maxMantissa || xm > repLimit)
|
||||
{
|
||||
g.doDropDigit(xm, xe);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (xm > maxMantissa || xm > repLimit)
|
||||
{
|
||||
g.doDropDigit(xm, xe);
|
||||
}
|
||||
}
|
||||
g.doRoundUp(
|
||||
xn,
|
||||
xm,
|
||||
xe,
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
cuspRoundingFixEnabled,
|
||||
"Number::addition overflow");
|
||||
xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -772,13 +892,26 @@ Number::operator+=(Number const& y)
|
||||
xe = ye;
|
||||
xn = yn;
|
||||
}
|
||||
while (xm < minMantissa && xm * 10 <= kMaxRep)
|
||||
while (xm < minMantissa && xm * 10 <= repLimit)
|
||||
{
|
||||
xm *= 10;
|
||||
xm -= g.pop();
|
||||
--xe;
|
||||
}
|
||||
g.doRoundDown(xn, xm, xe, minMantissa);
|
||||
g.doRoundDown(xn, xm, xe, minMantissa, cuspRoundingFix);
|
||||
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && xm != 0)
|
||||
{
|
||||
// make a new guard
|
||||
Guard g;
|
||||
if (xn)
|
||||
g.setNegative();
|
||||
while (xm > maxMantissa || xm > repLimit)
|
||||
{
|
||||
g.doDropDigit(xm, xe);
|
||||
}
|
||||
g.doRoundUp(
|
||||
xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow");
|
||||
}
|
||||
}
|
||||
|
||||
negative_ = xn;
|
||||
@@ -825,9 +958,11 @@ Number::operator*=(Number const& y)
|
||||
auto const& range = kRange.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
|
||||
auto const cuspRoundingFix = range.cuspRoundingFix;
|
||||
auto const repLimit =
|
||||
cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp;
|
||||
|
||||
while (zm > maxMantissa || zm > kMaxRep)
|
||||
while (zm > maxMantissa || zm > repLimit)
|
||||
{
|
||||
g.doDropDigit(zm, ze);
|
||||
}
|
||||
@@ -840,7 +975,7 @@ Number::operator*=(Number const& y)
|
||||
xe,
|
||||
minMantissa,
|
||||
maxMantissa,
|
||||
cuspRoundingFixEnabled,
|
||||
cuspRoundingFix,
|
||||
"Number::multiplication overflow : exponent is " + std::to_string(xe));
|
||||
negative_ = zn;
|
||||
mantissa_ = xm;
|
||||
@@ -882,7 +1017,7 @@ Number::operator/=(Number const& y)
|
||||
auto const& range = kRange.get();
|
||||
auto const& minMantissa = range.min;
|
||||
auto const& maxMantissa = range.max;
|
||||
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
|
||||
auto const cuspRoundingFix = range.cuspRoundingFix;
|
||||
|
||||
// Division operates on two large integers (16-digit for small
|
||||
// mantissas, 19-digit for large) using integer math. If the values
|
||||
@@ -1014,14 +1149,14 @@ Number::operator/=(Number const& y)
|
||||
// rounding fix is enabled, flag if there is still
|
||||
// a remainder from stage 2.
|
||||
bool const useTrailingRemainder =
|
||||
cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled;
|
||||
cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled;
|
||||
if (useTrailingRemainder)
|
||||
{
|
||||
dropped = partialNumerator % dm != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped);
|
||||
doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped);
|
||||
negative_ = zp;
|
||||
mantissa_ = static_cast<internalrep>(zm);
|
||||
exponent_ = ze;
|
||||
@@ -1033,6 +1168,8 @@ Number::operator/=(Number const& y)
|
||||
Number::
|
||||
operator rep() const
|
||||
{
|
||||
auto const& range = kRange.get();
|
||||
|
||||
rep drops = mantissa();
|
||||
int offset = exponent();
|
||||
Guard g;
|
||||
@@ -1053,7 +1190,7 @@ operator rep() const
|
||||
throw std::overflow_error("Number::operator rep() overflow");
|
||||
drops *= 10;
|
||||
}
|
||||
g.doRound(drops, "Number::operator rep() rounding overflow");
|
||||
g.doRound(drops, range.cuspRoundingFix, "Number::operator rep() rounding overflow");
|
||||
}
|
||||
return drops;
|
||||
}
|
||||
|
||||
@@ -798,44 +798,44 @@ doOverpayment(
|
||||
// (P * factor) / factor round-trip can leave the new principal one
|
||||
// scale-unit high, so these equalities do not hold on the pre-amendment
|
||||
// code path and must be gated to match the fix they verify.
|
||||
//
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
bool const fix320Enabled = rules.enabled(fixCleanup3_2_0);
|
||||
XRPL_ASSERT_IF(
|
||||
fix320Enabled,
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment : principal change agrees");
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
// The valueChange returned by tryOverpayment satisfies
|
||||
// valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
|
||||
// Using the loan-state identity v = p + i + m and the adjacent
|
||||
// `principal change agrees` assertion (dp = oldP - newP), this
|
||||
// rearranges into three independently-computable terms:
|
||||
//
|
||||
// 1. TVO change beyond what principal repayment alone explains:
|
||||
// newTVO - (oldTVO - dp)
|
||||
// 2. Management fee released by re-amortization (positive when
|
||||
// mfee decreased; zero when managementFeeRate == 0):
|
||||
// oldMfee - newMfee
|
||||
// 3. The overpayment's penalty interest part (= untrackedInterest
|
||||
// for the overpayment path; see computeOverpaymentComponents):
|
||||
// trackedInterestPart()
|
||||
[[maybe_unused]] Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
[[maybe_unused]] Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
[[maybe_unused]] Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
|
||||
XRPL_ASSERT_IF(
|
||||
fix320Enabled,
|
||||
[&] {
|
||||
Number const tvoChange = newRoundedLoanState.valueOutstanding -
|
||||
(totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
|
||||
Number const managementFeeReleased =
|
||||
managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
|
||||
Number const interestPart = overpaymentComponents.trackedInterestPart();
|
||||
return loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart;
|
||||
}(),
|
||||
"xrpl::detail::doOverpayment : interest paid agrees");
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta ==
|
||||
principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal change agrees");
|
||||
|
||||
XRPL_ASSERT_IF(
|
||||
fix320Enabled,
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment : principal payment matches");
|
||||
XRPL_ASSERT_PARTS(
|
||||
loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"interest paid agrees");
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
|
||||
"xrpl::detail::doOverpayment",
|
||||
"principal payment matches");
|
||||
}
|
||||
|
||||
// All validations passed, so update the proxy objects (which will
|
||||
// modify the actual Loan ledger object)
|
||||
@@ -1326,11 +1326,13 @@ computeOverpaymentComponents(
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate)
|
||||
{
|
||||
XRPL_ASSERT_IF(
|
||||
rules.enabled(fixCleanup3_2_0),
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
if (rules.enabled(fixCleanup3_2_0))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
overpayment > 0 && isRounded(asset, overpayment, loanScale),
|
||||
"xrpl::detail::computeOverpaymentComponents : valid overpayment "
|
||||
"amount");
|
||||
}
|
||||
|
||||
// First, deduct the fixed overpayment fee from the total amount.
|
||||
// This reduces the effective payment that will be applied to the loan.
|
||||
|
||||
@@ -736,10 +736,10 @@ unlockEscrowMPT(
|
||||
STAmount const& grossAmount,
|
||||
beast::Journal j)
|
||||
{
|
||||
XRPL_ASSERT_IF(
|
||||
!view.rules().enabled(fixTokenEscrowV1),
|
||||
netAmount == grossAmount,
|
||||
"xrpl::unlockEscrowMPT : netAmount == grossAmount");
|
||||
if (!view.rules().enabled(fixTokenEscrowV1))
|
||||
{
|
||||
XRPL_ASSERT(netAmount == grossAmount, "xrpl::unlockEscrowMPT : netAmount == grossAmount");
|
||||
}
|
||||
|
||||
auto const& issuer = netAmount.getIssuer();
|
||||
auto const& mptIssue = netAmount.get<MPTIssue>();
|
||||
|
||||
@@ -300,7 +300,7 @@ VaultWithdraw::doApply()
|
||||
<< "VaultWithdraw: " //
|
||||
"Cannot burn all outstanding shares while unrealized loss is non-zero";
|
||||
return tefINTERNAL;
|
||||
// LCOV_EXCL_END
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
STAmount const allAvailable{vaultAsset, *assetsAvailable};
|
||||
|
||||
@@ -43,6 +43,15 @@ class Number_test : public beast::unit_test::Suite
|
||||
return out;
|
||||
}
|
||||
|
||||
static BigInt
|
||||
toBigInt(Number const& n)
|
||||
{
|
||||
BigInt v = n.mantissa();
|
||||
for (int i = 0; i < n.exponent(); ++i)
|
||||
v *= 10;
|
||||
return v;
|
||||
}
|
||||
|
||||
using dec = boost::multiprecision::cpp_dec_float_50;
|
||||
|
||||
template <class T = dec>
|
||||
@@ -169,28 +178,37 @@ public:
|
||||
auto const scale = Number::getMantissaScale();
|
||||
testcase << "test_add " << to_string(scale);
|
||||
|
||||
using Case = std::tuple<Number, Number, Number>;
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'066, -15}},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{-6'555'555'555'555'555, -29},
|
||||
Number{-1'000'000'000'000'066, -15}},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{-9'999'999'999'999'344, -16}},
|
||||
{Number{-6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{9'999'999'999'999'344, -16}},
|
||||
{Number{}, Number{5}, Number{5}},
|
||||
{Number{5}, Number{}, Number{5}},
|
||||
{Number{5'555'555'555'555'555, -32768},
|
||||
Number{-5'555'555'555'555'554, -32768},
|
||||
Number{0}},
|
||||
{Number{-9'999'999'999'999'999, -31},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{9'999'999'999'999'990, -16}}});
|
||||
BEAST_EXPECT(Number::getround() == Number::RoundingMode::ToNearest);
|
||||
|
||||
using Case = std::tuple<Number, Number, Number, int>;
|
||||
auto const cSmall = std::to_array<Case>({
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'066, -15},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{-6'555'555'555'555'555, -29},
|
||||
Number{-1'000'000'000'000'066, -15},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{-9'999'999'999'999'344, -16},
|
||||
__LINE__},
|
||||
{Number{-6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{9'999'999'999'999'344, -16},
|
||||
__LINE__},
|
||||
{Number{}, Number{5}, Number{5}, __LINE__},
|
||||
{Number{5}, Number{}, Number{5}, __LINE__},
|
||||
{Number{5'555'555'555'555'555, -32768},
|
||||
Number{-5'555'555'555'555'554, -32768},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{-9'999'999'999'999'999, -31},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{9'999'999'999'999'990, -16},
|
||||
__LINE__},
|
||||
});
|
||||
auto const cLarge = std::to_array<Case>(
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items from C
|
||||
@@ -198,45 +216,57 @@ public:
|
||||
{
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'065'556, -18}},
|
||||
Number{1'000'000'000'000'065'556, -18},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{-6'555'555'555'555'555, -29},
|
||||
Number{-1'000'000'000'000'065'556, -18}},
|
||||
Number{-1'000'000'000'000'065'556, -18},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{-6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
{Number{}, Number{5}, Number{5}},
|
||||
{Number{5}, Number{}, Number{5}},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{}, Number{5}, Number{5}, __LINE__},
|
||||
{Number{5}, Number{}, Number{5}, __LINE__},
|
||||
{Number{5'555'555'555'555'555'000, -32768},
|
||||
Number{-5'555'555'555'555'554'000, -32768},
|
||||
Number{0}},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{-9'999'999'999'999'999, -31},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{9'999'999'999'999'990, -16}},
|
||||
Number{9'999'999'999'999'990, -16},
|
||||
__LINE__},
|
||||
// Items from cSmall expanded for the larger mantissa
|
||||
{Number{1'000'000'000'000'000'000, -18},
|
||||
Number{6'555'555'555'555'555'555, -35},
|
||||
Number{1'000'000'000'000'000'066, -18}},
|
||||
Number{1'000'000'000'000'000'066, -18},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000'000, -18},
|
||||
Number{-6'555'555'555'555'555'555, -35},
|
||||
Number{-1'000'000'000'000'000'066, -18}},
|
||||
Number{-1'000'000'000'000'000'066, -18},
|
||||
__LINE__},
|
||||
{Number{-1'000'000'000'000'000'000, -18},
|
||||
Number{6'555'555'555'555'555'555, -35},
|
||||
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
|
||||
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{-6'555'555'555'555'555'555, -35},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
|
||||
{Number{}, Number{5}, Number{5}},
|
||||
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{}, Number{5}, Number{5}, __LINE__},
|
||||
{Number{5'555'555'555'555'555'555, -32768},
|
||||
Number{-5'555'555'555'555'555'554, -32768},
|
||||
Number{0}},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}},
|
||||
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}, __LINE__},
|
||||
// Test extremes
|
||||
{
|
||||
// Each Number operand rounds up, so the actual mantissa is
|
||||
@@ -244,6 +274,7 @@ public:
|
||||
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
|
||||
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
|
||||
Number{2, 19},
|
||||
__LINE__,
|
||||
},
|
||||
{
|
||||
// Does not round. Mantissas are going to be > kMaxRep, so if
|
||||
@@ -254,21 +285,25 @@ public:
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
|
||||
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
|
||||
Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}},
|
||||
__LINE__,
|
||||
},
|
||||
});
|
||||
auto const cLargeLegacy = std::to_array<Case>({
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}},
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__},
|
||||
});
|
||||
auto const cLargeCorrected = std::to_array<Case>({
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}},
|
||||
{Number{Number::kMaxRep},
|
||||
Number{6, -1},
|
||||
Number{(Number::kMaxRep / 10) + 1, 1},
|
||||
__LINE__},
|
||||
});
|
||||
auto test = [this](auto const& c) {
|
||||
for (auto const& [x, y, z] : c)
|
||||
for (auto const& [x, y, z, line] : c)
|
||||
{
|
||||
auto const result = x + y;
|
||||
std::stringstream ss;
|
||||
ss << x << " + " << y << " = " << result << ". Expected: " << z;
|
||||
BEAST_EXPECTS(result == z, ss.str());
|
||||
expect(result == z, ss.str(), __FILE__, line);
|
||||
}
|
||||
};
|
||||
if (scale == MantissaRange::MantissaScale::Small)
|
||||
@@ -308,80 +343,149 @@ public:
|
||||
auto const scale = Number::getMantissaScale();
|
||||
testcase << "test_sub " << to_string(scale);
|
||||
|
||||
using Case = std::tuple<Number, Number, Number>;
|
||||
using Case = std::tuple<Number, Number, Number, int>;
|
||||
auto const cSmall = std::to_array<Case>(
|
||||
{{Number{1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{9'999'999'999'999'344, -16}},
|
||||
Number{9'999'999'999'999'344, -16},
|
||||
__LINE__},
|
||||
{Number{6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{-9'999'999'999'999'344, -16}},
|
||||
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
|
||||
Number{-9'999'999'999'999'344, -16},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'001, -15},
|
||||
Number{-1'000'000'000'000'000, -30}},
|
||||
Number{-1'000'000'000'000'000, -30},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'001, -15},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'000, -30}}});
|
||||
auto const cLarge = std::to_array<Case>(
|
||||
Number{1'000'000'000'000'000, -30},
|
||||
__LINE__}});
|
||||
auto const cLargeAll = std::to_array<Case>(
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items from C
|
||||
// with larger mantissa
|
||||
{
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{6'555'555'555'555'555, -29},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{6'555'555'555'555'555, -29},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'001, -15},
|
||||
Number{-1'000'000'000'000'000, -30}},
|
||||
Number{-1'000'000'000'000'000, -30},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'001, -15},
|
||||
Number{1'000'000'000'000'000, -15},
|
||||
Number{1'000'000'000'000'000, -30}},
|
||||
Number{1'000'000'000'000'000, -30},
|
||||
__LINE__},
|
||||
// Items from cSmall expanded for the larger mantissa
|
||||
{Number{1'000'000'000'000'000'000, -18},
|
||||
Number{6'555'555'555'555'555'555, -32},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{6'555'555'555'555'555'555, -32},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
|
||||
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000'000, -18},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{0}},
|
||||
Number{0},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000'000, -18},
|
||||
Number{1'000'000'000'000'000'001, -18},
|
||||
Number{-1'000'000'000'000'000'000, -36}},
|
||||
Number{-1'000'000'000'000'000'000, -36},
|
||||
__LINE__},
|
||||
{Number{1'000'000'000'000'000'001, -18},
|
||||
Number{1'000'000'000'000'000'000, -18},
|
||||
Number{1'000'000'000'000'000'000, -36}},
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}},
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{1, 0},
|
||||
Number{(Number::kMaxRep / 10) + 1, 1}},
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::kMaxRep}},
|
||||
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}},
|
||||
Number{1'000'000'000'000'000'000, -36},
|
||||
__LINE__},
|
||||
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__},
|
||||
});
|
||||
// Note that items with extremely large mantissas need to be
|
||||
// calculated, because otherwise they overflow uint64. Items from C
|
||||
// with larger mantissa
|
||||
auto const cLargeLegacy = std::to_array<Case>({
|
||||
// Anything larger than kMaxRep rounds up
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{1, 0},
|
||||
Number{(Number::kMaxRep / 10) + 1, 1},
|
||||
__LINE__},
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::kMaxRep},
|
||||
__LINE__},
|
||||
{Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}},
|
||||
Number{1, 0},
|
||||
Number{(Number::kMaxRep / 10) + 1, 1},
|
||||
__LINE__},
|
||||
{Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::kMaxRep},
|
||||
__LINE__},
|
||||
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__},
|
||||
});
|
||||
auto const cLarge = std::to_array<Case>({
|
||||
// kMaxRep + 1 is below the half-way point, so it rounds down to kMaxRep when the Number
|
||||
// is created.
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{1, 0},
|
||||
Number{Number::kMaxRep - 1},
|
||||
__LINE__},
|
||||
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::kMaxRep - 3},
|
||||
__LINE__},
|
||||
// kMaxRepUp -1 is above the half-way point, so it rounds up to kMaxRepUp when the
|
||||
// Number is created. Subtracting 1 from that rounds up again. A little non-intuitive.
|
||||
{Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}},
|
||||
Number{1, 0},
|
||||
Number{(Number::kMaxRep / 10) + 1, 1},
|
||||
__LINE__},
|
||||
// Subtracting 3 gets back down to kMaxRep
|
||||
{Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}},
|
||||
Number{3, 0},
|
||||
Number{Number::kMaxRep},
|
||||
__LINE__},
|
||||
// 2^63 is the same as kMaxRep+1
|
||||
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep - 3}, __LINE__},
|
||||
});
|
||||
auto test = [this](auto const& c) {
|
||||
for (auto const& [x, y, z] : c)
|
||||
for (auto const& [x, y, z, line] : c)
|
||||
{
|
||||
auto const result = x - y;
|
||||
std::stringstream ss;
|
||||
ss << x << " - " << y << " = " << result << ". Expected: " << z;
|
||||
BEAST_EXPECTS(result == z, ss.str());
|
||||
expect(result == z, ss.str(), __FILE__, line);
|
||||
}
|
||||
};
|
||||
if (scale == MantissaRange::MantissaScale::Small)
|
||||
switch (scale)
|
||||
{
|
||||
test(cSmall);
|
||||
}
|
||||
else
|
||||
{
|
||||
test(cLarge);
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
test(cSmall);
|
||||
break;
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
test(cLargeAll);
|
||||
test(cLargeLegacy);
|
||||
break;
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
test(cLargeAll);
|
||||
test(cLarge);
|
||||
break;
|
||||
default:
|
||||
BEAST_EXPECT(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,38 +1385,38 @@ public:
|
||||
auto const scale = Number::getMantissaScale();
|
||||
testcase << "testToString " << to_string(scale);
|
||||
|
||||
auto test = [this](Number const& n, std::string const& expected) {
|
||||
auto test = [this](Number const& n, std::string const& expected, int line) {
|
||||
auto const result = to_string(n);
|
||||
std::stringstream ss;
|
||||
ss << "to_string(" << result << "). Expected: " << expected;
|
||||
BEAST_EXPECTS(result == expected, ss.str());
|
||||
expect(result == expected, ss.str(), __FILE__, line);
|
||||
};
|
||||
|
||||
test(Number(-2, 0), "-2");
|
||||
test(Number(0, 0), "0");
|
||||
test(Number(2, 0), "2");
|
||||
test(Number(25, -3), "0.025");
|
||||
test(Number(-25, -3), "-0.025");
|
||||
test(Number(25, 1), "250");
|
||||
test(Number(-25, 1), "-250");
|
||||
test(Number(2, 20), "2e20");
|
||||
test(Number(-2, -20), "-2e-20");
|
||||
test(Number(-2, 0), "-2", __LINE__);
|
||||
test(Number(0, 0), "0", __LINE__);
|
||||
test(Number(2, 0), "2", __LINE__);
|
||||
test(Number(25, -3), "0.025", __LINE__);
|
||||
test(Number(-25, -3), "-0.025", __LINE__);
|
||||
test(Number(25, 1), "250", __LINE__);
|
||||
test(Number(-25, 1), "-250", __LINE__);
|
||||
test(Number(2, 20), "2e20", __LINE__);
|
||||
test(Number(-2, -20), "-2e-20", __LINE__);
|
||||
// Test the edges
|
||||
// ((exponent < -(25)) || (exponent > -(5)))))
|
||||
// or ((exponent < -(28)) || (exponent > -(8)))))
|
||||
test(Number(2, -10), "0.0000000002");
|
||||
test(Number(2, -11), "2e-11");
|
||||
test(Number(2, -10), "0.0000000002", __LINE__);
|
||||
test(Number(2, -11), "2e-11", __LINE__);
|
||||
|
||||
test(Number(-2, 10), "-20000000000");
|
||||
test(Number(-2, 11), "-2e11");
|
||||
test(Number(-2, 10), "-20000000000", __LINE__);
|
||||
test(Number(-2, 11), "-2e11", __LINE__);
|
||||
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
|
||||
test(Number::min(), "1e-32753");
|
||||
test(Number::max(), "9999999999999999e32768");
|
||||
test(Number::lowest(), "-9999999999999999e32768");
|
||||
test(Number::min(), "1e-32753", __LINE__);
|
||||
test(Number::max(), "9999999999999999e32768", __LINE__);
|
||||
test(Number::lowest(), "-9999999999999999e32768", __LINE__);
|
||||
{
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
|
||||
@@ -1320,62 +1424,136 @@ public:
|
||||
BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999);
|
||||
test(
|
||||
Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()},
|
||||
"9999999999999999");
|
||||
"9999999999999999",
|
||||
__LINE__);
|
||||
test(
|
||||
Number{true, (maxMantissa * 1000) + 999, -3, Number::Normalized()},
|
||||
"-9999999999999999");
|
||||
"-9999999999999999",
|
||||
__LINE__);
|
||||
|
||||
test(Number{std::numeric_limits<std::int64_t>::max(), -3}, "9223372036854775");
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), -3},
|
||||
"9223372036854775",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), -3}),
|
||||
"-9223372036854775");
|
||||
"-9223372036854775",
|
||||
__LINE__);
|
||||
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::min(), 0}, "-9223372036854775e3");
|
||||
Number{std::numeric_limits<std::int64_t>::min(), 0},
|
||||
"-9223372036854775e3",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::min(), 0}),
|
||||
"9223372036854775e3");
|
||||
"9223372036854775e3",
|
||||
__LINE__);
|
||||
}
|
||||
break;
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
// Test the edges
|
||||
// ((exponent < -(28)) || (exponent > -(8)))))
|
||||
test(Number::min(), "1e-32750");
|
||||
test(Number::max(), "9223372036854775807e32768");
|
||||
test(Number::lowest(), "-9223372036854775807e32768");
|
||||
test(Number::min(), "1e-32750", __LINE__);
|
||||
test(Number::max(), "9223372036854775807e32768", __LINE__);
|
||||
test(Number::lowest(), "-9223372036854775807e32768", __LINE__);
|
||||
{
|
||||
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
|
||||
|
||||
auto const maxMantissa = Number::maxMantissa();
|
||||
BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL);
|
||||
test(
|
||||
Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990");
|
||||
Number{false, maxMantissa, 0, Number::Normalized{}},
|
||||
"9999999999999999990",
|
||||
__LINE__);
|
||||
test(
|
||||
Number{true, maxMantissa, 0, Number::Normalized{}}, "-9999999999999999990");
|
||||
Number{true, maxMantissa, 0, Number::Normalized{}},
|
||||
"-9999999999999999990",
|
||||
__LINE__);
|
||||
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0}, "9223372036854775807");
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0},
|
||||
"9223372036854775807",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), 0}),
|
||||
"-9223372036854775807");
|
||||
"-9223372036854775807",
|
||||
__LINE__);
|
||||
|
||||
// Because the absolute value of min is larger than max, it
|
||||
// will be scaled down to fit under max. Since we're
|
||||
// rounding towards zero, the 8 at the end is dropped.
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::min(), 0},
|
||||
"-9223372036854775800");
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::min(), 0}),
|
||||
"9223372036854775800");
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
// Because the absolute value of min() is larger than max(), it
|
||||
// will be scaled down to fit under max(). Since we're
|
||||
// rounding towards zero, the 8 at the end is dropped.
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::min(), 0},
|
||||
"-9223372036854775800",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::min(), 0}),
|
||||
"9223372036854775800",
|
||||
__LINE__);
|
||||
break;
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
// Because the absolute value of min() is larger than max(), it
|
||||
// will be rounded down toward max()
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::min(), 0},
|
||||
"-9223372036854775807",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::min(), 0}),
|
||||
"9223372036854775807",
|
||||
__LINE__);
|
||||
break;
|
||||
default:
|
||||
BEAST_EXPECT(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
// Rounding to nearest, since the mantissa is bigger than kMaxRep, the 8
|
||||
// will be dropped, and since that is bigger than 5, the result will be
|
||||
// rounded up from 0 to 1.
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0} + 1,
|
||||
"9223372036854775810",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
|
||||
"-9223372036854775810",
|
||||
__LINE__);
|
||||
break;
|
||||
case MantissaRange::MantissaScale::Large:
|
||||
// Rounding to nearest, since the mantissa is below the halfway point from
|
||||
// kMaxRep to kMaxRep up, it will be rounded down to kMaxRep
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0} + 1,
|
||||
"9223372036854775807",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
|
||||
"-9223372036854775807",
|
||||
__LINE__);
|
||||
break;
|
||||
default:
|
||||
BEAST_EXPECT(false);
|
||||
break;
|
||||
}
|
||||
// Rounding to nearest, since the mantissa is above the halfway point from kMaxRep
|
||||
// to kMaxRep up, it will be rounded up to kMaxRepUp.
|
||||
test(
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0} + 1, "9223372036854775810");
|
||||
Number{std::numeric_limits<std::int64_t>::max(), 0} + 2,
|
||||
"9223372036854775810",
|
||||
__LINE__);
|
||||
test(
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
|
||||
"-9223372036854775810");
|
||||
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 2),
|
||||
"-9223372036854775810",
|
||||
__LINE__);
|
||||
break;
|
||||
default:
|
||||
BEAST_EXPECT(false);
|
||||
@@ -1624,7 +1802,7 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testUpwardRoundsDown()
|
||||
testEdgeCases()
|
||||
{
|
||||
auto const scale = Number::getMantissaScale();
|
||||
{
|
||||
@@ -1644,21 +1822,18 @@ public:
|
||||
BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue);
|
||||
|
||||
// What Number actually stored.
|
||||
BigInt storedValue = BigInt(product.mantissa());
|
||||
for (int i = 0; i < product.exponent(); ++i)
|
||||
storedValue *= 10;
|
||||
BigInt storedValue = toBigInt(product);
|
||||
|
||||
BigInt const signedDifference = storedValue - exactProduct;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << fmt(BigInt(kAValue)) << "\n"
|
||||
log << " a = " << fmt(BigInt(kAValue)) << "\n"
|
||||
<< " b = " << fmt(BigInt(kBValue)) << "\n"
|
||||
<< " exact a*b = " << fmt(exactProduct) << "\n"
|
||||
<< " stored = " << fmt(storedValue) << "\n"
|
||||
<< " stored - exact = " << fmt(signedDifference) << "\n"
|
||||
<< " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n"
|
||||
<< " stored.mantissa = " << product.mantissa() << "\n"
|
||||
<< " stored.exponent = " << product.exponent() << "\n";
|
||||
<< " stored.exponent = " << product.exponent() << "\n\n";
|
||||
log.flush();
|
||||
|
||||
switch (scale)
|
||||
@@ -1731,15 +1906,14 @@ public:
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
log << " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (negative => Upward gave value BELOW truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n\n";
|
||||
log.flush();
|
||||
|
||||
// Upward invariant: stored >= exact. Bug: stored < exact.
|
||||
@@ -1781,15 +1955,14 @@ public:
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
log << " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (positive => Downward gave value ABOVE truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n\n";
|
||||
log.flush();
|
||||
|
||||
// invariant: stored <= exact. Bug: stored > exact.
|
||||
@@ -1838,15 +2011,14 @@ public:
|
||||
dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent());
|
||||
dec const diff = stored - exact;
|
||||
|
||||
log << "\n"
|
||||
<< " a = " << aValue << "\n"
|
||||
log << " a = " << aValue << "\n"
|
||||
<< " b = " << bValue << "\n"
|
||||
<< " exact a/b = " << fmt(exact) << "\n"
|
||||
<< " stored a/b = " << fmt(stored) << "\n"
|
||||
<< " stored - exact = " << fmt(diff)
|
||||
<< " (negative => ToNearest gave value BELOW truth)\n"
|
||||
<< " quotient.mantissa = " << quotient.mantissa() << "\n"
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n";
|
||||
<< " quotient.exponent = " << quotient.exponent() << "\n\n";
|
||||
log.flush();
|
||||
|
||||
// invariant: stored >= exact. Bug: stored < exact.
|
||||
@@ -1869,6 +2041,131 @@ public:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale);
|
||||
|
||||
constexpr auto kMaxRep = Number::kMaxRep;
|
||||
|
||||
// Both ToNearest and Downward should round to `below`
|
||||
auto const actual = static_cast<std::uint64_t>(kMaxRep) + 1;
|
||||
Number const below{static_cast<std::int64_t>(kMaxRep), 0};
|
||||
Number const above{
|
||||
false, static_cast<std::uint64_t>(kMaxRep) + 3, 0, Number::Unchecked{}};
|
||||
|
||||
auto construct = [](Number::RoundingMode mode) {
|
||||
NumberRoundModeGuard const roundGuard{mode};
|
||||
return Number(false, actual, 0, Number::Normalized{});
|
||||
};
|
||||
Number const upward = construct(Number::RoundingMode::Upward);
|
||||
|
||||
Number const toNearest = construct(Number::RoundingMode::ToNearest);
|
||||
|
||||
Number const downward = construct(Number::RoundingMode::Downward);
|
||||
|
||||
log << " actual = " << actual << " (kMaxRep + 1)\n"
|
||||
<< " below = " << below << " (kMaxRep, distance 1)\n"
|
||||
<< " above = " << above << " (kMaxRep + 3, distance 2)\n"
|
||||
<< " Upward = " << upward << "\n"
|
||||
<< " ToNearest = " << toNearest << "\n"
|
||||
<< " Downward = " << downward << "\n\n";
|
||||
log.flush();
|
||||
|
||||
switch (scale)
|
||||
{
|
||||
case MantissaRange::MantissaScale::Small:
|
||||
// With the small mantissa, everything rounds up
|
||||
|
||||
// Upward round UP
|
||||
BEAST_EXPECT(upward > above);
|
||||
|
||||
// ToNearest rounds UP when the DOWN neighbor is strictly closer
|
||||
BEAST_EXPECT(toNearest > above);
|
||||
BEAST_EXPECT(toNearest == below);
|
||||
|
||||
// Downward undershoots: it returns a value below `below`
|
||||
BEAST_EXPECT(downward < below);
|
||||
|
||||
// Both should have given the same answer, but they differ
|
||||
BEAST_EXPECT(toNearest > downward);
|
||||
|
||||
break;
|
||||
|
||||
case MantissaRange::MantissaScale::LargeLegacy:
|
||||
// Upward round UP
|
||||
BEAST_EXPECT(upward == above);
|
||||
|
||||
// ToNearest rounds UP when the DOWN neighbor is strictly closer
|
||||
BEAST_EXPECT(toNearest == above);
|
||||
BEAST_EXPECT(toNearest > below);
|
||||
|
||||
// Downward undershoots: it returns a value below `below`
|
||||
BEAST_EXPECT(downward < below);
|
||||
|
||||
// Both should have given the same answer, but they differ
|
||||
BEAST_EXPECT(toNearest > downward);
|
||||
|
||||
break;
|
||||
default:
|
||||
// Covers "Large" and any newly added scales
|
||||
|
||||
// Upward round UP
|
||||
BEAST_EXPECT(upward == above);
|
||||
|
||||
// ToNearest rounds UP when the DOWN neighbor is strictly closer
|
||||
BEAST_EXPECT(toNearest != above);
|
||||
BEAST_EXPECT(toNearest == below);
|
||||
|
||||
// Downward undershoots: it returns a value below `below`
|
||||
BEAST_EXPECT(downward == below);
|
||||
|
||||
// Both should have given the same answer, but they differ
|
||||
BEAST_EXPECT(toNearest == downward);
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale);
|
||||
|
||||
Number const a{1LL, 20};
|
||||
Number const b{-1'000'000'000'000'000'001LL};
|
||||
|
||||
BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"});
|
||||
if (scale != MantissaRange::MantissaScale::Small)
|
||||
BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"});
|
||||
else
|
||||
BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000000"});
|
||||
|
||||
Number sum;
|
||||
{
|
||||
NumberRoundModeGuard const roundGuard{Number::RoundingMode::TowardsZero};
|
||||
sum = a + b;
|
||||
}
|
||||
|
||||
BigInt const exact = toBigInt(a) + toBigInt(b);
|
||||
BigInt const stored = toBigInt(sum);
|
||||
BigInt const diff = stored - exact;
|
||||
|
||||
log << " a = " << a << "\n b = " << b
|
||||
<< "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str()
|
||||
<< "\n difference = " << diff.str() << "\n\n";
|
||||
log.flush();
|
||||
|
||||
if (scale == MantissaRange::MantissaScale::Small)
|
||||
{
|
||||
BEAST_EXPECT(stored == exact);
|
||||
}
|
||||
else if (scale == MantissaRange::MantissaScale::LargeLegacy)
|
||||
{
|
||||
BEAST_EXPECT(stored > exact);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(stored < exact);
|
||||
BEAST_EXPECT(diff < 0);
|
||||
BEAST_EXPECT(-diff < pow10<BigInt>(sum.exponent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1899,7 +2196,7 @@ public:
|
||||
testRounding();
|
||||
testInt64();
|
||||
|
||||
testUpwardRoundsDown();
|
||||
testEdgeCases();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user