Skip to content

fix(cli): parse Vec<u64>, Vec<u128> and Vec<bool> arguments from comma-separated lists - #266

Open
nodersteam wants to merge 2 commits into
logos-co:mainfrom
noders-team:vec-primitive-cli-args
Open

fix(cli): parse Vec<u64>, Vec<u128> and Vec<bool> arguments from comma-separated lists#266
nodersteam wants to merge 2 commits into
logos-co:mainfrom
noders-team:vec-primitive-cli-args

Conversation

@nodersteam

@nodersteam nodersteam commented Aug 28, 2026

Copy link
Copy Markdown

Description

spel only had list parsers for Vec<u8> and Vec<u32> (parse_vec in spel-cli/src/parse.rs). Any other Vec<primitive> instruction argument fell through to ParsedValue::Raw, and to_dynamic_value then failed with type mismatch: expected Vec { vec: Primitive("u128") }, got Raw("...") - an instruction with, say, tranches: Vec<u128> could not be called from the CLI at all, not even with an empty list.

This PR parses comma-separated lists for Vec<u64>, Vec<u128> and Vec<bool> element by element with the existing primitive parser and serializes them as a length-prefixed sequence, the same risc0 serde shape the guest deserializes. An empty (or whitespace-only) string is an empty list; a malformed element is reported with its index (Element [1]: Invalid u128 'seven'), and an empty element inside a non-empty list ("1,,2", ",") is an error rather than being dropped.

Verified end to end against a program whose instruction carries tranches: Vec<u128>: spel --dry-run produces instruction bytes identical to risc0_zkvm::serde::to_vec of the same Rust Instruction value for both --tranches 300,700 and --tranches "", and the same build created schedules on the public LEZ testnet whose on-chain state decodes to [300, 700].

Changes

  • spel-cli/src/parse.rs: new ParsedValue::Seq(Vec<ParsedValue>) (with Display[300, 700]); parse_vec arm for u64 / u128 / bool elements. Vec<u8> / Vec<u32> keep their current representations and behaviour. Note for release notes: ParsedValue is reachable through the spel lib target, so the new variant is an additive change to that enum (nothing in the workspace matches on it outside spel-cli itself).
  • spel-cli/src/serialize.rs: (Vec<T>, Seq(items))DynamicValue::Seq.
  • README.md: row in the argument-format table.
  • Tests: parse.rs - CSV parsing for u128 (incl. u128::MAX), u64, bool; empty / whitespace-only string → empty list; empty element in a non-empty list is rejected with its index; error names the offending element; Display; u8/u32 unchanged. serialize.rs - Vec<u128> → length prefix + four u32 words per element; empty vec → [0]; risc0 serde round-trip through a Deserialize-derived instruction with Vec<u64>, Vec<u128>, Vec<bool> and an empty Vec<u128>.

Checklist

  • Builds cleanly (cargo build --manifest-path spel-cli/Cargo.toml)
  • Tests pass (cargo test --manifest-path spel-cli/Cargo.toml: 111 passed; cargo test --workspace --all-targets --exclude spel --exclude spel-ffi-compile-test: green; cargo fmt --all --check: clean; cargo clippy reports nothing in the changed hunks)
  • README updated (argument-format table)
  • New public methods have doc comments (the new enum variant is documented inline; no new public functions)
  • Branch is off main

Reviewer note: the new parse_vec arm returns an error on a malformed element instead of the Raw fallback the u8/u32 arms use (that fallback exists so serialize.rs can retry a u32 CSV, which the new types do not need). Happy to align either way.

Copilot AI 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.

🟡 Changes recommended

The new CSV vec parser currently silently drops empty elements (e.g. ",", "1,,2"), which can mask user input errors and alter the intended argument value.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR extends spel-cli’s IDL-aware CLI argument parsing and risc0-compatible serialization so that additional Vec<primitive> instruction arguments (Vec<u64>, Vec<u128>, Vec<bool>) can be provided as comma-separated lists and correctly serialized as length-prefixed sequences.

Changes:

  • Added ParsedValue::Seq(Vec<ParsedValue>) plus CSV parsing for Vec<u64>, Vec<u128>, and Vec<bool> in spel-cli/src/parse.rs.
  • Mapped (Vec<T>, Seq(items)) into DynamicValue::Seq during serialization in spel-cli/src/serialize.rs, with added serialization/round-trip tests.
  • Documented the new CLI formats in README.md.
File summaries
File Description
spel-cli/src/parse.rs Adds ParsedValue::Seq and parses Vec<u64>/Vec<u128>/Vec<bool> from comma-separated input.
spel-cli/src/serialize.rs Converts ParsedValue::Seq into DynamicValue::Seq and adds risc0 serde shape/round-trip tests.
README.md Documents the CLI input format for the newly supported vector types.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread spel-cli/src/parse.rs Outdated
U32Array(Vec<u32>), // [u32; N] / ProgramId
ByteArrayVec(Vec<Vec<u8>>), // Vec<[u8; 32]>
StringVec(Vec<String>), // Vec<String>
Seq(Vec<ParsedValue>), // Vec<T> for any other primitive T (u64, u128, bool)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fair point. ParsedValue is reachable through the spel lib target, but nothing in the workspace consumes it outside spel-cli itself (the lib exists for the binary), and the crate is not published, so there is no downstream exhaustive match to break today. I have noted the new variant as an additive enum change in the PR description so it can be carried into the release notes; if you would rather guard the enum with #[non_exhaustive], happy to add that in this PR.

Comment thread spel-cli/src/parse.rs
Comment on lines +304 to +320
// Vec<u64> / Vec<u128> / Vec<bool> — comma-separated values, parsed element by
// element with the primitive parser; an empty (or whitespace-only) string is an
// empty list.
IdlType::Primitive(p) if p == "u64" || p == "u128" || p == "bool" => {
let mut items = Vec::new();
for (i, part) in raw
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.enumerate()
{
let item =
parse_primitive(part, p).map_err(|e| format!("Element [{}]: {}", i, e))?;
items.push(item);
}
Ok(ParsedValue::Seq(items))
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed, that hid mistakes. de054bf changes it: a fully empty (or whitespace-only) string is still an empty list, but an empty element inside a non-empty list (",", "1,,2", "1,2,") is now an error that names the index, e.g. Element [1]: empty element in Vec<u64>. Test parse_vec_rejects_empty_elements covers u64 and bool; README row updated.

Comment thread spel-cli/src/parse.rs Outdated
U32Array(Vec<u32>), // [u32; N] / ProgramId
ByteArrayVec(Vec<Vec<u8>>), // Vec<[u8; 32]>
StringVec(Vec<String>), // Vec<String>
Seq(Vec<ParsedValue>), // Vec<T> for any other primitive T (u64, u128, bool)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in de054bf: the comment now reads Vec<u64> / Vec<u128> / Vec<bool>, one element per entry, matching what parse_vec actually constructs.

@vpavlin

vpavlin commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

@nodersteam I think Copilot's comments are worth addressing, but otherwise it looks sensible

@alexeymoskalev-devops

Copy link
Copy Markdown

Thanks for the look, @vpavlin. Addressed Copilot's three comments in de054bf: empty elements inside a list are now rejected with their index (fully empty string stays an empty list), the Seq comment names the exact element types, and the additive ParsedValue variant is called out in the description for the release notes. Tests: 111 in spel-cli, fmt clean.

…a-separated lists

Instruction arguments of these types fell through to ParsedValue::Raw and
the transaction failed to build with a type mismatch; only Vec<u8> and
Vec<u32> had list parsers. Parse the list element by element with the
existing primitive parser into a new ParsedValue::Seq, serialize it as a
length-prefixed sequence, and report a malformed element with its index.
An empty string is an empty list.

Covered by parse and serialize unit tests, including a risc0 serde
round-trip through a Deserialize-derived instruction.
…ents

An empty element inside a non-empty comma-separated list ("1,,2", ",")
is now an error naming the element index instead of being silently
dropped. A fully empty string is still an empty list. Also tightens the
ParsedValue::Seq comment to the three element types that produce it.
@alexeymoskalev-devops

alexeymoskalev-devops commented Sep 9, 2026

Copy link
Copy Markdown

Rebased onto main (f60680a) to pick up the new multisig-e2e-test job and scripts/multisig-e2e-test.sh from #267 — the branch predated that script, so the job could not find it on the head checkout. No code changes; both commits are identical in content (git range-diff clean). cargo test -p spel passes locally including the new exchange_flow tests.

@vpavlin the fresh CI run on the rebased head is waiting for workflow approval (fork PR) — could you hit "Approve and run" when you get a chance? Thanks.

vpavlin added a commit that referenced this pull request Sep 9, 2026
Five sequencer-backed jobs checked out
`github.event.pull_request.head.sha`, so they ran the PR's branch tip
while the workflow definition came from main. A PR opened before a new
test script lands therefore fails on a file it never could have had:

    scripts/multisig-e2e-test.sh: No such file or directory
    Process completed with exit code 127

That is #266's current red check, and every other open PR is one
commit-add away from the same thing — nine of them lack that script at
their head right now. The unit and E2E jobs never had the override, so
they went green on the same commit, which is what makes the failure
look like the contributor's fault.

The quieter half of the problem is that those jobs tested each PR
against whatever base it branched from, so a PR could pass while being
broken against current main.

`actions/checkout` with no `ref:` already does the right thing: the
merge ref on pull_request, the pushed commit on push — which is what
the `||` expression was hand-rolling, minus the staleness.

SPEL_REF has to move with it. It reaches `spel init --spel-rev` and
lands in the scaffolded guest manifest as

    spel-framework = { git = "...", rev = "refs/pull/N/head" }

so it decides which framework the guest compiles against. Left at
/head while the checkout moved to /merge, a run would take its scripts
from the merged tree and its framework from the stale head — the two
halves disagreeing is worse than both being stale. Cargo resolves the
merge ref fine; verified it locks to the same commit `git ls-remote`
reports for refs/pull/N/merge.

One deliberate behaviour change: refs/pull/N/merge exists only while a
PR is conflict-free, so a conflicted PR now fails to resolve the ref
instead of testing against a base it no longer merges into. That seems
right — a green check on a conflicted PR is describing a tree nobody
will ever ship — but it is a change, not a fix.


Claude-Session: https://claude.ai/code/session_01S9qsH6Um6shweCPEN3Z6ph

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants