Skip to content

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

Draft
decofe wants to merge 7 commits intomasterfrom
zerosnacks/test-runner-support
Draft

feat(forge): make solar analysis optional in test runner#14255
decofe wants to merge 7 commits intomasterfrom
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants