marser 0.1.0

Parser combinator toolkit with matcher-level backtracking and rich error reporting.
use std::collections::HashMap;

use crate::context::ParserContext;
use crate::trace::{
    ExplicitMarkerEndOutcome, NodeTrace, NodeTraceKind, NodeTraceStatus, RuleIdentity,
    RuleSourceMetadata, TraceEventKind, TraceLocation, TraceMarkerFailureSnapshot,
    TraceMarkerPhase, TraceSession,
};

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct RuleMetadataKey {
    file: &'static str,
    line: u32,
    column: u32,
    rule_name: Option<String>,
}

pub(crate) struct ExplicitMarkerTraceParams {
    pub marker_id: u64,
    pub phase: TraceMarkerPhase,
    pub pos: usize,
    pub label: Option<String>,
    pub usage_metadata: RuleSourceMetadata,
    pub definition_metadata: Option<RuleSourceMetadata>,
    pub end_outcome: Option<ExplicitMarkerEndOutcome>,
    pub marker_failure: Option<TraceMarkerFailureSnapshot>,
}

struct ExplicitMarkerRecord {
    marker_id: u64,
    marker_phase: TraceMarkerPhase,
    runtime_kind: TraceEventKind,
    status: NodeTraceStatus,
    pos: usize,
    label: Option<String>,
    usage_metadata: RuleSourceMetadata,
    definition_metadata: Option<RuleSourceMetadata>,
    marker_failure: Option<TraceMarkerFailureSnapshot>,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct TraceState {
    pub(crate) session: TraceSession,
    next_id: u64,
    next_marker_id: u64,
    next_rule_id: u64,
    rule_ids: HashMap<RuleMetadataKey, u64>,
}

impl<'src> ParserContext<'src> {
    #[inline]
    pub fn attach_trace_session(&mut self, session: TraceSession) {
        self.trace = Some(TraceState {
            session,
            next_id: 0,
            next_marker_id: 0,
            next_rule_id: 0,
            rule_ids: HashMap::new(),
        });
    }

    #[inline]
    pub fn take_trace_session(&mut self) -> Option<TraceSession> {
        self.trace.take().map(|t| t.session)
    }

    #[inline]
    pub fn next_trace_marker_id(&mut self) -> u64 {
        let trace = self.trace.get_or_insert_with(|| TraceState {
            session: TraceSession::new(),
            ..Default::default()
        });
        let id = trace.next_marker_id;
        trace.next_marker_id = trace.next_marker_id.saturating_add(1);
        id
    }

    pub fn trace_explicit_marker(&mut self, params: ExplicitMarkerTraceParams) {
        debug_assert!(
            params.marker_failure.is_none() || matches!(params.phase, TraceMarkerPhase::End),
            "marker_failure only allowed on explicit trace End"
        );
        debug_assert!(
            !matches!(params.phase, TraceMarkerPhase::None),
            "explicit trace marker events should not use phase None"
        );

        let (runtime_kind, status) = match params.phase {
            TraceMarkerPhase::Start => (TraceEventKind::ParserEnter, NodeTraceStatus::Enter),
            TraceMarkerPhase::End => {
                match params
                    .end_outcome
                    .expect("explicit trace End requires outcome")
                {
                    ExplicitMarkerEndOutcome::Success => {
                        (TraceEventKind::ParserExit, NodeTraceStatus::Success)
                    }
                    ExplicitMarkerEndOutcome::SoftFail => {
                        (TraceEventKind::MatchFail, NodeTraceStatus::Fail)
                    }
                    ExplicitMarkerEndOutcome::HardError => {
                        (TraceEventKind::MatchHardError, NodeTraceStatus::Fail)
                    }
                }
            }
            TraceMarkerPhase::None => unreachable!("explicit marker phase None is invalid"),
        };

        self.record_explicit_marker(ExplicitMarkerRecord {
            marker_id: params.marker_id,
            marker_phase: params.phase,
            runtime_kind,
            status,
            pos: params.pos,
            label: params.label,
            usage_metadata: params.usage_metadata,
            definition_metadata: params.definition_metadata,
            marker_failure: params.marker_failure,
        });
    }

    fn record_explicit_marker(&mut self, record: ExplicitMarkerRecord) {
        let Some(trace) = self.trace.as_mut() else {
            return;
        };

        let node_id = trace.next_id;
        trace.next_id = trace.next_id.saturating_add(1);

        let rule = Some(resolve_rule_identity(
            trace,
            record.usage_metadata,
            record.label.as_ref(),
        ));
        let usage_loc = rule.as_ref().map(|r| TraceLocation {
            file: r.rule_file.clone(),
            line: r.rule_line,
            column: r.rule_column,
        });
        let definition_loc = record.definition_metadata.map(|meta| TraceLocation {
            file: meta.file.to_string(),
            line: meta.line,
            column: meta.column,
        });
        let marker_failure = if matches!(record.marker_phase, TraceMarkerPhase::End) {
            record.marker_failure
        } else {
            None
        };

        trace.session.record(NodeTrace {
            node_id,
            parent_node_id: None,
            usage_loc: usage_loc.clone(),
            definition_loc: definition_loc.or(usage_loc),
            kind: NodeTraceKind::Runtime,
            status: record.status,
            label: record.label,
            input_start: record.pos,
            input_end: record.pos,
            is_step_marker: true,
            trace_marker_id: Some(record.marker_id),
            marker_phase: record.marker_phase,
            is_explicit_trace_marker: true,
            runtime_kind: Some(record.runtime_kind),
            rule,
            error_sink_len: self.error_sink.len(),
            error_stack_len: self.error_stack.len(),
            marker_failure,
        });
    }
}

fn resolve_rule_identity(
    trace: &mut TraceState,
    metadata: RuleSourceMetadata,
    label: Option<&String>,
) -> RuleIdentity {
    let derived_rule_name = metadata
        .rule_name
        .map(str::to_string)
        .or_else(|| label.cloned());
    let key = RuleMetadataKey {
        file: metadata.file,
        line: metadata.line,
        column: metadata.column,
        rule_name: derived_rule_name.clone(),
    };
    let rule_id = if let Some(id) = trace.rule_ids.get(&key) {
        *id
    } else {
        let id = trace.next_rule_id;
        trace.next_rule_id = trace.next_rule_id.saturating_add(1);
        trace.rule_ids.insert(key, id);
        id
    };
    RuleIdentity {
        rule_id,
        rule_name: derived_rule_name,
        rule_file: metadata.file.to_string(),
        rule_line: metadata.line,
        rule_column: metadata.column,
    }
}