Skip to content

fix: preserve explicit discriminant_size values in IDL snapshot#64

Merged
adiman9 merged 2 commits intomainfrom
fixes
Mar 19, 2026
Merged

fix: preserve explicit discriminant_size values in IDL snapshot#64
adiman9 merged 2 commits intomainfrom
fixes

Conversation

@adiman9
Copy link
Contributor

@adiman9 adiman9 commented Mar 19, 2026

Only apply the heuristic discriminant_size calculation when the value wasn't already present in the JSON (discriminant_size = 0 indicates absent/defaulted). This prevents overriding explicitly set values.

Only apply the heuristic discriminant_size calculation when the value
wasn't already present in the JSON (discriminant_size = 0 indicates
absent/defaulted). This prevents overriding explicitly set values.
@vercel
Copy link

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperstack-docs Ready Ready Preview, Comment Mar 19, 2026 11:11am

Request Review

@greptile-apps
Copy link

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR lands two bug fixes in IdlSnapshot's custom Deserialize impl, both related to how discriminant_size is determined from a JSON snapshot.

  • Null-discriminant fix (b373cde): ix.get("discriminant").is_some() returned true for "discriminant": null, incorrectly classifying Anchor-style instructions as Steel-style and producing discriminant_size = 1 on round-trips. The fix adds an is_null() guard so only a non-null value is treated as present.

  • Explicit-value preservation fix (0ccdd91): The heuristic always overwrote intermediate.discriminant_size, even when the JSON already contained an explicit value. The fix skips the heuristic when intermediate.discriminant_size != 0 (relying on serde's default producing 0 for an absent key). This is the same sentinel concern raised in the prior review round.

Key observations:

  • The null-discriminant fix is logically sound but has no dedicated test — the closest existing test (test_snapshot_serde) masks the bug because its instruction also has a non-empty discriminator, making is_steel_discriminant false regardless of has_discriminant.
  • IdlInstructionSnapshot has no #[serde(skip_serializing_if = "Option::is_none")] on the discriminant field, which is exactly what causes None to serialize as "discriminant": null and triggers this code path in the first place.
  • Both fixes are directionally correct; the primary remaining risk is the == 0 sentinel (covered in the prior review) and the absence of regression tests for either fixed path.

Confidence Score: 3/5

  • Safe to merge with low risk, but both fixed paths remain untested and the == 0 sentinel is fragile.
  • The two fixes are logically sound and target real bugs. The risk is limited to one small file with a contained change. However, neither the null-discriminant path nor the explicit-value preservation path is covered by a targeted regression test, meaning a future refactor could silently reintroduce either bug. The == 0 sentinel (pre-existing concern from last round) remains unaddressed.
  • hyperstack-idl/src/snapshot.rs — specifically the Deserialize impl's heuristic branch and the guard at line 80.

Important Files Changed

Filename Overview
hyperstack-idl/src/snapshot.rs Two independent bug fixes: (1) has_discriminant now correctly treats null JSON values as absent, preventing misclassification of Anchor-style instructions as Steel-style on round-trips; (2) the heuristic discriminant_size is only applied when the JSON field was absent (sentinel == 0). The sentinel approach is fragile (noted in prior review), and the null-fix path has no targeted test.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Deserialize JSON value] --> B{instructions array\npresent & non-empty?}
    B -- No --> C[heuristic = 8]
    B -- Yes --> D{All instructions pass\nSteel check?}

    D --> E[For each instruction]
    E --> F{discriminant key\npresent AND non-null?}
    F -- Yes\nnew fix --> G[has_discriminant = true]
    F -- No / null\nnew fix --> H[has_discriminant = false]

    G --> I{has_discriminator\nnon-null non-empty?}
    H --> I

    I --> J{is_steel_discriminant\nhas_discriminant &&\n!has_discriminator}
    I --> K{is_steel_short_discriminator\n!has_discriminant &&\ndisc_len == 1}

    J --> L[Steel?]
    K --> L

    L -- all true --> M[heuristic = 1]
    L -- any false --> C

    M --> N[from_value to intermediate struct]
    C --> N

    N --> O{intermediate.discriminant_size == 0?\nnew fix}
    O -- Yes\nabsent from JSON --> P[use heuristic value]
    O -- No\nexplicit in JSON --> Q[keep explicit value]

    P --> R[IdlSnapshot.discriminant_size]
    Q --> R
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 47-54

Comment:
**`has_discriminant` null-fix lacks a targeted test**

The fix correctly converts `ix.get("discriminant").is_some()` to a null-aware check, but no test exercises the exact failure path it closes.

The existing `test_snapshot_serde` test creates an instruction with `discriminant: None`, which serde serialises to `"discriminant": null`. However, that instruction also carries an 8-byte `discriminator` array, so `has_discriminator = true` and therefore `is_steel_discriminant = has_discriminant && !has_discriminator` evaluates to `false` **regardless** of whether `has_discriminant` is `true` or `false`. The bug is invisible in that test.

The actual regression path is: instruction has `"discriminant": null` **and** no (or null/empty) `discriminator`. Consider adding a test like:

```rust
#[test]
fn test_null_discriminant_not_treated_as_steel() {
    // Anchor-style instruction: discriminant serialised as null, no discriminator.
    // Before the fix, has_discriminant was true → is_steel_discriminant was true → wrong size.
    let json = r#"{
        "name": "my_program",
        "version": "0.1.0",
        "accounts": [],
        "instructions": [{
            "name": "do_thing",
            "discriminant": null,
            "accounts": [],
            "args": []
        }]
    }"#;
    let snap: IdlSnapshot = serde_json::from_str(json).unwrap();
    assert_eq!(snap.discriminant_size, 8,
        "null discriminant must not be treated as Steel-style");
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: handle null dis..."

Comment on lines +71 to +75
// Only use the heuristic if discriminant_size wasn't already present in the JSON
// (discriminant_size = 0 means it was absent / defaulted).
if intermediate.discriminant_size == 0 {
intermediate.discriminant_size = discriminant_size;
}
Copy link

Choose a reason for hiding this comment

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

P1 0 used as sentinel collides with a valid explicit value

The fix assumes intermediate.discriminant_size == 0 unambiguously means "the field was absent from the JSON". That holds because #[serde(default)] will produce 0 for a missing field, and 0 is not a meaningful discriminant size.

However, if a consumer ever serialises a snapshot with "discriminant_size": 0 (e.g. as an intentional sentinel they control, or simply by mistake), the heuristic will silently overwrite it — the same class of bug this PR is trying to fix.

A safer approach is to peek at the raw value (which is already available before the intermediate struct is constructed) to check whether the key is literally present:

let explicit_discriminant_size = value
    .get("discriminant_size")
    .and_then(|v| v.as_u64())
    .map(|n| n as usize);

// ...existing heuristic computation...

let mut intermediate: IdlSnapshotIntermediate = serde_json::from_value(value)
    .map_err(|e| DeError::custom(format!("Failed to deserialize IDL: {}", e)))?;

if explicit_discriminant_size.is_none() {
    intermediate.discriminant_size = discriminant_size;
}

This way the distinction between "key absent" and "key present with value 0" is unambiguous, and neither silently overwrites the other.

Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 71-75

Comment:
**`0` used as sentinel collides with a valid explicit value**

The fix assumes `intermediate.discriminant_size == 0` unambiguously means "the field was absent from the JSON". That holds because `#[serde(default)]` will produce `0` for a missing field, and `0` is not a meaningful discriminant size.

However, if a consumer ever serialises a snapshot with `"discriminant_size": 0` (e.g. as an intentional sentinel they control, or simply by mistake), the heuristic will silently overwrite it — the same class of bug this PR is trying to fix.

A safer approach is to peek at the raw `value` (which is already available before the intermediate struct is constructed) to check whether the key is literally present:

```rust
let explicit_discriminant_size = value
    .get("discriminant_size")
    .and_then(|v| v.as_u64())
    .map(|n| n as usize);

// ...existing heuristic computation...

let mut intermediate: IdlSnapshotIntermediate = serde_json::from_value(value)
    .map_err(|e| DeError::custom(format!("Failed to deserialize IDL: {}", e)))?;

if explicit_discriminant_size.is_none() {
    intermediate.discriminant_size = discriminant_size;
}
```

This way the distinction between "key absent" and "key present with value 0" is unambiguous, and neither silently overwrites the other.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +71 to +75
// Only use the heuristic if discriminant_size wasn't already present in the JSON
// (discriminant_size = 0 means it was absent / defaulted).
if intermediate.discriminant_size == 0 {
intermediate.discriminant_size = discriminant_size;
}
Copy link

Choose a reason for hiding this comment

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

P2 No test covers the fixed scenario

The existing test_ore_instructions_have_discriminators test only verifies the heuristic path (the ore.json fixture has no discriminant_size key, so the heuristic fires). There is no test that:

  1. Provides a JSON with Steel-style instructions (heuristic would yield 1) and an explicit "discriminant_size": 8.
  2. Asserts that 8 is preserved after deserialisation.

Without this, a future refactor could silently re-introduce the regression. Consider adding a unit test like:

#[test]
fn test_explicit_discriminant_size_is_preserved() {
    // Steel-style instructions (discriminant field, no discriminator),
    // but discriminant_size is explicitly overridden to 8.
    let json = r#"{
        "name": "my_program",
        "version": "0.1.0",
        "discriminant_size": 8,
        "accounts": [],
        "instructions": [{
            "name": "do_thing",
            "discriminant": {"value": 0},
            "accounts": [],
            "args": []
        }]
    }"#;

    let snapshot: IdlSnapshot = serde_json::from_str(json)
        .expect("should deserialize");

    assert_eq!(
        snapshot.discriminant_size, 8,
        "explicit discriminant_size should not be overridden by heuristic"
    );
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: hyperstack-idl/src/snapshot.rs
Line: 71-75

Comment:
**No test covers the fixed scenario**

The existing `test_ore_instructions_have_discriminators` test only verifies the heuristic path (the ore.json fixture has no `discriminant_size` key, so the heuristic fires). There is no test that:

1. Provides a JSON with Steel-style instructions (heuristic would yield `1`) **and** an explicit `"discriminant_size": 8`.
2. Asserts that `8` is preserved after deserialisation.

Without this, a future refactor could silently re-introduce the regression. Consider adding a unit test like:

```rust
#[test]
fn test_explicit_discriminant_size_is_preserved() {
    // Steel-style instructions (discriminant field, no discriminator),
    // but discriminant_size is explicitly overridden to 8.
    let json = r#"{
        "name": "my_program",
        "version": "0.1.0",
        "discriminant_size": 8,
        "accounts": [],
        "instructions": [{
            "name": "do_thing",
            "discriminant": {"value": 0},
            "accounts": [],
            "args": []
        }]
    }"#;

    let snapshot: IdlSnapshot = serde_json::from_str(json)
        .expect("should deserialize");

    assert_eq!(
        snapshot.discriminant_size, 8,
        "explicit discriminant_size should not be overridden by heuristic"
    );
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@adiman9 adiman9 merged commit 9e60c87 into main Mar 19, 2026
10 checks passed
@adiman9 adiman9 deleted the fixes branch March 19, 2026 11:16
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.

1 participant