g | x | w | all
Bytes Lang Time Link
059Pip211119T042919ZDLosc
079Vyxal 3 Ṡ231218T201017ZFmbalbue
231Haskell211129T212934ZRadek
701brainfuck211124T161624Ztjjfvi
138Ruby211125T221505Zlonelyel
330Haskell211128T000238ZNatte
118Ruby211127T003447ZNatte
093Perl 5 + nF M5.10.0211119T130721ZDom Hast
047Charcoal211118T074916ZNeil
188C clang211120T012057Za stone
046Jelly211120T030657ZUnrelate
05005AB1E211118T082051ZKevin Cr
045Stax211118T163902ZRazetime
069Vyxal oj211118T035102Zlyxal
205Retina211118T121625ZNeil
141JavaScript ES2022211118T060350Ztsh

Pip, 65 60 59 bytes

OsX4FiPp:#_FIa^C X^Y"><+-.,[]"P[sX4pRXXsRA Uvis(gs.y@?i|9)]

The BF program and the nine explanation strings are taken as command-line arguments. Alternately, you can use the -r flag to take them from stdin. Attempt This Online!

Explanation

First, the setup:

OsX4  Pp:#_FIa^C X^Y"><+-.,[]"
 sX4                            Space character, repeated 4 times
O                               Output it without trailing newline
                   Y"><+-.,[]"  Yank that string into y (BF commands)
                  ^             Split into a list of chars
                 X              Convert to a regex that matches any one of
                                those chars
               C                Wrap the regex in a capture group
             a^                 Split the first program arg on that regex
                                (As in Python, splitting on a regex wrapped in
                                a group keeps the matches of the regex in the
                                result list)
           FI                   Filter that list on this function:
         #_                      Length of item
                                (This gets rid of empty strings while not getting
                                rid of "0", which is falsey in Pip)
       p:                       Assign the result to p (the processed program)
      P                         Print it with trailing newline

Then, the main loop:

Fi...P[sX4pRXXsRA Uvis(gs.y@?i|9)]
Fi                                  For i in
  ...                               the quantity we just assigned to p and printed:
      [                          ]   List containing:
       sX4                            1. Four spaces
                                      2. Current chunk, padded:
          pR                           In p, replace
            XX                         Regex matching any character
              s                        With space
               RA                      Replace the element at index
                   v                   v (initially -1)
                  U                    Incremented each time through the loop
                    i                  With i, the current chunk
                     s                4. One more space before explanation
                                      5. Explanation:
                        s.y            BF commands with a space prefixed
                           @?i         Index of current chunk in that string
                                       (1 to 8 if BF command, 0 or nil otherwise)
                               |9      Logical OR with 9
                                       (1 to 8 if BF command, 9 otherwise)
                      (g         )     Item in program args at that index
     P                               Concatenate and print that whole list

Vyxal 3 , 80 79 bytes

Dð4×§,0$"<>+-.,[]"£([¥nc[⁰Lm-ꜝð×§¹,|n§1#x)4m+ð×§¥nḞꜝ:[#?Ḣin§⁰Lm-ð×§,0|n§1][ð§¹,

Try it Online!

Explanation:

Setup:

Dð4×§,0$"<>+-.,[]"£(­⁡​‎‎⁡⁠⁢‏⁠‎⁡⁠⁣‏⁠‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁡‏⁠‎⁡⁠⁢⁢‏‏​⁡⁠⁡‌⁣​‎⁠⁠⁠⁠‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏⁠‎⁡⁠⁣⁣‏⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏⁠‎⁡⁠⁤⁤‏⁠‎⁡⁠⁢⁡⁡‏⁠‎⁡⁠⁢⁡⁢‏⁠‎⁡⁠⁢⁡⁣‏‏​⁡⁠⁡‌⁤​‎⁠⁠⁠⁠‎⁡⁠⁢⁡⁤‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁢⁣‏⁠‎⁡⁠⁢⁤‏⁠‏​⁡⁠⁡‌⁢⁢​‎⁠⁠⁠⁠⁠‏​⁡⁠⁡‌­
 ð4×§                 ## ‎⁡Print "    " without newline
D    ,                ## ‎⁢Print the first line of input with newline
        "<>+-.,[]"£   ## ‎⁣Put "<>+-.,[]" in the register
      0$              ## ‎⁢⁡Push the 0 in the stack (Swapped because the top of the stack is used for looping)
                   (  ## ‎⁤Loop every char in the first line of input

Looping part:

[¥nc[⁰Lm-ꜝð×§¹,|n§1#x)4m+ð×§¥nḞꜝ:[#?Ḣin§⁰Lm-ð×§,0|n§1]­⁡​‎‎⁡⁠⁡‏⁠‎⁡⁠⁢‏⁠‎⁡⁠⁣‏⁠‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏⁠‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁢⁢‏⁠‎⁡⁠⁢⁣‏⁠‎⁡⁠⁢⁤‏⁠‎⁡⁠⁣⁡‏⁠‎⁡⁠⁣⁢‏⁠‎⁡⁠⁣⁣‏⁠‎⁡⁠⁣⁤‏⁠‎⁡⁠⁤⁡‏⁠‎⁡⁠⁤⁢‏⁠‎⁡⁠⁤⁣‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁤⁤‏⁠‎⁡⁠⁢⁡⁡‏⁠‎⁡⁠⁢⁡⁢‏⁠‎⁡⁠⁢⁡⁣‏⁠‎⁡⁠⁢⁡⁤‏⁠‎⁡⁠⁢⁢⁡‏⁠‎⁡⁠⁢⁢⁢‏‏​⁡⁠⁡‌⁤​‎‎⁡⁠⁢⁢⁣‏⁠‎⁡⁠⁢⁢⁤‏⁠‎⁡⁠⁢⁣⁡‏⁠‎⁡⁠⁢⁣⁢‏⁠‎⁡⁠⁢⁣⁣‏⁠‎⁡⁠⁢⁣⁤‏‏​⁡⁠⁡‌⁢⁡​‎‎⁡⁠⁢⁤⁡‏⁠‎⁡⁠⁢⁤⁢‏⁠‎⁡⁠⁢⁤⁣‏⁠‎⁡⁠⁢⁤⁤‏⁠‏​⁡⁠⁡‌⁢⁢​‎‎⁡⁠⁣⁡⁡‏⁠‎⁡⁠⁣⁡⁢‏⁠⁠‏​⁡⁠⁡‌⁢⁣​‎‎⁡⁠⁣⁡⁣‏⁠‎⁡⁠⁣⁡⁤‏⁠‎⁡⁠⁣⁢⁡‏⁠‎⁡⁠⁣⁢⁢‏⁠‎⁡⁠⁣⁢⁣‏⁠‎⁡⁠⁣⁢⁤‏⁠‎⁡⁠⁣⁣⁡‏⁠‎⁡⁠⁣⁣⁢‏⁠‎⁡⁠⁣⁣⁣‏⁠‎⁡⁠⁣⁣⁤‏⁠‎⁡⁠⁣⁤⁡‏⁠‎⁡⁠⁣⁤⁢‏⁠‎⁡⁠⁣⁤⁣‏⁠‎⁡⁠⁣⁤⁤‏⁠‎⁡⁠⁤⁡⁡‏‏​⁡⁠⁡‌⁢⁤​‎‎⁡⁠⁤⁡⁢‏⁠‎⁡⁠⁤⁡⁣‏⁠‎⁡⁠⁤⁡⁤‏⁠‎⁡⁠⁤⁢⁡‏⁠‎⁡⁠⁤⁢⁢‏‏​⁡⁠⁡‌­
[¥nc[                                                   ## ‎⁡if the current character is in "<>+-.,[]"
     ⁰Lm-ꜝð×§¹,                                         ## ‎⁢print that many spaces, then the second line of input, with newline.
               |n§1#x)                                  ## ‎⁣else print the current character and continue with the 1 in the top of the stack.
                      4m+ð×§                            ## ‎⁤Print that many spaces
                            ¥nḞꜝ                        ## ‎⁢⁡index of the current character (1 to 8, 0 if not found)
                                :[                      ## ‎⁢⁢if the index of the current character is 1 or more
                                  #?Ḣin§⁰Lm-ð×§,0       ## ‎⁢⁣Print that many spaces, then the right explanation, with newline, and push 0
                                                 |n§1]  ## ‎⁢⁤else, print the current character, and push 1

Final part:

[ð§¹,­⁡​‎‎⁡⁠⁡‏⁠⁠⁠⁠‏​⁡⁠⁡‌⁢​‎‎⁡⁠⁡‏‏​⁡⁠⁡‌⁣​‎‎⁡⁠⁢‏⁠‎⁡⁠⁣‏⁠⁠‏​⁡⁠⁡‌⁤​‎‎⁡⁠⁤‏⁠‎⁡⁠⁢⁡‏‏​⁡⁠⁡‌­
[      ## ‎⁡If the top of stack is 1 or greater
       ## ‎⁢(I have no idea why that works)
 ð§    ## ‎⁣print " " without newline
   ¹,  ## ‎⁤Print the second line of input, with newline
💎

Created with the help of Luminespire.

Haskell, 235 231 bytes

import Data.List
main=interact$unlines.map("    "++).f"><+-.,[]".lines
f o(i:e)|let t=not.(`elem`o);p=(>>" ").concat;w((c:r):t)=(c:r++p t++' ':maybe(e!!8)id(lookup c$zip o e)):[p[c:r]++l|l<-w t];w[]=[]=i:w(groupBy(\x y->t x&&t y)i)

Try it online!

Old answer:

Haskell, 235 bytes

import Data.List
main=interact$unlines.map("    "++).f.lines
o="><+-.,[]"
t=not.(`elem`o)
p=(>>" ").concat
f(i:e)|let w((c:r):t)=(c:r++p t++' ':maybe(last e)id(lookup c$zip o e)):[p[c:r]++l|l<-w t];w[]=[]=i:(w$groupBy(\x y->t x&&t y)i)

Try it online!

The answer is not final, will probably be golfed further.

brainfuck, 811 775 709 701 bytes

>>-[-[--->]<<-]>--...<++++++++++.>>>>>>>>>>+[->>>>>>+<<,[[-<+<+>>]<----------[[-]->]-[+>-]<<<+[-<[-]>]<[-<+>>+<]++++++[->-------<]>>++++++[-<<++++++>>]>+<<-[<->>]>[<]<-[<------->>]>[<]<-[<-->>]>[<]<-[<-------->>]>[<]<--------------[<--->>]>[<]<--[<---->>]>[<]+++++[-<------>]<+[<----->>]>[<]<--[<------>>]>[<]<[-]<<+>[>>>-]<[>>>]<<<->>++++[->++++++++<]+>>>+>]<]+[<<<<<<]>>>>[.>>>>>>]>>>>-<<<<<<<<<<[<<<<<<]<<<<<.>>>+[->>>>>>>>>>>>>[>>>>>>]<<<<<[<[<<.<<<<]>>>>>>[>>>>>>]<<<<<.>>>>[>+>>>>>[<<<<.>>>>>+>>>>>]<<<<[>>>.>>>]>>>.>>>[.>>>>>>]>>[<<<<<<]<]<<<[>->>>+>[>>>.>>>]>>-<<<<<.<[<<<<<<]<[->>>>>>>[>>>>>>]+>>>>[>>>>>>]>>-<<<<<<[<<<<<<]<]>+[>>>>>>]+>>>>[.>>>>>>]>>[<<<<<<]<<<<]+<<<.>>]>]>[<<<<<<]<<<<<<...

Try it online!

This could probably be golfed more, as this is, like, the second brainfuck program I've written.

Explanation

I should really write a program to do this for me...

Fullscreen Explanation

#####
# Execution will be divided into two phases: input and output.
# The input phase will input all characters and lay them out into memory with metadata.
# The output phase will then loop over the memory and print various segments of it, forming the explanation.

###############
# Input Phase #
###############

#####
# First, let's take a look at the memory layout.
# We'll divide the memory into 6-byte chunks.
# The chunks will be laid out in this format:
#   - first header chunk, to denote the start
#   - second header chunk, to denote the start
#   - first source chunk
#   - second source chunk
#   - ...
#   - last source chunk
#   - primary separator chunk (in place of newline)
#   - first chunk of first explanation
#   - second chunk of first explanation
#   - ...
#   - last chunk of first explanation
#   - separator chunk (in place of newline)
#   - first chunk of second explanation
#   - ......
#   - last chunk of last explanation
#   - eof chunk
# The first header chunk will have the following bytes:
#   0 10 96 0 0 0
# (10 is newline, and 96 is backtick)
# The second header chunk will consist entirely of zeroes.

                     #####
                     # Initialize the header chunks and output the initial code fence:
>>                     # Move to cell #2 (0-indexed).
-[-[--->]<<-]>--       # Set cell #2 to backtick (96) (pulled from https://esolangs.org/wiki/Brainfuck_constants).
...                    # Print it thrice (fine, there is a tiny bit of output in this phase).
<++++++++++.           # Set cell #1 to newline (10) and print it.
>>>>>>>>>>+          # Move to cell #5 of the second header chunk and temporarily set it to 1.

                     #####
                     # Next, read the input into chunks and initialize them.
                     # Every non-header non-eof chunk will be initialized to the following format:
                     #   #0: [char]  (0 if separator)
                     #   #1: [kind]  (which operator the char is)
                     #   #2: [mark]  (currently 1; this will occasionally be temporarily set to 0 as a "bookmark")
                     #   #3: 32      (the code for space, used for output)
                     #   #4: [noop]  (1 if no-op source chunk or non-primary separator, 0 otherwise)
                     #   #5: [done]  (currently 0, undone)
                     # During the initialization, some cells are used for temporary values.
[                    # While the current cell (#5 of the preceding chunk) is non-zero (and thus known to be 1), loop over the input:
  -                    # Subtract 1 from the cell, [done], returning it to 0 (undone)
  >                    # Move to the active chunk.
                       # Cell #5 will be used for positioning the pointer after a branch ('alignment'):
  >>>>>+               # Set cell #5 to 1
                       #####
                       # Next, we'll read a character.
  <<,                  # Move to cell #3 and input a character into it (temp).
  [                    # If this character is non-zero (not eof):
    [-<+<+>>]            # Duplicate this character into cells #1 and #2 [temp], erasing this cell, #3.
    <----------          # Move to cell #2 and subtract 10 (code for newline) from it.
                         # The chunk is now:
                         #   #0: 0
                         #   #1: char (temp)
                         # > #2: char - 10 (temp)
                         #   #3: 0
                         #   #4: 0
                         #   #5: 1 (alignment)
                         #####
                         # Next, if char is newline (10), we'll set cell #1 to 0.
    [[-]->]              # If cell #2 (char - 10) is non-zero (the char is not a newline), set this cell to 255 and move to cell #3.
    -[+>-]               # Move to the first cell to the right set to 1 (cell #5), and set it to 0.
                         # (The above is done without any net change to other cells.) 
    <<<+                 # Move to cell #2 and add one to it; this cell is now 1 if char is a newline, or 0 otherwise.
    [-<[-]>]             # If cell #2 is 1, set it back to 0, move to cell #1, clear it, and move back to cell #2.
                         # From now on, char will be 0 if the character was a newline.
                         # The chunk is now:
                         #   #0: 0
                         #   #1: char (temp)
                         # > #2: 0
                         #   #3: 0
                         #   #4: 0
                         #   #5: 0
                         #####
                         # Next we'll inialize cell #0 [char] and put a copy of char into cell #2.
    <                    # Move to cell #1.
    [-<+>>+<]            # Duplicate this value to cells #0 and #2, clearing this cell.
                         # The chunk is now:
                         #   #0: [char]
                         # > #1: 0
                         #   #2: char (temp)
                         #   #3: 0
                         #   #4: 0
                         #   #5: 0
                         #####
                         # Next, we'll initialize cell #1 [kind] to a number denoting the 'kind' of the char is.
                         # The kind is an index from 0 to 8 matching the order "N+-<>[],." (where N stands for any no-op).
                         # The kind could be determined by directly checking if the character matches an operation (char == op).
                         # In BF, however, it's easier to check if something is not equal to X than if it is equal to X.
                         # We'll start cell #1 [kind] at the sum of the kind, 36 (1+2+3+4+5+6+7+8).
                         # For each operation, we'll check if the character does not match that operation, (char != op).
                         # To do so, we will use cell #2 for checking whether (char - op) is non-zero.
                         # If the character does not match that operation, we will subtract its kind from the sum.
                         # After doing so for all of the operations, the result will be the kind of the operation that the character matches.
                         # We'll do this not in kind order, but in op order: "+" (43), then "," (44), then "-" (45), etc.
                         #####
>>++++++[-<<++++++>>]    # Set cell #1 to 36 and move to cell #3.
>+                       # Set cell #4 to 1 to be used for alignment.
                         # The chunk is now:
                         #   #0: [char]
                         #   #1: 36 (sum of all kinds)
                         #   #2: char (char - 0)
                         #   #3: 0
                         # > #4: 1 (alignment)
                         #   #5: 0
                         #####
                         # The next set of lines are based on using the following pattern for each of the operations:
                           # Subtract from cell #2 the difference between the char codes for this operation and the previous.
                           # If cell #2 is not zero (if char != op):
                             # Remove this kind from the sum by subtracting from cell #1 (kind_sum) the kind of this operation.
                           # Return to cell #2 (using cell #4 for alignment).
                           # The resulting chunk will be:
                           #   #0: [char]
                           #   #1: kind_sum
                           # > #2: char (char - op)
                           #   #3: 0
                           #   #4: 1 (alignment)
                           #   #5: 0
                         # For '+', check 43:
    <++++++[-<------->]<-  # Subtract 43 (6 * 7 + 1) from cell #2, the difference between '+' and 0.
                           # The value in cell #2 is now char - 43.
    [                      # If the cell #2 is non-zero (char != 43; the character is not '+'):
      <->>                   # Subtract from cell #1 the kind of '+', 1.
    ]                      # End if.
    >[<]<                  # Return to cell #2 (using cell #4 for alignment).
                         # (For the remaining operations, the pattern will be shown in a condensed form.)
                         # For ',', check 44: 
    -                      # Subtract 1 (the difference between ',' and '+').
    [<------->>]>[<]<      # If not zero, remove 7 from kind_sum. Return to cell #2.
                         # For '-', check 45:
    -                      # Subtract 1.
    [<-->>]>[<]<           # If not zero, remove 2 from kind_sum. Return to cell #2.
                         # For '.', check 46: 
    -                      # Subtract 1.
    [<-------->>]>[<]<     # If not zero, remove 8 from kind_sum. Return to cell #2.
                         # For '<', check 60:
    --------------         # Subtract 14.
    [<--->>]>[<]<          # If not zero, remove 3 from kind_sum. Return to cell #2.
                         # For '>', check 62:
    --                     # Subtract 2.
    [<---->>]>[<]<         # If not zero, remove 4 from kind_sum. Return to cell #2.
                         # For '[', check 91
    >+++++[-<------>]<+    # Subtract 29 (5 * 6 - 1).
    [<----->>]>[<]<        # If not zero, remove 5 from kind_sum. Return to cell #2.
                         # For ']', check 93
    --                     # Subtract 2.
    [<------>>]>[<]<       # If not zero, remove 6 from kind_sum. Return to cell #2.
                         #
    [-]                  # Set cell #2 to zero.
                         # The chunk is now:
                         #   #0: [char]
                         #   #1: [kind]
                         # > #2: 0
                         #   #3: 0
                         #   #4: 1 (temp)
                         #   #5: 0
                         #####
                         # Next, cell #4 [noop] will be initialized to 0 if char is not a no-op.
    <<+                  # Move to cell #0, adding 1 (temporarily, for alignment)
    >                    # Move to cell #1.
    [>>>-]               # If cell #1 [kind] is non-zero (the char is not a no-op),
                         #  move to cell #4 [noop] and subtract 1 to set it to 0.
    <[>>>]               # Move to cell #3, using cell #0 for realignment.
    <<<-                 # Move to cell #0 and subtract 1 from it, returning it to char.
                         # The chunk is now:
                         # > #0: [char]
                         #   #1: [kind]
                         #   #2: 0
                         #   #3: 0
                         #   #4: [noop] (1 if no-op, 0 otherwise)
                         #   #5: 0
                         #####
                         # Next, cell #2 [mark] will be initialized to 1 (unmarked) and cell #3 [space] will be initialized to 32 (code for space).
    >>++++[->++++++++<]  # Move to cell #2 and set cell #3 to 32 (4 * 8)
    +                    # Set #2 [mark] to 1 (unmarked)
    >>>+                 # Move to cell #5 and set it to 1 (temporarily, so the input loop continues; this will be unset in the next iteration).
    >                    # Move to cell #0 of the next chunk.
  ]                    # End "if non-eof".
  <                  # If at cell #0 of the next chunk (from the preceding branch for processing a source chunk), move to cell #5 of the current chunk;
                     #   otherwise (at cell #3 after the source chunks), move to cell #2.
]                    # End input loop.
                     # Every non-header non-eof chunk is now:
                     #   #0: [char]  (0 if separator)
                     #   #1: [kind]  (which operator the char is)
                     #   #2: [mark]  (currently 1; this will occasionally be temporarily set to 0 as a "bookmark")
                     #   #3: 32      (the code for space, used for output)
                     #   #4: [noop]  (1 if no-op source chunk or non-primary separator, 0 otherwise)
                     #   #5: [done]  (currently 0, undone)
                     # Since the loop ended, it reached eof, and the head is at cell #2 of the eof chunk.

################
# Output Phase #
################

                     #####
                     # First, we will return to the header chunks.
+                    # Set cell #2 to 1 (used later to save a byte).
[<<<<<<]             # Move left one chunk at a time until cell #2 is zero, reaching the second header chunk.
>>>>                 # Move to cell #0 of the first source chunk.
[.>>>>>>]            # While cell #0 is non-zero, print it and move to the next chunk 
                     #  (this writes the first line of the explanation, the code verbatim).
                     # Now, the head is at cell #0 of the primary separator chunk.
>>>>-<<<<            # Set its cell #4 to 0 (used later to break a loop).
<<<<<<[<<<<<<]       # Move to the next 0-char chunk to the left (this is the second header chunk).
<<<<<.               # Move to cell #1 of the first header chunk (newline) and output it (ending the first line).

                     #####
                     # As we go through each line of the explanation, we'll follow this psuedo-code:
                     #   - Output a space for every source character marked as 'done'
                     #   - Output the characters for the active operations (multiple if it's a chain of no-ops, only one otherwise)
                     #   - Mark all active operations 'done'
                     #   - Output a space for every source character after the active ones
                     #   - Output a space followed by the associated explanation
                     # These steps will be expanded on later.

>>>                  # Move to cell #4 of the first header chunk.
+                    # Set it to one to start the loop.
[                    # Begin a loop to print subsequent lines:
                       #####
                       # First, we will find the first not done chunk.
  -                    # Set this cell, #4 of the first header chunk, back to 0.
  >>>>>>>>>>>>>        # Move to cell #5 [done] of the first source chunk.
  [>>>>>>]             # Go to the first not done cell to the right (it might be the current chunk).
  <<<<<                # Go to cell #0 [char].
  [                    # If it's non-zero (if it's a source chunk):
                         #####
                         # Next, we will output the leading spaces and the first active character.
    <[<<.<<<<]           # Go to cell #5 of the first not done chunk to the left
                         #  (this is the second header chunk), outputting spaces along the way.
    >>>>>>               # Go to cell #5 [done] of the next chunk.
    [>>>>>>]             # Move to cell #5 of the first non-done cell to the right.
    <<<<<.               # Move to cell #0 [char] of this chunk (the first active source chunk) and output it.
    >>>>                 # Move to cell #4 [noop] of this chunk.
  
                         #####
                         # Next, the following branch will process a sequence of no-op chunks. 
                         # This is handled differently from non-no-op chunks because sequential no-op characters are printed in one line.
    [                    # If it's non-zero (if this character is a no-op):
      >+                   # Set cell #5 [done].
                           #####
                           # First, we will output each subsequent active character and mark their chunks as done.
      >>>>>                # Move to cell #4 [noop] of the next chunk.
      [                    # While the current chunk is a no-op:
        <<<<.                # Move to cell #0 [char] and output it.
        >>>>>+               # Move to cell #5 [done] and set it to 1.
        >>>>>                # Move to cell #4 [noop] of the next chunk.
                             # (Note that the primary separator has cell #4 [noop] set to 0 in order to break this loop)
      ]                    # End loop. Now the head is at the cell #4 [noop] of the first non-no-op chunk.
                           #####
                           # Next, we will output the trailing spaces and the no-op explanation.
      <<<<[>>>.>>>]        # Move to the primary separator chunk, outputting a space for each skipped chunk.
      >>>.>>>              # Move to cell #0 of the first chunk of the first explanation, outputting a space.
      [.>>>>>>]            # Move to the next non-explanation chunk, outputting each character.
      >>                   # Move to cell #2 [mark].
      [<<<<<<]             # Move to cell #2 of the second header chunk.
      <                    # Move to cell #1.
    ]                    # End if.
    <<<                  # If at cell #1 of the second header chunk (from the preceding branch for processing a no-op), move to cell #4 of the first header chunk;
                         #  otherwise, (still at the cell $4 [noop] of the current source chunk), move to cell #1 [kind].

                         #####
                         # Next, the following branch will process a non-no-op chunk.
                         # This is handled separately from no-op chunks because operators are printed on separate lines.
    [                    # If cell #1 [kind] is non-zero (if in a non-no-op source chunk):
                           #####
                           # First, we will output the trailing spaces and find the primary separator chunk.
      >-                   # Move to cell #2 [mark] and set it to 0 (this marks the cell to return to later).
      >>>+                 # Move to cell #5 [done] and set it to 1.
      >[>>>.>>>]           # Move to the primary separator chunk, outputting a space for each skipped chunk.
      >>-                  # Mark this chunk (the primary separator chunk).
      <<<<<.<              # Move to cell #2 [mark] of the previous chunk, outputting a space.
      [<<<<<<]             # Move to the first marked (mark == 0) cell to the left (the active source chunk).
      <                    # Move to cell #1 [kind].
                           #####
                           # Next, we'll locate the correct explanation.
                           # We're at cell #2 [kind], which acts as an index to the explanation.
                           # The primary separator (which precedes the 0th (no-op) explanation) is marked.
      [-                   # Continually decrement the kind:
                             # Each iteration will unmark the currently marked separator and mark the next separator.
        >>>>>>>[>>>>>>]      # Move to the first marked cell to the right.
        +                    # Unmark this chunk.
        >>>>                 # Move to cell #0 of the next chunk.
        [>>>>>>]             # Move through the explanation chunks to the next separator chunk.
        >>-                  # Mark this separator chunk.
        <<<<<<[<<<<<<]<      # Return cell #1 [kind] of the active source chunk (as it's marked).
      ]                    # End loop.
                           #####
                           # Next, we will output the explanation we have located.
      >+                   # Unmark the current source chunk.
      [>>>>>>]             # Move to the marked separator chunk.
      +                    # Unmark this chunk.
      >>>>                 # Move to cell #0 [char] of the next chunk.
      [.>>>>>>]            # While cell #0 [char] is non-zero, output it and move to the next chunk.
      >>[<<<<<<]           # Return to the second header chunk.
      <<<<                 # Move to cell #4 of the first header chunk.
    ]                    # End if.

                         # From either branch, we are now at cell #4 of the first header chunk.
    +                    # Set cell #4 to 1 (to continue the loop).
    <<<.                 # Move to cell #1 (newline) and output it.
    >>                   # Move to cell #3 (which is 0).
  ]                    # End if.
  >                    # If at the first header chunk (from the preceding branch for processing a source chunk), move to cell #4 (which is 1, continuing the loop);
                       #  otherwise (still at the primary separator), move to cell #1 (which is 0, ending the loop).
]                    # End loop.

#####
# Lastly, we will print the final code fence.
>[<<<<<<]            # Move to cell #2 of the second header chunk.
<<<<<<...            # Move to the first header chunk and output the backtick thrice.

#####
# Finally, we can enjoy the fruits of our efforts: https://pastebin.com/raw/C9yYHx3F.

Ruby, 143 142 138 bytes

->i{c,*e=i.split ?\n;puts" "*(m=4)+c;c.scan(/[^<>+-.,\[\]]+|./){|b|r=" ".*c.size+5;r[m,z=b.size]=b;puts r+e["<>+-.,[]".index(b)||8];m+=z}}

Try it online!

The function expects input to be lines separated by \n. It scans the program using regexp /[^<>+\.,\[\]-]+|./. It outputs to stdout instead of returning. Which is all there is to it.

Thanks to @Fmbalbuena for removing a space!

Saved 4 more bytes by re-reading ruby golfing tips :)

Haskell, 330 bytes

import Data.List
main=interact$unlines.f.lines
f(c:a)=(s 4++c):(map(p c.(a%)).tail.inits.t)c
t r|r==[]=[]|q x=[x]:t y|0<1=a:t b where(x:y)=r;(a,b)=span(not.q)r
q=(`elem`n)
p c(a,e)=s(4+(l.init)a)++last a++s(1+length c-l a)++e
s=(`replicate`' ')
n="><+-.,[]"
e%j=(j,e!!(n#last j))
l=length.concat
(s:z)#c|[s]==c=0|z==[]=1|0<1=1+z#c

Try it online!

Explanation:

import Data.List
main=interact$unlines.f.lines
f(c:a)=(s 4++c):(map(p c.(a%)).tail.inits.t)c

import Data.List inits(prefixes), this is used to keep track of the number of spaces
call f with lines of input, then output each line
f = " " * 4 + code +
map on each prefix of each command or noop:
select explanation and then pad with spaces

t r|r==[]=[]|q x=[x]:t y|0<1=a:t b where(x:y)=r;(a,b)=span(not.q)r
q=(`elem`n)
n="><+-.,[]"

t splits the code into commands or noop
q checks if a char is a command
n is the string "><+-.,[]"

p c(a,e)=s(4+(l.init)a)++last a++s(1+length c-l a)++e
s=(`replicate`' ')
e%j=(j,e!!(n#last j))
(s:z)#c|[s]==c=0|z==[]=1|0<1=1+z#c

p pads a (prefix, explanation)-pair with spaces
s returns n spaces
% creates said (prefix, explanation)-pair
# returns the index of the command in n or 8 if it's a noop

Ruby, 118 bytes

c,*a=*$<
print" "*l=4,c
c.scan(/[^+-.\[\]<>\n]+|./){puts" "*l+$&+" "*(c.size+4-l+=$&.size)+a["><+-.,[]".index($&)||8]}

Try it online!

Explanation

c,*a=*$<                                 # assign 'c' to first line and 'a' to the rest of input
print" "*l=4,c                           # output code and assign 'l' to 4 at the same time
c.scan(/[^+-.\[\]<>\n]+|./)              # for every match:
{puts" "*l+$&+" "*(c.size+4-l+=$&.size)+ # output the padded match, update 'l' to keep track of the spaces
a["><+-.,[]".index($&)||8]}              # output the correct explanation

inspiration taken from lonelyelk

Perl 5 + -nF -M5.10.0, 93 bytes

Thanks to @Fmbalbuena for noticing I'd missed printing the unaltered string! Thanks to @emanresu A and @Neil's regex wizardry saving 14 (!!) bytes!

@@=<>;say$"x4,$_,map$"x(4+$-).$_.$"x(@F-($-+=y///c)).$@[index'><+-.,[]',$_],/[^]<>+-.[
]+|./g

Try it online!

Explanation

First print 4 x (string repetition operator) $" (which defaults to space), then store the length of the input (implicitly obtained via -F into @F) in $=, and store the explanations in @@. Then, for each matching piece of code (either one char from -<>+.,[], or any number of any other chars) concatenate the following with $\: 4 + $- (which starts out as 0) x $", followed by the piece of code ($_), followed by the original length, minus $- (to which we add the current length of this code piece) x $" and finally, using the index function on the string ><+-.,[], the message associated to the current piece of code (index returns -1 for missing index, for which Perl returns the last element of the list). $_ is output followed by $\ thanks to -p flag.


Perl 5 + -F/([^]<>+-.[]+|.)/ -M5.10.0, 92 bytes

The regex used in the above, can be provided via -F to save a byte.

$==y///c;@@=<>;say$"x4,$_,map/./&&$"x(4+$-).$_.$"x($=-($-+=y///c)).$@[index'><+-.,[]',$_],@F

Try it online!

Charcoal, 54 48 47 bytes

× ⁴PSFθ«⊞υ⌕><+-.,[]ι¿⁺²Σ✂υ±²↓P⊟υι»↑⸿↓≔E⁹SηEυ§ηι

Try it online! Link is to verbose version of code. Explanation:

× ⁴

Indent by 4 spaces.

PS

Print the code without moving the cursor.

Fθ«

Loop over the characters in the code.

⊞υ⌕><+-.,[]ι

Push the index of the appropriate explanation to the predefined empty list. NOPs will have an index of -1, which will cyclically index to the last explanation, but is also useful for coalescing consecutive NOPs below.

¿⁺²Σ✂υ±²

If there are not two consecutive NOPs, then...

... move down a line, else...

P⊟υ

... discard one of the NOPs.

ι

Output the current character.

»↑⸿

Move to the start of the next column.

Move to the first row of explanations.

≔E⁹Sη

Read in the explanations.

Eυ§ηι

Print each appropriate explanation.

Today I discovered a bug in Charcoal's Split command which unintentionally results in partial regex support: Split(<string>, <list>) is supposed to split the string at any occurrence of any word in <list>, but due to the bug what actually happens is that Split(<pattern>, [<string>]) escapes the string and splits it on the pattern. Unfortunately the original approach has since been golfed down from its original 54 bytes which this approach ties with, but that's still good going considering the duplication of the command characters:

× ⁴≔Φ⪪“ xMT¦T~≧_x”⟦S⟧ιθP⪫θωFθ⁺¶ι↑⸿↓≔E⁹SηEθ§η⌕><+-.,[]ι

Try it online! Link is to verbose version of code. Note: Backslashes are not supported because this is relying on buggy Charcoal behaviour. Explanation:

× ⁴

Indent by 4 spaces.

≔Φ⪪“ xMT¦T~≧_x”⟦S⟧ιθ

Extract command characters or runs of non-command non-backslash characters from the input.

P⪫θω

Print the code without moving the cursor.

Fθ⁺¶ι

Output each command on its own line, but increasing the indent as it goes.

↑⸿

Move to the start of the next column.

Move to the first row of explanations.

≔E⁹Sη

Read in the explanations.

Eθ§η⌕><+-.,[]ι

Find and output the appropriate explanation for each command, or the last explanation for non-commands.

C (clang), 191 188 bytes

-3 thanks to @ceilingcat

*S="><+-.,[]";i;l;d;f(char*a,**e){for(i=5,l=printf("    %s \n",a);*a;printf("%*s%s\n",l-i,"",e[d?d-(int)S:8]))for(d=index(S,*a),printf("%*c",i++,*a);!d&!index(S,*++a);)i+=write(1,a,*a>0);}

Try it online! Assumes explanations are in the order > < + - . , [ ] NOP.

Jelly, 46 bytes

®Ṭ€z0aḷżṚ⁸ṭṚŻ€4¡o⁶ð“><+-.,[]”iⱮxṠoŒQÄ©ŒQƲ$$ịŻ€

Try it online!

Program on the left and list of descriptions on the right. I suspect this can lose a few bytes fairly easily, but I'm posting it now in order to preserve one of the most hideous things I have ever written for posterity.

05AB1E, 53 52 50 bytes

.γ"><+-.,[]"©såN>*}DU¹g>j|VεXN>£JvÁ}Y®XNèkè«}¹š4ú»

-2 bytes thanks to @ovs.

Outputs the lines with four leading spaces.

Try it online.

Explanation:

.γ                 # Group the first (implicit) input-string into parts:
  "><+-.,[]"       #  Push the string "><+-.,[]"
            ©      #  Store it in variable `®` (without popping)
             s     #  Swap to get the current character
              å    #  Check if it's in the string
               N>* #  Multiply it by the 1-based index
                   #  (this ensures all characters in that string become unique
                   #  values, but all unknown characters map to the same value 0)
}DU                # After the group-by, store a copy in variable `X`
   ¹g              # Push the length of the first input-string
     >             # Increase it by 1
      j            # Make all strings of that length by padding leading spaces
|                  # Push a list of all remaining input-lines
 V                 # Pop and store it in variable `Y`
      ε            # Map over the parts:
       X           #  Push the parts from variable `X`
        N>£        #  Leave the first map-index + 1 amount of parts
           J       #  Join them together
            v }    #  Pop and loop its length amount of times:
             Á     #   Rotate the string once towards the right
         XNè       #  Get the map-index'th part from variable `X`
        ®   k      #  Get the index of that part from variable `®`
       Y     è     #  Use that to index into the list of lines `Y`
              «    #  Append the two strings together
      }¹š          # After the map: prepend the first input-string
         4ú        # Pad each string with 4 leading spaces
           »       # Join the list by newlines
                   # (after which the result is output implicitly)

Stax, 45 bytes

å☺m█Q╗½§ÄN!╢┼♫mù}Jt→v)╢+Θ☼î╔↓bÉü╡⌐CÇ-♠•¿♪FM¥2

Run and debug it

Made this after Fmbalbuena's challenge in TNB. Will try to post something in Pip as well.

Found a nice save after Neil pointed out a mistake.

Explanation

LBc4NtPY{"><+-.,[]"XI^i*}/zsFs%Ntcaay%^(x_Iasn@as+4NtPs
LBc4NtPY{"                                              No Operation
          >                                             Move the pointer to the right
           <                                            Move the pointer to the left
            +                                           Increment the memory cell at the pointer
             -                                          Decrement the memory cell at the pointer
              .                                         Output the character signified by the cell at the pointer
               ,                                        Input a character and store it in the cell at the pointer
                [                                       Jump past the matching ] if the cell at the pointer is 0
                 ]                                      Jump back to the matching [ if the cell at the pointer is nonzero
                  "XI^i*}/zsFs%Ntcaay%^(x_Iasn@as       No Operation
                                                 +      Increment the memory cell at the pointer
                                                  4NtPs No Operation

Vyxal oj, 69 bytes

:4$꘍,`([%^])`\\`><+-,.[]`f:→+∑%ṡ';:vL¦0pṪZƛ÷$꘍nh←=Tuw∨□ḣ∇‟İ„L›„↲p4$꘍∑

Try it Online!

Explained

`([%^])`

Push the string "([%^])" to the stack. This will serve as the basis for a regular expression that groups on non-BF commands

\\

Push a backslash to the stack. This will serve to escape all the characters in the BF character set ([ and ] need to be escaped, because they interfere with the range set)

`><+-,.[]`f

Push the string "><+-,.[]" to the stack, and turn it into a list of characters. It's important it's turned into a list of characters because the backslash will be prepended to each character using vectorised addition.

:→

But first, put the character set into the ghost variable for later storage - there's no BF character set element in vyxal, so to save bytes, it needs to be stored.

+∑

Prepend the backslash to each character just like I said I would, and convert it back to a single string.

%

And place that into the regex pattern string from earlier using string formatting

ṡ';

Split the input program on that regex, and keep only non-empty strings. This splits the program into single bf chars and groups of NOPs.

:vL¦0pṪZ

Get the length of each group, get the cumulative sum of that, prepend a 0 and zip with each group. This has the effect of calculating how many spaces to prepend to each group.

ƛ

To each item X = [spaces, character_group] in that, do the following

÷ð*p

Prepend spaces spaces to character_group

nh←=Tuw∨

Get the index of character_group in the BF character set stored in the ghost variable. This is basically a hacky version of because doesn't work on the version the site uses (it's fixed in the 2.6 preleases, but I'm sticking with 2.4.1)

□ḣ

Wrap all inputs (program and explanation strings) into a single list and push the program separately from the explanation strings.

∇‟İ

Do some weird stack manipulation to retrieve the corresponding explanation string using the index generated by nh←=Tuw∨.

„L›„↲$+

Left justify the character group with the prepended spaces with extra spaces to match the length of the input program + 1. And add that to the explanation using some extra weird stack manipulation.

4ð*p∑

Indent that 4 spaces, and convert into a single string.

;⁋

Join the entire list on newlines. The j flag would usually take care of this, but 69 is a funny number.

Retina, 207 205 bytes

*\K````
*\0G`
\G.
$& $&¶
m,-10T`_><+\-.,[]p`d`.$
(?<=^(.+¶)*.) 9¶(?=. 9)

+`^(.+¶)*(.)+ .¶(?!¶|(?<-2> )*(?(2)$))
$& 
,-10P`.+
+`(\d)( *)(¶(.+¶)*¶)
$2$1*_$3
+`(_)+(¶(.+¶)*(?<-1>.*¶)+(.*))
$4$2
(¶.*){9}$
```

Try it online! Explanation:

*\K````

Output the leading fence.

*\0G`

Output the line of code.

\G.
$& $&¶

Split the line of code into characters. For each character, repeat it twice, joined with a space.

m,-10T`_><+\-.,[]p`d`.$

Transliterate the last character of each pair to a digit 1-8, with unknown characters becoming 9.

(?<=^(.+¶)*.) 9¶(?=. 9)

Join together all runs of unknown characters.

+`^(.+¶)*(.)+ .¶(?!¶|(?<-2> )*(?(2)$))
$& 

(note trailing space) Align each run after the end of the previous run.

,-10P`.+

Pad all of the explanation lines to the same width.

+`(\d)( *)(¶(.+¶)*¶)
$2$1*_$3

Convert all of the transliterated digits to runs of underscores and move them to the end of the line.

+`(_)+(¶(.+¶)*(?<-1>.*¶)+(.*))
$4$2

Replace each run of underscores with the appropriate line of explanation.

(¶.*){9}$
```

Replace all of the explanations with the trailing fence.

JavaScript (ES2022), 141 bytes

s=>a=>"    "+s+s.replace(/(?:[^+-.<>[\]]+|.)(?=(.*))/g,(c,e)=>`
`+(c+e.replace(/.|$/g,' ')).padStart(s.length+5)+a.at('><+-.,[]'.indexOf(c)))

Try it online!

f: (source_code: string) => (explain_array: string[]) => string