docs: function main (composition) {
    const openwhisk = require('openwhisk')
    let wsk
  
    const isObject = obj => typeof obj === 'object' && obj !== null && !Array.isArray(obj)
  
    // compile ast to fsm
    const compiler = {
      sequence (parent, node) {
        return [{ parent, type: 'pass' }, ...compile(parent, ...node.components)]
      },
  
      action (parent, node) {
        return [{ parent, type: 'action', name: node.name }]
      },
  
      async (parent, node) {
        const body = [...compile(parent, ...node.components)]
        return [{ parent, type: 'async', return: body.length + 2 }, ...body, { parent, type: 'stop' }, { parent, type: 'pass' }]
      },
  
      function (parent, node) {
        return [{ parent, type: 'function', exec: node.function.exec }]
      },
  
      finally (parent, node) {
        const finalizer = compile(parent, node.finalizer)
        const fsm = [{ parent, type: 'try' }, ...compile(parent, node.body), { parent, type: 'exit' }, ...finalizer]
        fsm[0].catch = fsm.length - finalizer.length
        return fsm
      },
  
      let (parent, node) {
        return [{ parent, type: 'let', let: node.declarations }, ...compile(parent, ...node.components), { parent, type: 'exit' }]
      },
  
      mask (parent, node) {
        return [{ parent, type: 'let', let: null }, ...compile(parent, ...node.components), { parent, type: 'exit' }]
      },
  
      try (parent, node) {
        const handler = [...compile(parent, node.handler), { parent, type: 'pass' }]
        const fsm = [{ parent, type: 'try' }, ...compile(parent, node.body), { parent, type: 'exit' }, ...handler]
        fsm[0].catch = fsm.length - handler.length
        fsm[fsm.length - handler.length - 1].next = handler.length
        return fsm
      },
  
      if_nosave (parent, node) {
        const consequent = compile(parent, node.consequent)
        const alternate = [...compile(parent, node.alternate), { parent, type: 'pass' }]
        const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.test), { parent, type: 'choice', then: 1, else: consequent.length + 1 }, ...consequent, ...alternate]
        fsm[fsm.length - alternate.length - 1].next = alternate.length
        return fsm
      },
  
      while_nosave (parent, node) {
        const body = compile(parent, node.body)
        const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.test), { parent, type: 'choice', then: 1, else: body.length + 1 }, ...body, { parent, type: 'pass' }]
        fsm[fsm.length - 2].next = 2 - fsm.length
        return fsm
      },
  
      dowhile_nosave (parent, node) {
        const fsm = [{ parent, type: 'pass' }, ...compile(parent, node.body), ...compile(parent, node.test), { parent, type: 'choice', else: 1 }, { parent, type: 'pass' }]
        fsm[fsm.length - 2].then = 2 - fsm.length
        return fsm
      }
    }
  
    function compile (parent, node) {
      if (arguments.length === 1) return [{ parent, type: 'empty' }]
      if (arguments.length === 2) return Object.assign(compiler[node.type](node.path || parent, node), { path: node.path })
      return Array.prototype.slice.call(arguments, 1).reduce((fsm, node) => { fsm.push(...compile(parent, node)); return fsm }, [])
    }
  
    const fsm = compile('', composition)
  
    const conductor = {
      choice ({ p, node, index }) {
        p.s.state = index + (p.params.value ? node.then : node.else)
      },
  
      try ({ p, node, index }) {
        p.s.stack.unshift({ catch: index + node.catch })
      },
  
      let ({ p, node, index }) {
        p.s.stack.unshift({ let: JSON.parse(JSON.stringify(node.let)) })
      },
  
      exit ({ p, node, index }) {
        if (p.s.stack.length === 0) return internalError(`pop from an empty stack`)
        p.s.stack.shift()
      },
  
      action ({ p, node, index }) {
        return { method: 'action', action: node.name, params: p.params, state: { $resume: p.s } }
      },
  
      function ({ p, node, index }) {
        return Promise.resolve().then(() => run(node.exec.code, p))
          .catch(error => {
            console.error(error)
            return { error: `Function combinator threw an exception at AST node root${node.parent} (see log for details)` }
          })
          .then(result => {
            if (typeof result === 'function') result = { error: `Function combinator evaluated to a function type at AST node root${node.parent}` }
            // if a function has only side effects and no return value, return params
            p.params = JSON.parse(JSON.stringify(result === undefined ? p.params : result))
            inspect(p)
            return step(p)
          })
      },
  
      empty ({ p, node, index }) {
        inspect(p)
      },
  
      pass ({ p, node, index }) {
      },
  
      async ({ p, node, index, inspect, step }) {
        p.params.$resume = { state: p.s.state, stack: [{ marker: true }].concat(p.s.stack) }
        p.s.state = index + node.return
        if (!wsk) wsk = openwhisk({ ignore_certs: true })
        return wsk.actions.invoke({ name: process.env.__OW_ACTION_NAME, params: p.params })
          .then(response => ({ method: 'async', activationId: response.activationId, sessionId: p.s.session }), error => {
            console.error(error) // invoke failed
            return { error: `Async combinator failed to invoke composition at AST node root${node.parent} (see log for details)` }
          })
          .then(result => {
            p.params = result
            inspect(p)
            return step(p)
          })
      },
  
      stop ({ p, node, index, inspect, step }) {
        p.s.state = -1
      }
    }
  
    function finish (p) {
      return p.params.error ? p.params : { params: p.params }
    }
  
    const internalError = error => Promise.reject(error) // terminate composition execution and record error
  
    // wrap params if not a dictionary, branch to error handler if error
    function inspect (p) {
      if (!isObject(p.params)) p.params = { value: p.params }
      if (p.params.error !== undefined) {
        p.params = { error: p.params.error } // discard all fields but the error field
        p.s.state = -1 // abort unless there is a handler in the stack
        while (p.s.stack.length > 0 && !p.s.stack[0].marker) {
          if ((p.s.state = p.s.stack.shift().catch || -1) >= 0) break
        }
      }
    }
  
    // run function f on current stack
    function run (f, p) {
      // handle let/mask pairs
      const view = []
      let n = 0
      for (let frame of p.s.stack) {
        if (frame.let === null) {
          n++
        } else if (frame.let !== undefined) {
          if (n === 0) {
            view.push(frame)
          } else {
            n--
          }
        }
      }
  
      // update value of topmost matching symbol on stack if any
      function set (symbol, value) {
        const element = view.find(element => element.let !== undefined && element.let[symbol] !== undefined)
        if (element !== undefined) element.let[symbol] = JSON.parse(JSON.stringify(value))
      }
  
      // collapse stack for invocation
      const env = view.reduceRight((acc, cur) => cur.let ? Object.assign(acc, cur.let) : acc, {})
      let main = '(function(){try{const require=arguments[2];'
      for (const name in env) main += `var ${name}=arguments[1]['${name}'];`
      main += `return eval((function(){return(${f})})())(arguments[0])}finally{`
      for (const name in env) main += `arguments[1]['${name}']=${name};`
      main += '}})'
      try {
        return (1, eval)(main)(p.params, env, require)
      } finally {
        for (const name in env) set(name, env[name])
      }
    }
  
    function step (p) {
      // final state, return composition result
      if (p.s.state < 0 || p.s.state >= fsm.length) {
        console.log(`Entering final state`)
        console.log(JSON.stringify(p.params))
        return
      }
  
      // process one state
      const node = fsm[p.s.state] // json definition for index state
      if (node.path !== undefined) console.log(`Entering composition${node.path}`)
      const index = p.s.state // current state
      p.s.state = p.s.state + (node.next || 1) // default next state
      if (typeof conductor[node.type] !== 'function') return internalError(`unexpected "${node.type}" combinator`)
      return conductor[node.type]({ p, index, node, inspect, step }) || step(p)
    }
  
    // do invocation
    return (params) => {
      // extract parameters
      const $resume = params.$resume || {}
      delete params.$resume
      $resume.session = $resume.session || process.env.__OW_ACTIVATION_ID
  
      // current state
      const p = { s: Object.assign({ state: 0, stack: [], resuming: true }, $resume), params }
  
      // step and catch all errors
      return Promise.resolve().then(() => {
        if (typeof p.s.state !== 'number') return internalError('state parameter is not a number')
        if (!Array.isArray(p.s.stack)) return internalError('stack parameter is not an array')
  
        if ($resume.resuming) inspect(p) // handle error objects when resuming
  
        return step(p)
      }).catch(error => {
        const message = (typeof error.error === 'string' && error.error) || error.message || (typeof error === 'string' && error)
        p.params = { error: message ? `Internal error: ${message}` : 'Internal error' }
      }).then(params => params || finish(p)) // params is defined iff execution will be resumed
    }
  }