| // 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 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), |
| }; |
| } |
| } |