mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-11-10 04:37:11 +00:00
Improve compilation time in standard JSON mode and honor requested output (#461)
# Description
Compilation times for resolc were significantly slower than solc when:
* Multiple files were included in the JSON input file via
`--standard-json`
* And none of the files requested code generation
* (If only a subset of files requested code generation, both resolc and
solc were very slow)
Reason:
* Resolc unconditionally generated bytecode for all contracts, even if
not requested.
* When compiling to PolkaVM bytecode, we need to pass additional flags
(e.g. `irOptimized`) to solc. If these flags were not explicitly
requested, resolc also added these to the requested output since PolkaVM
bytecode was always generated.
## Breaking Changes
The following changes are breaking, but fix the semantics to match its
intended behavior:
* Compiling with `--standard-json` now compiles to PolkaVM bytecode
***only*** for the contracts explicitly requested in the
`outputSelection`.
* Compiling with `--standard-json` now outputs ***only*** the explicitly
requested output (e.g. `evm.assembly`, `evm.bytecode`, `ast`, etc.).
```jsonc
// Example:
{
"settings": {
// ...
"outputSelection": {
"src/common/GasService.sol": {
// Contracts in this file will generate bytecode.
// Only these fields of the JSON output selection will be in the `contracts` output.
"*": ["abi", "evm.methodIdentifiers", "metadata", "evm.bytecode"],
// Only this field of the JSON output selection will be in the `sources` output.
"": ["ast"]
},
"src/common/Gateway.sol": {
// No contracts in this file will generate bytecode.
"*": ["abi", "evm.methodIdentifiers", "metadata"],
// No `ast` will be in the `sources` output (only the automatically added `id`,
// similar to solc as this is not a configurable output selection).
"": []
},
}
}
}
```
## Performance Improvements
When e.g. the standard JSON input file includes multiple files and
requests output for every contract as per this setting:
```jsonc
{
"settings": {
// ...
"optimizer": { "enabled": true, "runs": 1 },
"viaIR": true,
"outputSelection": {
"src/common/GasService.sol": {
"*": ["abi", "evm.methodIdentifiers", "metadata", "evm.bytecode"],
},
// Rest of files..
}
}
}
```
The differences in compilation times depending on whether `evm.bytecode`
is selected in every file, one file, or no files can be seen below:
| Solc Optimizer (runs=1) | ViaIR | Request `abi`,
`evm.methodIdentifiers`, `metadata` | Request `evm.bytecode` | resolc
before | resolc after | solc v0.8.33 |
|:-----------------------:|:-----:|:---------:|:---------:|:-------------:|:------------:|:----------:|
| ✅ | ✅ | All files | All files | `~14.5s` | `~14.5s` | `~13.5s` |
| ✅ | ✅ | All files | One file | `~14.5s` | `~450ms` | `~13.5s` |
| ✅ | ✅ | All files | No files | `~14.5s` | `~390ms` | `~350ms` |
**Observations:**
* `resolc before` is roughly the same no matter the request
* This is due to unconditionally generating bytecode
* When not requesting bytecode for any contracts:
* `resolc after` now compiles in ~390ms (vs ~14.5s), similar to `solc`
* Neither `solc` nor `resolc after` generates bytecode when none of the
contracts request it
* When requesting bytecode for all contracts in _only one_ file:
* `resolc after` now compiles in ~450ms (vs ~14.5s)
* `solc` still compiles in the same time as when requesting bytecode for
all contracts in all files
* This is something they have tried to improve (see
[Solidity#15373](https://github.com/argotorg/solidity/issues/15373) and
[Solidity#15615](https://github.com/argotorg/solidity/issues/15615)),
and the intended behavior seems to now be that bytecode should only be
generated when explicitly requested for each contract.
* Even though PRs have merged to try to fix it, their issue seems to
persist as of v0.8.33.
# Open Questions
* Foundry:
* [x] Pruning of the AST:
* Just saw [this past
commit](https://github.com/paritytech/revive/pull/385) to not prune the
AST for Foundry compatibility.
* Can Foundry not explicitly request `"": "ast"`?
* **Answer:**
* Pruning the AST when not requested should be fine, as long as Foundry
explicitly requests it when needed.
* Will be tested by Foundry maintainer.
# Review Notes
Changes in `docs/` (~25 files) can be skipped as they are automatically
updated after adding content to `book/` and rebuilding it.
# Resolved Issues
Closes #326
---------
Co-authored-by: xermicus <cyrill@parity.io>
This commit is contained in:
1 parent
95ae938ddf
commit
a4163512ff
52 files changed
+3827
-187
No files matched your search
@@ -1,17 +1,179 @@
|
||||
//! The tests for running resolc with standard JSON option.
|
||||
|
||||
use revive_solc_json_interface::{
|
||||
PolkaVMDefaultHeapMemorySize, PolkaVMDefaultStackMemorySize, SolcStandardJsonOutput,
|
||||
PolkaVMDefaultHeapMemorySize, PolkaVMDefaultStackMemorySize,
|
||||
SolcStandardJsonInputSettingsSelectionFileFlag, SolcStandardJsonOutput,
|
||||
};
|
||||
|
||||
use crate::cli_utils::{
|
||||
assert_command_success, assert_equal_exit_codes, execute_resolc_with_stdin_input,
|
||||
execute_solc_with_stdin_input, STANDARD_JSON_CONTRACTS_PATH,
|
||||
execute_solc_with_stdin_input, STANDARD_JSON_ALL_OUTPUTS_PATH, STANDARD_JSON_CONTRACTS_PATH,
|
||||
STANDARD_JSON_NO_EVM_CODEGEN_COMPLEX_PATH, STANDARD_JSON_NO_EVM_CODEGEN_PATH,
|
||||
STANDARD_JSON_NO_PVM_CODEGEN_PER_FILE_PATH, STANDARD_JSON_PVM_CODEGEN_ALL_WILDCARD_PATH,
|
||||
STANDARD_JSON_PVM_CODEGEN_ONE_FILE_PATH, STANDARD_JSON_PVM_CODEGEN_PER_FILE_PATH,
|
||||
STANDARD_JSON_YUL_NO_PVM_CODEGEN_PATH, STANDARD_JSON_YUL_PVM_CODEGEN_PATH,
|
||||
};
|
||||
|
||||
const JSON_OPTION: &str = "--standard-json";
|
||||
|
||||
/// A subset of contracts and sources expected to exist in the JSON output.
|
||||
struct ExpectedOutput {
|
||||
contracts: Vec<ExpectedContract>,
|
||||
sources: Vec<ExpectedSource>,
|
||||
}
|
||||
|
||||
/// A contract expected to exist in the JSON output.
|
||||
struct ExpectedContract {
|
||||
/// The file path.
|
||||
path: &'static str,
|
||||
/// The contract name.
|
||||
name: &'static str,
|
||||
/// All contract-level fields of the JSON output selection expected to exist,
|
||||
/// such as `metadata`, `irOptimized`, etc. If `evm.bytecode` was requested,
|
||||
/// both `evm` and `evm.bytecode` should be expected.
|
||||
fields: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// A source expected to exist in the JSON output.
|
||||
struct ExpectedSource {
|
||||
/// The file path.
|
||||
path: &'static str,
|
||||
/// All file-level fields of the JSON output selection expected to exist,
|
||||
/// such as `ast`, as well as the `id` field.
|
||||
fields: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// Asserts that the `expected` subset of contracts and sources match the ones in the `actual` output.
|
||||
/// If expected sources or contracts are empty, asserts that the respective actual output is also empty.
|
||||
fn assert_output_matches(actual: &SolcStandardJsonOutput, expected: &ExpectedOutput) {
|
||||
assert_sources_output_matches(actual, expected);
|
||||
assert_contracts_output_matches(actual, expected);
|
||||
}
|
||||
|
||||
/// Asserts that the `expected` subset of sources match the ones in the `actual` output.
|
||||
/// If expected sources is empty, asserts that the actual output is also empty.
|
||||
fn assert_sources_output_matches(actual: &SolcStandardJsonOutput, expected: &ExpectedOutput) {
|
||||
if expected.sources.is_empty() {
|
||||
assert!(actual.sources.is_empty(), "sources should not be populated");
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
actual.sources.len() >= expected.sources.len(),
|
||||
"at least {} sources should be populated",
|
||||
expected.sources.len()
|
||||
);
|
||||
|
||||
for expected_source in &expected.sources {
|
||||
let actual_source = actual
|
||||
.sources
|
||||
.get(expected_source.path)
|
||||
.unwrap_or_else(|| panic!("the file `{}` should exist", expected_source.path));
|
||||
let actual_source_json = serde_json::to_value(actual_source).unwrap();
|
||||
|
||||
// Verify that every expected output exists.
|
||||
for field in &expected_source.fields {
|
||||
assert!(
|
||||
actual_source_json.get(field).is_some(),
|
||||
"the `{field}` for the file `{}` should be generated",
|
||||
expected_source.path
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that every unexpected output is omitted.
|
||||
let all_fields = &["id", "ast"];
|
||||
for field in all_fields {
|
||||
if !expected_source.fields.contains(field) {
|
||||
assert!(
|
||||
actual_source_json.get(field).is_none(),
|
||||
"the `{field}` for the file `{}` should not be generated",
|
||||
expected_source.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that the `expected` subset of contracts match the ones in the `actual` output.
|
||||
/// If expected contracts is empty, asserts that the actual output is also empty.
|
||||
fn assert_contracts_output_matches(actual: &SolcStandardJsonOutput, expected: &ExpectedOutput) {
|
||||
if expected.contracts.is_empty() {
|
||||
assert!(
|
||||
actual.contracts.is_empty(),
|
||||
"contracts should not be populated"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assert!(
|
||||
actual.contracts.len() >= expected.contracts.len(),
|
||||
"at least {} contracts should be populated",
|
||||
expected.contracts.len()
|
||||
);
|
||||
|
||||
for expected_contract in &expected.contracts {
|
||||
let actual_contract = actual
|
||||
.contracts
|
||||
.get(expected_contract.path)
|
||||
.unwrap_or_else(|| panic!("the file `{}` should exist", expected_contract.path))
|
||||
.get(expected_contract.name)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"the contract `{}` in file `{}` should exist",
|
||||
expected_contract.name, expected_contract.path
|
||||
)
|
||||
});
|
||||
let actual_contract_json = serde_json::to_value(actual_contract).unwrap();
|
||||
|
||||
// Verify that every expected output exists (e.g. `evm.bytecode`).
|
||||
for field in &expected_contract.fields {
|
||||
let mut parts = field.split('.');
|
||||
let (parent_field, child_field) = (parts.next().unwrap(), parts.next());
|
||||
let parent_output = actual_contract_json.get(parent_field);
|
||||
|
||||
assert!(
|
||||
parent_output.is_some(),
|
||||
"the `{parent_field}` for the contract `{}` should be generated",
|
||||
expected_contract.name,
|
||||
);
|
||||
if let Some(child_field) = child_field {
|
||||
assert!(
|
||||
parent_output.unwrap().get(child_field).is_some(),
|
||||
"the `{field}` for the contract `{}` should be generated",
|
||||
expected_contract.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that every unexpected output is omitted.
|
||||
for flag in SolcStandardJsonInputSettingsSelectionFileFlag::all() {
|
||||
let field = serde_json::to_string(flag)
|
||||
.unwrap()
|
||||
.trim_matches('"')
|
||||
.to_owned();
|
||||
if !expected_contract.fields.contains(&field.as_str()) {
|
||||
let mut parts = field.split('.');
|
||||
let (parent_field, child_field) = (parts.next().unwrap(), parts.next());
|
||||
let parent_output = actual_contract_json.get(parent_field);
|
||||
|
||||
if let Some(child_field) = child_field {
|
||||
assert!(
|
||||
parent_output.is_none_or(|p| p.get(child_field).is_none()),
|
||||
"the `{field}` for the contract `{}` should not be generated",
|
||||
expected_contract.name,
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
parent_output.is_none(),
|
||||
"the `{parent_field}` for the contract `{}` should not be generated",
|
||||
expected_contract.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that the standard JSON output has at least one error with the given `error_message`.
|
||||
fn assert_standard_json_errors_contain(output: &SolcStandardJsonOutput, error_message: &str) {
|
||||
assert!(
|
||||
@@ -23,6 +185,11 @@ fn assert_standard_json_errors_contain(output: &SolcStandardJsonOutput, error_me
|
||||
);
|
||||
}
|
||||
|
||||
/// Asserts that the standard JSON output has no errors with severity `error`.
|
||||
fn assert_no_errors(output: &SolcStandardJsonOutput) {
|
||||
assert!(!output.errors.iter().any(|error| error.is_error()));
|
||||
}
|
||||
|
||||
/// Converts valid JSON text to `SolcStandardJsonOutput`.
|
||||
fn to_solc_standard_json_output(json_text: &str) -> SolcStandardJsonOutput {
|
||||
serde_json::from_str(json_text).unwrap()
|
||||
@@ -35,7 +202,7 @@ fn runs_with_valid_input_file() {
|
||||
assert_command_success(&resolc_result, "Providing a valid input file to stdin");
|
||||
|
||||
let resolc_output = to_solc_standard_json_output(&resolc_result.stdout);
|
||||
assert!(!resolc_output.errors.iter().any(|error| error.is_error()));
|
||||
assert_no_errors(&resolc_output);
|
||||
|
||||
assert!(
|
||||
resolc_result.stdout.contains("contracts"),
|
||||
@@ -52,7 +219,7 @@ fn no_evm_codegen_requested() {
|
||||
assert_command_success(&result, "EVM codegen std json input fixture should build");
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert!(!output.errors.iter().any(|msg| msg.severity == "error"))
|
||||
assert_no_errors(&output);
|
||||
}
|
||||
|
||||
/// A variant with more complex contracts.
|
||||
@@ -71,7 +238,229 @@ fn no_evm_codegen_requested_complex() {
|
||||
);
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert!(!output.errors.iter().any(|msg| msg.severity == "error"))
|
||||
assert_no_errors(&output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_outputs_requested() {
|
||||
let result = execute_resolc_with_stdin_input(&[JSON_OPTION], STANDARD_JSON_ALL_OUTPUTS_PATH);
|
||||
assert_command_success(&result, "the all output std JSON input fixture");
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert_no_errors(&output);
|
||||
|
||||
let expected_contract_fields = &[
|
||||
"abi",
|
||||
"metadata",
|
||||
"devdoc",
|
||||
"userdoc",
|
||||
"storageLayout",
|
||||
"irOptimized",
|
||||
"ir",
|
||||
"evm",
|
||||
"evm.methodIdentifiers",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.assembly",
|
||||
];
|
||||
let expected = ExpectedOutput {
|
||||
contracts: vec![
|
||||
ExpectedContract {
|
||||
path: "src/Counter.sol",
|
||||
name: "Counter",
|
||||
fields: expected_contract_fields.into(),
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "script/Counter.s.sol",
|
||||
name: "CounterScript",
|
||||
fields: expected_contract_fields.into(),
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "lib/forge-std/src/mocks/MockERC20.sol",
|
||||
name: "MockERC20",
|
||||
fields: expected_contract_fields.into(),
|
||||
},
|
||||
],
|
||||
sources: vec![
|
||||
ExpectedSource {
|
||||
path: "src/Counter.sol",
|
||||
fields: vec!["id", "ast"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "script/Counter.s.sol",
|
||||
fields: vec!["id", "ast"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "lib/forge-std/src/mocks/MockERC20.sol",
|
||||
fields: vec!["id", "ast"],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_output_matches(&output, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pvm_codegen_requested() {
|
||||
let files = &[
|
||||
STANDARD_JSON_PVM_CODEGEN_ALL_WILDCARD_PATH,
|
||||
STANDARD_JSON_PVM_CODEGEN_PER_FILE_PATH,
|
||||
STANDARD_JSON_PVM_CODEGEN_ONE_FILE_PATH,
|
||||
];
|
||||
|
||||
for file in files {
|
||||
let result = execute_resolc_with_stdin_input(&[JSON_OPTION], file);
|
||||
assert_command_success(&result, &format!("the `{file}` input fixture"));
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert_no_errors(&output);
|
||||
|
||||
let requests_codegen_for_one_file = *file == STANDARD_JSON_PVM_CODEGEN_ONE_FILE_PATH;
|
||||
let expected_contract_fields_with_codegen = &[
|
||||
"abi",
|
||||
"metadata",
|
||||
"evm",
|
||||
"evm.bytecode",
|
||||
"evm.methodIdentifiers",
|
||||
];
|
||||
let expected_contract_fields_without_codegen =
|
||||
&["abi", "metadata", "evm", "evm.methodIdentifiers"];
|
||||
|
||||
let expected = ExpectedOutput {
|
||||
contracts: vec![
|
||||
ExpectedContract {
|
||||
path: "src/common/GasService.sol",
|
||||
name: "GasService",
|
||||
// This is the contract expected to have codegen for all files tested here.
|
||||
fields: expected_contract_fields_with_codegen.into(),
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "src/common/Gateway.sol",
|
||||
name: "Gateway",
|
||||
fields: if requests_codegen_for_one_file {
|
||||
expected_contract_fields_without_codegen.into()
|
||||
} else {
|
||||
expected_contract_fields_with_codegen.into()
|
||||
},
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "src/common/MessageDispatcher.sol",
|
||||
name: "MessageDispatcher",
|
||||
fields: if requests_codegen_for_one_file {
|
||||
expected_contract_fields_without_codegen.into()
|
||||
} else {
|
||||
expected_contract_fields_with_codegen.into()
|
||||
},
|
||||
},
|
||||
],
|
||||
sources: vec![
|
||||
ExpectedSource {
|
||||
path: "src/common/GasService.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "src/common/Gateway.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "src/common/MessageDispatcher.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_output_matches(&output, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pvm_codegen_requested() {
|
||||
let result =
|
||||
execute_resolc_with_stdin_input(&[JSON_OPTION], STANDARD_JSON_NO_PVM_CODEGEN_PER_FILE_PATH);
|
||||
assert_command_success(
|
||||
&result,
|
||||
"the no PVM codegen std JSON per file input fixture",
|
||||
);
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert_no_errors(&output);
|
||||
|
||||
let expected = ExpectedOutput {
|
||||
contracts: vec![
|
||||
ExpectedContract {
|
||||
path: "src/common/GasService.sol",
|
||||
name: "GasService",
|
||||
fields: vec!["abi", "evm", "evm.methodIdentifiers", "metadata"],
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "src/common/Gateway.sol",
|
||||
name: "Gateway",
|
||||
fields: vec!["abi", "evm", "evm.methodIdentifiers", "metadata"],
|
||||
},
|
||||
ExpectedContract {
|
||||
path: "src/common/MessageDispatcher.sol",
|
||||
name: "MessageDispatcher",
|
||||
fields: vec!["abi", "evm", "evm.methodIdentifiers", "metadata"],
|
||||
},
|
||||
],
|
||||
sources: vec![
|
||||
ExpectedSource {
|
||||
path: "src/common/GasService.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "src/common/Gateway.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
ExpectedSource {
|
||||
path: "src/common/MessageDispatcher.sol",
|
||||
fields: vec!["id"],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_output_matches(&output, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yul_pvm_codegen_requested() {
|
||||
let result =
|
||||
execute_resolc_with_stdin_input(&[JSON_OPTION], STANDARD_JSON_YUL_PVM_CODEGEN_PATH);
|
||||
assert_command_success(&result, "the PVM codegen from Yul std JSON input fixture");
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert_no_errors(&output);
|
||||
|
||||
let expected = ExpectedOutput {
|
||||
contracts: vec![ExpectedContract {
|
||||
path: "Test",
|
||||
name: "Return",
|
||||
fields: vec![
|
||||
"evm",
|
||||
"evm.bytecode",
|
||||
"evm.deployedBytecode",
|
||||
"evm.assembly",
|
||||
],
|
||||
}],
|
||||
sources: vec![],
|
||||
};
|
||||
assert_output_matches(&output, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yul_no_pvm_codegen_requested() {
|
||||
let result =
|
||||
execute_resolc_with_stdin_input(&[JSON_OPTION], STANDARD_JSON_YUL_NO_PVM_CODEGEN_PATH);
|
||||
assert_command_success(
|
||||
&result,
|
||||
"the no PVM codegen from Yul std JSON input fixture",
|
||||
);
|
||||
|
||||
let output = to_solc_standard_json_output(&result.stdout);
|
||||
assert_no_errors(&output);
|
||||
|
||||
let expected = ExpectedOutput {
|
||||
contracts: vec![],
|
||||
sources: vec![],
|
||||
};
|
||||
assert_output_matches(&output, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user