docs: function sampleParseTree (rhs, config) {
    var pt = this
    var rng = (config ? config.rng : null) || Math.random
    if (typeof(rhs.map) !== 'function')
      console.error ('sampleParseTree type error: rhs (' + typeof(rhs) + ') = ' + JSON.stringify(rhs))
    return rhs.map (function (node, n) {
      var result, index
      if (typeof(node) === 'string')
        result = node
      else if (config && config.quoteLevel > 0) {
        if (node.type === 'func' && node.funcname === 'unquote')
  	result = { type: 'func',
                     funcname: 'unquote',
                     args: pt.sampleParseTree (node.args, extend ({}, config, { quoteLevel: config.quoteLevel - 1 })) }
        else {
          result = extend ({}, node)
          if (node.type === 'func' && node.funcname === 'quote')
            config = extend ({}, config, { quoteLevel: config.quoteLevel + 1 })
          nodeArgKeys.forEach (function (key) {
            if (node[key])
              result[key] = pt.sampleParseTree (node[key], config)
          })
          if (node.opts)
            result.opts = node.opts.map (function (opt) { return pt.sampleParseTree (opt, config) })
        }
      } else {
        switch (node.type) {
        case 'root':
          result = { type: node.type,
                     rhs: pt.sampleParseTree (node.rhs, config) }
          break
        case 'assign':
  	result = { type: 'assign',
                     varname: node.varname,
  		   value: pt.sampleParseTree (node.value, config),
                     local: node.local ? pt.sampleParseTree (node.local, config) : undefined,
                     visible: node.visible }
          break
        case 'alt':
          index = pt.randomIndex (node.opts, rng)
  	result = { type: 'alt_sampled',
                     n: index,
                     rhs: pt.sampleParseTree (node.opts[index], config) }
          break
        case 'alt_sampled':
          result = { type: 'alt_sampled',
                     n: node.n,
                     rhs: pt.sampleParseTree (node.rhs, config) }
          break
        case 'rep':
          var n = Math.min (Math.floor (rng() * (node.max + 1 - node.min)) + node.min,
                            config && config.maxReps ? config.maxReps : pt.maxReps)
  	result = { type: 'rep_sampled',
                     n: n,
  		   reps: new Array(n).fill().map (function() { return pt.sampleParseTree (node.unit, config) }) }
          break
        case 'rep_sampled':
          result = { type: 'rep_sampled',
                     n: node.n,
                     reps: node.reps.map (function (rep) { return pt.sampleParseTree (rep, config) }) }
          break
        case 'cond':
  	result = { type: 'cond',
                     test: node.test,
  		   t: pt.sampleParseTree (node.t, config),
                     f: pt.sampleParseTree (node.f, config) }
          break
        case 'func':
  	result = { type: 'func',
                     funcname: node.funcname,
  		   args: (node.funcname === 'strictquote'
                            ? node.args
                            : (node.funcname === 'quote'
                               ? pt.sampleParseTree (node.args, extend ({}, config, { quoteLevel: (config && config.quoteLevel || 0) + 1 }))
                               : pt.sampleParseTree (node.args, config))) }
          break
        case 'lookup':
  	result = node
          break
        default:
        case 'sym':
  	result = { type: 'sym' };
          ['name','id','method','user'].forEach (function (key) {
            if (typeof(node[key]) !== 'undefined')
              result[key] = node[key]
          })
          if (node.bind)
            result.bind = pt.sampleParseTree (node.bind, config)
  	break
        }
        if (node.footer)
          result.footer = node.footer
      }
      return result
    })
  }