trufflesuite/truffle

View on GitHub
packages/debugger/lib/data/selectors/index.js

Summary

Maintainability
F
1 wk
Test Coverage
import debugModule from "debug";
const debug = debugModule("debugger:data:selectors");

import { createSelectorTree, createLeaf } from "reselect-tree";
import jsonpointer from "json-pointer";
import merge from "lodash/merge";
import semver from "semver";

import {
  stableKeccak256,
  makePath,
  topLevelNodeTypes,
  isTopLevelNode,
  peelAwayPotentialEVMNoOp
} from "lib/helpers";

import trace from "lib/trace/selectors";
import evm from "lib/evm/selectors";
import sourcemapping from "lib/sourcemapping/selectors";
import stacktrace from "lib/stacktrace/selectors";

import * as Codec from "@truffle/codec";

/**
 * @private
 */
const identity = x => x;

function solidityVersionHasNoNow(compiler) {
  return (
    compiler &&
    compiler.name === "solc" &&
    //want to include prerelease versions of 0.7.0
    semver.satisfies(compiler.version, "~0.7 || >=0.7.0", {
      includePrerelease: true
    })
  );
}

function findAncestorOfType(node, types, scopes, pointer = null, root = null) {
  //note: you may want to include "SourceUnit" and "YulObject" as fallback types when using
  //this function for convenience.
  //you only need to pass pointer and root if you want this function to work
  //from Yul.  Otherwise you can omit those and you'll get null if you happen
  //to be in Yul.
  while (node && !types.includes(node.nodeType)) {
    if (node.id !== undefined) {
      node = scopes[scopes[node.id].parentId].definition;
    } else {
      if (pointer === null || root === null || pointer === "") {
        //if we're trying to go up from the root but are still in Yul,
        //or if we weren't given pointer and root at all,
        //admit failure and return null
        return null;
      }
      pointer = pointer.replace(/\/[^/]*$/, ""); //chop off end
      node = jsonpointer.get(root, pointer);
    }
  }
  return node;
}

//given a modifier invocation (or inheritance specifier) node,
//get the node for the actual modifier (or constructor)
function modifierForInvocation(invocation, scopes) {
  let rawId; //raw referencedDeclaration ID extracted from the AST.
  //if it's a modifier this is what we want, but if it's base
  //constructor, we'll get the contract instead, and need to find its
  //constructor.
  switch (invocation.nodeType) {
    case "ModifierInvocation":
      rawId = invocation.modifierName.referencedDeclaration;
      break;
    case "InheritanceSpecifier":
      rawId = invocation.baseName.referencedDeclaration;
      break;
    default:
      debug("bad invocation node");
  }
  let rawNode = scopes[rawId].definition;
  switch (rawNode.nodeType) {
    case "ModifierDefinition":
      return rawNode;
    case "ContractDefinition":
      return rawNode.nodes.find(
        node =>
          node.nodeType === "FunctionDefinition" &&
          Codec.Ast.Utils.functionKind(node) === "constructor"
      );
    default:
      //we should never hit this case
      return undefined;
  }
}

//see data.views.contexts for an explanation
function debuggerContextToDecoderContext(context) {
  let {
    context: contextHash,
    contractName,
    binary,
    contractId,
    contractKind,
    isConstructor,
    abi,
    payable,
    compiler,
    compilationId
  } = context;
  return {
    context: contextHash,
    contractName,
    binary,
    contractId,
    contractKind,
    isConstructor,
    abi: Codec.AbiData.Utils.computeSelectors(abi),
    fallbackAbi: {
      fallback: (abi || []).find(item => item.type === "fallback") || null,
      receive: (abi || []).find(item => item.type === "receive") || null
    },
    payable,
    compiler,
    compilationId
  };
}

//spoofed definitions we'll need
//we'll give them id -1 to indicate that they're spoofed

export const NOW_DEFINITION = {
  id: -1,
  src: "0:0:-1",
  name: "now",
  nodeType: "VariableDeclaration",
  typeDescriptions: {
    typeIdentifier: "t_uint256",
    typeString: "uint256"
  }
};

export const MSG_DEFINITION = {
  id: -1,
  src: "0:0:-1",
  name: "msg",
  nodeType: "VariableDeclaration",
  typeDescriptions: {
    typeIdentifier: "t_magic_message",
    typeString: "msg"
  }
};

export const TX_DEFINITION = {
  id: -1,
  src: "0:0:-1",
  name: "tx",
  nodeType: "VariableDeclaration",
  typeDescriptions: {
    typeIdentifier: "t_magic_transaction",
    typeString: "tx"
  }
};

export const BLOCK_DEFINITION = {
  id: -1,
  src: "0:0:-1",
  name: "block",
  nodeType: "VariableDeclaration",
  typeDescriptions: {
    typeIdentifier: "t_magic_block",
    typeString: "block"
  }
};

function spoofThisDefinition(contractName, contractId, contractKind) {
  let formattedName = contractName.replace(/\$/g, "$$".repeat(3));
  //note that string.replace treats $'s specially in the replacement string;
  //we want 3 $'s for each $ in the input, so we need to put *6* $'s in the
  //replacement string
  return {
    id: -1,
    src: "0:0:-1",
    name: "this",
    nodeType: "VariableDeclaration",
    typeDescriptions: {
      typeIdentifier: "t_contract$_" + formattedName + "_$" + contractId,
      typeString: contractKind + " " + contractName
    }
  };
}

const data = createSelectorTree({
  state: state => state.data,

  /**
   * data.views
   */
  views: {
    /**
     * data.views.atLastInstructionForSourceRange
     */
    atLastInstructionForSourceRange: createLeaf(
      [sourcemapping.current.isSourceRangeFinal],
      final => final
    ),

    /**
     * data.views.scopes (namespace)
     */
    scopes: {
      /**
       * data.views.scopes (selector)
       * the raw scopes data, just with intermediate
       * layers cut out
       * (no inheritance, no inlining)
       */
      _: createLeaf(["/info/scopes"], scopes =>
        Object.assign(
          {},
          ...Object.entries(scopes).map(([sourceId, { byAstRef: nodes }]) => ({
            [sourceId]: nodes
          }))
        )
      ),

      /**
       * data.views.scopes.inlined
       * inlines, but still no inheritance data
       */
      inlined: createLeaf(
        ["./_", sourcemapping.views.sources],

        (scopes, sources) =>
          Object.assign(
            {},
            ...Object.entries(scopes).map(([sourceId, nodes]) => ({
              [sourceId]: Object.assign(
                {},
                ...Object.entries(nodes).map(([astRef, scope]) => ({
                  [astRef]: {
                    ...scope,
                    definition: jsonpointer.get(
                      sources[scope.sourceId].ast,
                      scope.pointer
                    )
                  }
                }))
              )
            }))
          )
      )
    },

    /**
     * data.views.userDefinedTypesByCompilation
     */
    userDefinedTypesByCompilation: createLeaf(
      [
        "/info/userDefinedTypes",
        "./referenceDeclarations",
        "./scopes/inlined",
        sourcemapping.views.sources
      ],
      (userDefinedTypes, referenceDeclarations, scopes, sources) => {
        let typesByCompilation = {};
        for (const { sourceId, id } of userDefinedTypes) {
          const node = scopes[sourceId][id].definition;
          const { compilationId, compiler, internal } = sources[sourceId];
          if (internal) {
            continue; //just to be sure, we assume generated sources don't define types
          }
          const type = Codec.Ast.Import.definitionToStoredType(
            node,
            compilationId,
            compiler,
            referenceDeclarations[compilationId]
          );
          if (!typesByCompilation[compilationId]) {
            typesByCompilation[compilationId] = {
              compiler,
              types: {}
            };
          }
          typesByCompilation[compilationId].types[type.id] = type;
        }
        return typesByCompilation;
      }
    ),

    /**
     * data.views.userDefinedTypes
     * user-defined types for passing to the decoder
     * NOTE: *not* grouped by compilation or anything, this is flat
     */
    userDefinedTypes: createLeaf(
      ["./userDefinedTypesByCompilation"],
      Codec.Format.Types.forgetCompilations
    ),

    /**
     * data.views.contractAllocationInfo
     */
    contractAllocationInfo: createLeaf(
      [
        "/info/userDefinedTypes",
        "/views/scopes/inlined",
        "/info/contracts",
        sourcemapping.views.sources,
        evm.info.contexts
      ],
      (userDefinedTypes, scopes, contracts, sources, contexts) =>
        Object.values(userDefinedTypes)
          .filter(
            ({ sourceId, id }) =>
              !sources[sourceId].internal && //again, assuming internal sources don't define contracts
              scopes[sourceId][id].definition.nodeType === "ContractDefinition"
          )
          .map(({ sourceId, id }) => {
            debug("id: %O", id);
            const compilationId = sources[sourceId].compilationId;
            debug("compilationId: %O", compilationId);
            const contract = contracts[compilationId].byAstId[id];
            const deployedContext = contexts[contract.deployedContext];
            const constructorContext = contexts[contract.constructorContext];
            const immutableReferences = (deployedContext || {})
              .immutableReferences;
            return {
              contractNode: scopes[sourceId][id].definition,
              compilationId,
              immutableReferences,
              compiler: sources[sourceId].compiler,
              abi: contract.abi,
              deployedContext,
              constructorContext
            };
          })
    ),

    /**
     * data.views.referenceDeclarations
     * grouped by compilation because that's how codec wants it;
     * for simplicity, we will assume that generated sources never define types!
     */
    referenceDeclarations: createLeaf(
      [
        "./scopes/inlined",
        "/info/userDefinedTypes",
        "/info/taggedOutputs",
        sourcemapping.views.sources
      ],
      (scopes, userDefinedTypes, taggedOutputs, sources) =>
        merge(
          {},
          ...userDefinedTypes.concat(taggedOutputs).map(({ id, sourceId }) => {
            const source = sources[sourceId];
            return source.internal
              ? {} //exclude these
              : {
                  [source.compilationId]: {
                    [id]: scopes[sourceId][id].definition
                  }
                };
          })
        )
    ),

    /**
     * data.views.mappingKeys
     */
    mappingKeys: createLeaf(
      ["/proc/mappedPaths", "/current/address"],
      (mappedPaths, address) =>
        []
          .concat(
            ...Object.values(
              (mappedPaths.byAddress[address] || { byType: {} }).byType
            ).map(({ bySlotAddress }) => Object.values(bySlotAddress))
          )
          .filter(slot => slot.key !== undefined)
    ),

    /**
     * data.views.blockNumber
     * returns block number as string
     */
    blockNumber: createLeaf([evm.transaction.globals.block], block =>
      block.number.toString()
    ),

    /**
     * data.views.blockHash
     */
    blockHash: createLeaf([evm.transaction.blockHash], identity),

    /**
     * data.views.txIndex
     */
    txIndex: createLeaf([evm.transaction.txIndex], identity),

    /**
     * data.views.instances
     * same as evm.current.codex.instances, but we just map address => binary,
     * we don't bother with context, and also the code is a Uint8Array
     */
    instances: createLeaf([evm.current.codex.instances], instances =>
      Object.assign(
        {},
        ...Object.entries(instances).map(([address, { binary }]) => ({
          [address]: Codec.Conversion.toBytes(binary)
        }))
      )
    ),

    /**
     * data.views.contexts
     * same as evm.info.contexts, but:
     * 1. we strip out fields irrelevant to codec
     * 2. we alter abi in a few ways ways:
     * 2a. we strip out everything but functions
     * 2b. abi is now an object, not an array, and indexed by these signatures
     * 2c. fallback/receive stuff instead goes in the fallbackAbi field
     */
    contexts: createLeaf([evm.info.contexts], contexts =>
      Object.assign(
        {},
        ...Object.values(contexts).map(context => ({
          [context.context]: debuggerContextToDecoderContext(context)
        }))
      )
    )
  },

  /**
   * data.info
   */
  info: {
    /**
     * data.info.scopes
     */
    scopes: createLeaf(["/state"], state => state.info.scopes.bySourceId),

    /**
     * data.info.contracts
     */
    contracts: createLeaf(
      ["/state"],
      state => state.info.contracts.byCompilationId
    ),

    /**
     * data.info.allocations
     */
    allocations: {
      /**
       * data.info.allocations.storage
       */
      storage: createLeaf(["/state"], state => state.info.allocations.storage),

      /**
       * data.info.allocations.state
       */
      state: createLeaf(["/state"], state => state.info.allocations.state),

      /**
       * data.info.allocations.memory
       */
      memory: createLeaf(["/state"], state => state.info.allocations.memory),

      /**
       * data.info.allocations.abi
       */
      abi: createLeaf(["/state"], state => state.info.allocations.abi),

      /**
       * data.info.allocations.calldata
       */
      calldata: createLeaf(
        ["/state"],
        state => state.info.allocations.calldata
      ),

      /**
       * data.info.allocations.returndata
       */
      returndata: createLeaf(
        ["/state"],
        state => state.info.allocations.returndata
      ),

      /**
       * data.info.allocations.event
       */
      event: createLeaf(["/state"], state => state.info.allocations.event)
    },

    /**
     * data.info.userDefinedTypes
     */
    userDefinedTypes: createLeaf(
      ["/state"],
      state => state.info.userDefinedTypes
    ),

    /**
     * data.info.taggedOutputs
     * "Tagged outputs" means user-defined things that are output by a contract
     * (not input to a contract), and which are distinguished by (potentially
     * ambiguous) selectors.  So, events and custom errors are tagged outputs.
     * Function arguments are not tagged outputs (they're not outputs).
     * Return values are not tagged outputs (they don't have a selector).
     * Built-in errors (Error(string) and Panic(uint))... OK I guess those could
     * be considered tagged outputs, but we're only looking at user-defined ones
     * here.
     */
    taggedOutputs: createLeaf(["/state"], state => state.info.taggedOutputs)
  },

  /**
   * data.proc
   */
  proc: {
    /**
     * data.proc.assignments
     */
    assignments: createLeaf(["/state"], state => state.proc.assignments.byId),

    /**
     * data.proc.mappedPaths
     */
    mappedPaths: createLeaf(["/state"], state => state.proc.mappedPaths)
  },

  /**
   * data.current
   */
  current: {
    /**
     * data.current.state
     */
    state: {
      /**
       * data.current.state.stack
       */
      stack: createLeaf(
        [evm.current.state.stack],

        words => (words || []).map(word => Codec.Conversion.toBytes(word))
      ),

      /**
       * data.current.state.memory
       */
      memory: createLeaf(
        [evm.current.state.memory],

        words => Codec.Conversion.toBytes(words.join(""))
      ),

      /**
       * data.current.state.code
       */
      code: createLeaf([evm.current.context], ({ binary }) =>
        Codec.Conversion.toBytes(binary)
      ),

      /**
       * data.current.state.calldata
       */
      calldata: createLeaf(
        [evm.current.call],

        ({ data }) => Codec.Conversion.toBytes(data)
      ),

      /**
       * data.current.state.eventdata
       * usually undefined; used for log decoding
       */
      eventdata: createLeaf([evm.current.step.logData], data =>
        data !== null ? Codec.Conversion.toBytes(data) : undefined
      ),

      /**
       * data.current.state.eventtopics
       * usually undefined; used for log decoding
       */
      eventtopics: createLeaf([evm.current.step.logTopics], words =>
        words !== null
          ? words.map(word => Codec.Conversion.toBytes(word))
          : undefined
      ),

      /**
       * data.current.state.storage
       */
      storage: createLeaf(
        [evm.current.codex.storage],

        mapping =>
          Object.assign(
            {},
            ...Object.entries(mapping).map(([address, word]) => ({
              [`0x${address}`]: Codec.Conversion.toBytes(word)
            }))
          )
      ),

      /**
       * data.current.state.specials
       * I've named these after the solidity variables they correspond to,
       * which are *mostly* the same as the corresponding EVM opcodes
       * (FWIW: this = ADDRESS, sender = CALLER, value = CALLVALUE)
       */
      specials: createLeaf(
        ["/current/address", evm.current.call, evm.transaction.globals],
        (address, { sender, value }, { tx, block }) => ({
          this: Codec.Conversion.toBytes(address),

          sender: Codec.Conversion.toBytes(sender),

          value: Codec.Conversion.toBytes(value),

          //let's crack open that tx and block!
          ...Object.assign(
            {},
            ...Object.entries(tx).map(([variable, value]) => ({
              [variable]: Codec.Conversion.toBytes(value)
            }))
          ),

          ...Object.assign(
            {},
            ...Object.entries(block).map(([variable, value]) => ({
              [variable]: Codec.Conversion.toBytes(value)
            }))
          )
        })
      )
    },

    /**
     * data.current.compilationId
     */
    compilationId: createLeaf(
      [evm.current.context],
      context => context?.compilationId
    ),

    /**
     * data.current.sourceIndex
     */
    sourceIndex: createLeaf(
      [sourcemapping.current.source],
      ({ index }) => index
    ),

    /**
     * data.current.language
     */
    language: createLeaf(
      [sourcemapping.current.source],
      ({ language }) => language
    ),

    /**
     * data.current.internalSourceFor
     * returns null if in a user source
     */
    internalSourceFor: createLeaf(
      [sourcemapping.current.source],
      ({ internalFor }) => internalFor || null
    ),

    /**
     * data.current.root
     */
    root: createLeaf([sourcemapping.current.source], ({ ast }) => ast),

    /**
     * data.current.scopes (namespace)
     */
    scopes: {
      /**
       * data.current.scopes (selector)
       * Replacement for the old data.info.scopes;
       * that one now contains multi-compilation/context info, this
       * one contains only the current compilation/context
       */
      _: createLeaf(["./raw", "./inlined/raw"], (scopes, inlined) =>
        scopes && inlined
          ? Object.assign(
              {},
              ...Object.entries(scopes).map(([id, scope]) => {
                let definition = inlined[id].definition;
                if (definition.nodeType === "ContractDefinition") {
                  //contract definition case: process inheritance
                  debug("contract id %d", id);
                  let newScope = { ...scope };
                  //note that Solidity gives us the linearization in order from most
                  //derived to most base, but we want most base to most derived;
                  //annoyingly, reverse() is in-place, so we clone with slice() first
                  const linearizedBaseContractsFromBase =
                    definition.linearizedBaseContracts.slice().reverse();
                  linearizedBaseContractsFromBase.pop(); //remove the last element, i.e.,
                  //the contract itself, because we want to treat that one specially
                  //now, we put it all together
                  newScope.variables = []
                    .concat(
                      //concatenate the variables lists from the base classes
                      ...linearizedBaseContractsFromBase.map(
                        contractId => scopes[contractId].variables || []
                        //we need the || [] because contracts with no state variables
                        //have variables undefined rather than empty like you'd expect
                      )
                    )
                    .filter(
                      variable =>
                        inlined[variable.astRef].definition.visibility !==
                        "private"
                      //filter out private variables from the base classes
                    )
                    //add in the variables for the contract itself -- note that here
                    //private variables are not filtered out!
                    .concat(scopes[id].variables || [])
                    .filter(variable => {
                      //HACK: let's filter out those constants we don't know
                      //how to read.  they'll just clutter things up.
                      debug("variable %O", variable);
                      const definition = inlined[variable.astRef].definition;
                      return (
                        !(
                          definition.constant ||
                          definition.mutability === "constant"
                        ) || Codec.Ast.Utils.isSimpleConstant(definition.value)
                      );
                    });
                  return { [id]: newScope };
                } else if (definition.nodeType === "SourceUnit") {
                  //source unit case: process imports
                  let newScope = { ...scope };
                  //in this case, handling imports in some sort of tree fashion would
                  //be too much work.  we'll do this the easy way: by checking exported
                  //symbols for constants.
                  newScope.variables = Object.values(definition.exportedSymbols)
                    .map(
                      array => array[0] //I don't know why these are arrays...?
                    )
                    .filter(astRef => {
                      //restrict to variables, not other exported symbols!
                      const definition = inlined[astRef].definition;
                      return (
                        definition.nodeType === "VariableDeclaration" &&
                        (definition.constant ||
                          definition.mutability === "constant") &&
                        //HACK: we'll also again filter out constants we don't know how
                        //to read
                        Codec.Ast.Utils.isSimpleConstant(definition.value)
                      );
                    })
                    .map(astRef => ({
                      //we'll have to reconstruct the rest from just the astRef
                      astRef,
                      name: inlined[astRef].definition.name,
                      sourceId: inlined[astRef].sourceId
                    }));
                  return { [id]: newScope };
                } else {
                  //default case, nothing to process
                  return { [id]: scope };
                }
              })
            )
          : null
      ),

      /**
       * data.current.scopes.raw
       * Current scopes, with inheritance not handled and no inlining
       */
      raw: createLeaf(
        ["/views/scopes", sourcemapping.current.sourceIds],
        (scopes, sourceIds) =>
          sourceIds
            ? Object.assign({}, ...sourceIds.map(sourceId => scopes[sourceId]))
            : null
      ),

      /**
       * data.current.scopes.inlined (namespace)
       */
      inlined: {
        /**
         * data.current.scopes.inlined (selector)
         * Replacement for the old data.views.scopes.inlined;
         * that one now contains multi-compilation info, this
         * one contains only the current compilation/context
         *
         * Returns null if nothing loaded
         */
        _: createLeaf(["./raw", "../_"], (inlined, scopes) =>
          inlined
            ? Object.assign(
                {},
                ...Object.entries(inlined).map(([astRef, info]) => ({
                  [astRef]: {
                    ...info,
                    variables: scopes[astRef].variables
                  }
                }))
              )
            : null
        ),

        /**
         * data.current.scopes.inlined.raw
         * inlines definitions but does not account for inheritance
         */
        raw: createLeaf(
          ["/views/scopes/inlined", sourcemapping.current.sourceIds],
          (scopes, sourceIds) =>
            sourceIds
              ? Object.assign(
                  {},
                  ...sourceIds.map(sourceId => scopes[sourceId])
                )
              : null
        )
      }
    },

    /**
     * data.current.referenceDeclarations
     */
    referenceDeclarations: createLeaf(
      ["/views/referenceDeclarations", "./compilationId"],
      (scopes, compilationId) => scopes[compilationId]
    ),

    /**
     * data.current.allocations
     */
    allocations: {
      /**
       * data.current.allocations.state
       * Same as data.info.allocations.state, but uses the old allocation
       * format (more convenient for debugger) where members are stored by ID
       * in an object instead of by index in an array; also only holds things
       * from the current compilation
       * ALSO: if we're in a constructor, replaces all code pointers by appropriate
       * memory pointers :)
       */
      state: createLeaf(
        ["/info/allocations/state", "../compilationId", evm.current.context],
        (allAllocations, compilationId, { isConstructor }) => {
          debug("compilationId: %s", compilationId);
          debug("allAllocations: %o", allAllocations);
          const allocations = compilationId
            ? allAllocations[compilationId]
            : {};
          //several-deep clone
          let transformedAllocations = Object.assign(
            {},
            ...Object.entries(allocations).map(([id, allocation]) => ({
              [id]: {
                members: allocation.members.map(member => ({ ...member }))
              }
            }))
          );
          //if we're not in a constructor, we don't need to actually transform it.
          //if we are...
          if (isConstructor) {
            //...we must transform code pointers!
            for (const id in transformedAllocations) {
              const allocation = transformedAllocations[id];
              //here, the magic number 4 is the number of reserved memory slots
              //at the start of memory.  immutables go immediately afterward.
              let start = 4 * Codec.Evm.Utils.WORD_SIZE;
              for (const member of allocation.members) {
                //if it's not a code pointer, leave it alone
                if (
                  member.pointer.location === "code" ||
                  member.pointer.location === "nowhere"
                ) {
                  //if it is, transform it
                  member.pointer = {
                    location: "memory",
                    start,
                    length: Codec.Evm.Utils.WORD_SIZE
                  };
                  start += Codec.Evm.Utils.WORD_SIZE;
                }
              }
            }
          }
          //having now transformed code pointers if needed,
          //we now index by ID
          return Object.assign(
            {},
            ...Object.entries(transformedAllocations).map(
              ([id, allocation]) => ({
                [id]: {
                  members: Object.assign(
                    {},
                    ...allocation.members.map(memberAllocation => ({
                      [memberAllocation.definition.id]: memberAllocation
                    }))
                  )
                }
              })
            )
          );
        }
      )
    },

    /**
     * data.current.compiler
     */
    compiler: createLeaf([evm.current.context], context => context?.compiler),

    /**
     * data.current.bareLetsInYulAreHit
     */
    bareLetsInYulAreHit: createLeaf(
      ["./compiler"],
      compiler =>
        compiler !== undefined && //if no compiler we'll assume the old way I guess??
        compiler.name === "solc" &&
        semver.satisfies(compiler.version, ">=0.6.8", {
          includePrerelease: true
        })
    ),

    /**
     * data.current.node
     */
    node: createLeaf([sourcemapping.current.node], identity),

    /**
     * data.current.pointer
     */
    pointer: createLeaf([sourcemapping.current.pointer], identity),

    /**
     * data.current.astRef
     * returns null when not in a mapped source
     */
    astRef: createLeaf(
      [
        sourcemapping.current.node,
        sourcemapping.current.pointer,
        "./sourceIndex"
      ],
      (node, pointer, sourceIndex) =>
        node
          ? node.id !== undefined
            ? node.id
            : makePath(sourceIndex, pointer)
          : null
    ),

    /**
     * data.current.scope
     * old alias for data.current.node (deprecated)
     */
    scope: createLeaf(["./node"], identity),

    /**
     * data.current.contract
     * warning: may return null or similar, even though SourceUnit and YulObject are included
     * as fallbacks
     */
    contract: createLeaf(
      ["./node", "./scopes/inlined", "./pointer", "./root"],
      (node, scopes, pointer, root) => {
        const types = ["ContractDefinition", ...topLevelNodeTypes];
        return findAncestorOfType(node, types, scopes, pointer, root);
      }
    ),

    /**
     * data.current.contractForBytecode
     * contract node for the executing bytecode -- *not* the current position!
     * probably not what you usually want
     */
    contractForBytecode: createLeaf(
      [evm.current.context, "./scopes/inlined"],
      ({ contractId }, scopes) =>
        (scopes[contractId] || { definition: null }).definition
    ),

    /**
     * data.current.fallbackOutputForContext
     * returns null if none
     */
    fallbackOutputForContext: createLeaf(
      ["./contractForBytecode"],
      contract => {
        if (!contract) {
          return null;
        }
        const fallbackDefinition = contract.nodes.find(
          node =>
            node.nodeType === "FunctionDefinition" &&
            Codec.Ast.Utils.functionKind(node) === "fallback"
        );
        if (!fallbackDefinition) {
          return null;
        }
        return fallbackDefinition.returnParameters.parameters[0] || null;
      }
    ),

    /**
     * data.current.function
     * may be modifier rather than function!
     */
    function: createLeaf(
      ["./node", "./scopes/inlined", "./pointer", "./root"],
      (node, scopes, pointer, root) => {
        const types = [
          "FunctionDefinition",
          "ModifierDefinition",
          "ContractDefinition",
          ...topLevelNodeTypes
        ];
        return findAncestorOfType(node, types, scopes, pointer, root);
      }
    ),

    /**
     * data.current.inModifier
     */
    inModifier: createLeaf(
      ["./function"],
      node => node && node.nodeType === "ModifierDefinition"
    ),

    /**
     * data.current.inFunctionOrModifier
     */
    inFunctionOrModifier: createLeaf(
      ["./function"],
      node =>
        node &&
        (node.nodeType === "FunctionDefinition" ||
          node.nodeType === "ModifierDefinition")
    ),

    /**
     * data.current.functionDepth
     */

    functionDepth: createLeaf([sourcemapping.current.functionDepth], identity),

    /**
     * data.current.modifierDepth
     */

    modifierDepth: createLeaf([sourcemapping.current.modifierDepth], identity),

    /**
     * data.current.address
     * NOTE: this is the STORAGE address for the current call, not the CODE
     * address
     */

    address: createLeaf([evm.current.call], call => call.storageAddress),

    /**
     * data.current.functionsByProgramCounter
     */
    functionsByProgramCounter: createLeaf(
      [sourcemapping.current.functionsByProgramCounter],
      functions => functions
    ),

    /**
     * data.current.internalFunctionsTable
     */
    internalFunctionsTable: createLeaf(
      [evm.current.isIR, "./functionsByProgramCounter", "./functionsByIndex"],
      //for Solidity compiled with IR turned on, internal function pointers
      //are encoded in terms of an index rather than a PC value.
      (isIR, byPC, byIndex) => (isIR ? byIndex : byPC)
    ),

    /**
     * data.current.internalFunctionsTableKind
     */
    internalFunctionsTableKind: createLeaf([evm.current.isIR], isIR =>
      isIR ? "index" : "pcpair"
    ),

    /**
     * data.current.functionsByIndex
     */
    functionsByIndex: createLeaf(
      [evm.current.context, "./scopes/inlined"],
      ({ compilationId, contractId }, scopes) => {
        if (contractId === undefined) {
          return undefined;
        }
        const contractNode = scopes[contractId].definition;
        if (!contractNode.internalFunctionIDs) {
          //note that we could end up in this branch either because the version is <0.8.20,
          //or because the particular contract doesn't use internal function pointers.
          //in the former case, then, oh well, we can't decode.
          //in the latter case, well, it doesn't really matter what we return, does it?
          return undefined;
        }
        return Object.assign(
          //we start with the entry for the designated invalid function in index 0.
          //all other functions should have index 1 or greater.
          [{ isDesignatedInvalid: true }],
          ...Object.entries(contractNode.internalFunctionIDs).map(
            ([nodeId, index]) => {
              nodeId = Number(nodeId); //nodeId is a string initially, so let's make it a number
              let {
                definition: node,
                pointer,
                parentId: contractId
              } = scopes[nodeId];
              let { definition: contractNode, pointer: contractPointer } =
                scopes[contractId];
              if (contractNode.nodeType !== "ContractDefinition") {
                //i.e., if this is a free function; in this case the nodeType
                //of the parent should equal "SourceUnit"
                contractNode = null;
                contractId = null;
                contractPointer = null;
              }
              return {
                [index]: {
                  isDesignatedInvalid: false,
                  sourceIndex: Number(node.src.split(":")[2]), //to get the source index, we
                  //parse the node's source range, which has the form start:length:file
                  compilationId,
                  pointer,
                  node,
                  name: node.name,
                  id: nodeId,
                  mutability: Codec.Ast.Utils.mutability(node),
                  contractPointer,
                  contractNode,
                  contractName: contractNode ? contractNode.name : null,
                  contractId,
                  contractKind: contractNode ? contractNode.contractKind : null,
                  contractPayable: contractNode
                    ? Codec.Ast.Utils.isContractPayable(contractNode)
                    : null
                }
              };
            }
          )
        );
      }
    ),

    /**
     * data.current.context
     */
    context: createLeaf([evm.current.context], debuggerContextToDecoderContext),

    /**
     * data.current.fallbackBase
     * gives the stack position where a fallback input would start
     * this is 0 if there are no public or external functions, and 1 if there are
     */
    fallbackBase: createLeaf(
      ["./context"],
      ({ abi }) => (Object.keys(abi).length > 0 ? 1 : 0)
      //note ABI here has been transformed to include functions only
    ),

    /**
     * data.current.errorLocation
     * note: we can't get the actual node from stacktrace,
     * it doesn't store that
     */
    errorLocation: createLeaf(
      [stacktrace.current.innerReturnPosition, stacktrace.current.lastPosition],
      (innerLocation, lastLocation) => innerLocation || lastLocation || {}
    ),

    /**
     * data.current.errorNode
     * note: we can't get the actual node from stacktrace,
     * it only stores the ID
     */
    errorNode: createLeaf(
      ["./errorLocation", "/views/scopes/inlined"],
      (errorLocation, scopes) => {
        const sourceId = (errorLocation.source || {}).id;
        const astId = (errorLocation.node || {}).id;
        if (sourceId !== undefined && astId !== undefined) {
          return scopes[sourceId][astId].definition;
        } else {
          return null;
        }
      }
    ),

    /**
     * data.current.errorId
     * returns a codec-style ID, not just an AST ID
     * does not assume that the error is on the correct node...
     * this could be factored into two selectors (one that finds
     * the node and one that makes the ID)
     */
    errorId: createLeaf(
      ["./errorNode", "./compilationId"],
      (errorNode, compilationId) => {
        if (errorNode === null) {
          return undefined;
        }
        switch (errorNode.nodeType) {
          case "RevertStatement":
            //I don't think this case should happen, but I'm including it
            //for extra certainty
            errorNode = errorNode.errorCall;
          //DELIBERATE FALL-THROUGH
          case "FunctionCall":
            if (
              Codec.Ast.Utils.functionClass(errorNode.expression) !== "error"
            ) {
              return undefined;
            }
            //this should work for both qualified & unqualified errors
            const errorId = errorNode.expression.referencedDeclaration;
            return Codec.Contexts.Import.makeTypeId(errorId, compilationId);
          default:
            //I'm not going to try to handle other cases that maybe could
            //occur with the optimizer on
            return undefined;
        }
      }
    ),

    /**
     * data.current.eventId
     * similar to errorId but for events
     * (and unlike errorId it can just use the current node!)
     */
    eventId: createLeaf(
      ["./node", "./compilationId"],
      (eventNode, compilationId) => {
        if (!eventNode) {
          return undefined;
        }
        switch (eventNode.nodeType) {
          case "EmitStatement":
            //I don't think this case should happen, but I'm including it
            //for extra certainty
            eventNode = eventNode.eventCall;
          //DELIBERATE FALL-THROUGH
          case "FunctionCall":
            if (
              Codec.Ast.Utils.functionClass(eventNode.expression) !== "event"
            ) {
              return undefined;
            }
            //this should work for both qualified & unqualified errors
            const eventId = eventNode.expression.referencedDeclaration;
            return Codec.Contexts.Import.makeTypeId(eventId, compilationId);
          default:
            //I'm not going to try to handle other cases that maybe could
            //occur with the optimizer on
            return undefined;
        }
      }
    ),

    /**
     * data.current.aboutToModify
     * HACK
     * This selector is used to catch those times when we go straight from a
     * modifier invocation into the modifier itself, skipping over the
     * definition node (this includes base constructor calls).  So it should
     * return true when:
     * 1. we're on the node corresponding to an argument to a modifier
     * invocation or base constructor call, or, if said argument is a type
     * conversion, its argument (or nested argument)
     * 2. the next node is not a FunctionDefinition, ModifierDefinition, or
     * in the same modifier / base constructor invocation
     */
    aboutToModify: createLeaf(
      [
        "./node",
        "./modifierInvocation",
        "./modifierArgumentIndex",
        "/next/node",
        "/next/modifierInvocation",
        evm.current.step.isContextChange
      ],
      (node, invocation, index, next, nextInvocation, isContextChange) => {
        //ensure: current instruction is not a context change (because if it is
        //we cannot rely on the data.next selectors, but also if it is we know
        //we're not about to call a modifier or base constructor!)
        //we also want to return false if we can't find things for whatever
        //reason (including if we're in Yul)
        if (
          isContextChange ||
          !node ||
          node.id === undefined ||
          !next ||
          next.id === undefined ||
          !invocation ||
          invocation.id === undefined ||
          !nextInvocation ||
          nextInvocation.id === undefined
        ) {
          return false;
        }

        //ensure: current position is in a ModifierInvocation or
        //InheritanceSpecifier (recall that SourceUnit was included as
        //fallback)
        if (isTopLevelNode(invocation)) {
          return false;
        }

        //ensure: next node is not a function definition or modifier definition
        if (
          next.nodeType === "FunctionDefinition" ||
          next.nodeType === "ModifierDefinition"
        ) {
          return false;
        }

        //ensure: next node is not in the same invocation
        if (
          !isTopLevelNode(nextInvocation) &&
          nextInvocation.id === invocation.id
        ) {
          return false;
        }

        //now: are we on the node corresponding to an argument, or, if
        //it's a potential EVM no-op, its nested argument?
        if (index === undefined) {
          return false;
        }
        let argument = invocation.arguments[index];
        do {
          if (node.id === argument.id) {
            return true;
          }
        } while ((argument = peelAwayPotentialEVMNoOp(argument)));
        return false;
      }
    ),

    /**
     * data.current.modifierInvocation
     */
    modifierInvocation: createLeaf(
      ["./node", "./scopes/inlined"],
      (node, scopes) => {
        const types = [
          "ModifierInvocation",
          "InheritanceSpecifier",
          ...topLevelNodeTypes
        ];
        return findAncestorOfType(node, types, scopes);
      }
    ),

    /**
     * data.current.modifierArgumentIndex
     * gets the index of the current modifier argument that you're in
     * (undefined when not in a modifier argument)
     */
    modifierArgumentIndex: createLeaf(
      ["./scopes", "./node", "./modifierInvocation"],
      (scopes, node, invocation) => {
        if (!invocation || isTopLevelNode(invocation)) {
          return undefined;
        }

        let pointer = scopes[node.id].pointer;
        let invocationPointer = scopes[invocation.id].pointer;

        //slice the invocation pointer off the beginning
        let difference = pointer.slice(invocationPointer.length);
        debug("difference %s", difference);
        let rawIndex = difference.match(/^\/arguments\/(\d+)/);
        //note that that \d+ is greedy
        debug("rawIndex %o", rawIndex);
        if (rawIndex === null) {
          return undefined;
        }
        return parseInt(rawIndex[1]);
      }
    ),

    /**
     * data.current.modifierBeingInvoked
     * gets the node corresponding to the modifier or base constructor
     * being invoked
     */
    modifierBeingInvoked: createLeaf(
      ["./modifierInvocation", "./scopes/inlined"],
      (invocation, scopes) => {
        if (!invocation || isTopLevelNode(invocation)) {
          return undefined;
        }

        return modifierForInvocation(invocation, scopes);
      }
    ),

    /**
     * data.current.onYulFunctionDefinitionWhileEntering
     */
    onYulFunctionDefinitionWhileEntering: createLeaf(
      [sourcemapping.current.onYulFunctionDefinitionWhileEntering],
      identity
    ),

    /**
     * data.current.identifiers (namespace)
     */
    identifiers: {
      /**
       * data.current.identifiers (selector)
       *
       * returns identifers and corresponding definition node ID or builtin name
       * (object entries look like [name]: {astRef: astRef}, [name]: {builtin: name})
       */
      _: createLeaf(
        [
          "/current/scopes/inlined",
          "/current/node",
          "/current/pointer",
          "/current/sourceIndex",
          "/current/language"
        ],

        (scopes, scope, pointer, sourceId, language) => {
          let variables = {};
          if (scope !== undefined) {
            let cur =
              scope.id !== undefined ? scope.id : makePath(sourceId, pointer);

            while (cur !== null && scopes[cur]) {
              debug("cur: %o", cur);
              debug("scopes[cur]: %o", scopes[cur]);
              variables = Object.assign(
                variables,
                ...(scopes[cur].variables || [])
                  .filter(variable => variable.name !== "") //exclude anonymous output params
                  .filter(variable => variables[variable.name] == undefined) //don't add shadowed vars
                  .map(variable => ({
                    [variable.name]: { astRef: variable.astRef }
                  }))
              );

              if (scopes[cur].definition.nodeType === "YulFunctionDefinition") {
                //Yul functions make the outside invisible
                break;
              }

              if (scopes[cur].parentId !== undefined) {
                cur = scopes[cur].parentId; //may be null!
                //(undefined means we don't know what's up,
                //null means there's nothing)
              } else {
                //in this case, cur must be a source-and-pointer, so we'll step
                //up that way (skipping over any arrays)
                cur = cur.replace(/\/[^/]*(\/\d+)?$/, "");
              }
            }
          }

          let builtins = {
            msg: { builtin: "msg" },
            tx: { builtin: "tx" },
            block: { builtin: "block" },
            this: { builtin: "this" },
            now: { builtin: "now" }
          };

          if (
            language !== "Solidity" ||
            (scope &&
              (scope.nodeType.startsWith("Yul") ||
                scope.nodeType === "InlineAssembly"))
          ) {
            //Solidity builtins are for Solidity only!
            return variables;
          }

          return { ...builtins, ...variables };
        }
      ),

      /**
       * data.current.identifiers.definitions (namespace)
       */
      definitions: {
        /**
         * data.current.identifiers.definitions (selector)
         * definitions for current variables, by identifier
         */
        _: createLeaf(
          ["/current/scopes/inlined", "../_", "./this", "/current/compiler"],

          (scopes, identifiers, thisDefinition, compiler) => {
            debug("identifiers: %O", identifiers);
            let variables = Object.assign(
              {},
              ...Object.entries(identifiers).map(([identifier, variable]) => {
                if (variable.astRef !== undefined) {
                  let { definition } = scopes[variable.astRef];
                  return { [identifier]: definition };
                  //there used to be separate code for Yul variables here,
                  //but now that's handled in definitionToType
                } else {
                  return {}; //skip over builtins; we'll handle those separately
                }
              })
            );
            let builtins = {
              msg: MSG_DEFINITION,
              tx: TX_DEFINITION,
              block: BLOCK_DEFINITION
            };
            //only include this when it has a proper definition
            if (thisDefinition) {
              builtins.this = thisDefinition;
            }
            //only include now on versions prior to 0.7.0
            if (!solidityVersionHasNoNow(compiler)) {
              debug("adding now");
              builtins.now = NOW_DEFINITION;
            }
            return { ...builtins, ...variables };
          }
        ),

        /**
         * data.current.identifiers.definitions.this
         *
         * returns a spoofed definition for the this variable
         */
        this: createLeaf(["/current/contract"], contractNode =>
          contractNode && contractNode.nodeType === "ContractDefinition"
            ? spoofThisDefinition(
                contractNode.name,
                contractNode.id,
                contractNode.contractKind
              )
            : null
        )
      },

      /**
       * data.current.identifiers.sections
       * used for printing out the variables in sections
       */
      sections: createLeaf(
        ["./definitions", "./refs", "/current/scopes/inlined"],
        (definitions, refs, scopes) => {
          let sections = {
            builtin: [],
            global: [],
            contract: [],
            local: []
          };
          if (!scopes) {
            //if no transaction is loaded, there are no variables
            return sections;
          }
          for (const [identifier, ref] of Object.entries(refs)) {
            if (identifier in definitions) {
              switch (ref.location) {
                case "special":
                  sections.builtin.push(identifier);
                  break;
                case "stack":
                  sections.local.push(identifier);
                  break;
                case "storage":
                case "code":
                case "nowhere":
                case "memory":
                  sections.contract.push(identifier);
                  break;
                case "definition":
                  //in this case, look up whether its scope
                  //is a SourceUnit or a ContractDefinition
                  const definition = definitions[identifier];
                  const scope = scopes[definition.scope].definition;
                  if (scope.nodeType === "SourceUnit") {
                    sections.global.push(identifier);
                  } else if (scope.nodeType === "ContractDefinition") {
                    sections.contract.push(identifier);
                  }
                  //other cases shouldn't happen
                  break;
                //other cases shouldn't happen
              }
            }
          }
          return sections;
        }
      ),

      /**
       * data.current.identifiers.refs
       *
       * current variables' value refs
       */
      refs: createLeaf(
        [
          "/proc/assignments",
          "./_",
          "./definitions",
          "/current/scopes/inlined",
          "/current/compilationId",
          "/current/internalSourceFor", //may be null
          "/current/functionDepth", //for pruning things too deep on stack
          "/current/modifierDepth", //when it's useful
          "/current/inModifier"
        ],

        (
          assignments,
          identifiers,
          definitions,
          scopes,
          compilationId,
          internalFor,
          currentDepth,
          modifierDepth,
          inModifier
        ) =>
          Object.assign(
            {},
            ...Object.entries(identifiers).map(
              ([identifier, { astRef, builtin }]) => {
                let id;
                debug("astRef: %o", astRef);
                debug("builtin: %s", builtin);

                //is this an ordinary variable or a builtin?
                if (astRef !== undefined) {
                  //ordinary variable case
                  //first: is this a contract variable?
                  id = stableKeccak256({
                    astRef,
                    compilationId,
                    internalFor
                  });
                  //if not contract, it's local, so identify by stackframe (& etc)
                  if (!(id in assignments)) {
                    id = stableKeccak256({
                      astRef,
                      compilationId,
                      internalFor,
                      stackframe: currentDepth,
                      modifierDepth: inModifier ? modifierDepth : null
                    });
                  }
                  debug("id after local: %s", id);
                  //if it's not that either, but it's a constant, maybe it's a
                  //global (if it is, whip up an assignment rather than extracting
                  //one from assignments!)
                  if (!(id in assignments)) {
                    const definition = definitions[identifier];
                    debug("global definition: %o", definition);
                    if (definition.scope !== undefined) {
                      const scope = scopes[definition.scope].definition;
                      if (
                        scope.nodeType === "SourceUnit" &&
                        (definition.constant === true ||
                          definition.mutability === "constant")
                      ) {
                        return {
                          [identifier]: {
                            location: "definition",
                            definition: definition.value
                          }
                        };
                      }
                    }
                  }
                } else {
                  //it's a builtin
                  id = stableKeccak256({
                    builtin
                  });
                }

                //if we still didn't find it, oh well
                debug("id: %s", id);

                let { ref } = assignments[id] || {};
                if (!ref) {
                  return {}; //don't add anything
                }
                return {
                  [identifier]: ref
                };
              }
            )
          )
      )
    },

    /**
     * data.current.returnStatus
     */
    returnStatus: createLeaf(
      [evm.current.step.returnStatus],
      status => (status === null ? undefined : status) //convert null to undefined to be safe
    ),

    /**
     * data.current.returnAllocation
     */
    returnAllocation: createLeaf(
      [
        evm.current.call,
        "/current/context",
        "/info/allocations/calldata",
        "./fallbackOutputForContext"
      ],
      (
        { data: calldata },
        { context, isConstructor, fallbackAbi },
        { constructorAllocations, functionAllocations },
        contractHasFallbackOutput //just using truthiness here
      ) => {
        if (isConstructor) {
          //we're in a constructor call
          let allocation = constructorAllocations[context];
          if (!allocation) {
            return null;
          }
          return allocation.output;
        } else {
          //usual case
          let selector = calldata.slice(0, 2 + 4 * 2); //extract first 4 bytes of hex string
          debug("selector: %s", selector);
          debug("bySelector: %o", functionAllocations[context]);
          let allocation = (functionAllocations[context] || {})[selector];
          if (allocation) {
            return allocation.output;
          } else {
            //we're in a fallback or receive, presumably.
            //so is it a fallback, and does it have output?
            if (
              (calldata !== "0x" || fallbackAbi.receive === null) &&
              fallbackAbi.fallback !== null && //this check is redundant, but let's include it
              contractHasFallbackOutput
            ) {
              return Codec.AbiData.Allocate.FallbackOutputAllocation;
            } else {
              return null;
            }
          }
        }
      }
    ),

    /**
     * data.current.isCall
     */
    isCall: createLeaf([evm.current.step.isCall], identity),

    /**
     * data.current.isCreate
     */
    isCreate: createLeaf([evm.current.step.isCreate], identity),

    /**
     * data.current.currentCallIsCreate
     */
    currentCallIsCreate: createLeaf(
      [evm.current.call],
      call => call.binary !== undefined
    ),

    /**
     * data.current.callContext
     * note that we convert to decoder context!
     */
    callContext: createLeaf(
      [evm.current.step.callContext],
      debuggerContextToDecoderContext
    ),

    /**
     * data.current.isPop
     */
    isPop: createLeaf([evm.current.step.isPop], identity)
  },

  /**
   * data.next
   */
  next: {
    /**
     * data.next.state
     * Yes, I'm just repeating the code for data.current.state.stack here;
     * not worth the trouble to factor out
     */
    state: {
      /**
       * data.next.state.stack
       */
      stack: createLeaf(
        [evm.next.state.stack],

        words => (words || []).map(word => Codec.Conversion.toBytes(word))
      ),

      /**
       * data.next.state.returndata
       * NOTE: this is only for use by decodeReturnValue(); this is *not*
       * an accurate reflection of the current contents of returndata!
       * we don't track that at the moment
       */
      returndata: createLeaf([evm.current.step.returnValue], data =>
        Codec.Conversion.toBytes(data)
      ),

      /**
       * data.next.state.calldata
       * NOTE: this is only for use by decodeCall(); this is *not*
       * necessarily the actual next contents of calldata!
       */
      calldata: createLeaf(
        [
          evm.current.step.isCall,
          evm.current.step.isCreate,
          evm.current.step.callData,
          evm.current.step.createBinary
        ],
        (isCall, isCreate, data, binary) => {
          if (!isCall && !isCreate) {
            return null;
          }
          return Codec.Conversion.toBytes(isCall ? data : binary);
        }
      )
    },

    //HACK WARNING
    //the following selectors depend on sourcemapping.next
    //do not use them when the current instruction is a context change!

    /**
     * data.next.node
     */
    node: createLeaf([sourcemapping.next.node], identity),

    /**
     * data.next.pointer
     */
    pointer: createLeaf([sourcemapping.next.pointer], identity),

    /**
     * data.next.modifierInvocation
     * Note: yes, I'm just repeating the code from data.current here but with
     * invalid added
     */
    modifierInvocation: createLeaf(
      ["./node", "/current/scopes/inlined", evm.current.step.isContextChange],
      (node, scopes, invalid) => {
        //don't attempt this at a context change!
        //(also don't attempt this if we can't find the node for whatever
        //reason)
        if (invalid) {
          return undefined;
        }
        const types = [
          "ModifierInvocation",
          "InheritanceSpecifier",
          ...topLevelNodeTypes
        ];
        //again, SourceUnit and YulObject are included as fallbacks
        return findAncestorOfType(node, types, scopes);
      }
    ),

    /**
     * data.next.modifierBeingInvoked
     */
    modifierBeingInvoked: createLeaf(
      [
        "./modifierInvocation",
        "/current/scopes/inlined",
        evm.current.step.isContextChange
      ],
      (invocation, scopes, invalid) => {
        if (invalid || !invocation || isTopLevelNode(invocation)) {
          return undefined;
        }

        return modifierForInvocation(invocation, scopes);
      }
    )
    //END HACK WARNING
  },

  /**
   * data.nextUserStep
   */
  nextUserStep: {
    /**
     * data.nextUserStep.state
     * Yes, I'm just repeating the code for data.current.state.stack here;
     * not worth the trouble to factor out
     * HACK: this assumes we're not about to change context! don't use this if we
     * are!
     */
    state: {
      /**
       * data.nextUserStep.state.stack
       */
      stack: createLeaf(
        [sourcemapping.current.nextUserStep],

        step =>
          ((step || {}).stack || []).map(word => Codec.Conversion.toBytes(word))
      )
    }
  },

  /**
   * data.nextOfSameDepth
   */
  nextOfSameDepth: {
    /**
     * data.nextOfSameDepth.state
     * Yes, I'm just repeating the code for data.current.state.stack here but
     * with an extra guard... *still* not worth the trouble to factor out
     * HOWEVER, this one also returns null if there is no nextOfSameDepth
     */
    state: {
      /**
       * data.nextOfSameDepth.state.stack
       */
      stack: createLeaf(
        [trace.nextOfSameDepth],

        step =>
          step
            ? (step.stack || []).map(word => Codec.Conversion.toBytes(word))
            : null
      )
    }
  }
});

export default data;