Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion fuzz/corpus/format_idempotence/basic.lisp
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
(defun f (x) (+ x 1))
(defun f (x)
(+ x 1))
3 changes: 2 additions & 1 deletion fuzz/corpus/format_idempotence/numeric-dispatch.lisp
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
(defparameter *z* (list #C(0.0 1.0) #2A((1 2) (3 4)) #S(p :x 1)))
(defparameter *z*
(list #C(0.0 1.0) #2A((1 2) (3 4)) #S(p :x 1)))
3 changes: 2 additions & 1 deletion fuzz/corpus/parse/basic.lisp
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
(defun f (x) (+ x 1))
(defun f (x)
(+ x 1))
1 change: 1 addition & 0 deletions fuzz/corpus/parse/feature-expressions.lisp
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#+(and sbcl (not win32)) (defun x () t)

#-sbcl (defun x () nil)
3 changes: 2 additions & 1 deletion fuzz/corpus/parse/numeric-dispatch.lisp
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
(defparameter *z* (list #C(0.0 1.0) #2A((1 2) (3 4)) #S(p :x 1)))
(defparameter *z*
(list #C(0.0 1.0) #2A((1 2) (3 4)) #S(p :x 1)))
2 changes: 1 addition & 1 deletion packages/core/cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ pub struct SelectorArgs {
/// Select the smallest expression containing byte offset.
#[arg(long, group = "selector-base")]
pub at: Option<usize>,
/// Select the smallest expression at LINE[:COLUMN], both 1-based.
/// Select the smallest expression at `LINE[:COLUMN]`, both 1-based.
#[arg(long, value_name = "LINE[:COLUMN]", group = "selector-base")]
pub line_column: Option<LinePosition>,
/// Select the definition named SYMBOL.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/cli/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ fn stdout_is_terminal() -> bool {
*STDOUT_IS_TERMINAL.get_or_init(|| std::io::stdout().is_terminal())
}

/// Forces [`stdout_is_terminal`]'s cache to resolve now, before something
/// Forces `stdout_is_terminal`'s cache to resolve now, before something
/// else changes what fd 1 points at. A no-op once the cache already holds a
/// value, which is exactly the "first check wins" contract callers need.
pub fn prime_stdout_terminal_cache() {
Expand Down Expand Up @@ -201,7 +201,7 @@ impl Painter {
}
}

/// Colorizes a [`crate::diff::unified_diff`] result for a terminal: green
/// Colorizes a `crate::diff::unified_diff` result for a terminal: green
/// `+` lines, red `-` lines, cyan hunk headers, bold file headers.
///
/// Takes the finished diff text rather than folding into `unified_diff`
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/diagnosis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ pub struct Diagnosis {
/// The byte position the failure names, when it names one.
///
/// Populated from whichever typed variant already carries a position —
/// [`ParseError`] always does, a handful of [`StructureError`] and
/// `ParseError` always does, a handful of [`StructureError`] and
/// [`SelectionError`] variants do — rather than by adding a span to every
/// variant. A caller holding the source text can render a caret under it;
/// [`render_caret`] does exactly that for the CLI's own stderr output.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub struct DiffStat {

/// Counts hunks and changed lines in a string [`unified_diff`] produced.
///
/// Parses the rendered text rather than the internal [`DiffOp`] sequence:
/// Parses the rendered text rather than the internal `DiffOp` sequence:
/// `unified_diff` already has an "omitted, too large" fallback whose shape
/// this must agree with too, and re-deriving these counts from the same
/// text a caller can also print keeps the two forms of output from ever
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub struct GateFailure(pub String);

/// Builds a gate failure, ready to return from a command entry point.
///
/// Returns [`CommandFailure`] rather than [`GateFailure`] so that the ~30
/// Returns `CommandFailure` rather than [`GateFailure`] so that the ~30
/// existing `return Err(gate_failure(format!(...)))` sites keep compiling
/// unchanged: `Err(...)` performs no conversion of its own, only `?` does. The
/// bare variant is still reachable as `CommandFailure::Gate` for a caller that
Expand Down
10 changes: 5 additions & 5 deletions packages/core/cli/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const CLEANUP_QUARANTINE_MODE: u32 = 0o700;
///
/// Still the default and no longer the only possible value: a run may lower it
/// with `--max-input-bytes` or `PAREDIT_MAX_INPUT_BYTES`, which is what
/// [`max_source_input_bytes`] resolves. Kept public and unchanged because it
/// `max_source_input_bytes` resolves. Kept public and unchanged because it
/// is the documented default and several callers quote it as such.
pub const MAX_SOURCE_INPUT_BYTES: u64 = paredit_core_safety::limits::DEFAULT_MAX_INPUT_BYTES;

Expand Down Expand Up @@ -1199,17 +1199,17 @@ pub struct WritabilityCheck {
/// Checks whether `path` could be written to right now, changing nothing.
///
/// Reuses the exact staging step a real write goes through — the same
/// batch-level refusals ([`ensure_writes_are_permitted`]), the same
/// batch-level refusals (`ensure_writes_are_permitted`), the same
/// symlink/regular-file refusals, the same parent-directory permission
/// check, the same write lock (see [`acquire_write_lock`]), and, because
/// check, the same write lock (see `acquire_write_lock`), and, because
/// staging writes a same-size placeholder into a sibling file on the same
/// filesystem before ever touching `path` itself, the same evidence of
/// whether there is room for a write of about this size — then discards the
/// staged sibling it created instead of publishing it.
///
/// The backup copy is the one staging step this deliberately skips, via
/// [`StagingIntent::Probe`]: it exists only to roll a publish back, and a
/// probe never publishes. See [`StagingIntent`] for what it used to cost.
/// `StagingIntent::Probe`: it exists only to roll a publish back, and a
/// probe never publishes. See `StagingIntent` for what it used to cost.
///
/// `--dry-run` is checked *first*, before any lock or sibling file. The
/// guarantee that flag makes — nothing is written to disk — has to hold for
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/pager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! `println!` — lands in the pager without knowing it exists.
//!
//! Unix-only, like the rest of this crate's raw-fd and xattr code
//! ([`crate::shared::io`]'s macOS ACL preservation is the other example):
//! (`crate::shared::io`'s macOS ACL preservation is the other example):
//! `dup2` and `/bin/sh` are POSIX, and there is no Windows pager convention
//! to fall back to. `--paginate` is simply inert there.
//!
Expand Down
29 changes: 24 additions & 5 deletions packages/core/cli/src/report/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,13 @@ impl Budget {
/// papered over: the counts stay, and they are what a caller needs to
/// decide how to narrow the request.
pub fn apply(self, report: &mut Json, trimmable: &[&str]) -> Option<Truncation> {
if self.unlimited() || approximate_tokens(report) <= self.0 {
// The one full serialization this function pays: every later step
// updates this byte count incrementally instead of reserializing the
// whole document again, which is what made this function cost
// O(document size) per halving step on a report with several large
// trimmable arrays.
let mut total_bytes = serde_json::to_string(report).map_or(0, |text| text.len());
if self.unlimited() || total_bytes.div_ceil(BYTES_PER_TOKEN) <= self.0 {
return None;
}

Expand All @@ -110,7 +116,7 @@ impl Budget {
// truncated list that still has a prefix is far more useful than
// an empty one, and this converges in a handful of steps.
loop {
if approximate_tokens(report) <= self.0 {
if total_bytes.div_ceil(BYTES_PER_TOKEN) <= self.0 {
break;
}
let Some(array) = report.get_mut(key).and_then(Json::as_array_mut) else {
Expand All @@ -119,9 +125,22 @@ impl Budget {
if array.is_empty() {
break;
}
array.truncate(array.len() / 2);
let new_len = array.len() / 2;
// What halving removes from the document's serialized byte
// count: the dropped elements' own JSON (serialized as a
// slice, not the whole document), minus the two bracket
// characters that framed it as its own array, plus one
// boundary comma to reattach the surviving prefix to the
// array's closing bracket — except when nothing survives, in
// which case the array becomes `[]` and there is no comma to
// add back.
let dropped_slice =
serde_json::to_string(&array[new_len..]).map_or(0, |text| text.len());
let boundary_comma = usize::from(new_len > 0);
total_bytes -= dropped_slice.saturating_sub(2) + boundary_comma;
array.truncate(new_len);
}
if approximate_tokens(report) <= self.0 {
if total_bytes.div_ceil(BYTES_PER_TOKEN) <= self.0 {
break;
}
}
Expand All @@ -139,7 +158,7 @@ impl Budget {
.collect();

Some(Truncation {
approximate_tokens: approximate_tokens(report),
approximate_tokens: total_bytes.div_ceil(BYTES_PER_TOKEN),
budget: self.0,
trimmed,
})
Expand Down
10 changes: 2 additions & 8 deletions packages/core/cli/src/report/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,7 @@ pub fn dot(graph: &Graph) -> String {
}

fn dot_node(ids: &BTreeMap<&str, String>, node: &Node) -> String {
let id = ids
.get(node.label.as_str())
.cloned()
.unwrap_or_else(|| "n?".to_owned());
let id = ids.get(node.label.as_str()).map_or("n?", String::as_str);
let shape = match node.shape {
NodeShape::Definition => "box",
NodeShape::External => "ellipse",
Expand Down Expand Up @@ -283,10 +280,7 @@ pub fn mermaid(graph: &Graph) -> String {
}

fn mermaid_node(ids: &BTreeMap<&str, String>, node: &Node) -> String {
let id = ids
.get(node.label.as_str())
.cloned()
.unwrap_or_else(|| "n0".to_owned());
let id = ids.get(node.label.as_str()).map_or("n0", String::as_str);
let label = mermaid_text(&node.label);
match node.shape {
NodeShape::Definition => format!("{id}[\"{label}\"]"),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/cli/src/report/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ fn print_text<F: Finding>(
);
continue;
}
let path_str = terminal_safe(&report.path.display()).to_string();
for finding in &report.findings {
let mut row = vec![
finding.kind().to_owned(),
terminal_safe(&report.path.display()).to_string(),
path_str.clone(),
report.line_of(finding).to_string(),
];
row.extend(
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ pub struct RuntimeSettings {
/// `None` keeps the existing strict-UTF-8 read path. Read-only commands
/// work normally against a legacy (Shift_JIS, EUC-JP, ISO-8859-1, ...)
/// source file once this is set; a write is refused instead of silently
/// re-encoding the file to UTF-8 — see [`writes_are_supported`].
/// re-encoding the file to UTF-8 — see `writes_are_supported`.
pub source_encoding: Option<&'static encoding_rs::Encoding>,
}

Expand Down
105 changes: 104 additions & 1 deletion packages/core/cli/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ pub fn note_partial_file_failures(failures: &[FileFailure]) {
///
/// - **Order is preserved.** Results come back in input order regardless of
/// which thread finished first, so the report's bytes do not depend on
/// scheduling. Since [`claim_order_by_descending_size`] means the workers do
/// scheduling. Since `claim_order_by_descending_size` means the workers do
/// not even *start* the files in input order, that is now an explicit
/// reassembly rather than a happy accident of the partitioning: each result
/// travels back paired with the index of the file it came from and is placed
Expand Down Expand Up @@ -949,6 +949,109 @@ where
analyze(file, resolved, &tree, &input)
}

/// [`analyze_files`]'s scheduling — parallel above the same worker threshold,
/// serial below it, largest-file-first claim order, every failure kept, same
/// [`FileAnalysis`]/[`FileFailure`] result shape — with no assumption at all
/// about what "read" or "parse" mean for one file. The caller's `process`
/// closure owns the whole per-file step, read included.
///
/// [`analyze_files`] reads through this crate's own ambient-authority path
/// (`read_input_dialect_and_tree`), which is right for the common case —
/// files a workspace walk already discovered — but wrong for a caller with
/// its own containment guarantee to enforce on top of an arbitrary path list,
/// e.g. a `--root`-confined, symlink-refusing, TOCTOU-guarded read, as
/// `refactor-workflow`'s checkpoint and manifest commands already have and
/// must not lose by routing through a helper that reads for them. Handing the
/// whole step to the caller means this function never has an
/// ambient-authority read to bypass in the first place — a caller that needs
/// dialect detection or Lisp parsing does that inside `process` too, using
/// [`read_input_dialect_and_tree`] or [`parse_document`] directly.
pub fn analyze_files_raw<T, E, F>(files: &[PathBuf], process: F) -> FileAnalysis<T>
where
T: Send,
E: std::error::Error + Send,
F: Fn(&PathBuf) -> Result<T, E> + Sync,
{
let workers = worker_count(files.len());
let results: Vec<Result<T, E>> = if workers.get() == 1 {
files.iter().map(&process).collect()
} else {
analyze_in_parallel_raw(files, &process, workers.get())
};

let mut analysis = FileAnalysis {
succeeded: Vec::with_capacity(results.len()),
failed: Vec::new(),
};
for (file, result) in files.iter().zip(results) {
match result {
Ok(value) => analysis.succeeded.push(value),
Err(error) => analysis.failed.push(FileFailure {
file: file.clone(),
message: crate::error::error_chain(&error),
}),
}
}
analysis
}

/// [`analyze_in_parallel`] for [`analyze_files_raw`].
fn analyze_in_parallel_raw<T, E, F>(
files: &[PathBuf],
process: &F,
workers: usize,
) -> Vec<Result<T, E>>
where
T: Send,
E: Send,
F: Fn(&PathBuf) -> Result<T, E> + Sync,
{
use std::sync::atomic::{AtomicUsize, Ordering};

let claim_order = claim_order_by_descending_size(files);
let cursor = AtomicUsize::new(0);
let claim_order = &claim_order;
let cursor = &cursor;

let claimed: Vec<Vec<(usize, Result<T, E>)>> = std::thread::scope(|scope| {
let handles = (0..workers)
.map(|_| {
scope.spawn(move || {
let mut mine = Vec::new();
loop {
let position = cursor.fetch_add(1, Ordering::Relaxed);
let Some(&index) = claim_order.get(position) else {
break;
};
mine.push((index, process(&files[index])));
}
mine
})
})
.collect::<Vec<_>>();

let mut claimed = Vec::with_capacity(workers);
for handle in handles {
match handle.join() {
Ok(mine) => claimed.push(mine),
Err(payload) => std::panic::resume_unwind(payload),
}
}
claimed
});

let mut slots: Vec<Option<Result<T, E>>> = files.iter().map(|_| None).collect();
for mine in claimed {
for (index, result) in mine {
slots[index] = Some(result);
}
}
slots
.into_iter()
.map(|slot| slot.expect("every index is claimed exactly once"))
.collect()
}

/// How many workers to use for `count` files.
///
/// A thread costs more than parsing a handful of small files, so a short list
Expand Down
2 changes: 1 addition & 1 deletion packages/core/cli/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const FALLBACK_WIDTH: usize = 80;
/// The terminal's column count for stdout, or a fallback in this order:
/// an ioctl reading (the terminal's actual width), then `COLUMNS` (what a
/// shell exports, and what a wrapper without a real tty can still set), then
/// [`FALLBACK_WIDTH`].
/// `FALLBACK_WIDTH`.
#[must_use]
pub fn width() -> usize {
ioctl_width().or_else(columns_env).unwrap_or(FALLBACK_WIDTH)
Expand Down
Loading
Loading