Skip to content

feat(forge): make solar analysis optional in test runner#14255

Closed
decofe wants to merge 7 commits into
masterfrom
zerosnacks/test-runner-support
Closed

feat(forge): make solar analysis optional in test runner#14255
decofe wants to merge 7 commits into
masterfrom
zerosnacks/test-runner-support

Conversation

@decofe
Copy link
Copy Markdown
Contributor

@decofe decofe commented Apr 10, 2026

Summary

Decouple the test runner from requiring Solidity-specific internals, enabling non-Solidity compilers to use Foundry's fuzzing/testing/cheatcode infrastructure.

Motivation

Tweet thread from @real_philogy requesting the ability to plug a custom compiler into Foundry. Companion to foundry-rs/compilers#368.

Changes

Core: analysis made optional

  • MultiContractRunner.analysis: Arc<solar::sema::Compiler>Option<Arc<...>>
  • TestRunnerConfig::executor: conditionally calls set_analysis only when Some

New compiler-agnostic entry point

  • PreLinkedArtifacts struct — bundles deployable contracts, known contracts, libs, fuzz literals, and inline config
  • MultiContractRunnerBuilder::build_from_artifacts() — builds a test runner from pre-linked artifacts directly, bypassing ProjectCompileOutput, solar, and the linker entirely

Bridge types (local mirrors of foundry-compilers types)

  • FuzzLiteral enum + LiteralsDictionary::from_fuzz_literals(literals, max_values) — converts compiler-provided literals to LiteralMaps with the same semantics as the solar path (uint/int width fan-out, string hashing, right-padding, max cap)
  • InlineConfigEntry + InlineConfig::from_entries(entries, profiles) — converts compiler-provided config overrides to NatSpec-based InlineConfig
  • LiteralsDictionary::from_maps() — direct constructor from pre-built LiteralMaps
  • InlineConfig::from_natspecs() — constructor from pre-parsed NatSpec entries

No behavior change

The standard Solidity path still populates all fields via the existing build() method.

Testing

cargo check -p forge and cargo clippy -p forge -p foundry-config -p foundry-evm-fuzz -- -D warnings pass.

How a non-Solidity compiler would use this

// 1. Compile your language to ABI + bytecode
let (deployable, known, libs) = my_compiler.compile_and_link(sources)?;

// 2. Optionally extract fuzz literals from your AST
let fuzz_dict = LiteralsDictionary::from_fuzz_literals(my_literals, max_values);

// 3. Optionally provide per-test config
let inline_cfg = InlineConfig::from_entries(my_config_entries, &profiles)?;

// 4. Build the runner — fuzzing, cheatcodes, invariant testing all work
let runner = MultiContractRunnerBuilder::new(config)
    .build_from_artifacts(
        PreLinkedArtifacts {
            deployable_contracts: deployable,
            known_contracts: known,
            libs_to_deploy: libs,
            fuzz_literals: fuzz_dict,
            inline_config: inline_cfg,
        },
        evm_env, tx_env, evm_opts,
    )?;

Next steps

  • Add integration test exercising the build_from_artifacts path with analysis: None
  • Once foundry-compilers publishes TestRunnerSupport trait, replace local FuzzLiteral/InlineConfigEntry mirrors with re-exports

Prompted by: zerosnacks

Decouple the test runner from requiring a solar compiler instance:

- Make `analysis` field optional (`Option<Arc<solar::sema::Compiler>>`)
  in MultiContractRunner, allowing non-Solidity compilers to skip it
- Add `LiteralsDictionary::from_maps()` for pre-collected fuzz literals
- Add `InlineConfig::from_natspecs()` for pre-parsed inline config

No behavior change — the standard Solidity path still populates all
fields. This is groundwork for compiler-agnostic test runner support.

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
decofe and others added 4 commits April 10, 2026 18:49
Add PreLinkedArtifacts struct and build_from_artifacts() method on
MultiContractRunnerBuilder, enabling non-Solidity compilers to use
Foundry's test runner without going through ProjectCompileOutput.

Also adds:
- FuzzLiteral enum + LiteralsDictionary::from_fuzz_literals() bridge
  with max_values cap (matching existing solar path behavior)
- InlineConfigEntry + InlineConfig::from_entries() bridge for
  per-test config overrides from any compiler

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Add tests for:
- FuzzLiteral → LiteralMaps bridge (7 tests covering address, uint, int,
  string, fixed bytes, dynamic bytes, and max cap enforcement)
- InlineConfigEntry → InlineConfig bridge (4 tests covering empty, function
  level, contract level, and invalid profile rejection)

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
- Export PreLinkedArtifacts from forge crate
- Export InlineConfigEntry from foundry-config crate
- from_entries() now adds forge-config: prefix automatically so
  compiler implementors don't need to know about Foundry internals
- Update tests to match new prefix-free API

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
- Add libraries: Libraries field so build_from_artifacts() has parity
  with build() instead of silently defaulting to empty
- Add #[derive(Debug)] for diagnostics

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
@decofe decofe marked this pull request as ready for review April 10, 2026 19:36
@decofe decofe marked this pull request as draft April 10, 2026 19:46
decofe and others added 2 commits April 10, 2026 20:08
Proves a non-Solidity compiler can use Foundry's test runner:
- Creates a minimal contract from raw EVM bytecode (13-byte constructor
  deploying a 1-byte STOP runtime) with a testAlwaysPass() ABI entry
- Builds a MultiContractRunner via build_from_artifacts() with no
  Solidity compilation, no solar analysis, no linker
- Runs tests and verifies: 1 contract, 0 failures, 1 passing test

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
Replace minimal STOP-only contract with a multi-function contract that
exercises the full test runner lifecycle from raw EVM bytecode:

- setUp() stores 0x42 at slot 0
- testAlwaysPass() returns (STOP) → pass
- testAlwaysFail() reverts → fail
- testStorageWasSet() loads slot 0, checks == 0x42 → pass

Verifies: 2 passes, 1 failure, correct per-test status, setUp → test
state propagation — all without Solidity.

Co-Authored-By: zerosnacks <95942363+zerosnacks@users.noreply.github.com>
@zerosnacks zerosnacks closed this Apr 30, 2026
@github-project-automation github-project-automation Bot moved this to Done in Foundry Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants