code: function replace(node, {enter, leave}) {
      let rep = (enter && enter(node)) || node
  
      switch (rep.type) {
          // regular non-leaf nodes
          case 'Apply':
              for (let i = 0; i < rep.args.length; i++) {
                  const arg = rep.args[i]
                  rep.args[i] = replace(arg, {enter, leave})
              }
              break
  
          // Skip leaf nodes because they're handled by the enter/leave calls at
          // the start/end of replace.
          case 'Identifier':
          case 'Number':
          case 'Ellipsis':
              break
  
          // irregular non-leaf nodes
          case 'Parentheses':
              rep.body = replace(rep.body, {enter, leave})
              break
  
          case 'List':
          case 'Sequence':
              for (let i = 0; i < rep.items.length; i++) {
                  const item = rep.items[i]
                  rep.items[i] = replace(item, {enter, leave})
              }
              break
  
          case 'System':
              for (let i = 0; i < rep.relations.length; i++) {
                  const rel = rep.relations[i]
                  rep.relations[i] = replace(rel, {enter, leave})
              }
              break
  
          case 'Placeholder':
              // TODO(kevinb) handle children of the placeholder
              // e.g. we there might #a_0 could match x_0, y_0, z_0, etc.
              break
  
          default:
              throw new Error('unrecognized node')
      }
  
      return (leave && leave(rep)) || rep
  }
docs: replace - visit all nodes in the tree with the ability to replace them.
  
  This function may modify the node passed in and/or any of its descendants.
  
  If neither 'enter' nor 'leave' return a value, the node is unchanged.
  If 'enter' returns a new node, the children of the new node will be traversed
  instead of the old one.  If both 'enter' and 'leave' return values, the
  value returned by 'leave' is the node that will end up in the new AST.