Invoking the compiler
In being able to test a pointer dereference implementation, it is necessary to pair each tested pointer with associated EVM code that makes the pointer meaningful. To avoid solutions such as pre-compiling Solidity or handwriting EVM bytecode, the @ethdebug/pointers reference implementation's integration tests are written so that each test case is described in terms of Solidity code that the testing infrastructure compiles when executing the test.
The strategy taken by these tests is to use Solidity's constructor
mechanism
for allowing tests to specify variable assignment and mutation logic without
needing to manage deployed contract instances. All these integration test cases
thus observe pointers only via the trace of a contract creation transaction.
Integration logic
This testing infrastructure includes the compileCreateBytecode()
function,
which accepts input resembling Solidity's compiler input data as argument (i.e.,
the collection of source contents by path and additional contract target
information) and asynchronously returns Data
with the create (deployment)
bytecode for the target contract.
/**
* Compile a collection of sources and return the create (deployment) bytecode
* for a particular target contract
*/
export async function compileCreateBytecode({
sources,
target
}: CompileOptions): Promise<Data> {
if (!solc) {
throw new Error("Unable to load solc");
}
const input = {
language: "Solidity",
sources,
settings: {
outputSelection: {
"*": {
"*": ["ir", "*"],
"": ["*"]
}
},
viaIR: true,
optimizer: {
enabled: true
}
}
};
const output = JSON.parse(
solc.compile(
JSON.stringify(input),
)
);
const { errors = [] } = output;
if (errors.length > 0) {
throw new Error(`Compilation error: ${JSON.stringify(errors, undefined, 2)}`);
}
const {
evm: {
bytecode: createBytecode
}
} = output.contracts[target.path][target.contractName];
return Data.fromHex(`0x${createBytecode.object}`);
}
The CompileOptions
interface
/**
* Organizes the sources being compiled by their path identifier, as well
* as includes information about which contract's bytecode is desired
*/
export interface CompileOptions {
sources: {
[path: string]: {
content: string
}
};
target: {
path: string;
contractName: string;
};
}
"Syntactic sugar"-like helper function
To avoid test cases' needing to describe their associated code samples in terms
of source content by path, test cases that require only a single source file can
use the singleSourceCompilation()
helper function that provides a more
succinct method for generating CompileOptions
objects:
/**
* "Syntactic sugar"-like helper function to initialize CompileOptions for
* compiling only a single source file.
*/
export function singleSourceCompilation(options: {
path: string;
contractName: string;
content: string;
}): CompileOptions {
const { path, contractName, content: contentWithoutHeader } = options;
const spdxLicenseIdentifier = "// SPDX-License-Identifier: UNLICENSED";
const pragma = "pragma solidity ^0.8.25;";
const header = `${spdxLicenseIdentifier}\n${pragma}\n`;
return {
sources: {
[path]: {
content: `${header}\n${contentWithoutHeader}\n`
}
},
target: {
path,
contractName
}
};
}