{
  "id": "P142",
  "slug": "input-sanitizer",
  "key": "P142-input-sanitizer",
  "title": "Input sanitizer",
  "summary": "Cleanup steps for typed text, which of them are safe to repeat, and pipelines of safe steps that still change on a second pass because a later step hands back work an earlier one would still do.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P142 - Input sanitizer\n\nCleanup steps for text people type, which of them are safe to repeat, and\nwhich pipelines built from safe steps are not.\n\n- `main.eml` - the menu, the steps measured on the samples, the samples, a\n  pipeline run once, twice and three times, and every ordered pair of the\n  safe steps\n- `clean.eml` - the six steps, the samples, running a pipeline, whether a\n  second pass changes it, and the first step that would still change a text\n\nHow each part works:\n\n- The steps: trim, squeeze runs of spaces, delete every < and >, keep the\n  first 16 characters, escape every &, and escape only a bare &.\n- As in the corpus case `idempotence-witness`, a step is safe to retry when\n  a second application changes nothing. Escaping every & is not: `Tom &\n  Jerry` becomes `Tom &amp; Jerry` and then `Tom &amp;amp; Jerry`. Escaping\n  only an & that does not already begin `&amp;` is.\n- As in the corpus case `idempotence-does-not-compose`, a queue that\n  redelivers replays the whole pipeline, and a pipeline of safe steps can\n  still change on the second pass. Trim then unbracket turns `x <` into\n  `x `, and the second pass trims the space it exposed; trim then cut leaves\n  the spaces the cut ends on. 4 of the 20 ordered pairs change on the\n  samples.\n- The reason is measured beside it: a pair changes when the second step\n  hands back text the first step would still change. Whenever the first\n  step has no work left, the pair is safe. The converse does not hold:\n  escape a bare & then cut settles on `Fresh breads & jam`, yet the escape\n  would still change `Fresh breads &am` - the cut takes its change back\n  every time, and the text ends in a broken `&am`.\n\nWhat is checked: menu choices 1 to 5; 1 to 6 steps, each a digit from 1 to\n6; a text of up to 40 characters, or a single digit 1 to 9 for a sample. An\nempty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the steps and the samples;\n- trim then unbracket on `x <`, and the other order; escape every & on\n  `Tom & Jerry`; trim then cut on sample 8, and the other order;\n- escape a bare & on `AT&amp;T`, and then cut on sample 9; trim, squeeze\n  and unbracket on a text of its own; every pair.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- steps: a cancel, 17, seven digits and a;\n- texts: a cancel and a text of 41 characters; the digit 0 run as its own\n  text, and escape every & on `A & B`.\n\nBuilt on the verified corpus cases `idempotence-witness` (escaping as the\nstep that adds on a retry, and its guarded form) and\n`idempotence-does-not-compose` (pairs of idempotent steps that are not\nidempotent, and the mechanism measured).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P142 input sanitizer: the cleanup steps and whether each is safe to\n# repeat, the sample texts, a pipeline run once, twice and three times, and\n# every ordered pair of the safe steps measured on the samples.\nimport clean\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef q(s):\n    return \"'\" + s + \"'\"\n\ndef of_samples(n):\n    return str(n) + \" of \" + str(len(clean.samples()))\n\ndef show_steps():\n    \"The steps, each run twice on every sample:\" ^0\n    1 => k\n    for name in clean.names():\n        (\"  \" + str(k) + \" \" + pad(name, 17) + clean.what()[k - 1]) ^0\n        0 => bad\n        \"\" => witness\n        for x in clean.samples():\n            if not clean.settles([k], x):\n                bad + 1 => bad\n                if witness == \"\":\n                    clean.step(k, x) => once\n                    q(x) + \" -> \" + q(once) + \" -> \" + q(clean.step(k, once)) => witness\n        if bad == 0:\n            (\"      safe to repeat on all \" + str(len(clean.samples())) + \" samples\") ^0\n        else:\n            (\"      changes again on \" + of_samples(bad) + \" samples, as \" + witness) ^0\n        k + 1 => k\n\ndef show_samples():\n    \"The samples:\" ^0\n    1 => k\n    for x in clean.samples():\n        (\"  \" + str(k) + \" \" + q(x)) ^0\n        k + 1 => k\n\ndef step_list(steps):\n    \"\" => out\n    0 => i\n    for k in steps:\n        if i == 0:\n            clean.names()[k - 1] => out\n        else:\n            out + \", then \" + clean.names()[k - 1] => out\n        i + 1 => i\n    return out\n\ndef read_steps():\n    # the steps as a list of numbers, [] for a cancel, [0] when not valid\n    plain(input(\"steps, in order (digits 1 to 6, e.g. 13)> \")) => t\n    if t == \"\":\n        return []\n    if len(t) > 6:\n        return [0]\n    [] => steps\n    for c in t:\n        if c < \"1\" or c > \"6\":\n            return [0]\n        steps + [int(c)] => steps\n    return steps\n\ndef run_pipeline():\n    read_steps() => steps\n    if len(steps) == 0:\n        \"Cancelled.\" ^0\n        return\n    if steps[0] == 0:\n        \"Type 1 to 6 steps, each a digit from 1 to 6.\" ^0\n        return\n    input(\"text (a sample 1 to 9, or your own)> \") => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(t) == 1 and t >= \"1\" and t <= \"9\":\n        clean.samples()[int(t) - 1] => x\n    elif len(t) > 40:\n        \"Type a sample number or a text of up to 40 characters.\" ^0\n        return\n    else:\n        t => x\n    clean.run(steps, x) => once\n    clean.run(steps, once) => twice\n    clean.run(steps, twice) => thrice\n    (\"Pipeline: \" + step_list(steps) + \".\") ^0\n    (\"  text:          \" + q(x)) ^0\n    (\"  one pass:      \" + q(once)) ^0\n    (\"  two passes:    \" + q(twice)) ^0\n    (\"  three passes:  \" + q(thrice)) ^0\n    if twice == once:\n        \"A second pass changes nothing: replayed, this pipeline gives the same text.\" ^0\n        clean.work_left(steps, once) => w\n        if w >= 0:\n            (\"Yet \" + clean.names()[steps[w] - 1] + \" would still change \" + q(once) + \": a step after it takes the change back every time.\") ^0\n    else:\n        \"The second pass changed it: replayed, this pipeline does not give the same text.\" ^0\n        clean.work_left(steps, once) => w\n        steps[w] => k\n        if k == 5:\n            \"Escape every & changes the text again on every pass: it is not safe to repeat even alone.\" ^0\n        else:\n            (\"After one pass, \" + clean.names()[k - 1] + \" would still change the text: a step after it handed back work it had already done.\") ^0\n\ndef pairs():\n    [1, 2, 3, 4, 6] => safe\n    \"Every ordered pair of the safe steps, run twice on every sample:\" ^0\n    (\"  \" + pad(\"pair\", 34) + pad(\"second pass\", 20) + \"first step has work left\") ^0\n    0 => broken\n    0 => agree\n    0 => seen\n    [] => undone\n    for p in safe:\n        for r in safe:\n            if p != r:\n                0 => bad\n                0 => left\n                for x in clean.samples():\n                    if not clean.settles([p, r], x):\n                        bad + 1 => bad\n                    clean.run([p, r], x) => h\n                    if clean.step(p, h) != h:\n                        left + 1 => left\n                        if clean.settles([p, r], x) and len(undone) == 0:\n                            [p, r, h] => undone\n                if bad == 0:\n                    \"same\" => verdict\n                else:\n                    \"changes on \" + of_samples(bad) => verdict\n                    broken + 1 => broken\n                if left == 0:\n                    \"none\" => lw\n                else:\n                    \"on \" + of_samples(left) => lw\n                (\"  \" + pad(step_list([p, r]), 34) + pad(verdict, 20) + lw) ^0\n                if (bad == 0) == (left == 0):\n                    agree + 1 => agree\n                seen + 1 => seen\n    (\"Pairs of safe steps that change on a second pass: \" + str(broken) + \" of \" + str(seen) + \".\") ^0\n    (\"Pairs where the second pass changes exactly when the first step has work left: \" + str(agree) + \" of \" + str(seen) + \", on these samples.\") ^0\n    \"A pair stays safe while its second step never hands back text its first step would still change.\" ^0\n    if len(undone) > 0:\n        clean.names()[undone[0] - 1] => first\n        clean.names()[undone[1] - 1] => second\n        (\"The converse does not hold: with \" + step_list([undone[0], undone[1]]) + \", a second pass changes nothing, yet \" + first + \" would still change \" + q(undone[2]) + \" - \" + second + \" takes the change back every time.\") ^0\n\n\"== Input sanitizer ==\" ^0\n\"Cleanup steps for typed text: which are safe to repeat, and which pipelines of safe steps are not.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_steps()\n    elif choice == \"2\":\n        show_samples()\n    elif choice == \"3\":\n        run_pipeline()\n    elif choice == \"4\":\n        pairs()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import clean\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef q(s):\n    return \"'\" + s + \"'\"\n\ndef of_samples(n):\n    return str(n) + \" of \" + str(len(clean.samples()))\n\ndef show_steps():\n    print(\"The steps, each run twice on every sample:\")\n    k = 1\n    for name in clean.names():\n        print(\"  \" + str(k) + \" \" + pad(name, 17) + clean.what()[k - 1])\n        bad = 0\n        witness = \"\"\n        for x in clean.samples():\n            if not clean.settles([k], x):\n                bad = bad + 1\n                if witness == \"\":\n                    once = clean.step(k, x)\n                    witness = q(x) + \" -> \" + q(once) + \" -> \" + q(clean.step(k, once))\n        if bad == 0:\n            print(\"      safe to repeat on all \" + str(len(clean.samples())) + \" samples\")\n        else:\n            print(\"      changes again on \" + of_samples(bad) + \" samples, as \" + witness)\n        k = k + 1\n\ndef show_samples():\n    print(\"The samples:\")\n    k = 1\n    for x in clean.samples():\n        print(\"  \" + str(k) + \" \" + q(x))\n        k = k + 1\n\ndef step_list(steps):\n    out = \"\"\n    i = 0\n    for k in steps:\n        if i == 0:\n            out = clean.names()[k - 1]\n        else:\n            out = out + \", then \" + clean.names()[k - 1]\n        i = i + 1\n    return out\n\ndef read_steps():\n    t = plain(input(\"steps, in order (digits 1 to 6, e.g. 13)> \"))\n    if t == \"\":\n        return []\n    if len(t) > 6:\n        return [0]\n    steps = []\n    for c in t:\n        if c < \"1\" or c > \"6\":\n            return [0]\n        steps = steps + [int(c)]\n    return steps\n\ndef run_pipeline():\n    steps = read_steps()\n    if len(steps) == 0:\n        print(\"Cancelled.\")\n        return\n    if steps[0] == 0:\n        print(\"Type 1 to 6 steps, each a digit from 1 to 6.\")\n        return\n    t = input(\"text (a sample 1 to 9, or your own)> \")\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(t) == 1 and t >= \"1\" and t <= \"9\":\n        x = clean.samples()[int(t) - 1]\n    elif len(t) > 40:\n        print(\"Type a sample number or a text of up to 40 characters.\")\n        return\n    else:\n        x = t\n    once = clean.run(steps, x)\n    twice = clean.run(steps, once)\n    thrice = clean.run(steps, twice)\n    print(\"Pipeline: \" + step_list(steps) + \".\")\n    print(\"  text:          \" + q(x))\n    print(\"  one pass:      \" + q(once))\n    print(\"  two passes:    \" + q(twice))\n    print(\"  three passes:  \" + q(thrice))\n    if twice == once:\n        print(\"A second pass changes nothing: replayed, this pipeline gives the same text.\")\n        w = clean.work_left(steps, once)\n        if w >= 0:\n            print(\"Yet \" + clean.names()[steps[w] - 1] + \" would still change \" + q(once) + \": a step after it takes the change back every time.\")\n    else:\n        print(\"The second pass changed it: replayed, this pipeline does not give the same text.\")\n        w = clean.work_left(steps, once)\n        k = steps[w]\n        if k == 5:\n            print(\"Escape every & changes the text again on every pass: it is not safe to repeat even alone.\")\n        else:\n            print(\"After one pass, \" + clean.names()[k - 1] + \" would still change the text: a step after it handed back work it had already done.\")\n\ndef pairs():\n    safe = [1, 2, 3, 4, 6]\n    print(\"Every ordered pair of the safe steps, run twice on every sample:\")\n    print(\"  \" + pad(\"pair\", 34) + pad(\"second pass\", 20) + \"first step has work left\")\n    broken = 0\n    agree = 0\n    seen = 0\n    undone = []\n    for p in safe:\n        for r in safe:\n            if p != r:\n                bad = 0\n                left = 0\n                for x in clean.samples():\n                    if not clean.settles([p, r], x):\n                        bad = bad + 1\n                    h = clean.run([p, r], x)\n                    if clean.step(p, h) != h:\n                        left = left + 1\n                        if clean.settles([p, r], x) and len(undone) == 0:\n                            undone = [p, r, h]\n                if bad == 0:\n                    verdict = \"same\"\n                else:\n                    verdict = \"changes on \" + of_samples(bad)\n                    broken = broken + 1\n                if left == 0:\n                    lw = \"none\"\n                else:\n                    lw = \"on \" + of_samples(left)\n                print(\"  \" + pad(step_list([p, r]), 34) + pad(verdict, 20) + lw)\n                if (bad == 0) == (left == 0):\n                    agree = agree + 1\n                seen = seen + 1\n    print(\"Pairs of safe steps that change on a second pass: \" + str(broken) + \" of \" + str(seen) + \".\")\n    print(\"Pairs where the second pass changes exactly when the first step has work left: \" + str(agree) + \" of \" + str(seen) + \", on these samples.\")\n    print(\"A pair stays safe while its second step never hands back text its first step would still change.\")\n    if len(undone) > 0:\n        first = clean.names()[undone[0] - 1]\n        second = clean.names()[undone[1] - 1]\n        print(\"The converse does not hold: with \" + step_list([undone[0], undone[1]]) + \", a second pass changes nothing, yet \" + first + \" would still change \" + q(undone[2]) + \" - \" + second + \" takes the change back every time.\")\n\nprint(\"== Input sanitizer ==\")\nprint(\"Cleanup steps for typed text: which are safe to repeat, and which pipelines of safe steps are not.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_steps()\n    elif choice == \"2\":\n        show_samples()\n    elif choice == \"3\":\n        run_pipeline()\n    elif choice == \"4\":\n        pairs()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "clean.eml",
      "eml": "# P142 input sanitizer - cleanup steps for text people type, and the\n# pipelines built from them. As in the corpus case idempotence-witness, a\n# step is safe to retry when applying it again changes nothing: escaping\n# every \"&\" is not, escaping only a bare \"&\" is. As in the corpus case\n# idempotence-does-not-compose, a pipeline of steps that are each safe can\n# still change on the second pass - a queue that redelivers replays the\n# whole pipeline - and it does when a later step hands back text an earlier\n# step still has work on.\n\ndef names():\n    return [\"trim\", \"squeeze\", \"unbracket\", \"cut\", \"escape every &\", \"escape a bare &\"]\n\ndef what():\n    return [\"take the spaces off both ends\", \"make each run of spaces one space\", \"delete every < and >\", \"keep the first 16 characters\", \"write each & as &amp;\", \"write a & as &amp; unless it already begins &amp;\"]\n\ndef samples():\n    return [\"  Hello   world  \", \"Tom & Jerry\", \"a < b > c\", \"x <\", \"<b>Bold</b>  move\", \"Salt & pepper, please\", \"AT&amp;T\", \"one two three   four\", \"Fresh breads & jam\"]\n\ndef trim(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef squeeze(s):\n    \"\" => out\n    for c in s:\n        if c == \" \" and len(out) > 0 and out[len(out) - 1] == \" \":\n            pass\n        else:\n            out + c => out\n    return out\n\ndef unbracket(s):\n    \"\" => out\n    for c in s:\n        if c != \"<\" and c != \">\":\n            out + c => out\n    return out\n\ndef cut(s):\n    return s[0:16]\n\ndef escape_every(s):\n    \"\" => out\n    for c in s:\n        if c == \"&\":\n            out + \"&amp;\" => out\n        else:\n            out + c => out\n    return out\n\ndef escape_bare(s):\n    \"\" => out\n    0 => i\n    while i < len(s):\n        if s[i] == \"&\" and s[i:i + 5] == \"&amp;\":\n            out + \"&amp;\" => out\n            i + 5 => i\n        elif s[i] == \"&\":\n            out + \"&amp;\" => out\n            i + 1 => i\n        else:\n            out + s[i] => out\n            i + 1 => i\n    return out\n\ndef step(k, s):\n    if k == 1:\n        return trim(s)\n    if k == 2:\n        return squeeze(s)\n    if k == 3:\n        return unbracket(s)\n    if k == 4:\n        return cut(s)\n    if k == 5:\n        return escape_every(s)\n    return escape_bare(s)\n\ndef run(steps, s):\n    s => out\n    for k in steps:\n        step(k, out) => out\n    return out\n\ndef settles(steps, s):\n    # the pipeline is idempotent on s: a second pass changes nothing\n    run(steps, s) => once\n    return run(steps, once) == once\n\ndef work_left(steps, h):\n    # the position of the first step that would still change h, or -1\n    0 => i\n    for k in steps:\n        if step(k, h) != h:\n            return i\n        i + 1 => i\n    return 0 - 1\n",
      "python": "def names():\n    return [\"trim\", \"squeeze\", \"unbracket\", \"cut\", \"escape every &\", \"escape a bare &\"]\n\ndef what():\n    return [\"take the spaces off both ends\", \"make each run of spaces one space\", \"delete every < and >\", \"keep the first 16 characters\", \"write each & as &amp;\", \"write a & as &amp; unless it already begins &amp;\"]\n\ndef samples():\n    return [\"  Hello   world  \", \"Tom & Jerry\", \"a < b > c\", \"x <\", \"<b>Bold</b>  move\", \"Salt & pepper, please\", \"AT&amp;T\", \"one two three   four\", \"Fresh breads & jam\"]\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef squeeze(s):\n    out = \"\"\n    for c in s:\n        if c == \" \" and len(out) > 0 and out[len(out) - 1] == \" \":\n            pass\n        else:\n            out = out + c\n    return out\n\ndef unbracket(s):\n    out = \"\"\n    for c in s:\n        if c != \"<\" and c != \">\":\n            out = out + c\n    return out\n\ndef cut(s):\n    return s[0:16]\n\ndef escape_every(s):\n    out = \"\"\n    for c in s:\n        if c == \"&\":\n            out = out + \"&amp;\"\n        else:\n            out = out + c\n    return out\n\ndef escape_bare(s):\n    out = \"\"\n    i = 0\n    while i < len(s):\n        if s[i] == \"&\" and s[i:i + 5] == \"&amp;\":\n            out = out + \"&amp;\"\n            i = i + 5\n        elif s[i] == \"&\":\n            out = out + \"&amp;\"\n            i = i + 1\n        else:\n            out = out + s[i]\n            i = i + 1\n    return out\n\ndef step(k, s):\n    if k == 1:\n        return trim(s)\n    if k == 2:\n        return squeeze(s)\n    if k == 3:\n        return unbracket(s)\n    if k == 4:\n        return cut(s)\n    if k == 5:\n        return escape_every(s)\n    return escape_bare(s)\n\ndef run(steps, s):\n    out = s\n    for k in steps:\n        out = step(k, out)\n    return out\n\ndef settles(steps, s):\n    once = run(steps, s)\n    return run(steps, once) == once\n\ndef work_left(steps, h):\n    i = 0\n    for k in steps:\n        if step(k, h) != h:\n            return i\n        i = i + 1\n    return 0 - 1\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n3\n\n3\n17\n3\n1234561\n3\na\n3\n1\n\n3\n1\nthis text is longer than forty characters!\n3\n2\n0\n3\n5\nA & B\n5\n",
      "screen": "== Input sanitizer ==\nCleanup steps for typed text: which are safe to repeat, and which pipelines of safe steps are not.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> \nCancelled.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 17\nType 1 to 6 steps, each a digit from 1 to 6.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 1234561\nType 1 to 6 steps, each a digit from 1 to 6.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> a\nType 1 to 6 steps, each a digit from 1 to 6.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 1\ntext (a sample 1 to 9, or your own)> \nCancelled.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 1\ntext (a sample 1 to 9, or your own)> this text is longer than forty characters!\nType a sample number or a text of up to 40 characters.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 2\ntext (a sample 1 to 9, or your own)> 0\nPipeline: squeeze.\n  text:          '0'\n  one pass:      '0'\n  two passes:    '0'\n  three passes:  '0'\nA second pass changes nothing: replayed, this pipeline gives the same text.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 5\ntext (a sample 1 to 9, or your own)> A & B\nPipeline: escape every &.\n  text:          'A & B'\n  one pass:      'A &amp; B'\n  two passes:    'A &amp;amp; B'\n  three passes:  'A &amp;amp;amp; B'\nThe second pass changed it: replayed, this pipeline does not give the same text.\nEscape every & changes the text again on every pass: it is not safe to repeat even alone.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n13\n4\n3\n31\n4\n3\n5\n2\n3\n14\n8\n3\n41\n8\n3\n6\n7\n3\n64\n9\n3\n123\n  <i>Fish</i> &  chips  \n4\n5\n",
      "screen": "== Input sanitizer ==\nCleanup steps for typed text: which are safe to repeat, and which pipelines of safe steps are not.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 1\nThe steps, each run twice on every sample:\n  1 trim             take the spaces off both ends\n      safe to repeat on all 9 samples\n  2 squeeze          make each run of spaces one space\n      safe to repeat on all 9 samples\n  3 unbracket        delete every < and >\n      safe to repeat on all 9 samples\n  4 cut              keep the first 16 characters\n      safe to repeat on all 9 samples\n  5 escape every &   write each & as &amp;\n      changes again on 4 of 9 samples, as 'Tom & Jerry' -> 'Tom &amp; Jerry' -> 'Tom &amp;amp; Jerry'\n  6 escape a bare &  write a & as &amp; unless it already begins &amp;\n      safe to repeat on all 9 samples\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 2\nThe samples:\n  1 '  Hello   world  '\n  2 'Tom & Jerry'\n  3 'a < b > c'\n  4 'x <'\n  5 '<b>Bold</b>  move'\n  6 'Salt & pepper, please'\n  7 'AT&amp;T'\n  8 'one two three   four'\n  9 'Fresh breads & jam'\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 13\ntext (a sample 1 to 9, or your own)> 4\nPipeline: trim, then unbracket.\n  text:          'x <'\n  one pass:      'x '\n  two passes:    'x'\n  three passes:  'x'\nThe second pass changed it: replayed, this pipeline does not give the same text.\nAfter one pass, trim would still change the text: a step after it handed back work it had already done.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 31\ntext (a sample 1 to 9, or your own)> 4\nPipeline: unbracket, then trim.\n  text:          'x <'\n  one pass:      'x'\n  two passes:    'x'\n  three passes:  'x'\nA second pass changes nothing: replayed, this pipeline gives the same text.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 5\ntext (a sample 1 to 9, or your own)> 2\nPipeline: escape every &.\n  text:          'Tom & Jerry'\n  one pass:      'Tom &amp; Jerry'\n  two passes:    'Tom &amp;amp; Jerry'\n  three passes:  'Tom &amp;amp;amp; Jerry'\nThe second pass changed it: replayed, this pipeline does not give the same text.\nEscape every & changes the text again on every pass: it is not safe to repeat even alone.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 14\ntext (a sample 1 to 9, or your own)> 8\nPipeline: trim, then cut.\n  text:          'one two three   four'\n  one pass:      'one two three   '\n  two passes:    'one two three'\n  three passes:  'one two three'\nThe second pass changed it: replayed, this pipeline does not give the same text.\nAfter one pass, trim would still change the text: a step after it handed back work it had already done.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 41\ntext (a sample 1 to 9, or your own)> 8\nPipeline: cut, then trim.\n  text:          'one two three   four'\n  one pass:      'one two three'\n  two passes:    'one two three'\n  three passes:  'one two three'\nA second pass changes nothing: replayed, this pipeline gives the same text.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 6\ntext (a sample 1 to 9, or your own)> 7\nPipeline: escape a bare &.\n  text:          'AT&amp;T'\n  one pass:      'AT&amp;T'\n  two passes:    'AT&amp;T'\n  three passes:  'AT&amp;T'\nA second pass changes nothing: replayed, this pipeline gives the same text.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 64\ntext (a sample 1 to 9, or your own)> 9\nPipeline: escape a bare &, then cut.\n  text:          'Fresh breads & jam'\n  one pass:      'Fresh breads &am'\n  two passes:    'Fresh breads &am'\n  three passes:  'Fresh breads &am'\nA second pass changes nothing: replayed, this pipeline gives the same text.\nYet escape a bare & would still change 'Fresh breads &am': a step after it takes the change back every time.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 3\nsteps, in order (digits 1 to 6, e.g. 13)> 123\ntext (a sample 1 to 9, or your own)>   <i>Fish</i> &  chips  \nPipeline: trim, then squeeze, then unbracket.\n  text:          '  <i>Fish</i> &  chips  '\n  one pass:      'iFish/i & chips'\n  two passes:    'iFish/i & chips'\n  three passes:  'iFish/i & chips'\nA second pass changes nothing: replayed, this pipeline gives the same text.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 4\nEvery ordered pair of the safe steps, run twice on every sample:\n  pair                              second pass         first step has work left\n  trim, then squeeze                same                none\n  trim, then unbracket              changes on 1 of 9   on 1 of 9\n  trim, then cut                    changes on 1 of 9   on 1 of 9\n  trim, then escape a bare &        same                none\n  squeeze, then trim                same                none\n  squeeze, then unbracket           changes on 1 of 9   on 1 of 9\n  squeeze, then cut                 same                none\n  squeeze, then escape a bare &     same                none\n  unbracket, then trim              same                none\n  unbracket, then squeeze           same                none\n  unbracket, then cut               same                none\n  unbracket, then escape a bare &   same                none\n  cut, then trim                    same                none\n  cut, then squeeze                 same                none\n  cut, then unbracket               same                none\n  cut, then escape a bare &         changes on 2 of 9   on 2 of 9\n  escape a bare &, then trim        same                none\n  escape a bare &, then squeeze     same                none\n  escape a bare &, then unbracket   same                none\n  escape a bare &, then cut         same                on 1 of 9\nPairs of safe steps that change on a second pass: 4 of 20.\nPairs where the second pass changes exactly when the first step has work left: 19 of 20, on these samples.\nA pair stays safe while its second step never hands back text its first step would still change.\nThe converse does not hold: with escape a bare &, then cut, a second pass changes nothing, yet escape a bare & would still change 'Fresh breads &am' - cut takes the change back every time.\n\n1) steps  2) samples  3) run a pipeline  4) every pair  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "idempotence-witness",
      "caseId": "222-idempotence-witness",
      "title": "Safe once, not safe twice"
    },
    {
      "slug": "idempotence-does-not-compose",
      "caseId": "327-idempotence-does-not-compose",
      "title": "Idempotence does not compose — three retry-safe routines, two retry-unsafe pairs"
    }
  ],
  "updated": "2026-10-18"
}
