use adze_glr_core::{Action, ParseTable, StateId};
use adze_ir::RuleId;
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaError {
InvalidActionEncoding {
action: Action,
encoded_value: u16,
reason: String,
},
InvalidStateId {
state_id: u16,
max_states: usize,
},
InvalidSymbolId {
symbol_id: u16,
max_symbols: usize,
},
InvalidProductionId {
production_id: u16,
max_productions: usize,
},
DuplicateActionEntry {
state: u16,
symbol: u16,
},
MissingAcceptState,
InvalidEOFHandling {
reason: String,
},
CompressedTableIntegrity {
reason: String,
},
}
impl std::fmt::Display for SchemaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaError::InvalidActionEncoding {
action,
encoded_value,
reason,
} => write!(
f,
"Invalid action encoding: {:?} encoded as 0x{:04X} - {}",
action, encoded_value, reason
),
SchemaError::InvalidStateId {
state_id,
max_states,
} => write!(f, "State ID {} exceeds maximum {}", state_id, max_states),
SchemaError::InvalidSymbolId {
symbol_id,
max_symbols,
} => write!(f, "Symbol ID {} exceeds maximum {}", symbol_id, max_symbols),
SchemaError::InvalidProductionId {
production_id,
max_productions,
} => write!(
f,
"Production ID {} exceeds maximum {}",
production_id, max_productions
),
SchemaError::DuplicateActionEntry { state, symbol } => {
write!(
f,
"Duplicate action entry for state {}, symbol {}",
state, symbol
)
}
SchemaError::MissingAcceptState => write!(f, "No Accept action found in parse table"),
SchemaError::InvalidEOFHandling { reason } => {
write!(f, "Invalid EOF handling: {}", reason)
}
SchemaError::CompressedTableIntegrity { reason } => {
write!(f, "Compressed table integrity check failed: {}", reason)
}
}
}
}
impl std::error::Error for SchemaError {}
#[must_use = "validation result must be checked"]
pub fn validate_action_encoding(action: &Action) -> Result<u16, SchemaError> {
match action {
Action::Error => Ok(0x0000),
Action::Shift(state) => {
let state_val = state.0;
if state_val == 0 {
Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: 0,
reason: "Shift(0) would encode as 0x0000, which is reserved for Error"
.to_string(),
})
} else if state_val >= 0x8000 {
Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: state_val,
reason: "Shift state >= 0x8000 would have high bit set, conflicting with Reduce encoding".to_string(),
})
} else {
Ok(state_val)
}
}
Action::Reduce(production_id) => {
let prod_val = production_id.0;
if prod_val >= 0x7FFF {
Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: 0x8000 | prod_val,
reason: "Reduce production ID >= 0x7FFF would encode as 0xFFFF (Accept)"
.to_string(),
})
} else {
Ok(0x8000 | prod_val)
}
}
Action::Accept => Ok(0xFFFF),
Action::Recover => Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: 0,
reason: "Recover actions are not encoded in parse tables (runtime only)".to_string(),
}),
Action::Fork(_) => Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: 0,
reason: "Fork actions are not encoded in parse tables (runtime only)".to_string(),
}),
_ => Err(SchemaError::InvalidActionEncoding {
action: action.clone(),
encoded_value: 0,
reason: "Unknown action variant cannot be encoded".to_string(),
}),
}
}
#[must_use = "validation result must be checked"]
pub fn validate_action_decoding(encoded: u16, expected: &Action) -> Result<(), SchemaError> {
let decoded = decode_action_from_encoding(encoded);
if &decoded != expected {
Err(SchemaError::InvalidActionEncoding {
action: expected.clone(),
encoded_value: encoded,
reason: format!(
"Encoding 0x{:04X} decodes to {:?}, not {:?}",
encoded, decoded, expected
),
})
} else {
Ok(())
}
}
fn decode_action_from_encoding(encoded: u16) -> Action {
if encoded == 0xFFFF {
Action::Accept
} else if encoded == 0 {
Action::Error
} else if encoded & 0x8000 != 0 {
Action::Reduce(RuleId(encoded & 0x7FFF))
} else {
Action::Shift(StateId(encoded))
}
}
#[must_use = "validation result must be checked"]
pub fn validate_parse_table(table: &ParseTable) -> Result<(), Vec<SchemaError>> {
let mut errors = Vec::new();
let mut seen_actions: HashSet<(u16, u16)> = HashSet::new();
let mut has_accept = false;
for (state_idx, action_row) in table.action_table.iter().enumerate() {
for (symbol_idx, action_cell) in action_row.iter().enumerate() {
for action in action_cell {
match validate_action_encoding(action) {
Ok(encoded) => {
if let Err(e) = validate_action_decoding(encoded, action) {
errors.push(e);
}
if matches!(action, Action::Accept) {
has_accept = true;
}
}
Err(e) => errors.push(e),
}
let key = (state_idx as u16, symbol_idx as u16);
if action_cell.len() == 1 && seen_actions.contains(&key) {
errors.push(SchemaError::DuplicateActionEntry {
state: state_idx as u16,
symbol: symbol_idx as u16,
});
}
seen_actions.insert(key);
if let Action::Shift(next_state) = action
&& (next_state.0 as usize) >= table.action_table.len()
{
errors.push(SchemaError::InvalidStateId {
state_id: next_state.0,
max_states: table.action_table.len(),
});
}
if let Action::Reduce(production_id) = action {
if production_id.0 == 0x7FFF {
errors.push(SchemaError::InvalidProductionId {
production_id: production_id.0,
max_productions: 0x7FFF,
});
}
}
}
}
}
if !has_accept {
errors.push(SchemaError::MissingAcceptState);
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_encoding() {
assert_eq!(validate_action_encoding(&Action::Error), Ok(0x0000));
}
#[test]
fn test_accept_encoding() {
assert_eq!(validate_action_encoding(&Action::Accept), Ok(0xFFFF));
}
#[test]
fn test_shift_encoding() {
assert_eq!(validate_action_encoding(&Action::Shift(StateId(1))), Ok(1));
assert_eq!(
validate_action_encoding(&Action::Shift(StateId(100))),
Ok(100)
);
assert_eq!(
validate_action_encoding(&Action::Shift(StateId(0x7FFF))),
Ok(0x7FFF)
);
}
#[test]
fn test_reduce_encoding() {
assert_eq!(
validate_action_encoding(&Action::Reduce(RuleId(0))),
Ok(0x8000)
);
assert_eq!(
validate_action_encoding(&Action::Reduce(RuleId(1))),
Ok(0x8001)
);
assert_eq!(
validate_action_encoding(&Action::Reduce(RuleId(100))),
Ok(0x8064)
);
}
#[test]
fn test_shift_zero_invalid() {
let result = validate_action_encoding(&Action::Shift(StateId(0)));
assert!(result.is_err());
match result {
Err(SchemaError::InvalidActionEncoding { action, .. }) => {
assert_eq!(action, Action::Shift(StateId(0)));
}
_ => panic!("Expected InvalidActionEncoding error"),
}
}
#[test]
fn test_reduce_overflow_invalid() {
let result = validate_action_encoding(&Action::Reduce(RuleId(0x7FFF)));
assert!(result.is_err());
}
#[test]
fn test_decoding_roundtrip() {
let test_cases = vec![
(0x0000, Action::Error),
(0x0001, Action::Shift(StateId(1))),
(0x7FFF, Action::Shift(StateId(0x7FFF))),
(0x8000, Action::Reduce(RuleId(0))),
(0x8001, Action::Reduce(RuleId(1))),
(0xFFFE, Action::Reduce(RuleId(0x7FFE))),
(0xFFFF, Action::Accept),
];
for (encoded, expected_action) in test_cases {
let decoded = decode_action_from_encoding(encoded);
assert_eq!(
decoded, expected_action,
"Encoding 0x{:04X} should decode to {:?}",
encoded, expected_action
);
}
}
}