Home Reference Source

lib/solidity/selectors/index.js

import debugModule from "debug";
const debug = debugModule("debugger:solidity:selectors");

import { createSelectorTree, createLeaf } from "reselect-tree";
import SolidityUtils from "truffle-solidity-utils";
import CodeUtils from "truffle-code-utils";

import evm from "lib/evm/selectors";

function getSourceRange(instruction = {}) {
  return {
    start: instruction.start || 0,
    length: instruction.length || 0,
    lines: instruction.range || {
      start: {
        line: 0, column: 0
      },
      end: {
        line: 0, column: 0
      }
    }
  };
}

let solidity = createSelectorTree({
  /**
   * solidity.state
   */
  state: (state) => state.solidity,

  /**
   * solidity.info
   */
  info: {
    /**
     * solidity.info.sources
     */
    sources: createLeaf(['/state'], (state) => state.info.sources.byId),

    /**
     * solidity.info.sourceMaps
     */
    sourceMaps: createLeaf(['/state'], (state) => state.info.sourceMaps.byContext)
  },

  /**
   * solidity.current
   */
  current: {

    /**
     * solidity.current.sourceMap
     */
    sourceMap: createLeaf(
      [evm.current.context, "/info/sourceMaps"],

      ({context}, sourceMaps) => sourceMaps[context] || {}
    ),

    /**
     * solidity.current.functionDepth
     */
    functionDepth: (state) => state.solidity.proc.functionDepth,

    /**
     * solidity.current.instructions
     */
    instructions: createLeaf(
      ["/info/sources", evm.current.context, "./sourceMap"],

      (sources, {binary}, {sourceMap}) => {
        let instructions = CodeUtils.parseCode(binary);

        if (!sourceMap) {
          // Let's create a source map to use since none exists. This source map
          // maps just as many ranges as there are instructions, and ensures every
          // instruction is marked as "jumping out". This will ensure all
          // available debugger commands step one instruction at a time.
          //
          // This is kindof a hack; perhaps this should be broken out into separate
          // context types. TODO
          sourceMap = "";
          for (var i = 0; i < instructions.length; i++) {
            sourceMap += i + ":" + i + ":1:-1;";
          }
        }

        var lineAndColumnMappings = Object.assign({},
          ...Object.entries(sources).map(
            ([id, {source}]) => ({
              [id]: SolidityUtils.getCharacterOffsetToLineAndColumnMapping(source || "")
            })
          )
        );
        var humanReadableSourceMap = SolidityUtils.getHumanReadableSourceMap(sourceMap);

        let primaryFile = humanReadableSourceMap[0].file;
        debug("primaryFile %o", primaryFile);

        return instructions
          .map( (instruction, index) => {
            // lookup source map by index and add `index` property to
            // instruction
            //

            const sourceMap = humanReadableSourceMap[index] || {};

            return {
              instruction: { ...instruction, index },
              sourceMap,
            };
          })
          .map( ({ instruction, sourceMap}) => {
            // add source map information to instruction, or defaults
            //

            const { jump, start = 0, length = 0, file = primaryFile } = sourceMap;
            const lineAndColumnMapping = lineAndColumnMappings[file] || {};
            const range = {
              start: lineAndColumnMapping[start] ||
                { line: null, column: null },
              end: lineAndColumnMapping[start + length] ||
                { line: null, column: null }
            };

            if (range.start.line === null) {
              debug("sourceMap %o", sourceMap);
            }

            return {
              ...instruction,

              jump,
              start,
              length,
              file,
              range
            }
          });
      }
    ),

    /**
     * solidity.current.instructionAtProgramCounter
     */
    instructionAtProgramCounter: createLeaf(
      ["./instructions"],

      (instructions) => {
        let map = [];
        instructions.forEach(function(instruction) {
          map[instruction.pc] = instruction;
        });

        // fill in gaps in map by defaulting to the last known instruction
        let lastSeen = null;
        for (let [pc, instruction] of map.entries()) {
          if (instruction) {
            lastSeen = instruction;
          } else {
            map[pc] = lastSeen;
          }
        }
        return map;
      }
    ),

    /**
     * solidity.current.instruction
     */
    instruction: createLeaf(
      ["./instructionAtProgramCounter", evm.current.step.programCounter],

      (map, pc) => map[pc] || {}
    ),

    /**
     * solidity.current.source
     */
    source: createLeaf(
      ["/info/sources", "./instruction"],

      (sources, {file: id}) => sources[id] || {}
    ),

    /**
     * solidity.current.sourceRange
     */
    sourceRange: createLeaf(["./instruction"], getSourceRange),

    /**
     * solidity.current.isSourceRangeFinal
     */
    isSourceRangeFinal: createLeaf(
      [
        "./instructionAtProgramCounter",
        evm.current.step.programCounter,
        evm.next.step.programCounter
      ],

      (map, current, next) => {
        if (!map[next]) {
          return true;
        }

        current = map[current];
        next = map[next];

        return (
          current.start != next.start ||
          current.length != next.length ||
          current.file != next.file
        );
      }
    ),

    /**
     * solidity.current.isMultiline
     */
    isMultiline: createLeaf(
      ["./sourceRange"],

      ( {lines} ) => lines.start.line != lines.end.line
    ),

    /**
     * solidity.current.willJump
     */
    willJump: createLeaf([evm.current.step.isJump], (isJump) => isJump),

    /**
     * solidity.current.jumpDirection
     */
    jumpDirection: createLeaf(
      ["./instruction"], (i = {}) => (i.jump || "-")
    )
  }
});

export default solidity;