diff options
Diffstat (limited to 'lib/compilers/solidity.js')
-rw-r--r-- | lib/compilers/solidity.js | 184 |
1 files changed, 155 insertions, 29 deletions
diff --git a/lib/compilers/solidity.js b/lib/compilers/solidity.js index 6053de4ce..383177ff0 100644 --- a/lib/compilers/solidity.js +++ b/lib/compilers/solidity.js @@ -22,16 +22,17 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. +import * as fs from 'fs'; import path from 'path'; -import _ from 'underscore'; +import {BaseCompiler} from '../base-compiler'; -import { BaseCompiler } from '../base-compiler'; - -import { ClangParser } from './argument-parsers'; +import {ClangParser} from './argument-parsers'; export class SolidityCompiler extends BaseCompiler { - static get key() { return 'solidity'; } + static get key() { + return 'solidity'; + } getSharedLibraryPathsAsArguments() { return []; @@ -43,8 +44,11 @@ export class SolidityCompiler extends BaseCompiler { optionsForFilter(filters, outputFilename, userOptions) { return [ - '--combined-json', 'asm', // We use it instead of `--asm-json` to have compacted json - '-o', 'contracts', + // We use --combined-json instead of `--asm-json` to have compacted json + '--combined-json', + 'asm,ast,generated-sources,generated-sources-runtime', + '-o', + 'contracts', ]; } @@ -62,36 +66,158 @@ export class SolidityCompiler extends BaseCompiler { return {asm: [{text: result.asm}]}; } + // solc gives us a character range for each asm instruction, + // so open the input file and figure out what line each + // character is on + const inputFile = fs.readFileSync(result.inputFilename); + let currentLine = 1; + const charToLine = inputFile.map(c => { + const line = currentLine; + if (c === '\n'.codePointAt(0)) { + ++currentLine; + } + return line; + }); + + const asm = JSON.parse(result.asm); return { - asm: Object.entries(JSON.parse(result.asm).contracts) - .sort(([_name1, data1], [_name2, data2]) => - data1.asm['.code'][0].begin - data2.asm['.code'][0].begin, - ) + asm: Object.entries(asm.contracts) + .sort(([_name1, data1], [_name2, data2]) => data1.asm['.code'][0].begin - data2.asm['.code'][0].begin) .map(([name, data]) => { - const processOpcodes = (opcodes, indent) => opcodes - .map(opcode => - `${opcode.name}${opcode.value !== undefined ? ` ${opcode.value}` : ''}`, - ) - .map(opcode => - (indent || '') + (opcode.startsWith('tag') ? opcode : `\t${opcode}`), - ); + // name is in the format of file:contract + // e.g. MyFile.sol:MyContract + const [sourceName, contractName] = name.split(':'); + + // to make the asm more readable, we rename the + // tags (jumpdests) to show what function they're + // part of. here we parse the AST so we know what + // range of characters belongs to each function. + const contractFunctions = asm.sources[sourceName].AST.nodes + .find(node => { + return node.nodeType === 'ContractDefinition' && node.name === contractName; + }) + .nodes.filter(node => { + return node.nodeType === 'FunctionDefinition'; + }) + .map(node => { + const [begin, length] = node.src.split(':').map(x => parseInt(x)); + + let name = node.kind === 'constructor' ? 'constructor' : node.name; + + // encode the args into the name so we can + // differentiate between overloads + if (node.parameters.parameters.length > 0) { + name += + '_' + + node.parameters.parameters + .map(paramNode => { + return paramNode.typeName.name; + }) + .join('_'); + } + + return { + name: name, + begin: begin, + end: begin + length, + tagCount: 0, + }; + }); + + // solc generates some code, for things like detecting + // and reverting if a multiplication results in + // integer overflow, etc. + const processGeneratedSources = generatedSourcesData => { + let generatedSources = {}; + for (const generatedSource of generatedSourcesData) { + generatedSources[generatedSource.id] = generatedSource.ast.statements.map(statement => { + const [begin, length] = statement.src.split(':').map(x => parseInt(x)); + return { + name: statement.name, + begin: begin, + end: begin + length, + tagCount: 0, + }; + }); + } + return generatedSources; + }; + + // there are two sets of generated sources, one for the code which deploys + // the contract (i.e. the constructor) 'generated-sources', and the code + // which is deployed and stored on-chain 'generated-sources-runtime' + const generatedSources = processGeneratedSources(data['generated-sources']); + const generatedSourcesRuntime = processGeneratedSources(data['generated-sources-runtime']); + + const processOpcodes = (opcodes, indent, generatedSources) => { + // first iterate the opcodes to find all the tags, + // and assign human-readable names to as many of + // them as we can + let tagNames = {}; + const processPossibleTagOpcode = (opcode, funcList) => { + if (opcode.name === 'tag') { + const func = funcList.find(func => { + return opcode.begin >= func.begin && opcode.end <= func.end; + }); + + if (func !== undefined) { + // a function can have multiple tags, so append + // a number to each + const tagName = `${func.name}_${func.tagCount}`; + + ++func.tagCount; + + tagNames[opcode.value] = tagName; + opcode.value = tagName; + } + } + }; + + for (const opcode of opcodes) { + // source 0 is the .sol file the user is + // editing, everything else is generated + // sources + if (opcode.source === 0) { + opcode.line = charToLine[opcode.begin]; + + processPossibleTagOpcode(opcode, contractFunctions); + } else { + processPossibleTagOpcode(opcode, generatedSources[opcode.source]); + } + } + + return opcodes.map(opcode => { + const name = `${opcode.name.startsWith('tag') ? indent : `${indent}\t`}${opcode.name}`; + + let value = opcode.value || ''; + if (opcode.name === 'PUSH [tag]') { + if (tagNames[value] !== undefined) { + value = tagNames[value]; + } + } + + return { + text: `${name} ${value}`, + source: {line: opcode.line, file: null}, + }; + }); + }; return [ - `// ${_.last(name.split(':'))}`, - '.code', - processOpcodes(data.asm['.code']), - '.data', + {text: `// ${contractName}`}, + // .code section is the code only run when deploying - the constructor + {text: '.code'}, + processOpcodes(data.asm['.code'], '', generatedSources), + {text: ''}, + // .data section is deployed bytecode - everything else + {text: '.data'}, Object.entries(data.asm['.data']).map(([id, {'.code': code}]) => [ - `\t${id}:`, - '\t\t.code', - processOpcodes(code, '\t\t'), + {text: `\t${id}:`}, + processOpcodes(code, '\t', generatedSourcesRuntime), ]), - '\n', ]; }) - .flat(Infinity) - .slice(0, -1) - .map(line => ({text: line})), + .flat(Infinity), }; } } |