name = "prettyprinter"
description = "Wadler/Leijen pretty-printer combinator library (quchen/prettyprinter). Workload narrowed to the `prettyprinter` sub-package; modern HEAD is the base, each patch reverse-applies a fix to install the original bug."
language = "haskell"
strategies = ["quickcheck", "hedgehog", "falsify", "smallcheck"]
crate = "prettyprinter"
base_commit = "55b16e1ddf682a6352bf4dc9f5443086f72be7b5"

# ---------------------------------------------------------------------------
# Variant: ribbon_width_round_d4cd9e1f_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["ribbon_width_round_d4cd9e1f_1"]

[tasks.source]
repo = "https://github.com/quchen/prettyprinter"
commits = ["f289c1782e564b24aa967f2cf42e65a0496e29d5"]
commit_subjects = ["Compute ribbon width with `floor` instead of `round` (#160)"]
origin = "internal"
summary = "Before #160, the ribbon-width clamp used `round` (banker's rounding). For half-integer products like `pageWidth * 0.5 = 1.5`, this rounded *up* to 2 instead of *down* to 1, letting documents occupy one extra column inside the ribbon. The fix swaps `round` for `floor`, so the ribbon never exceeds its nominal fraction."

[tasks.injection]
kind = "patch"
files = ["prettyprinter/src/Prettyprinter/Internal.hs"]
locations = [{ file = "prettyprinter/src/Prettyprinter/Internal.hs", line = 1897, symbol = "remainingWidth" }]
patch = "patches/ribbon_width_round_d4cd9e1f_1.patch"

[tasks.bug]
short_name = "ribbon_width_uses_round"
invariant = "Rendering `\"a\" <> softline' <> \"b\"` with `AvailablePerLine n 0.5` produces a layout consistent with `ribbonWidth = floor (n * 0.5)`. Equivalently, `softline'` becomes a hard newline iff `floor (n * 0.5) < 2`."
how_triggered = "Reverse-applying the patch swaps `floor` for `round` in `remainingWidth`. For `n = 3` the ribbon goes from 1 (correct) to 2 (buggy), so `softline'` no longer breaks the line and the output becomes `SChar 'a' (SChar 'b' SEmpty)` instead of the expected `SChar 'a' (SLine 0 (SChar 'b' SEmpty))`."

[[tasks.tasks]]
property = "RibbonWidthFloor"
witnesses = [
  { test_fn = "witness_ribbon_width_floor_case_lineLen3", note = "lineLength=3, ribbon=0.5: floor=1 forces newline; round=2 keeps flat" },
  { test_fn = "witness_ribbon_width_floor_case_lineLen7", note = "lineLength=7, ribbon=0.5: floor=3 keeps flat; round=4 also keeps flat - this case demonstrates the property holds at non-discriminating widths" },
]

# ---------------------------------------------------------------------------
# Variant: fuse_annotated_drops_content_b2c0a91e_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["fuse_annotated_drops_content_b2c0a91e_1"]

[tasks.source]
repo = "https://github.com/quchen/prettyprinter"
commits = ["ff555e19b7b17a74f16e6fb062256a57dabe4d92"]
commit_subjects = ["Fix fusion of annotated documents (#114)"]
origin = "internal"
summary = "Before #114, `fuse` matched `Annotated _ Empty -> Empty`, dropping the annotation entirely whenever the body fused (or already was) `Empty`. Renderers that translate annotations into structured output (e.g. terminal colour codes, tree renderers, the `renderSimplyDecorated` stack machine) silently lost annotation boundaries on documents containing empty annotated children. The fix changes the case to `Annotated ann x -> Annotated ann (go x)`, so fusion descends into the body and never strips the annotation."

[tasks.injection]
kind = "patch"
files = ["prettyprinter/src/Prettyprinter/Internal.hs"]
locations = [{ file = "prettyprinter/src/Prettyprinter/Internal.hs", line = 1666, symbol = "fuse" }]
patch = "patches/fuse_annotated_drops_content_b2c0a91e_1.patch"

[tasks.bug]
short_name = "fuse_drops_annotation_on_empty_body"
invariant = "For every annotated document `d`, the rendered output of `d` (using a renderer that emits start/end markers around annotations) equals the rendered output of `fuse Shallow d` and of `fuse Deep d`."
how_triggered = "Reverse-applying the patch reinstates the `Annotated _ Empty -> Empty` rewrite. A doc like `annotate ann (Empty <> Empty)` rewrites to `Empty` after fusion, so `renderSimplyDecorated` sees no `SAnnPush`/`SAnnPop` and emits an empty string, while the un-fused doc emits `<TAG></TAG>`."

[[tasks.tasks]]
property = "FuseAnnotatedDocs"
witnesses = [
  { test_fn = "witness_fuse_annotated_docs_case_a", note = "annotate 'A' (Empty<>Empty) must render as `<A></A>` both before and after fuse" },
  { test_fn = "witness_fuse_annotated_docs_case_z", note = "same with annotation 'Z'" },
]

# ---------------------------------------------------------------------------
# Variant: alter_annotations_unbalanced_a3fc77b1_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["alter_annotations_unbalanced_a3fc77b1_1"]

[tasks.source]
repo = "https://github.com/quchen/prettyprinter"
commits = ["5729c19b2a319e21635e92a5bdbf9b1ee4d48052"]
commit_subjects = ["Fix alterAnnotationsS removing only pushes, but not pops"]
origin = "internal"
summary = "Before this fix, `alterAnnotationsS` (and therefore `unAnnotateS`, which is `alterAnnotationsS (const Nothing)`) dropped every `SAnnPush` whose annotation mapped to `Nothing`, but kept the matching `SAnnPop`. The unbalanced output crashed the StackMachine renderer with `panicPeekedEmpty`. The fix tracks a `[Remove|DontRemove]` stack so each removed push also drops its corresponding pop. The patch reverse-applies by removing the `Remove:` track for the `Nothing` branch."

[tasks.injection]
kind = "patch"
files = ["prettyprinter/src/Prettyprinter/Internal.hs"]
locations = [{ file = "prettyprinter/src/Prettyprinter/Internal.hs", line = 1584, symbol = "alterAnnotationsS" }]
patch = "patches/alter_annotations_unbalanced_a3fc77b1_1.patch"

[tasks.bug]
short_name = "alter_annotations_drops_only_pushes"
invariant = "For any annotated document `d`, `alterAnnotationsS (const Nothing) (layoutSmart def d)` produces a stream containing no `SAnnPush` and no `SAnnPop`. Equivalently, `unAnnotateS` does not panic on any well-formed annotated layout."
how_triggered = "Reverse-applying the patch changes the `Nothing` branch from `go (Remove:stack) rest` to `go stack rest`. On a layout like `SAnnPush 'A' (SChar 'x' (SAnnPop SEmpty))`, the push is dropped but no `Remove` is recorded, so the matching pop sees an empty stack and triggers `panicPeekedEmpty`. The runner catches the exception via `try @SomeException` and reports `failed` with the panic message."

[[tasks.tasks]]
property = "AlterAnnotationsBalanced"
witnesses = [
  { test_fn = "witness_alter_annotations_balanced_case_a", note = "stripping annotations from `annotate 'A' (pretty 'x')` panics under the bug" },
  { test_fn = "witness_alter_annotations_balanced_case_b", note = "same with annotation 'B'" },
]

# ---------------------------------------------------------------------------
# Variant: rtw_indent_carries_to_blank_e7d52f8a_1
# ---------------------------------------------------------------------------
[[tasks]]
mutations = ["rtw_indent_carries_to_blank_e7d52f8a_1"]

[tasks.source]
repo = "https://github.com/quchen/prettyprinter"
commits = ["6ffbc8912c246329e8a811c97e266bea63315841", "9ea5da0a4cd05cdbddcc98a8edd62f4d0bb45219"]
commit_subjects = [
  "Fix #93: rTW restores indentation in the wrong spot",
  "Fix stripping of empty lines for real this time :-)",
]
origin = "internal"
summary = "Before #93 (and the earlier blank-line fix), `removeTrailingWhitespace` could re-emit the withheld indentation onto every intermediate empty `SLine` it produced from a run of blank lines, instead of zeroing them out. The historical patches edited an older `commitSpaces` shape that no longer exists; we synthesise an analogous mutation against the modern `prependEmptyLines` helper. Reverse-applying it makes intermediate blank lines carry the original indent again, so trailing whitespace re-appears in pretty output that goes through `removeTrailingWhitespace`."

[tasks.injection]
kind = "patch"
files = ["prettyprinter/src/Prettyprinter/Internal.hs"]
locations = [{ file = "prettyprinter/src/Prettyprinter/Internal.hs", line = 1743, symbol = "removeTrailingWhitespace.prependEmptyLines" }]
patch = "patches/rtw_indent_carries_to_blank_e7d52f8a_1.patch"

[tasks.bug]
short_name = "rtw_propagates_indent_on_blank_lines"
invariant = "After `removeTrailingWhitespace`, every `SLine i rest` whose `rest` begins with another `SLine` (i.e. the line is blank) has indentation `i = 0`."
how_triggered = "Reverse-applying the patch changes `prependEmptyLines is = foldr (\\_ sds -> SLine 0 sds) sds0 is` to `foldr (\\i sds -> SLine i sds) sds0 is`. Then `removeTrailingWhitespace (SLine 2 (SLine 0 (SChar 'x' SEmpty)))` returns `SLine 2 (SLine 0 (SChar 'x' SEmpty))` (with leading SLine indent 2) instead of the expected `SLine 0 (SLine 0 (SChar 'x' SEmpty))`."

[[tasks.tasks]]
property = "RtwIndentBlankLines"
witnesses = [
  { test_fn = "witness_rtw_indent_blank_lines_case_2_0", note = "(i1=2, i2=0): leading SLine indent must zero out" },
  { test_fn = "witness_rtw_indent_blank_lines_case_3_1", note = "(i1=3, i2=1): both intermediate indents must zero out" },
]
