Conversation
0xIcarus
reviewed
May 18, 2026
musitdev
reviewed
Jun 2, 2026
musitdev
left a comment
There was a problem hiding this comment.
New files should be copyrighted Movement or MoveIndustries.
Comment on lines
+4
to
+6
| // Copyright (c) The Diem Core Contributors | ||
| // Copyright (c) The Move Contributors | ||
| // SPDX-License-Identifier: Apache-2.0 |
There was a problem hiding this comment.
It's copyrighted Diem, it's a new file or a file moved from old Diem repo?
| @@ -0,0 +1,101 @@ | |||
| // Copyright (c) Aptos Foundation | |||
There was a problem hiding this comment.
The same this file is new or copied from Aptos?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Extends the Move
#[test]attribute with property-based fuzz testing, inspired by Foundry's fuzzer (crates/evm/fuzz/). Parameters of a#[test]function are treated as fuzz inputs and exercised with generated values; failing cases are shrunk to a minimal counterexample and (optionally) persisted to a replayable regression corpus.Failing fuzz cases are automatically shrunk to a minimal reproducing input and reported as
minimal counterexample: [...]. With--fuzz-corpus-dir, failing inputs are persisted and replayed on subsequent runs so a regression never silently disappears.Fuzz behavior and strategy (read this first)
1. Implicit fuzz on unassigned parameters
A
#[test]parameter that is not explicitly assigned (= v,in ..., or!= ...) is treated as an implicit fuzz input over an unrestricted domain. This intentionally replaces the legacy compiler's hard"Missing test parameter assignment in test"error.FuzzValueSourceregistered (the unit-test runner installsDefaultFuzzSource), a bare#[test] fun f(a: u64)expands into--fuzz-runscases (default 16) and runs."no fuzz value source registered"diagnostic instead of the old missing-assignment error.2. Multi-parameter semantics: fuzz dims are zipped, not Cartesian
This is the key design point and the reason fuzzing does not blow up combinatorially.
fuzz_runs = 16and three fuzz paramsa, b, c, the expansion produces 16 total cases, where case i uses(a[i], b[i], c[i]). Every parameter is drawn fresh in every run -- they vary together, exactly like QuickCheck/proptest. It is not "varyawhile holdingb,cfixed."a = [v1, v2, ...]) are Cartesian. Deterministic matrix dimensions form a Cartesian product, and the fuzz dimensions are zipped inside each Cartesian point. So#[test(a = [1,2,3])]with fuzzb, cproduces3 x 16 = 48cases.det_product x fuzz_runs, capped atMAX_FUZZ_CASES = 1024. Exceeding the cap is a compile error that names the offending expansion.fuzz_runsis theminof the fuzz dimensions' produced value counts (normally--fuzz-runs); a constraint that can only yield k < runs distinct values caps the run count at k.3. Per-type sampling strategy (
DefaultFuzzSource)Each draw is a weighted mix of three strategies, mirroring Foundry's knobs:
EDGE_WEIGHT) -- boundary values.--fuzz-dictionary-weight, default 40) -- values harvested from the program.u8 ... u2560, 1, 2, MAX/2-1, MAX/2, MAX/2+1, MAX-1, MAX), dictionary uints, and full-width random (drawn with enough limbs to cover the whole width, not narrowed throughu128). Range-aware: draws are sampled within an active range, with range-bracketed boundary values for edge coverage. A finite literal domain (in [a, b, c], no ranges) is drawn from directly so the run count is not capped.address/signer/&signer0x0, 0x1, 0x2), dictionary addresses, and random. Range-aware in-range sampling (a full-width random address essentially never lands in a bounded interval, so the sampler draws within the range). Explicit literal domains and fixtures are drained first so the user always sees the values they listed.bool{false, true}universe, biased by fixtures. Range constraints are rejected (b in lo..hiis meaningless forbool).Dictionary and fixtures.
FuzzDictionary::from_envmines named address aliases and module-constant values; per-parameter fixture pools are mined fromFIXTURE_<name>constants and fed ahead of random/edge values. RNG is an inline SplitMix64 (noproptestdependency), with the base--fuzz-seedmixed with a per-parameter salt so each parameter draws a distinct but reproducible stream.4. Domain / exclude constraints and validation
in ...builds the domain (allowed set: literals, ranges, unions);!=accumulates into the exclude set. Combiningin/!=on one parameter is allowed (domain narrows, excludes accumulate). Mixing=with!=/inis rejected.coerce_numeric_to_widthalready applies to concrete#[test(a = ...)]values.a != 300on au8is a clear compile error (value 300 is out of range for this integer parameter (max 255)), not a silently-wrappeda != 44. The same check covers integer literals, integer range bounds, and (defensively) address range bounds.5. Shrinking
On a fuzz failure, the runner walks
FuzzValueSource::shrinkup to 100 steps to find a minimal counterexample. A shrink candidate is accepted only if it reproduces the same failure -- same status code, sub-status, and abort location (MoveError's identity, which ignores the message). Accepting any error would let the shrinker wander onto an unrelated abort and report a counterexample for the wrong bug.OUT_OF_GASfailures are persisted but not shrunk: shrinking searches for a smaller input that still exhausts gas, but smaller inputs almost always consume less gas, so each probe is a full gas-bounded re-execution that nearly always fails to reproduce -- up to ~100x(#args) wasted executions for no benefit.6. Corpus persistence and replay
With
--fuzz-corpus-dir, failing inputs (preferring the shrunk-minimal vector) are appended to<corpus-dir>/failures/and replayed on the next run as<test>#regression[i]cases. Replays are bounded per function (MAX_REGRESSION_REPLAYS_PER_FN = 256); when a corpus exceeds the cap, the newest entries are replayed and the overflow is reported rather than silently truncated (compile-timeMAX_FUZZ_CASESdoes not cover post-planning replays).How to read this PR
Read in phase order; each phase is self-contained and compiles + tests independently.
!=,in,..,..=, pipe-union,[...]in attributeslegacy-move-compiler/src/parser/,expansion/,move-model/src/ast.rsParamSpec, matrix x fuzz expansion, implicit fuzz on absence, out-of-range validationmove-compiler-v2/src/plan_builder.rs,fuzz.rsmove-compiler-v2/src/fuzz.rs,tools/move-unit-test/src/lib.rsmove-compiler-v2/src/fuzz.rs,fuzz_corpus.rs,tools/move-unit-test/src/lib.rsandtest_runner.rsFile-by-file
Grammar layer (Phase 1)
legacy-move-compiler/src/parser/lexer.rsTok::PeriodPeriodEqual(..=) with longest-match in the.handler. No valid existing Move source contains the..=sequence (=can never start an expression/pattern after..), so tokenization of existing code is unaffected; the token is consumed only in attribute parsing.legacy-move-compiler/src/parser/ast.rsConstraintOp{Ne,In},Attribute_::Constrained,AttributeValue_::{List,Range,Union}. Cover new arms inattribute_name()andast_debug.legacy-move-compiler/src/parser/syntax.rsparse_attribute_valueinto_value/_range_value/_primary_valuewith precedenceunion > range > primary. Branchparse_attributeon!=and contextualin.legacy-move-compiler/src/expansion/ast.rsAttributeName_::Disambiguated(sym, slot)so multiple constrained entries on one parameter coexist in theUniqueMap.legacy-move-compiler/src/expansion/translate.rsConstrainedentries duringunique_attributes.move-model/src/ast.rsAttribute::Constrained,AttributeValue::{List,Range,Union},ConstraintOp.move-model/src/builder/module_builder.rstranslate_attributeinto a recursive helpertranslate_attribute_valuehandling all variants.Plan builder (Phases 2 + 4)
move-compiler-v2/src/plan_builder.rs:ParamSpec::{Concrete, Matrix, Fuzz}per parameter; absence impliesFuzz(implicit fuzz).parse_test_attributefoldsAssign/Constrained(In)/Constrained(Ne)into specs; rejects mixing=with!=/in.build_test_inforeturnsVec<ExpandedCase>(case + per-argArgOrigin). Cartesian over deterministic dims, zipped across fuzz dims; caps total atMAX_FUZZ_CASES = 1024. Expanded case names embed an ordinal (fn#3[a=...]) so identical-valued cases do not collide in the per-moduleBTreeMap.construct_test_plan_with_fuzz_sourcereturnsTestPlanBuild { plans, fuzz_metadata }so the runner can look up per-arg shrink info for a failing case.format_move_valueis nowpuband shared with the runner's counterexample renderer (so the expanded-case name and the shrink output format identically).construct_test_plan(env, filter)keeps the originalOption<Vec<ModuleTestPlan>>signature (usesNoFuzzSource).Fuzz value source (Phases 3 + 4)
move-compiler-v2/src/fuzz.rs:FuzzValueSource--sample(required),shrink/mutate(default no-op, purely additive).NoFuzzSource-- error-on-use stub.DefaultFuzzSource-- the strategy described above (edge/dictionary/random per type, range-aware sampling, fixtures, SplitMix64 RNG).reduce_into_range(single source of truth for[0, modulus)coercion),range_edge_endpoint(shared, clamped boundary picker used by both uint and address samplers),bigint_to_address(guards negatives to0x0, truncates >32-byte values; defensive given upstream validation).validate_uint_domain/validate_address_domainreject out-of-range constraint values, surfaced as labeled plan-build diagnostics.FuzzPlanMetadatasidecar:BTreeMap<(ModuleId, expanded_test_name), Vec<ArgOrigin>>with an O(log n) keyed lookup.Corpus (Phase 4)
move-compiler-v2/src/fuzz_corpus.rs(new):<corpus-dir>/{failures,seeds}/<addr>.<module>.<test>.bcs.WireValueproxy enum for serialization (MoveValuehas no free-standingDeserialize).load_failures,load_seeds,append_failure,append_seed-- idempotent (deduped by serialized bytes) via a sharedappend_entryhelper.serdeonmove-compiler-v2.Runner integration (Phase 4)
tools/move-unit-test/src/lib.rs:UnitTestingConfigCLI flags:--fuzz-runs(16),--fuzz-seed(0),--fuzz-dictionary-weight(40),--fuzz-corpus-dir(None).FuzzRunnerCtx { metadata, source, corpus_dir }, attached toTestPlan::runner_metadatavia type-erasedArc<dyn Any>.compile_to_test_planinstantiatesDefaultFuzzSource, populates the ctx, and replays the regression corpus into eachModuleTestPlan(bounded byMAX_REGRESSION_REPLAYS_PER_FN, overflow reported).tools/move-unit-test/src/test_runner.rs:SharedTestingConfig.fuzz_ctxextracted from the type-erased runner metadata.shrink_if_fuzz(same-failure acceptance),shrink_persist_and_note, andmove_error_ofhelpers;persist_to_corpuswrites the (preferably shrunk) failing args.OUT_OF_GASpersists without shrinking.legacy-move-compiler/src/unit_test/mod.rs:TestPlan.runner_metadata: Option<Arc<dyn Any + Send + Sync>>-- a downstream-runner extension point; legacy code never introspects it.Public API additions
All previously-public functions are kept (backwards-compatible). New surface:
move_compiler_v2::fuzz:FuzzConfig,FuzzDictionary,FixturePool,DefaultFuzzSource,NoFuzzSource,FuzzValueSource(trait),Domain,RangeSpec,ParamSpec,ArgOrigin,FuzzPlanMetadata.move_compiler_v2::plan_builder:TestPlanBuild,construct_test_plan_with_fuzz_source(introduced here),ExpandedCase,format_move_value.move_compiler_v2::fuzz_corpus:load_failures,load_seeds,append_failure,append_seed,failures_dir,seeds_dir.move_unit_test::FuzzRunnerCtx.Design rationale (the why)
oneof(...)inas a contextual keywordname in valueis unambiguous inside#[...].runs = Nmeans N total trials, not NNN. With Cartesian, 3 fuzz params x 16 runs = 4096 cases (over cap); with zip it is 16. Matrix dims still product.#[test(a = 300)]on au8already errors viacoerce_numeric_to_width. Wrappinga != 300intoa != 44would silently exclude a value the user never wrote and hide their mistake.FuzzValueSourcewith defaultshrink/mutatesample;NoFuzzSourceworks unchanged.param_name: &strnotSymbolSymbolPool-free so third-party impls drop in without move-model knowledge.TestPlan.runner_metadata: Arc<dyn Any>FuzzRunnerCtxreferencesmove-compiler-v2types;TestPlanlives inlegacy-move-compiler, which cannot depend on it. Type erasure bridges the layers.WireValueproxy in corpusMoveValueneeds aMoveTypeLayouttoDeserialize; a proxy covering exactly the sampled primitive surface keeps the on-disk format stable.Test coverage
move-compiler-v2testsuite: all fuzz golden fixtures pass (fuzz_*undertests/unit_test/test/).tools/move-unit-test: full suite passes, including end-to-end fuzz execution (tests/fuzz_runner.rs).aptos-framework: builds clean.Fixtures under
move-compiler-v2/tests/unit_test/test/:fuzz_matrix.move-- explicit matrix expansion (Cartesian).fuzz_implicit.move-- bare#[test]on parameters triggers implicit fuzz (documents the behavior change).fuzz_constraints.move-- every grammar form:!=,in, lists, ranges, unions, combined.fuzz_mix_assign_constraint.move-- rejects mixing=with!=/in.fuzz_primitives.move-- end-to-end fuzz onu64,u8,bool,address, multi-param.fuzz_fixtures.move--FIXTURE_*constants feed fixture pools.fuzz_empty_matrix.move--a = []is rejected at plan-build.fuzz_out_of_range.move-- out-of-range integer literal/range constraints are rejected with a clear diagnostic.tests/fuzz_runner.rs(move-unit-test) runs the plan and the MoveVM end-to-end: implicit-fuzz cases produce uniquely-named runs that all execute without panicking, and numeric matrices coerce + run.Known gaps / future work
FuzzValueSource::mutateand theseeds/subdirectory are wired, but no inspector captures MoveVM coverage maps, so the corpus stays regression-driven. Hook point: wrap a coverage inspector around the session intest_runner.rs::execute_via_move_vmand feed maps to aCorpusFuzzSourceoverDefaultFuzzSource.Type, so it is an additive extension in the source impl.should_failfuzz fixture asserting the shrunk output would lock it in (the golden-suite fuzz cases are stubs that always pass, so they cannot drive shrink).Recommended reviewer reading order
legacy-move-compiler/src/parser/syntax.rs-- grammar.move-compiler-v2/src/plan_builder.rs-- how attributes become test cases (the heart).move-compiler-v2/src/fuzz.rs-- value sampler, strategy, validation.move-compiler-v2/src/fuzz_corpus.rs-- disk format.tools/move-unit-test/src/lib.rs-- CLI + plan-build wiring + replay.tools/move-unit-test/src/test_runner.rs-- runner-side shrink/persist hooks.unit_testfixtures (.move+.exp) for end-to-end behavior.Quick verification commands
Type of Change
Which Components or Systems Does This Change Impact?
Checklist