mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
* Minor updates only * Added explicit radix parameter (10) to all Number.parseInt() calls throughout the codebase (new lint rule) * Updated several @ts-ignore comments to @ts-expect-error for better TypeScript practices (new lint rule) * Removed unnecessary @ts-ignore comments in some mode files (ditto) * Used "none return" based arrow functions for some map stuff * Replaced a `map()` call that didn't return anything to a for() loop * Fixed up some cypress stuff, noting work for the future
283 lines
13 KiB
TypeScript
283 lines
13 KiB
TypeScript
// 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 'node:fs/promises';
|
|
import path from 'node: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 getArgumentParserClass() {
|
|
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() {
|
|
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 = await fs.readFile(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);
|
|
if (!asm.contracts) {
|
|
return {asm: [{text: result.asm}]};
|
|
}
|
|
return {
|
|
asm: (Object.entries(asm.contracts) as [string, any][])
|
|
.filter(([_name, data]) => 'asm' in data) // ignore external contracts
|
|
.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 => Number.parseInt(x, 10));
|
|
|
|
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 => Number.parseInt(x, 10));
|
|
|
|
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 => Number.parseInt(x, 10));
|
|
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}]) => {
|
|
// some .data sections do not contain embedded .code
|
|
if (code === undefined) return [];
|
|
return [{text: `\t${id}:`}, processOpcodes(code, '\t', generatedSourcesRuntime)];
|
|
}),
|
|
];
|
|
})
|
|
.flat(Number.POSITIVE_INFINITY),
|
|
};
|
|
}
|
|
}
|