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:
LJandxermicus authored and GitHub committed 2026-01-28 23:04:22 +01:00
1 parent 95ae938ddf
commit a4163512ff
52 files changed
+3827 -187

No files matched your search

+394 -5
View File
@@ -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]