//! Deterministic, cross-process-stable hash of a `Module` for the
//! HIR-addressable object cache (issue #686).
//!
//! Contract:
//! - Same HIR → same hash, in the same process and across processes.
//! - Different HIR (any field that can affect generated bytes) → different hash.
//! - `HashMap` iteration order MUST NOT leak — keys are sorted before emit.
//! - No `Debug`, no `serde`, no `std::hash::Hash` on unordered structures.
//!
//! Adding a new HIR variant must force a build break in this file. To make
//! that the case, every `match` over a HIR enum is exhaustive (no wildcards
//! `_ =>`), and each variant is given an explicit `u16` tag. A new variant
//! → unhandled match arm → compile error.
//!
//! Used by `compute_object_cache_key` to fingerprint the post-transform
//! HIR that `perry_codegen::compile_module` actually consumes.
//!
//! # Module layout
//!
//! Originally a single ~4,100-line `stable_hash.rs`. Split (no behavior
//! change) into topical sub-modules:
//!
//! - `primitives` — `SH` trait + primitive impls (`bool`, `u32`, `f64`, …)
//! - `module`     — `Module`, `Import`, `Export`, `ModuleKind`, `ModuleInitKind`
//! - `types`      — `Type`, `TypeParam`, `ObjectType`, `PropertyInfo`, `FunctionType`
//! - `decls`      — `Class`, `Interface`, `Enum`, `Global`, `Decorator`, `Function`, `Param`
//! - `stmts`      — `Stmt`, `SwitchCase`, `CatchClause`
//! - `ops`        — `BinaryOp`, `UnaryOp`, `CompareOp`, `LogicalOp`, `UpdateOp`, `ArrayElement`, `CallArg`
//! - `widgets`    — `WidgetDecl` + the whole `Widget*` tree
//! - `expr`       — the giant `impl SH for Expr` (compacted to one arm per line)
//! - `tests`      — moved-out `#[cfg(test)]` module
//!
//! Sub-modules are private; this file re-exports the public surface
//! (`StableHasher`, `Djb2Hasher`, `hash_module`, `hash_module_with`).

mod decls;
mod expr;
mod module;
mod ops;
mod primitives;
mod stmts;
mod types;
mod widgets;

#[cfg(test)]
mod tests;

use crate::ir::Module;
use primitives::SH;

/// Streaming hasher — minimal API so any djb2/fnv/blake variant can plug in.
pub trait StableHasher {
    fn write(&mut self, bytes: &[u8]);
    fn finish(&self) -> u64;
}

/// Convenience: hash a module to a `u64` using the default djb2-equivalent.
pub fn hash_module(m: &Module) -> u64 {
    let mut h = Djb2Hasher::new();
    hash_module_with(m, &mut h);
    h.finish()
}

/// Streaming variant — fold the HIR hash into an existing `StableHasher`.
pub fn hash_module_with<H: StableHasher>(m: &Module, h: &mut H) {
    SH::hash(m, h);
}

/// Local djb2 implementation, mirrored from
/// `crates/perry/src/commands/compile/object_cache.rs:63-87`. Kept here so
/// `perry-hir` doesn't depend on `perry`. Both must agree on the algorithm
/// for the cross-process determinism test to be meaningful.
pub struct Djb2Hasher {
    h: u64,
}

impl Djb2Hasher {
    pub fn new() -> Self {
        Self { h: 5381 }
    }
}

impl Default for Djb2Hasher {
    fn default() -> Self {
        Self::new()
    }
}

impl StableHasher for Djb2Hasher {
    fn write(&mut self, bytes: &[u8]) {
        for b in bytes {
            self.h = self.h.wrapping_mul(33).wrapping_add(*b as u64);
        }
    }
    fn finish(&self) -> u64 {
        self.h
    }
}
