Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/doc/man/rustc.1
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Comma separated list of compiler information to print on stdout.
Equivalent to \fI\-C\ debuginfo=2\fR.
.TP
\fB\-O\fR
Equivalent to \fI\-C\ opt\-level=2\fR.
Equivalent to \fI\-C\ opt\-level=3\fR.
.TP
\fB\-o\fR \fIFILENAME\fR
Write output to \fIFILENAME\fR. Ignored if multiple \fI\-\-emit\fR outputs are specified which
Expand Down
197 changes: 197 additions & 0 deletions src/tools/update-rustc-man-opt-level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
r"""
Update the rustc manpage "-O" description to match `rustc --help`.
Usage (dry-run by default):
./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1
Apply changes (creates a timestamped backup):
./src/tools/update-rustc-man-opt-level.py --man-file src/doc/man/rustc.1 --apply
Force a level instead of querying rustc:
./src/tools/update-rustc-man-opt-level.py --man-file ... --expected-level 3 --apply
"""
from __future__ import annotations

import argparse
import datetime
import difflib
import shutil
import subprocess
import sys
import re
from pathlib import Path
from typing import Tuple

DEFAULT_RUSTC = "rustc"

# ANSI color codes
_CLR = {
"reset": "\033[0m",
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"bold": "\033[1m",
}


def colorize(line: str, color: str, enabled: bool) -> str:
if not enabled or color not in _CLR:
return line
return f"{_CLR[color]}{line}{_CLR['reset']}"


def get_rustc_opt_level(rustc_cmd: str = DEFAULT_RUSTC) -> int:
"""Query `rustc --help` and parse the opt-level mapped to -O."""
try:
proc = subprocess.run([rustc_cmd, "--help"], capture_output=True, text=True, check=True)
except FileNotFoundError:
raise RuntimeError(f"rustc not found at '{rustc_cmd}'")
except subprocess.CalledProcessError as e:
stderr = (e.stderr or "").strip()
raise RuntimeError(f"rustc --help failed: {stderr or e}") from e

help_text = (proc.stdout or "") + "\n" + (proc.stderr or "")
m = re.search(r'-O[^\n]*opt(?:\\-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern contains an unnecessary escape sequence \\- in the character class. In the pattern r'-O[^\n]*opt(?:\\-)?level\s*=\s*(\d+)', the (?:\\-)? part is attempting to match an optional hyphen in "opt-level". However, the backslash escape is only needed in the replacement context (for manpage formatting), not when parsing plain text output from rustc --help. The pattern should use (?:-)? instead to match an optional literal hyphen.

Suggested change
m = re.search(r'-O[^\n]*opt(?:\\-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)
m = re.search(r'-O[^\n]*opt(?:-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)

Copilot uses AI. Check for mistakes.
if not m:
m2 = re.search(r'Equivalent to\s+-C\s+opt(?:-)?level\s*=\s*(\d+)', help_text, flags=re.IGNORECASE)
if not m2:
raise RuntimeError("Could not find '-O' opt-level mapping in rustc --help output")
return int(m2.group(1))
return int(m.group(1))


def find_and_replace_manpage_content(content: str, new_level: int) -> Tuple[str, int]:
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function signature uses Tuple from the typing module. In Python 3.9+, the built-in tuple type can be used directly for type hints (PEP 585). Since the script uses from __future__ import annotations (line 15) which enables postponed evaluation of annotations, you can use lowercase tuple[str, int] instead of Tuple[str, int] for better consistency with modern Python style.

Copilot uses AI. Check for mistakes.
"""
Replace opt-level numbers in 'Equivalent to ... opt-level=N.' sentences tied to -O.
Conservative heuristic:
- Locate sentences starting with 'Equivalent to' up to the next period.
- Ensure the sentence mentions opt-level (accepting escaped '\-').
- Confirm a -O header appears within a lookback window before the sentence.
"""
replacements = 0
out_parts = []
last_index = 0

sentence_pattern = re.compile(r'Equivalent to([^\n\.]{0,800}?)\.', flags=re.IGNORECASE)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex on line 78 uses an overly broad limit of {0,800} characters for the sentence content between "Equivalent to" and the period. This could potentially match across multiple unrelated entries in the manpage. A more conservative limit (e.g., 200 characters) would be safer and still accommodate the expected sentence length while reducing false positives.

Suggested change
sentence_pattern = re.compile(r'Equivalent to([^\n\.]{0,800}?)\.', flags=re.IGNORECASE)
sentence_pattern = re.compile(r'Equivalent to([^\n\.]{0,200}?)\.', flags=re.IGNORECASE)

Copilot uses AI. Check for mistakes.

for m in sentence_pattern.finditer(content):
start, end = m.span()
sentence = m.group(0)

if not re.search(r'opt(?:\\-)?level', sentence, flags=re.IGNORECASE):
continue

num_match = re.search(r'(\d+)', sentence)
if not num_match:
continue
old_level = int(num_match.group(1))
if old_level == new_level:
continue

window_start = max(0, start - 1200)
window = content[window_start:start]

if not (
re.search(r'(^|\n)\s*-O\b', window)
or re.search(r'\\fB\\-?O\\fR', window)
or re.search(r'\\-O\b', window)
or re.search(r'\.B\s+-O\b', window)
or re.search(r'\\fB-?O\\fP', window)
):
Comment on lines +97 to +103
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 97-103 check for various patterns to detect the "-O" flag in the lookback window, but they don't use an early exit pattern. The condition uses not (A or B or C or D or E) which means all patterns must be checked even if the first one matches. Consider restructuring to if not any([...]) or using early return/continue logic for better readability and potential performance improvement.

Suggested change
if not (
re.search(r'(^|\n)\s*-O\b', window)
or re.search(r'\\fB\\-?O\\fR', window)
or re.search(r'\\-O\b', window)
or re.search(r'\.B\s+-O\b', window)
or re.search(r'\\fB-?O\\fP', window)
):
if not any([
re.search(r'(^|\n)\s*-O\b', window),
re.search(r'\\fB\\-?O\\fR', window),
re.search(r'\\-O\b', window),
re.search(r'\.B\s+-O\b', window),
re.search(r'\\fB-?O\\fP', window),
]):

Copilot uses AI. Check for mistakes.
continue

new_sentence = re.sub(r'(\d+)', str(new_level), sentence, count=1)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replacement logic on line 106 uses count=1 to replace only the first digit in the sentence. However, this could be problematic if the sentence structure changes to include other numbers before the opt-level value (e.g., "Equivalent to -C opt-level=3 (level 3 optimization)"). While unlikely, a more robust approach would be to specifically target the digit immediately following "opt-level=" or use the matched number's position from line 87-90.

Suggested change
new_sentence = re.sub(r'(\d+)', str(new_level), sentence, count=1)
# Replace only the number immediately following "opt-level=" (or "opt\-level=")
new_sentence = re.sub(
r'(opt(?:\\-)?level\s*=\s*)\d+',
r'\g<1>{}'.format(new_level),
sentence,
count=1,
)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function extracts the first number found in the sentence (line 87-90) and checks if it equals new_level to skip replacement (line 91-92). However, it then uses a generic regex substitution (line 106) that replaces the first digit in the entire sentence. If the sentence structure is unusual and contains digits before "opt-level=", this could cause a mismatch between what is checked and what is replaced. Consider using the span information from num_match to ensure the same number is replaced.

Suggested change
new_sentence = re.sub(r'(\d+)', str(new_level), sentence, count=1)
# Replace the specific number matched by num_match
num_start, num_end = num_match.span(1)
new_sentence = sentence[:num_start] + str(new_level) + sentence[num_end:]

Copilot uses AI. Check for mistakes.
out_parts.append(content[last_index:start])
out_parts.append(new_sentence)
last_index = end
replacements += 1

out_parts.append(content[last_index:])
return "".join(out_parts), replacements


def show_colored_diff(old: str, new: str, filename: str, color: bool) -> None:
old_lines = old.splitlines(keepends=True)
new_lines = new.splitlines(keepends=True)
diff_iter = difflib.unified_diff(old_lines, new_lines, fromfile=filename, tofile=filename + " (updated)", lineterm="")
for line in diff_iter:
if line.startswith("---") or line.startswith("+++"):
print(colorize(line.rstrip("\n"), "bold", color))
elif line.startswith("@@"):
print(colorize(line.rstrip("\n"), "yellow", color))
elif line.startswith("+"):
# avoid coloring the file header lines that also start with +++
print(colorize(line.rstrip("\n"), "green", color))
elif line.startswith("-"):
print(colorize(line.rstrip("\n"), "red", color))
else:
print(line.rstrip("\n"))
Comment on lines +117 to +131
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unified diff output in line 119 sets lineterm="" but the generator still yields lines with potential line endings from the splitlines(keepends=True) call. Each line is then manually stripped with rstrip("\n") in the print loop (lines 122-131). This is inconsistent - either use keepends=False and lineterm="\n", or keep keepends=True with lineterm="" but don't strip in the loop. The current approach may cause issues if the file contains \r\n line endings, as only \n is stripped.

Suggested change
old_lines = old.splitlines(keepends=True)
new_lines = new.splitlines(keepends=True)
diff_iter = difflib.unified_diff(old_lines, new_lines, fromfile=filename, tofile=filename + " (updated)", lineterm="")
for line in diff_iter:
if line.startswith("---") or line.startswith("+++"):
print(colorize(line.rstrip("\n"), "bold", color))
elif line.startswith("@@"):
print(colorize(line.rstrip("\n"), "yellow", color))
elif line.startswith("+"):
# avoid coloring the file header lines that also start with +++
print(colorize(line.rstrip("\n"), "green", color))
elif line.startswith("-"):
print(colorize(line.rstrip("\n"), "red", color))
else:
print(line.rstrip("\n"))
old_lines = old.splitlines(keepends=False)
new_lines = new.splitlines(keepends=False)
diff_iter = difflib.unified_diff(old_lines, new_lines, fromfile=filename, tofile=filename + " (updated)", lineterm="\n")
for line in diff_iter:
if line.startswith("---") or line.startswith("+++"):
print(colorize(line, "bold", color))
elif line.startswith("@@"):
print(colorize(line, "yellow", color))
elif line.startswith("+"):
# avoid coloring the file header lines that also start with +++
print(colorize(line, "green", color))
elif line.startswith("-"):
print(colorize(line, "red", color))
else:
print(line)

Copilot uses AI. Check for mistakes.


def backup_file(path: Path) -> Path:
ts = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backup timestamp format on line 135 uses %Y%m%dT%H%M%S which only includes precision to seconds. If the script is run multiple times within the same second (e.g., in automated tests or rapid iterations), subsequent backups would overwrite earlier ones. Consider adding microseconds (%f) or using a different conflict resolution strategy (e.g., checking if backup exists and adding a counter).

Suggested change
ts = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
ts = datetime.datetime.now().strftime("%Y%m%dT%H%M%S%f")

Copilot uses AI. Check for mistakes.
backup = path.with_name(path.name + f".bak.{ts}")
shutil.copy2(path, backup)
return backup


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Update rustc man page -O opt-level to match rustc --help")
p.add_argument("--man-file", "-m", required=True, help="Path to rustc man page file to update (e.g. src/doc/man/rustc.1)")
p.add_argument("--rustc-cmd", default=DEFAULT_RUSTC, help="rustc binary to query (default: rustc)")
p.add_argument("--expected-level", "-e", type=int, help="Use this level instead of querying rustc")
p.add_argument("--apply", action="store_true", help="Write changes to the man file (creates a backup). Without this flag runs in dry-run mode and prints a diff.")
p.add_argument("--no-color", action="store_true", help="Disable colored output")
return p.parse_args()


def main() -> int:
args = parse_args()
color = (not args.no_color) and sys.stdout.isatty()

man_path = Path(args.man_file)
if not man_path.exists():
print(f"Error: man file not found: {man_path}", file=sys.stderr)
return 2

try:
new_level = args.expected_level if args.expected_level is not None else get_rustc_opt_level(args.rustc_cmd)
except RuntimeError as e:
print(f"Error determining rustc opt-level: {e}", file=sys.stderr)
return 3

try:
content = man_path.read_text(encoding="utf-8")
except Exception as e:
print(f"Error reading man file {man_path}: {e}", file=sys.stderr)
return 4

new_content, replacements = find_and_replace_manpage_content(content, new_level)

if replacements == 0:
print("No replacements necessary (either already up-to-date or -O entry not found near an 'Equivalent to -C opt-level=' line).")
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message on line 175 could be more helpful by distinguishing between the two cases: (1) manpage is already up-to-date with the correct opt-level, and (2) the -O entry pattern wasn't found in the manpage. Currently both scenarios produce the same message, making troubleshooting more difficult. Consider tracking whether any "Equivalent to" sentences with opt-level were found at all, regardless of whether they needed updating.

Copilot uses AI. Check for mistakes.
return 0

header = f"Found {replacements} replacement(s). Proposed changes:"
print(colorize(header, "bold", color))
show_colored_diff(content, new_content, str(man_path), color)

if args.apply:
try:
backup = backup_file(man_path)
man_path.write_text(new_content, encoding="utf-8")
print(colorize(f"\nApplied changes to {man_path}. Backup saved to {backup}", "green", color))
except Exception as e:
print(f"Error writing updated man file: {e}", file=sys.stderr)
return 5
else:
print("\nDry-run only. Use --apply to write changes to disk.")

return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading