use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VariableReferenceError {
message: String,
}
impl VariableReferenceError {
fn new(msg: impl Into<String>) -> Self {
Self { message: msg.into() }
}
}
impl fmt::Display for VariableReferenceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "VariableReferenceError: {}", self.message)
}
}
impl std::error::Error for VariableReferenceError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScopeKind {
Locals = 1,
Package = 2,
Globals = 3,
}
impl TryFrom<i32> for ScopeKind {
type Error = VariableReferenceError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
1 => Ok(ScopeKind::Locals),
2 => Ok(ScopeKind::Package),
3 => Ok(ScopeKind::Globals),
other => Err(VariableReferenceError::new(format!(
"invalid ScopeKind discriminant: {other}; expected 1 (Locals), 2 (Package), or 3 (Globals)"
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VariableReference {
Scope {
frame_id: i32,
kind: ScopeKind,
},
EvalResult {
counter: i32,
},
Child {
parent: i32,
index: u32,
},
}
const SCOPE_MIN: i32 = 1;
const SCOPE_MAX: i32 = 999_999; const SCOPE_FRAME_ID_MAX: i32 = 99_999; const EVAL_BASE: i32 = 1_000_000;
const EVAL_MAX: i32 = 1_999_999_999;
const CHILD_BASE: i32 = 2_000_000_000;
impl VariableReference {
pub fn encode(&self) -> Option<i32> {
match self {
VariableReference::Scope { frame_id, kind } => {
if *frame_id < 0 || *frame_id > SCOPE_FRAME_ID_MAX {
return None;
}
let kind_disc = *kind as i32;
Some(frame_id * 10 + kind_disc)
}
VariableReference::EvalResult { counter } => {
if *counter < 0 {
return None;
}
let wire = EVAL_BASE.saturating_add(*counter);
if wire > EVAL_MAX {
return None;
}
Some(wire)
}
VariableReference::Child { parent, index } => {
if *parent < 0 {
return None;
}
let index_truncated = (*index & 0xFFFF) as i32;
let parent_clamped = (*parent).clamp(0, i32::MAX / 65_536);
let parent_shifted = parent_clamped * 65_536;
let packed = parent_shifted.saturating_add(index_truncated);
Some(CHILD_BASE.saturating_add(packed))
}
}
}
pub fn decode(raw: i32) -> Option<Self> {
if raw <= 0 {
return None;
}
if raw >= CHILD_BASE {
let packed = raw - CHILD_BASE;
let parent = packed >> 16;
let index = (packed & 0xFFFF) as u32;
return Some(VariableReference::Child { parent, index });
}
if (EVAL_BASE..=EVAL_MAX).contains(&raw) {
let counter = raw - EVAL_BASE;
return Some(VariableReference::EvalResult { counter });
}
if (SCOPE_MIN..=SCOPE_MAX).contains(&raw) {
let kind_disc = raw % 10;
let frame_id = raw / 10;
let kind = ScopeKind::try_from(kind_disc).ok()?;
return Some(VariableReference::Scope { frame_id, kind });
}
None
}
}
#[cfg(test)]
mod codec_unit_tests {
use super::*;
#[test]
fn scope_kind_tryfrom_valid() {
assert_eq!(ScopeKind::try_from(1), Ok(ScopeKind::Locals));
assert_eq!(ScopeKind::try_from(2), Ok(ScopeKind::Package));
assert_eq!(ScopeKind::try_from(3), Ok(ScopeKind::Globals));
}
#[test]
fn scope_kind_tryfrom_invalid() {
assert!(ScopeKind::try_from(0).is_err());
assert!(ScopeKind::try_from(4).is_err());
assert!(ScopeKind::try_from(-1).is_err());
}
#[test]
fn scope_encode_decode_basic() {
let s = VariableReference::Scope { frame_id: 5000, kind: ScopeKind::Locals };
let wire = s.encode().expect("frame_id=5000 is in [0,99_999]");
assert_eq!(wire, 50_001);
assert_eq!(VariableReference::decode(50_001), Some(s));
}
#[test]
fn evalresult_encode_decode_roundtrip() {
let e0 = VariableReference::EvalResult { counter: 0 };
let w0 = e0.encode().unwrap();
assert_eq!(w0, 1_000_000);
assert_eq!(VariableReference::decode(w0), Some(e0));
let e1 = VariableReference::EvalResult { counter: 1 };
let w1 = e1.encode().unwrap();
assert_eq!(w1, 1_000_001);
assert_eq!(VariableReference::decode(w1), Some(e1), "counter=1 must decode as EvalResult");
let e3 = VariableReference::EvalResult { counter: 3 };
let w3 = e3.encode().unwrap();
assert_eq!(w3, 1_000_003);
assert_eq!(VariableReference::decode(w3), Some(e3), "counter=3 must decode as EvalResult");
}
#[test]
fn scope_frame_id_max_boundary() {
let s_max = VariableReference::Scope { frame_id: 99_999, kind: ScopeKind::Locals };
let wire = s_max.encode().expect("frame_id=99_999 is the max valid frame_id");
assert_eq!(wire, 999_991);
assert_eq!(VariableReference::decode(wire), Some(s_max));
}
#[test]
fn scope_frame_id_over_max_returns_none() {
let over = VariableReference::Scope { frame_id: 100_000, kind: ScopeKind::Locals };
assert_eq!(
over.encode(),
None,
"frame_id=100_000 must be rejected (would overflow into EvalResult band)"
);
}
#[test]
fn child_encode_decode_base() {
let c = VariableReference::Child { parent: 0, index: 0 };
let wire = c.encode().unwrap();
assert_eq!(wire, 2_000_000_000);
assert_eq!(VariableReference::decode(2_000_000_000), Some(c));
}
#[test]
fn decode_zero_none() {
assert_eq!(VariableReference::decode(0), None);
}
#[test]
fn decode_negative_none() {
assert_eq!(VariableReference::decode(-1), None);
assert_eq!(VariableReference::decode(i32::MIN), None);
}
#[test]
fn decode_invalid_scope_kind_none() {
assert_eq!(VariableReference::decode(999_999), None);
}
#[test]
fn bands_are_disjoint_no_scope_wire_in_eval_range() {
let max_scope_wire = SCOPE_FRAME_ID_MAX * 10 + 3;
assert!(
max_scope_wire < EVAL_BASE,
"Scope max wire {max_scope_wire} must be < EvalResult base {EVAL_BASE}"
);
}
#[test]
fn evalresult_negative_counter_returns_none() {
assert_eq!(
VariableReference::EvalResult { counter: -1 }.encode(),
None,
"negative EvalResult counter must be rejected"
);
assert_eq!(
VariableReference::EvalResult { counter: i32::MIN }.encode(),
None,
"i32::MIN EvalResult counter must be rejected"
);
}
#[test]
fn evalresult_overflow_into_child_band_returns_none() {
assert_eq!(
VariableReference::EvalResult { counter: 1_998_999_999 }.encode(),
Some(1_999_999_999),
"counter=1_998_999_999 should encode to EVAL_MAX"
);
assert_eq!(
VariableReference::EvalResult { counter: 1_999_000_000 }.encode(),
None,
"counter that would push wire into Child band must be rejected"
);
}
#[test]
fn child_negative_parent_returns_none() {
assert_eq!(
VariableReference::Child { parent: -1, index: 0 }.encode(),
None,
"parent=-1 must be rejected (would land in EvalResult band)"
);
assert_eq!(
VariableReference::Child { parent: i32::MIN, index: 0 }.encode(),
None,
"parent=i32::MIN must be rejected"
);
}
}