Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit ad83c9a

Browse files
committed
Add checks via annotation that lists are sorted or exhaustive
This crate has a handful of lists that need to list all API and can't easily be verified. Additionally, some longer lists should be kept sorted so they are easier to look through. Resolve both of these by adding a check in `update-api-list.py` that looks for annotations and verifies the contents are as expected. Annotations are `verify-apilist-start`, `verify-apilist-end`, `verify-sorted-start`, and `verify-sorted-end`. This includes fixes for anything that did not meet the criteria.
1 parent a728288 commit ad83c9a

File tree

6 files changed

+187
-21
lines changed

6 files changed

+187
-21
lines changed

crates/libm-test/benches/icount.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ libm_macros::for_each_function! {
5252
}
5353

5454
main!(
55-
library_benchmark_groups = icount_bench_acos_group,
55+
library_benchmark_groups =
56+
// verify-apilist-start
57+
// verify-sorted-start
58+
icount_bench_acos_group,
5659
icount_bench_acosf_group,
5760
icount_bench_acosh_group,
5861
icount_bench_acoshf_group,
@@ -169,6 +172,8 @@ main!(
169172
icount_bench_scalbnf16_group,
170173
icount_bench_scalbnf_group,
171174
icount_bench_sin_group,
175+
icount_bench_sincos_group,
176+
icount_bench_sincosf_group,
172177
icount_bench_sinf_group,
173178
icount_bench_sinh_group,
174179
icount_bench_sinhf_group,
@@ -192,4 +197,6 @@ main!(
192197
icount_bench_y1f_group,
193198
icount_bench_yn_group,
194199
icount_bench_ynf_group,
200+
// verify-sorted-end
201+
// verify-apilist-end
195202
);

crates/libm-test/src/mpfloat.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ libm_macros::for_each_function! {
132132
emit_types: [RustFn],
133133
skip: [
134134
// Most of these need a manual implementation
135+
// verify-sorted-start
135136
ceil,
136137
ceilf,
137138
ceilf128,
@@ -188,6 +189,7 @@ libm_macros::for_each_function! {
188189
truncf128,
189190
truncf16,yn,
190191
ynf,
192+
// verify-sorted-end
191193
],
192194
fn_extra: match MACRO_FN_NAME {
193195
// Remap function names that are different between mpfr and libm

crates/libm-test/tests/compare_built_musl.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ libm_macros::for_each_function! {
7979
ynf,
8080

8181
// Not provided by musl
82+
// verify-sorted-start
8283
ceilf128,
8384
ceilf16,
8485
copysignf128,
@@ -107,5 +108,6 @@ libm_macros::for_each_function! {
107108
sqrtf16,
108109
truncf128,
109110
truncf16,
111+
// verify-sorted-end
110112
],
111113
}

etc/update-api-list.py

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,33 @@
88

99
import difflib
1010
import json
11+
import re
1112
import subprocess as sp
1213
import sys
1314
from dataclasses import dataclass
14-
from glob import glob
15+
from glob import glob, iglob
1516
from pathlib import Path
16-
from typing import Any, TypeAlias
17+
from typing import Any, Callable, TypeAlias
1718

18-
ETC_DIR = Path(__file__).parent
19+
SELF_PATH = Path(__file__)
20+
ETC_DIR = SELF_PATH.parent
1921
ROOT_DIR = ETC_DIR.parent
2022

23+
# Loose approximation of what gets checked in to git, without needing `git ls-files`.
24+
DIRECTORIES = [".github", "ci", "crates", "etc", "src"]
25+
2126
# These files do not trigger a retest.
2227
IGNORED_SOURCES = ["src/libm_helper.rs"]
2328

2429
IndexTy: TypeAlias = dict[str, dict[str, Any]]
2530
"""Type of the `index` item in rustdoc's JSON output"""
2631

2732

33+
def eprint(*args, **kwargs):
34+
"""Print to stderr."""
35+
print(*args, file=sys.stderr, **kwargs)
36+
37+
2838
@dataclass
2939
class Crate:
3040
"""Representation of public interfaces and function defintion locations in
@@ -146,7 +156,7 @@ def write_function_list(self, check: bool) -> None:
146156
if check:
147157
with open(out_file, "r") as f:
148158
current = f.read()
149-
diff_and_exit(current, output)
159+
diff_and_exit(current, output, "function list")
150160
else:
151161
with open(out_file, "w") as f:
152162
f.write(output)
@@ -171,26 +181,123 @@ def write_function_defs(self, check: bool) -> None:
171181
if check:
172182
with open(out_file, "r") as f:
173183
current = f.read()
174-
diff_and_exit(current, output)
184+
diff_and_exit(current, output, "source list")
175185
else:
176186
with open(out_file, "w") as f:
177187
f.write(output)
178188

189+
def tidy_lists(self) -> None:
190+
"""In each file, check annotations indicating blocks of code should be sorted or should
191+
include all public API.
192+
"""
193+
for dirname in DIRECTORIES:
194+
dir = ROOT_DIR.joinpath(dirname)
195+
for fname in iglob("**", root_dir=dir, recursive=True):
196+
fpath = dir.joinpath(fname)
197+
if fpath.is_dir() or fpath == SELF_PATH:
198+
continue
199+
200+
lines = fpath.read_text().splitlines()
201+
202+
validate_delimited_block(
203+
fpath,
204+
lines,
205+
"verify-sorted-start",
206+
"verify-sorted-end",
207+
ensure_sorted,
208+
)
209+
210+
validate_delimited_block(
211+
fpath,
212+
lines,
213+
"verify-apilist-start",
214+
"verify-apilist-end",
215+
lambda p, n, lines: self.ensure_contains_api(p, n, lines),
216+
)
217+
218+
def ensure_contains_api(self, fpath: Path, line_num: int, lines: list[str]):
219+
"""Given a list of strings, ensure that each public function we have is named
220+
somewhere.
221+
"""
222+
not_found = []
223+
for func in self.public_functions:
224+
# The function name may be on its own or somewhere in a snake case string.
225+
pat = re.compile(rf"(\b|_){func}(\b|_)")
226+
found = next((line for line in lines if pat.search(line)), None)
227+
228+
if found is None:
229+
not_found.append(func)
230+
231+
if len(not_found) == 0:
232+
return
233+
234+
relpath = fpath.relative_to(ROOT_DIR)
235+
eprint(f"functions not found at {relpath}:{line_num}: {not_found}")
236+
exit(1)
237+
238+
239+
def validate_delimited_block(
240+
fpath: Path,
241+
lines: list[str],
242+
start: str,
243+
end: str,
244+
validate: Callable[[Path, int, list[str]], None],
245+
) -> None:
246+
"""Identify blocks of code wrapped within `start` and `end`, collect their contents
247+
to a list of strings, and call `validate` for each of those lists.
248+
"""
249+
relpath = fpath.relative_to(ROOT_DIR)
250+
block_lines = []
251+
block_start_line: None | int = None
252+
for line_num, line in enumerate(lines):
253+
line_num += 1
254+
255+
if start in line:
256+
block_start_line = line_num
257+
continue
258+
259+
if end in line:
260+
if block_start_line is None:
261+
eprint(f"`{end}` without `{start}` at {relpath}:{line_num}")
262+
exit(1)
263+
264+
validate(fpath, block_start_line, block_lines)
265+
block_lines = []
266+
block_start_line = None
267+
continue
268+
269+
if block_start_line is not None:
270+
block_lines.append(line)
271+
272+
if block_start_line is not None:
273+
eprint(f"`{start}` without `{end}` at {relpath}:{block_start_line}")
274+
exit(1)
275+
276+
277+
def ensure_sorted(fpath: Path, block_start_line: int, lines: list[str]) -> None:
278+
"""Ensure that a list of lines is sorted, otherwise print a diff and exit."""
279+
relpath = fpath.relative_to(ROOT_DIR)
280+
diff_and_exit(
281+
"".join(lines),
282+
"".join(sorted(lines)),
283+
f"sorted block at {relpath}:{block_start_line}",
284+
)
179285

180-
def diff_and_exit(actual: str, expected: str):
286+
287+
def diff_and_exit(actual: str, expected: str, name: str):
181288
"""If the two strings are different, print a diff between them and then exit
182289
with an error.
183290
"""
184291
if actual == expected:
185-
print("output matches expected; success")
292+
print(f"{name} output matches expected; success")
186293
return
187294

188295
a = [f"{line}\n" for line in actual.splitlines()]
189296
b = [f"{line}\n" for line in expected.splitlines()]
190297

191298
diff = difflib.unified_diff(a, b, "actual", "expected")
192299
sys.stdout.writelines(diff)
193-
print("mismatched function list")
300+
print(f"mismatched {name}")
194301
exit(1)
195302

196303

@@ -223,23 +330,31 @@ def base_name(name: str) -> tuple[str, str]:
223330
return (name, "f64")
224331

225332

333+
def ensure_updated_list(check: bool) -> None:
334+
"""Runner to update the function list and JSON, or check that it is already up
335+
to date.
336+
"""
337+
crate = Crate()
338+
crate.write_function_list(check)
339+
crate.write_function_defs(check)
340+
341+
if check:
342+
crate.tidy_lists()
343+
344+
226345
def main():
227346
"""By default overwrite the file. If `--check` is passed, print a diff instead and
228347
error if the files are different.
229348
"""
230349
match sys.argv:
231350
case [_]:
232-
check = False
351+
ensure_updated_list(False)
233352
case [_, "--check"]:
234-
check = True
353+
ensure_updated_list(True)
235354
case _:
236355
print("unrecognized arguments")
237356
exit(1)
238357

239-
crate = Crate()
240-
crate.write_function_list(check)
241-
crate.write_function_defs(check)
242-
243358

244359
if __name__ == "__main__":
245360
main()

0 commit comments

Comments
 (0)