// Copyright (c) 2022, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // 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 Semver from 'semver'; import {BaseCompiler} from '../base-compiler.js'; import {asSafeVer} from '../utils.js'; import {ClangParser} from './argument-parsers.js'; export class SolidityCompiler extends BaseCompiler { static get key() { return 'solidity'; } override getSharedLibraryPathsAsArguments() { return []; } override getArgumentParser() { return ClangParser; } override optionsForFilter() { return [ // We use --combined-json instead of `--asm-json` to have compacted json '--combined-json', Semver.lt(asSafeVer(this.compiler.semver), '0.8.0', true) ? 'asm,ast' : 'asm,ast,generated-sources,generated-sources-runtime', '-o', 'contracts', ]; } override isCfgCompiler(/*compilerVersion*/) { return false; } override getOutputFilename(dirPath: string) { return path.join(dirPath, 'contracts/combined.json'); } override async processAsm(result) { // Handle "error" documents. if (!result.asm.includes('\n') && result.asm[0] === '<') { 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 hasOldJSONLayout = Semver.lt(asSafeVer(this.compiler.semver), '0.8.0', true); const hasGeneratedSources = Semver.gte(asSafeVer(this.compiler.semver), '0.8.0', true); const asm = JSON.parse(result.asm); return { asm: (Object.entries(asm.contracts) as [string, any][]) .sort(([_name1, data1], [_name2, data2]) => data1.asm['.code'][0].begin - data2.asm['.code'][0].begin) .map(([name, data]) => { // 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. let contractFunctions; // the layout of this JSON has changed between versions... if (hasOldJSONLayout) { contractFunctions = ( asm.sources[sourceName].AST.children.find(node => { return node.name === 'ContractDefinition' && node.attributes.name === contractName; }).children ?? [] ) .filter(node => { return node.name === 'FunctionDefinition'; }) .map(node => { const [begin, length] = node.src.split(':').map(x => parseInt(x)); let name = node.attributes.isConstructor ? 'constructor' : node.attributes.name; // encode the args into the name so we can // differentiate between overloads if (node.children[0].children.length > 0) { name += '_' + node.children[0].children .map(paramNode => { return paramNode.attributes.type; }) .join('_'); } return { name: name, begin: begin, end: begin + length, tagCount: 0, }; }); } else { 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 => { const 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; }; // (0.8.x onwards only!) // 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 = hasGeneratedSources ? processGeneratedSources(data['generated-sources']) : {}; const generatedSourcesRuntime = hasGeneratedSources ? 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 const 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 (from version 0.8.x onwards). // if source is undefined, then this is // a compiler version which doesn't // provide one (< 0.6.x), but we can // infer 0 in this case. if (opcode.source === 0 || opcode.source === undefined) { 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 [ {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']) as [string, any][]).map(([id, {'.code': code}]) => [ {text: `\t${id}:`}, processOpcodes(code, '\t', generatedSourcesRuntime), ]), ]; }) .flat(Infinity), }; } }