Skip to content

fix: address special_map off-by-one #7068

Draft
federico-stacks wants to merge 12 commits into
stacks-network:developfrom
federico-stacks:chore/refactor-special-map
Draft

fix: address special_map off-by-one #7068
federico-stacks wants to merge 12 commits into
stacks-network:developfrom
federico-stacks:chore/refactor-special-map

Conversation

@federico-stacks

@federico-stacks federico-stacks commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Description

As discussed in #7022 (comment), this PR aims to address two points about special_map function:

  • Fix an off-by-one condition that caused an extra loop iteration (not a functional bug, but still unintended behavior). Actually demonstrated to be a relevant bug (fix: address special_map off-by-one  #7068 (review))
  • Simplify the algorithm, which is currently somewhat difficult to follow

The proposed approach removes explicit index handling (addressing the first point) and reduces complexity by relying on iterators instead of precomputing a Vec<Vec<Value>> of arguments (addressing the second point).

As a side effect, this should reduce allocations and avoid reallocations that were previously needed for precomputed structures. In terms of execution speed, I haven’t observed any significant improvements so far.

Note: This PR is based on #7022 and should be merged after it.

Applicable issues

  • fixes #

Additional info (benefits, drawbacks, caveats)

Checklist

  • Test coverage for new or modified code paths
  • For new Clarity features or consensus changes, add property tests (see docs/property-testing.md)
  • Changelog fragment(s) or "no changelog" label added (see changelog.d/README.md)
  • Required documentation changes (e.g., rpc/openapi.yaml for RPC endpoints, event-dispatcher.md for new events)
  • New clarity functions have corresponding PR in clarity-benchmarking repo

@federico-stacks federico-stacks self-assigned this Apr 2, 2026
@coveralls

coveralls commented Apr 2, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 24833235977

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.03%) to 85.744%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 8 uncovered changes across 1 file (14 of 22 lines covered, 63.64%).
  • 3174 coverage regressions across 85 files.

Uncovered Changes

File Changed Covered %
clarity/src/vm/functions/sequences.rs 22 14 63.64%

Coverage Regressions

3174 previously-covered lines in 85 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
stackslib/src/chainstate/stacks/db/transactions.rs 208 97.07%
stackslib/src/net/inv/epoch2x.rs 206 80.3%
stackslib/src/net/chat.rs 201 93.01%
stackslib/src/chainstate/stacks/miner.rs 190 83.5%
stacks-node/src/nakamoto_node/miner.rs 153 87.04%
stackslib/src/chainstate/stacks/db/mod.rs 135 86.21%
clarity/src/vm/costs/mod.rs 125 83.57%
stackslib/src/net/api/postblock_proposal.rs 125 80.14%
stacks-node/src/nakamoto_node/relayer.rs 107 86.64%
stackslib/src/config/mod.rs 101 68.84%

Coverage Stats

Coverage Status
Relevant Lines: 218304
Covered Lines: 187182
Line Coverage: 85.74%
Coverage Strength: 17361502.45 hits per line

💛 - Coveralls

@cylewitruk-stacks cylewitruk-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks materially better than the previous PR(s) -- no comments from me! I'll trigger a Copilot run just for an extra pair of eyes on any subtleties 👍

Comment thread changelog.d/7068-refactor-special-map.changed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Clarity’s higher-order sequence operations to simplify special_map (and related fold/filter paths) by switching from precomputed argument matrices to lazy iteration over SequenceData, reducing intermediate allocations and removing an unintended extra iteration.

Changes:

  • Refactor special_map/special_fold/special_filter to use lazy SequenceData iteration and apply_evaluated() (pre-evaluated argument dispatch).
  • Add SequenceIter + IntoIterator for SequenceData and update try_retain to pass Value to predicates.
  • Add additional tests for variadic map shortest-sequence behavior and introduce a new end-to-end Criterion benchmark.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
clarity/src/vm/tests/sequences.rs Adds test coverage for variadic map stopping at the shortest sequence.
clarity/src/vm/mod.rs Introduces shared call precondition checking, dispatch_args, and apply_evaluated().
clarity/src/vm/functions/sequences.rs Refactors filter/fold/map to use iterators + apply_evaluated().
clarity/benches/sequence_ops.rs Updates benchmark helper to match try_retain predicate signature change.
clarity/benches/sequence_higher_order.rs Adds new end-to-end benchmarks for fold/map/filter.
clarity/Cargo.toml Registers the new sequence_higher_order benchmark target.
clarity-types/src/types/mod.rs Adds SequenceIter, IntoIterator for SequenceData, and updates try_retain predicate type.
clarity-types/src/tests/types/mod.rs Updates tests to match try_retain(Value) predicate signature.
changelog.d/7068-refactor-special-map.changed Adds changelog entry for the special_map refactor.
changelog.d/7022-fold-map-optimization.changed Adds changelog entry for fold/map/filter performance work.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread clarity/src/vm/mod.rs

@cylewitruk-stacks cylewitruk-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@aaronb-stacks

Copy link
Copy Markdown
Contributor

Fix an off-by-one condition that caused an extra loop iteration (not a functional bug, but still unintended behavior)

I think it's important to be sure that the error behavior here is the same as the prior implementation, otherwise, this PR will have consensus impacts.

@federico-stacks

Copy link
Copy Markdown
Contributor Author

Fix an off-by-one condition that caused an extra loop iteration (not a functional bug, but still unintended behavior)

I think it's important to be sure that the error behavior here is the same as the prior implementation, otherwise, this PR will have consensus impacts.

@aaronb-stacks here the in-depth discussion: https://github.com/stacks-network/stacks-core/pull/7022/changes#r2980910603

Basically the off-by-one never cause an error, but just causing an extra loop run on the first part of the special_map algorithm (when collecting args). Then the potential extra args, were "dropped" during the apply_evalueted() because the loop is led by the min among the arguments lengths

Comment thread clarity/src/vm/functions/sequences.rs Outdated

@francesco-stacks francesco-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the off-by-one is a correct fix, but causes a consensus-breaking change. The current code erroneously handles some edge cases. We'll probably have to epoch gate the fix

/// `(map + empty-list non-empty-list)` with typed-but-runtime-empty first sequence.
///
/// Variadic native `+` tolerates a 1-arg call and returns the single value. The old
/// algorithm pushes the first element of the non-empty list at position 0, then
/// the apply loop runs with `previous_len = None`, invoking `apply_evaluated(+, [10])`.
///
/// - develop: `(ok (list 10))` - tx succeeds with a non-empty result.
/// - this PR: `(ok (list))` - tx succeeds with an empty result.
#[test]
fn map_empty_first_variadic_native_ccall() {
    contract_call_consensus_test!(
        contract_name: "map-mt-first-nat",
        contract_code: "
            (define-data-var xs (list 10 int) (list))
            (define-data-var ys (list 10 int) (list 10 20))
            (define-public (trigger)
                (ok (map + (var-get xs) (var-get ys))))
        ",
        function_name: "trigger",
        function_args: &[],
    );
}

/// `(map + non-empty empty non-empty)` with typed-but-runtime-empty middle sequence.
///
/// Old algorithm: first loop pushes `[[1], [2], [3]]`; second loop (empty) does
/// nothing but sets `min_args_len = 0`; third loop's element at index 0 satisfies
/// `0 > 0 == false`, appending into the first group to produce `[[1, 10], [2], [3]]`.
/// The apply loop processes `[1, 10]` with `previous_len = None`, invoking `+(1, 10) = 11`.
///
/// - develop: `(ok (list 11))` - tx succeeds with one element.
/// - this PR: `(ok (list))` - tx succeeds with empty result.
#[test]
fn map_empty_middle_variadic_native_ccall() {
    contract_call_consensus_test!(
        contract_name: "map-mt-mid-nat",
        contract_code: "
            (define-data-var a (list 10 int) (list 1 2 3))
            (define-data-var b (list 10 int) (list))
            (define-data-var c (list 10 int) (list 10 20))
            (define-public (trigger)
                (ok (map + (var-get a) (var-get b) (var-get c))))
        ",
        function_name: "trigger",
        function_args: &[],
    );
}

/// `(map user-fn empty-list non-empty-list)` where the user function has fixed arity 2.
///
/// Old algorithm pushes `[[10]]` and invokes `apply_evaluated(add, [10])`. A
/// user-defined function with arity 2 rejects a 1-arg call in
/// `DefinedFunction::execute_apply` (callables.rs:189) -> `IncorrectArgumentCount(2, 1)`
/// runtime error, aborting the transaction.
///
/// - develop: tx aborted with `IncorrectArgumentCount(2, 1)`.
/// - this PR: `(ok (list))` - tx succeeds with an empty result.
#[test]
fn map_empty_first_user_fn_ccall() {
    contract_call_consensus_test!(
        contract_name: "map-mt-first-user",
        contract_code: "
            (define-data-var xs (list 10 int) (list))
            (define-data-var ys (list 10 int) (list 10 20))
            (define-private (add (x int) (y int)) (+ x y))
            (define-public (trigger)
                (ok (map add (var-get xs) (var-get ys))))
        ",
        function_name: "trigger",
        function_args: &[],
    );
}

@federico-stacks federico-stacks changed the title chore: refactor special_map to simplify logic fix: address special_map off-by-one Apr 23, 2026
@federico-stacks federico-stacks marked this pull request as draft April 23, 2026 15:09
@federico-stacks

Copy link
Copy Markdown
Contributor Author

Converted to draft, since we want to epoch gate this change for the next hard-fork

@CLAassistant

CLAassistant commented May 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants