<commit_before>import debugModule from "debug";
const debug = debugModule("codec:abi:allocate");

import * as AbiData from "@truffle/codec/abi-data/types";
import * as Import from "@truffle/codec/abi-data/import";
import * as AbiDataUtils from "@truffle/codec/abi-data/utils";
import * as Evm from "@truffle/codec/evm";
import * as Common from "@truffle/codec/common";
import * as Compiler from "@truffle/codec/compiler";
import * as Ast from "@truffle/codec/ast";
import * as Contexts from "@truffle/codec/contexts";
import * as Pointer from "@truffle/codec/pointer";
import {
  AbiAllocation,
  AbiAllocations,
  AbiMemberAllocation,
  AbiSizeInfo,
  CalldataAndReturndataAllocation,
  CalldataAllocation,
  ReturndataAllocation,
  CalldataAllocations,
  CalldataAllocationTemporary,
  CalldataArgumentAllocation,
  ReturndataArgumentAllocation,
  ContractAllocationInfo,
  EventAllocation,
  EventAllocations,
  EventAllocationTemporary,
  EventArgumentAllocation
} from "./types";
import { DecodingMode } from "@truffle/codec/types";
import * as Format from "@truffle/codec/format";
import partition from "lodash.partition";

export {
  AbiAllocations,
  AbiSizeInfo,
  CalldataAllocation,
  ReturndataAllocation,
  CalldataAndReturndataAllocation,
  ContractAllocationInfo,
  EventAllocation
};

interface AbiAllocationInfo {
  size?: number; //left out for types that don't go in the abi
  dynamic?: boolean; //similarly
  allocations: AbiAllocations;
}

interface EventParameterInfo {
  type: Format.Types.Type;
  name: string;
  indexed: boolean;
}

export function getAbiAllocations(
  userDefinedTypes: Format.Types.TypesById
): AbiAllocations {
  let allocations: AbiAllocations = {};
  for (const dataType of Object.values(userDefinedTypes)) {
    if (dataType.typeClass === "struct") {
      try {
        allocations = allocateStruct(dataType, userDefinedTypes, allocations);
      } catch (_) {
        //if allocation fails... oh well, allocation fails, we do nothing and just move on :P
        //note: a better way of handling this would probably be to *mark* it
        //as failed rather than throwing an exception as that would lead to less
        //recomputation, but this is simpler and I don't think the recomputation
        //should really be a problem
      }
    }
  }
  return allocations;
}

function allocateStruct(
  dataType: Format.Types.StructType,
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations: AbiAllocations
): AbiAllocations {
  //NOTE: dataType here should be a *stored* type!
  //it is up to the caller to take care of this
  return allocateMembers(
    dataType.id,
    dataType.memberTypes,
    userDefinedTypes,
    existingAllocations
  );
}

//note: we will still allocate circular structs, even though they're not allowed in the abi, because it's
//not worth the effort to detect them.  However on mappings or internal functions, we'll vomit (allocate null)
function allocateMembers(
  parentId: string,
  members: Format.Types.NameTypePair[],
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations: AbiAllocations,
  start: number = 0
): AbiAllocations {
  let dynamic: boolean = false;
  //note that we will mutate the start argument also!

  //don't allocate things that have already been allocated
  if (parentId in existingAllocations) {
    return existingAllocations;
  }

  let allocations = { ...existingAllocations }; //otherwise, we'll be adding to this, so we better clone

  let memberAllocations: AbiMemberAllocation[] = [];

  for (const member of members) {
    let length: number;
    let dynamicMember: boolean;
    ({ size: length, dynamic: dynamicMember, allocations } = abiSizeAndAllocate(
      member.type,
      userDefinedTypes,
      allocations
    ));

    //vomit on illegal types in calldata -- note the short-circuit!
    if (length === undefined) {
      allocations[parentId] = null;
      return allocations;
    }

    let pointer: Pointer.AbiPointer = {
      location: "abi",
      start,
      length
    };

    memberAllocations.push({
      name: member.name,
      type: member.type,
      pointer
    });

    start += length;
    dynamic = dynamic || dynamicMember;
  }

  allocations[parentId] = {
    members: memberAllocations,
    length: dynamic ? Evm.Utils.WORD_SIZE : start,
    dynamic
  };

  return allocations;
}

//first return value is the actual size.
//second return value is whether the type is dynamic
//both will be undefined if type is a mapping or internal function
//third return value is resulting allocations, INCLUDING the ones passed in
function abiSizeAndAllocate(
  dataType: Format.Types.Type,
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations?: AbiAllocations
): AbiAllocationInfo {
  switch (dataType.typeClass) {
    case "bool":
    case "address":
    case "contract":
    case "int":
    case "uint":
    case "fixed":
    case "ufixed":
    case "enum":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: false,
        allocations: existingAllocations
      };

    case "string":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: true,
        allocations: existingAllocations
      };

    case "bytes":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: dataType.kind === "dynamic",
        allocations: existingAllocations
      };

    case "mapping":
      return {
        allocations: existingAllocations
      };

    case "function":
      switch (dataType.visibility) {
        case "external":
          return {
            size: Evm.Utils.WORD_SIZE,
            dynamic: false,
            allocations: existingAllocations
          };
        case "internal":
          return {
            allocations: existingAllocations
          };
      }

    case "array": {
      switch (dataType.kind) {
        case "dynamic":
          return {
            size: Evm.Utils.WORD_SIZE,
            dynamic: true,
            allocations: existingAllocations
          };
        case "static":
          if (dataType.length.isZero()) {
            //arrays of length 0 are static regardless of base type
            return {
              size: 0,
              dynamic: false,
              allocations: existingAllocations
            };
          }
          const { size: baseSize, dynamic, allocations } = abiSizeAndAllocate(
            dataType.baseType,
            userDefinedTypes,
            existingAllocations
          );
          return {
            //WARNING!  The use of toNumber() here may throw an exception!
            //I'm judging this OK since if you have arrays that large we have bigger problems :P
            size: dataType.length.toNumber() * baseSize,
            dynamic,
            allocations
          };
      }
    }

    case "struct": {
      let allocations: AbiAllocations = existingAllocations;
      let allocation: AbiAllocation | null | undefined =
        allocations[dataType.id];
      if (allocation === undefined) {
        //if we don't find an allocation, we'll have to do the allocation ourselves
        const storedType = <Format.Types.StructType>(
          userDefinedTypes[dataType.id]
        );
        if (!storedType) {
          throw new Common.UnknownUserDefinedTypeError(
            dataType.id,
            Format.Types.typeString(dataType)
          );
        }
        allocations = allocateStruct(
          storedType,
          userDefinedTypes,
          existingAllocations
        );
        allocation = allocations[storedType.id];
      }
      //having found our allocation, if it's not null, we can just look up its size and dynamicity
      if (allocation !== null) {
        return {
          size: allocation.length,
          dynamic: allocation.dynamic,
          allocations
        };
      }
      //if it is null, this type doesn't go in the abi
      else {
        return {
          allocations
        };
      }
    }

    case "tuple": {
      //Warning! Yucky wasteful recomputation here!
      let size = 0;
      let dynamic = false;
      //note that we don't just invoke allocateStruct here!
      //why not? because it has no ID to store the result in!
      //and we can't use a fake like -1 because there might be a recursive call to it,
      //and then the results would overwrite each other
      //I mean, we could do some hashing thing or something, but I think it's easier to just
      //copy the logic in this one case (sorry)
      for (let member of dataType.memberTypes) {
        let { size: memberSize, dynamic: memberDynamic } = abiSizeAndAllocate(
          member.type,
          userDefinedTypes,
          existingAllocations
        );
        size += memberSize;
        dynamic = dynamic || memberDynamic;
      }
      return { size, dynamic, allocations: existingAllocations };
    }
  }
}

//assumes you've already done allocation! don't use if you haven't!
/**
 * @protected
 */
export function abiSizeInfo(
  dataType: Format.Types.Type,
  allocations?: AbiAllocations
): AbiSizeInfo {
  let { size, dynamic } = abiSizeAndAllocate(dataType, null, allocations);
  //the above line should work fine... as long as allocation is already done!
  //the middle argument, userDefinedTypes, is only needed during allocation
  //again, this function is only for use if allocation is done, so it's safe to pass null here
  return { size, dynamic };
}

//allocates an external call
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
//NOTE: returns undefined if attempting to allocate a constructor but we don't have the
//bytecode for the constructor
function allocateCalldataAndReturndata(
  abiEntry: AbiData.FunctionAbiEntry | AbiData.ConstructorAbiEntry,
  contractNode: Ast.AstNode | undefined,
  referenceDeclarations: Ast.AstNodes,
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined,
  constructorContext?: Contexts.DecoderContext
): CalldataAndReturndataAllocation | undefined {
  //first: determine the corresponding function node
  //(simultaneously: determine the offset)
  let node: Ast.AstNode | undefined = undefined;
  let inputParametersFull: Ast.AstNode[];
  let outputParametersFull: Ast.AstNode[];
  let inputParametersAbi: AbiData.AbiParameter[];
  let outputParametersAbi: AbiData.AbiParameter[];
  let offset: number; //refers to INPUT offset; output offset is always 0
  switch (abiEntry.type) {
    case "constructor":
      if (!constructorContext) {
        return undefined;
      }
      let rawLength = constructorContext.binary.length;
      offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring
      //for a constructor, we only want to search the particular contract
      if (contractNode) {
        node = contractNode.nodes.find(functionNode =>
          AbiDataUtils.definitionMatchesAbi(
            //note this needn't actually be a function node, but then it will
            //return false (well, unless it's a getter node!)
            abiEntry,
            functionNode,
            referenceDeclarations
          )
        );
      }
      //if we can't find it, we'll handle this below
      break;
    case "function":
      offset = Evm.Utils.SELECTOR_SIZE;
      //search through base contracts, from most derived (left) to most base (right)
      if (contractNode) {
        const linearizedBaseContracts = contractNode.linearizedBaseContracts;
        node = linearizedBaseContracts.reduce(
          (foundNode: Ast.AstNode, baseContractId: number) => {
            if (foundNode !== undefined) {
              return foundNode; //once we've found something, we don't need to keep looking
            }
            let baseContractNode = referenceDeclarations[baseContractId];
            if (baseContractNode === undefined) {
              return null; //return null rather than undefined so that this will propagate through
              //(i.e. by returning null here we give up the search)
              //(we don't want to continue due to possibility of grabbing the wrong override)
            }
            return baseContractNode.nodes.find(
              //may be undefined! that's OK!
              functionNode =>
                AbiDataUtils.definitionMatchesAbi(
                  abiEntry,
                  functionNode,
                  referenceDeclarations
                )
            );
          },
          undefined //start with no node found
        );
      }
      break;
  }
  //now: get the parameters (both full-mode & ABI)
  if (node) {
    switch (node.nodeType) {
      case "FunctionDefinition":
        //normal case
        inputParametersFull = node.parameters.parameters;
        outputParametersFull = node.returnParameters.parameters; //this exists even for constructors!
        break;
      case "VariableDeclaration":
        //getter case
        ({
          inputs: inputParametersFull,
          outputs: outputParametersFull
        } = Ast.Utils.getterParameters(node, referenceDeclarations));
        break;
    }
  } else {
    inputParametersFull = undefined;
    outputParametersFull = undefined;
  }
  inputParametersAbi = abiEntry.inputs;
  switch (abiEntry.type) {
    case "function":
      outputParametersAbi = abiEntry.outputs;
    case "constructor":
      //we just leave this empty for constructors
      outputParametersAbi = [];
  }
  //now: do the allocation!
  let {
    allocation: abiAllocationInput,
    mode: inputMode
  } = allocateDataArguments(
    inputParametersFull,
    inputParametersAbi,
    userDefinedTypes,
    abiAllocations,
    compiler,
    offset
  );
  let {
    allocation: abiAllocationOutput,
    mode: outputMode
  } = allocateDataArguments(
    outputParametersFull,
    outputParametersAbi,
    userDefinedTypes,
    abiAllocations,
    compiler
    //note no offset
  );
  //finally: transform the allocation appropriately
  let inputArgumentsAllocation = abiAllocationInput.members.map(member => ({
    ...member,
    pointer: {
      location: "calldata" as const,
      start: member.pointer.start,
      length: member.pointer.length
    }
  }));
  let outputArgumentsAllocation = abiAllocationOutput.members.map(member => ({
    ...member,
    pointer: {
      location: "returndata" as const,
      start: member.pointer.start,
      length: member.pointer.length
    }
  }));
  let inputsAllocation: CalldataAllocation = {
    abi: abiEntry,
    offset,
    arguments: inputArgumentsAllocation,
    allocationMode: inputMode
  };
  let outputsAllocation: ReturndataAllocation;
  switch (abiEntry.type) {
    case "function":
      outputsAllocation = {
        selector: new Uint8Array(), //empty by default
        arguments: outputArgumentsAllocation,
        allocationMode: outputMode,
        kind: "return" as const
      };
      break;
    case "constructor":
      outputsAllocation = constructorOutputAllocation;
      break;
  }
  return { input: inputsAllocation, output: outputsAllocation };
}

interface AbiAllocationAndMode {
  allocation: AbiAllocation;
  mode: DecodingMode;
}

//note: allocateEvent doesn't use this because it needs additional
//handling for indexed parameters (maybe these can be unified in
//the future though?)
function allocateDataArguments(
  fullModeParameters: Ast.AstNode[] | undefined,
  abiParameters: AbiData.AbiParameter[],
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined,
  offset: number = 0
): AbiAllocationAndMode {
  let allocationMode: DecodingMode = fullModeParameters ? "full" : "abi"; //can degrade
  let parameterTypes: Format.Types.NameTypePair[];
  let abiAllocation: AbiAllocation;
  if (allocationMode === "full") {
    let id = "-1"; //fake ID that doesn't matter
    parameterTypes = fullModeParameters.map(parameter => ({
      name: parameter.name,
      type: Ast.Import.definitionToType(parameter, compiler) //if node is defined, compiler had also better be!
    }));
    //now: perform the allocation!
    try {
      abiAllocation = allocateMembers(
        id,
        parameterTypes,
        userDefinedTypes,
        abiAllocations,
        offset
      )[id];
    } catch {
      //if something goes wrong, switch to ABI mdoe
      allocationMode = "abi";
    }
  }
  if (allocationMode === "abi") {
    //THIS IS DELIBERATELY NOT AN ELSE
    //this is the ABI case.  we end up here EITHER
    //if node doesn't exist, OR if something went wrong
    //during allocation
    let id = "-1"; //fake irrelevant ID
    parameterTypes = abiParameters.map(parameter => ({
      name: parameter.name,
      type: Import.abiParameterToType(parameter)
    }));
    abiAllocation = allocateMembers(
      id,
      parameterTypes,
      userDefinedTypes,
      abiAllocations,
      offset
    )[id];
  }
  return { allocation: abiAllocation, mode: allocationMode };
}

interface EventParameterInfo {
  name: string;
  type: Format.Types.Type;
  indexed: boolean;
}

//allocates an event
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
function allocateEvent(
  abiEntry: AbiData.EventAbiEntry,
  contractNode: Ast.AstNode | undefined,
  contextHash: string,
  referenceDeclarations: Ast.AstNodes,
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined
): EventAllocation | undefined {
  let parameterTypes: EventParameterInfo[];
  let id: string;
  //first: determine the corresponding event node
  //search through base contracts, from most derived (right) to most base (left)
  let node: Ast.AstNode | undefined = undefined;
  let definedIn: Format.Types.ContractType | undefined = undefined;
  let allocationMode: DecodingMode = "full"; //degrade to abi as needed
  debug("allocating ABI: %O", abiEntry);
  if (contractNode) {
    let linearizedBaseContractsMinusSelf = contractNode.linearizedBaseContracts.slice();
    linearizedBaseContractsMinusSelf.shift(); //remove self
    //first: check same contract for the event
    node = contractNode.nodes.find(eventNode =>
      AbiDataUtils.definitionMatchesAbi(
        //note this needn't actually be an event node, but then it will
        //return false
        abiEntry,
        eventNode,
        referenceDeclarations
      )
    );
    //if we found the node, great!  If not...
    if (!node) {
      debug("didn't find node in base contract...");
      //let's search for the node among the base contracts.
      //but if we find it...
      //[note: the following code is overcomplicated; it was used
      //when we were trying to get the actual node, it's overcomplicated
      //now that we're just determining its presence.  oh well]
      node = linearizedBaseContractsMinusSelf.reduce(
        (foundNode: Ast.AstNode, baseContractId: number) => {
          if (foundNode !== undefined) {
            return foundNode; //once we've found something, we don't need to keep looking
          }
          let baseContractNode = referenceDeclarations[baseContractId];
          if (baseContractNode === undefined) {
            return null; //return null rather than undefined so that this will propagate through
            //(i.e. by returning null here we give up the search)
            //(we don't want to continue due to possibility of grabbing the wrong override)
          }
          return baseContractNode.nodes.find(
            //may be undefined! that's OK!
            eventNode =>
              AbiDataUtils.definitionMatchesAbi(
                //note this needn't actually be a event node, but then it will return false
                abiEntry,
                eventNode,
                referenceDeclarations
              )
          );
        },
        undefined //start with no node found
      );
      if (node) {
        //...if we find the node in an ancestor, we
        //deliberately *don't* allocate!  instead such cases
        //will be handled during a later combination step
        debug("bailing out for later handling!");
        debug("ABI: %O", abiEntry);
        return undefined;
      }
    }
  }
  //otherwise, leave node undefined
  if (node) {
    debug("found node");
    //if we found the node, let's also turn it into a type
    definedIn = <Format.Types.ContractType>(
      Ast.Import.definitionToStoredType(node, compiler)
    ); //can skip 3rd argument here
  } else {
    //if no node, have to fall back into ABI mode
    debug("falling back to ABI because no node");
    allocationMode = "abi";
  }
  //now: construct the list of parameter types, attaching indexedness info
  //and overall position (for later reconstruction)
  let indexed: EventParameterInfo[];
  let nonIndexed: EventParameterInfo[];
  let abiAllocation: AbiAllocation; //the untransformed allocation for the non-indexed parameters
  if (allocationMode === "full") {
    let id = node.id.toString();
    let parameters = node.parameters.parameters;
    parameterTypes = parameters.map(definition => ({
      //note: if node is defined, compiler had better be defined, too!
      type: Ast.Import.definitionToType(definition, compiler),
      name: definition.name,
      indexed: definition.indexed
    }));<commit_msg>Account for events inherited from interfaces<commit_after>import debugModule from "debug";
const debug = debugModule("codec:abi:allocate");

import * as AbiData from "@truffle/codec/abi-data/types";
import * as Import from "@truffle/codec/abi-data/import";
import * as AbiDataUtils from "@truffle/codec/abi-data/utils";
import * as Evm from "@truffle/codec/evm";
import * as Common from "@truffle/codec/common";
import * as Compiler from "@truffle/codec/compiler";
import * as Ast from "@truffle/codec/ast";
import * as Contexts from "@truffle/codec/contexts";
import * as Pointer from "@truffle/codec/pointer";
import {
  AbiAllocation,
  AbiAllocations,
  AbiMemberAllocation,
  AbiSizeInfo,
  CalldataAndReturndataAllocation,
  CalldataAllocation,
  ReturndataAllocation,
  CalldataAllocations,
  CalldataAllocationTemporary,
  CalldataArgumentAllocation,
  ReturndataArgumentAllocation,
  ContractAllocationInfo,
  EventAllocation,
  EventAllocations,
  EventAllocationTemporary,
  EventArgumentAllocation
} from "./types";
import { DecodingMode } from "@truffle/codec/types";
import * as Format from "@truffle/codec/format";
import partition from "lodash.partition";

export {
  AbiAllocations,
  AbiSizeInfo,
  CalldataAllocation,
  ReturndataAllocation,
  CalldataAndReturndataAllocation,
  ContractAllocationInfo,
  EventAllocation
};

interface AbiAllocationInfo {
  size?: number; //left out for types that don't go in the abi
  dynamic?: boolean; //similarly
  allocations: AbiAllocations;
}

interface EventParameterInfo {
  type: Format.Types.Type;
  name: string;
  indexed: boolean;
}

export function getAbiAllocations(
  userDefinedTypes: Format.Types.TypesById
): AbiAllocations {
  let allocations: AbiAllocations = {};
  for (const dataType of Object.values(userDefinedTypes)) {
    if (dataType.typeClass === "struct") {
      try {
        allocations = allocateStruct(dataType, userDefinedTypes, allocations);
      } catch (_) {
        //if allocation fails... oh well, allocation fails, we do nothing and just move on :P
        //note: a better way of handling this would probably be to *mark* it
        //as failed rather than throwing an exception as that would lead to less
        //recomputation, but this is simpler and I don't think the recomputation
        //should really be a problem
      }
    }
  }
  return allocations;
}

function allocateStruct(
  dataType: Format.Types.StructType,
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations: AbiAllocations
): AbiAllocations {
  //NOTE: dataType here should be a *stored* type!
  //it is up to the caller to take care of this
  return allocateMembers(
    dataType.id,
    dataType.memberTypes,
    userDefinedTypes,
    existingAllocations
  );
}

//note: we will still allocate circular structs, even though they're not allowed in the abi, because it's
//not worth the effort to detect them.  However on mappings or internal functions, we'll vomit (allocate null)
function allocateMembers(
  parentId: string,
  members: Format.Types.NameTypePair[],
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations: AbiAllocations,
  start: number = 0
): AbiAllocations {
  let dynamic: boolean = false;
  //note that we will mutate the start argument also!

  //don't allocate things that have already been allocated
  if (parentId in existingAllocations) {
    return existingAllocations;
  }

  let allocations = { ...existingAllocations }; //otherwise, we'll be adding to this, so we better clone

  let memberAllocations: AbiMemberAllocation[] = [];

  for (const member of members) {
    let length: number;
    let dynamicMember: boolean;
    ({ size: length, dynamic: dynamicMember, allocations } = abiSizeAndAllocate(
      member.type,
      userDefinedTypes,
      allocations
    ));

    //vomit on illegal types in calldata -- note the short-circuit!
    if (length === undefined) {
      allocations[parentId] = null;
      return allocations;
    }

    let pointer: Pointer.AbiPointer = {
      location: "abi",
      start,
      length
    };

    memberAllocations.push({
      name: member.name,
      type: member.type,
      pointer
    });

    start += length;
    dynamic = dynamic || dynamicMember;
  }

  allocations[parentId] = {
    members: memberAllocations,
    length: dynamic ? Evm.Utils.WORD_SIZE : start,
    dynamic
  };

  return allocations;
}

//first return value is the actual size.
//second return value is whether the type is dynamic
//both will be undefined if type is a mapping or internal function
//third return value is resulting allocations, INCLUDING the ones passed in
function abiSizeAndAllocate(
  dataType: Format.Types.Type,
  userDefinedTypes: Format.Types.TypesById,
  existingAllocations?: AbiAllocations
): AbiAllocationInfo {
  switch (dataType.typeClass) {
    case "bool":
    case "address":
    case "contract":
    case "int":
    case "uint":
    case "fixed":
    case "ufixed":
    case "enum":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: false,
        allocations: existingAllocations
      };

    case "string":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: true,
        allocations: existingAllocations
      };

    case "bytes":
      return {
        size: Evm.Utils.WORD_SIZE,
        dynamic: dataType.kind === "dynamic",
        allocations: existingAllocations
      };

    case "mapping":
      return {
        allocations: existingAllocations
      };

    case "function":
      switch (dataType.visibility) {
        case "external":
          return {
            size: Evm.Utils.WORD_SIZE,
            dynamic: false,
            allocations: existingAllocations
          };
        case "internal":
          return {
            allocations: existingAllocations
          };
      }

    case "array": {
      switch (dataType.kind) {
        case "dynamic":
          return {
            size: Evm.Utils.WORD_SIZE,
            dynamic: true,
            allocations: existingAllocations
          };
        case "static":
          if (dataType.length.isZero()) {
            //arrays of length 0 are static regardless of base type
            return {
              size: 0,
              dynamic: false,
              allocations: existingAllocations
            };
          }
          const { size: baseSize, dynamic, allocations } = abiSizeAndAllocate(
            dataType.baseType,
            userDefinedTypes,
            existingAllocations
          );
          return {
            //WARNING!  The use of toNumber() here may throw an exception!
            //I'm judging this OK since if you have arrays that large we have bigger problems :P
            size: dataType.length.toNumber() * baseSize,
            dynamic,
            allocations
          };
      }
    }

    case "struct": {
      let allocations: AbiAllocations = existingAllocations;
      let allocation: AbiAllocation | null | undefined =
        allocations[dataType.id];
      if (allocation === undefined) {
        //if we don't find an allocation, we'll have to do the allocation ourselves
        const storedType = <Format.Types.StructType>(
          userDefinedTypes[dataType.id]
        );
        if (!storedType) {
          throw new Common.UnknownUserDefinedTypeError(
            dataType.id,
            Format.Types.typeString(dataType)
          );
        }
        allocations = allocateStruct(
          storedType,
          userDefinedTypes,
          existingAllocations
        );
        allocation = allocations[storedType.id];
      }
      //having found our allocation, if it's not null, we can just look up its size and dynamicity
      if (allocation !== null) {
        return {
          size: allocation.length,
          dynamic: allocation.dynamic,
          allocations
        };
      }
      //if it is null, this type doesn't go in the abi
      else {
        return {
          allocations
        };
      }
    }

    case "tuple": {
      //Warning! Yucky wasteful recomputation here!
      let size = 0;
      let dynamic = false;
      //note that we don't just invoke allocateStruct here!
      //why not? because it has no ID to store the result in!
      //and we can't use a fake like -1 because there might be a recursive call to it,
      //and then the results would overwrite each other
      //I mean, we could do some hashing thing or something, but I think it's easier to just
      //copy the logic in this one case (sorry)
      for (let member of dataType.memberTypes) {
        let { size: memberSize, dynamic: memberDynamic } = abiSizeAndAllocate(
          member.type,
          userDefinedTypes,
          existingAllocations
        );
        size += memberSize;
        dynamic = dynamic || memberDynamic;
      }
      return { size, dynamic, allocations: existingAllocations };
    }
  }
}

//assumes you've already done allocation! don't use if you haven't!
/**
 * @protected
 */
export function abiSizeInfo(
  dataType: Format.Types.Type,
  allocations?: AbiAllocations
): AbiSizeInfo {
  let { size, dynamic } = abiSizeAndAllocate(dataType, null, allocations);
  //the above line should work fine... as long as allocation is already done!
  //the middle argument, userDefinedTypes, is only needed during allocation
  //again, this function is only for use if allocation is done, so it's safe to pass null here
  return { size, dynamic };
}

//allocates an external call
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
//NOTE: returns undefined if attempting to allocate a constructor but we don't have the
//bytecode for the constructor
function allocateCalldataAndReturndata(
  abiEntry: AbiData.FunctionAbiEntry | AbiData.ConstructorAbiEntry,
  contractNode: Ast.AstNode | undefined,
  referenceDeclarations: Ast.AstNodes,
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined,
  constructorContext?: Contexts.DecoderContext
): CalldataAndReturndataAllocation | undefined {
  //first: determine the corresponding function node
  //(simultaneously: determine the offset)
  let node: Ast.AstNode | undefined = undefined;
  let inputParametersFull: Ast.AstNode[];
  let outputParametersFull: Ast.AstNode[];
  let inputParametersAbi: AbiData.AbiParameter[];
  let outputParametersAbi: AbiData.AbiParameter[];
  let offset: number; //refers to INPUT offset; output offset is always 0
  switch (abiEntry.type) {
    case "constructor":
      if (!constructorContext) {
        return undefined;
      }
      let rawLength = constructorContext.binary.length;
      offset = (rawLength - 2) / 2; //number of bytes in 0x-prefixed bytestring
      //for a constructor, we only want to search the particular contract
      if (contractNode) {
        node = contractNode.nodes.find(functionNode =>
          AbiDataUtils.definitionMatchesAbi(
            //note this needn't actually be a function node, but then it will
            //return false (well, unless it's a getter node!)
            abiEntry,
            functionNode,
            referenceDeclarations
          )
        );
      }
      //if we can't find it, we'll handle this below
      break;
    case "function":
      offset = Evm.Utils.SELECTOR_SIZE;
      //search through base contracts, from most derived (left) to most base (right)
      if (contractNode) {
        const linearizedBaseContracts = contractNode.linearizedBaseContracts;
        node = linearizedBaseContracts.reduce(
          (foundNode: Ast.AstNode, baseContractId: number) => {
            if (foundNode !== undefined) {
              return foundNode; //once we've found something, we don't need to keep looking
            }
            let baseContractNode = referenceDeclarations[baseContractId];
            if (baseContractNode === undefined) {
              return null; //return null rather than undefined so that this will propagate through
              //(i.e. by returning null here we give up the search)
              //(we don't want to continue due to possibility of grabbing the wrong override)
            }
            return baseContractNode.nodes.find(
              //may be undefined! that's OK!
              functionNode =>
                AbiDataUtils.definitionMatchesAbi(
                  abiEntry,
                  functionNode,
                  referenceDeclarations
                )
            );
          },
          undefined //start with no node found
        );
      }
      break;
  }
  //now: get the parameters (both full-mode & ABI)
  if (node) {
    switch (node.nodeType) {
      case "FunctionDefinition":
        //normal case
        inputParametersFull = node.parameters.parameters;
        outputParametersFull = node.returnParameters.parameters; //this exists even for constructors!
        break;
      case "VariableDeclaration":
        //getter case
        ({
          inputs: inputParametersFull,
          outputs: outputParametersFull
        } = Ast.Utils.getterParameters(node, referenceDeclarations));
        break;
    }
  } else {
    inputParametersFull = undefined;
    outputParametersFull = undefined;
  }
  inputParametersAbi = abiEntry.inputs;
  switch (abiEntry.type) {
    case "function":
      outputParametersAbi = abiEntry.outputs;
    case "constructor":
      //we just leave this empty for constructors
      outputParametersAbi = [];
  }
  //now: do the allocation!
  let {
    allocation: abiAllocationInput,
    mode: inputMode
  } = allocateDataArguments(
    inputParametersFull,
    inputParametersAbi,
    userDefinedTypes,
    abiAllocations,
    compiler,
    offset
  );
  let {
    allocation: abiAllocationOutput,
    mode: outputMode
  } = allocateDataArguments(
    outputParametersFull,
    outputParametersAbi,
    userDefinedTypes,
    abiAllocations,
    compiler
    //note no offset
  );
  //finally: transform the allocation appropriately
  let inputArgumentsAllocation = abiAllocationInput.members.map(member => ({
    ...member,
    pointer: {
      location: "calldata" as const,
      start: member.pointer.start,
      length: member.pointer.length
    }
  }));
  let outputArgumentsAllocation = abiAllocationOutput.members.map(member => ({
    ...member,
    pointer: {
      location: "returndata" as const,
      start: member.pointer.start,
      length: member.pointer.length
    }
  }));
  let inputsAllocation: CalldataAllocation = {
    abi: abiEntry,
    offset,
    arguments: inputArgumentsAllocation,
    allocationMode: inputMode
  };
  let outputsAllocation: ReturndataAllocation;
  switch (abiEntry.type) {
    case "function":
      outputsAllocation = {
        selector: new Uint8Array(), //empty by default
        arguments: outputArgumentsAllocation,
        allocationMode: outputMode,
        kind: "return" as const
      };
      break;
    case "constructor":
      outputsAllocation = constructorOutputAllocation;
      break;
  }
  return { input: inputsAllocation, output: outputsAllocation };
}

interface AbiAllocationAndMode {
  allocation: AbiAllocation;
  mode: DecodingMode;
}

//note: allocateEvent doesn't use this because it needs additional
//handling for indexed parameters (maybe these can be unified in
//the future though?)
function allocateDataArguments(
  fullModeParameters: Ast.AstNode[] | undefined,
  abiParameters: AbiData.AbiParameter[],
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined,
  offset: number = 0
): AbiAllocationAndMode {
  let allocationMode: DecodingMode = fullModeParameters ? "full" : "abi"; //can degrade
  let parameterTypes: Format.Types.NameTypePair[];
  let abiAllocation: AbiAllocation;
  if (allocationMode === "full") {
    let id = "-1"; //fake ID that doesn't matter
    parameterTypes = fullModeParameters.map(parameter => ({
      name: parameter.name,
      type: Ast.Import.definitionToType(parameter, compiler) //if node is defined, compiler had also better be!
    }));
    //now: perform the allocation!
    try {
      abiAllocation = allocateMembers(
        id,
        parameterTypes,
        userDefinedTypes,
        abiAllocations,
        offset
      )[id];
    } catch {
      //if something goes wrong, switch to ABI mdoe
      allocationMode = "abi";
    }
  }
  if (allocationMode === "abi") {
    //THIS IS DELIBERATELY NOT AN ELSE
    //this is the ABI case.  we end up here EITHER
    //if node doesn't exist, OR if something went wrong
    //during allocation
    let id = "-1"; //fake irrelevant ID
    parameterTypes = abiParameters.map(parameter => ({
      name: parameter.name,
      type: Import.abiParameterToType(parameter)
    }));
    abiAllocation = allocateMembers(
      id,
      parameterTypes,
      userDefinedTypes,
      abiAllocations,
      offset
    )[id];
  }
  return { allocation: abiAllocation, mode: allocationMode };
}

interface EventParameterInfo {
  name: string;
  type: Format.Types.Type;
  indexed: boolean;
}

//allocates an event
//NOTE: returns just a single allocation; assumes primary allocation is already complete!
function allocateEvent(
  abiEntry: AbiData.EventAbiEntry,
  contractNode: Ast.AstNode | undefined,
  contextHash: string,
  referenceDeclarations: Ast.AstNodes,
  userDefinedTypes: Format.Types.TypesById,
  abiAllocations: AbiAllocations,
  compiler: Compiler.CompilerVersion | undefined
): EventAllocation | undefined {
  let parameterTypes: EventParameterInfo[];
  let id: string;
  //first: determine the corresponding event node
  //search through base contracts, from most derived (right) to most base (left)
  let node: Ast.AstNode | undefined = undefined;
  let definingContract: Ast.AstNode | undefined = undefined;
  let definedIn: Format.Types.ContractType | undefined = undefined;
  let allocationMode: DecodingMode = "full"; //degrade to abi as needed
  debug("allocating ABI: %O", abiEntry);
  if (contractNode) {
    let linearizedBaseContractsMinusSelf = contractNode.linearizedBaseContracts.slice();
    linearizedBaseContractsMinusSelf.shift(); //remove self
    //first: check same contract for the event
    node = contractNode.nodes.find(eventNode =>
      AbiDataUtils.definitionMatchesAbi(
        //note this needn't actually be an event node, but then it will
        //return false
        abiEntry,
        eventNode,
        referenceDeclarations
      )
    );
    if (node) {
      definingContract = contractNode;
    }
    //if we found the node, great!  If not...
    else {
      debug("didn't find node in base contract...");
      //let's search for the node among the base contracts.
      //but if we find it... we might not use it!
      ({ node, definingContract } = linearizedBaseContractsMinusSelf.reduce(
        (
          { node: foundNode, definingContract: foundContract },
          baseContractId: number
        ) => {
          if (foundNode !== undefined) {
            return foundNode; //once we've found something, we don't need to keep looking
            //note we check node, not definedIn, because latter can be defined even if
            //nothing found!
          }
          let baseContractNode = referenceDeclarations[baseContractId];
          if (baseContractNode === undefined) {
            return { node: null, definedIn: null }; //return null rather than undefined so that this will propagate through
            //(i.e. by returning null here we give up the search)
            //(we don't want to continue due to possibility of grabbing the wrong override)
          }
          let nodeFound = baseContractNode.nodes.find(
            //may be undefined! that's OK!
            eventNode =>
              AbiDataUtils.definitionMatchesAbi(
                //note this needn't actually be a event node, but then it will return false
                abiEntry,
                eventNode,
                referenceDeclarations
              )
          );
          return { node: nodeFound, definingContract: baseContractNode };
          //baseContractNode may be defined even if no node found, but we check node, not definedIn!
        },
        { node: undefined, definingContract: undefined } //start with nothing found
      ));
      if (node) {
        //...if we find the node in an ancestor, and that ancestor has a
        //is not an interface, we deliberately *don't* allocate!  instead such
        //cases will be handled during a later combination step
        //NOTE: This should *really* probably be, if that ancestor does not have
        //a deployed context.  However, we can't determine that down here, because
        //I didn't account for this problem originally!  While I could make it work
        //that way, I don't think it's enough of a difference to bother right now...
        if (definingContract.contractKind !== "interface") {
          return undefined;
        }
      }
    }
  }
  //otherwise, leave node and definingContract undefined
  if (node) {
    debug("found node");
    //if we found the node, let's also turn it into a type
    definedIn = <Format.Types.ContractType>(
      Ast.Import.definitionToStoredType(definingContract, compiler)
    ); //can skip 3rd argument here
  } else {
    //if no node, have to fall back into ABI mode
    debug("falling back to ABI because no node");
    allocationMode = "abi";
  }
  //now: construct the list of parameter types, attaching indexedness info
  //and overall position (for later reconstruction)
  let indexed: EventParameterInfo[];
  let nonIndexed: EventParameterInfo[];
  let abiAllocation: AbiAllocation; //the untransformed allocation for the non-indexed parameters
  if (allocationMode === "full") {
    let id = node.id.toString();
    let parameters = node.parameters.parameters;
    parameterTypes = parameters.map(definition => ({
      //note: if node is defined, compiler had better be defined, too!
      type: Ast.Import.definitionToType(definition, compiler),
      name: definition.name,
      indexed: definition.indexed
    }));
{'fraction_non_alphanumeric': 0.0877863474156786, 'fraction_numerical': 0.0006446414182111201, 'mean_word_length': 2.6354201540006694, 'pattern_counts': {'":': 54, '<': 7, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2757742', 'n_tokens_mistral': 11950, 'n_tokens_neox': 11666, 'n_words': 3811}