zirium 0.4.0

Lossless textual MLIR syntax parsing and semantic processing
Documentation
use super::*;
use crate::{
    lexer::{Token, lex},
    representation::SyntaxElement,
    source::{Source, TextRange},
};
use std::{
    collections::{HashMap, HashSet},
    env,
    hint::black_box,
    mem::size_of,
    sync::Arc,
    time::{Duration, Instant},
};

fn begin_measurement() -> (Instant, isize) {
    (Instant::now(), crate::benchmark_allocator::begin())
}

fn finish_measurement(started: Instant, baseline: isize) -> (Duration, usize) {
    (
        started.elapsed(),
        crate::benchmark_allocator::finish(baseline),
    )
}

#[derive(Clone, Copy)]
enum Shape {
    Primary,
    BlockRich,
}

impl Shape {
    fn from_env() -> Self {
        match env::var("ZIRIUM_PARSER_BENCH_SHAPE").as_deref() {
            Ok("block-rich") => Self::BlockRich,
            Ok("primary") | Err(_) => Self::Primary,
            Ok(value) => panic!("unknown ZIRIUM_PARSER_BENCH_SHAPE={value}"),
        }
    }
    fn name(self) -> &'static str {
        match self {
            Self::Primary => "primary",
            Self::BlockRich => "block-rich",
        }
    }
}

#[test]
#[ignore = "release-only parser construction measurement; see processing-benchmarks.md"]
fn measure_parser_construction_and_string_identity() {
    let smoke = env::var_os("ZIRIUM_PARSER_BENCH_SMOKE").is_some();
    let bytes = if smoke {
        64 * 1024
    } else {
        env::var("ZIRIUM_PARSER_BENCH_SIZE_MIB")
            .unwrap_or_else(|_| "10".into())
            .parse::<usize>()
            .expect("integer ZIRIUM_PARSER_BENCH_SIZE_MIB")
            * 1024
            * 1024
    };
    let runs = if smoke {
        1
    } else {
        env::var("ZIRIUM_PARSER_BENCH_RUNS")
            .unwrap_or_else(|_| "3".into())
            .parse::<usize>()
            .expect("integer ZIRIUM_PARSER_BENCH_RUNS")
    };
    let warmups = if smoke {
        1
    } else {
        env::var("ZIRIUM_PARSER_BENCH_WARMUPS")
            .unwrap_or_else(|_| "1".into())
            .parse::<usize>()
            .expect("integer ZIRIUM_PARSER_BENCH_WARMUPS")
    };
    assert!(runs > 0);
    let shape = Shape::from_env();
    let source_bytes = Arc::<[u8]>::from(fixture(shape, bytes));
    println!(
        "parser_construction fixture={} input_bytes={} warmups={} runs={} smoke={smoke}",
        shape.name(),
        bytes,
        warmups,
        runs
    );

    let source = Source::new(source_bytes.clone()).unwrap();
    let registry = DialectRegistry::baseline();
    report_whole_parse(shape.name(), source_bytes.clone(), registry, warmups, runs);
    report_phase("lex", warmups, runs, || {
        let (started, baseline) = begin_measurement();
        let output = lex(&source);
        let result = finish_measurement(started, baseline);
        black_box(output.tokens().len());
        (result, output.tokens().len())
    });

    let lexed = lex(&source);
    report_phase("events", warmups, runs, || {
        let (started, baseline) = begin_measurement();
        let (builder, diagnostics) =
            produce_operation_events(&lexed, source.bytes(), registry, ParserLimits::default())
                .unwrap();
        let events = builder.into_events();
        let result = finish_measurement(started, baseline);
        black_box((&events, &diagnostics));
        (result, events.len())
    });

    let (builder, _) =
        produce_operation_events(&lexed, source.bytes(), registry, ParserLimits::default())
            .unwrap();
    let events = builder.into_events();
    report_phase("compact", warmups, runs, || {
        let (started, baseline) = begin_measurement();
        let tree =
            SyntaxTree::from_events_unverified(events.clone(), lexed.tokens().to_vec()).unwrap();
        let result = finish_measurement(started, baseline);
        black_box(&tree);
        (result, tree.node_count())
    });

    let tree = SyntaxTree::from_events_unverified(events.clone(), lexed.tokens().to_vec()).unwrap();
    report_string_identity(source.bytes(), &tree);
    drop(events);
    drop(lexed);
    report_phase("verify", warmups, runs, || {
        let (started, baseline) = begin_measurement();
        tree.verify().unwrap();
        let result = finish_measurement(started, baseline);
        black_box(&tree);
        (result, tree.node_count())
    });
}

fn report_whole_parse(
    fixture: &str,
    source: Arc<[u8]>,
    registry: &DialectRegistry,
    warmups: usize,
    runs: usize,
) {
    let measure = || {
        let (started, baseline) = begin_measurement();
        let parsed = ParsedFile::parse_with_registry(source.clone(), registry).unwrap();
        let (elapsed, peak) = finish_measurement(started, baseline);
        let tree = parsed.syntax().tree();
        let sample = (
            elapsed,
            peak,
            tree.token_count(),
            tree.node_count(),
            tree.exact_retained_bytes(),
        );
        black_box(&parsed);
        sample
    };
    for _ in 0..warmups {
        black_box(measure());
    }
    let mut samples = (0..runs).map(|_| measure()).collect::<Vec<_>>();
    samples.sort_by_key(|sample| sample.0);
    let (elapsed, peak, tokens, nodes, retained) = samples[samples.len() / 2];
    println!(
        "parser_whole fixture={fixture} input_bytes={} median_ns={} incremental_peak_live_bytes={peak} tokens={tokens} nodes={nodes} exact_cst_retained_bytes={retained}",
        source.len(),
        elapsed.as_nanos(),
    );
}

fn report_phase(
    phase: &str,
    warmups: usize,
    runs: usize,
    mut measure: impl FnMut() -> ((Duration, usize), usize),
) {
    for _ in 0..warmups {
        black_box(measure());
    }
    let mut samples = (0..runs).map(|_| measure()).collect::<Vec<_>>();
    samples.sort_by_key(|sample| sample.0.0);
    let ((elapsed, peak), output_items) = samples[samples.len() / 2];
    println!(
        "parser_phase phase={phase} median_ns={} incremental_peak_live_bytes={peak} output_items={output_items}",
        elapsed.as_nanos()
    );
}

fn report_string_identity(source: &[u8], tree: &SyntaxTree) {
    let operation_names = tree
        .subtree(tree.root())
        .unwrap()
        .filter(|id| {
            matches!(
                tree.kind(*id),
                Some(SyntaxKind::Operation | SyntaxKind::DialectOperation)
            )
        })
        .filter_map(|id| {
            tree.elements(id)?
                .into_iter()
                .find_map(|element| match element {
                    SyntaxElement::Token { index, token } if token.kind() == TokenKind::String => {
                        Some(index)
                    }
                    _ => None,
                })
        })
        .collect::<HashSet<_>>();
    let classes = [
        (TokenKind::BareIdentifier, "bare"),
        (TokenKind::AtIdentifier, "at"),
        (TokenKind::PercentIdentifier, "percent"),
        (TokenKind::CaretIdentifier, "caret"),
        (TokenKind::ExclamationIdentifier, "exclamation"),
        (TokenKind::HashIdentifier, "hash"),
        (TokenKind::String, "operation-string"),
    ];
    let mut all_spellings = Vec::new();
    for (kind, name) in classes {
        let spellings = tree
            .tokens(tree.root())
            .unwrap()
            .iter()
            .enumerate()
            .filter(|(index, token)| {
                token.kind() == kind
                    && (kind != TokenKind::String || operation_names.contains(index))
            })
            .map(|(_, token)| token_text(source, token))
            .collect::<Vec<_>>();
        let unique = spellings.iter().copied().collect::<HashSet<_>>();
        let unique_bytes = unique.iter().map(|text| text.len()).sum::<usize>();
        let current = spellings.len() * size_of::<TextRange>();
        let candidate = current
            + spellings.len() * size_of::<u32>()
            + unique.len() * (size_of::<u32>() + size_of::<u64>() + 2 * size_of::<usize>())
            + unique_bytes;
        println!(
            "string_identity class={name} frequency={} unique={} current_range_record_bytes={current} candidate_range_plus_id_interner_bytes={candidate} unique_spelling_bytes={unique_bytes}",
            spellings.len(),
            unique.len()
        );
        all_spellings.extend(spellings);
    }
    let mut interner = HashMap::<&[u8], u32>::new();
    for spelling in &all_spellings {
        let next = interner.len() as u32;
        interner.entry(spelling).or_insert(next);
    }
    let started = Instant::now();
    let checksum = all_spellings
        .iter()
        .map(|spelling| *interner.get(spelling).unwrap() as u64)
        .sum::<u64>();
    println!(
        "string_identity_lookup lookups={} elapsed_ns={} checksum={}",
        all_spellings.len(),
        started.elapsed().as_nanos(),
        black_box(checksum)
    );
}

fn token_text<'a>(source: &'a [u8], token: &Token) -> &'a [u8] {
    &source[token.range().start() as usize..token.range().end() as usize]
}

fn fixture(shape: Shape, bytes: usize) -> Vec<u8> {
    match shape {
        Shape::Primary => primary_fixture(bytes),
        Shape::BlockRich => block_rich_fixture(bytes),
    }
}

fn primary_fixture(bytes: usize) -> Vec<u8> {
    let prefix = b"\"bench.container\"() ({\n^bb:\n%seed = \"bench.source\"() : () -> i32\n";
    let suffix = b"}) : () -> ()\n";
    let line = b"\"bench.op\"() {tag = \"zirium\"} : () -> () // deterministic parser benchmark padding xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n";
    exact_fixture(bytes, prefix, line, suffix)
}

fn block_rich_fixture(bytes: usize) -> Vec<u8> {
    let mut output = b"builtin.module {\n".to_vec();
    let suffix = b"}\n";
    let mut index = 0;
    loop {
        let function = format!(
            "func.func @f{index}() {{\n^entry:\n%value = arith.constant 1 : i32\ncf.br ^middle\n^middle:\n\"bench.use\"(%value) : (i32) -> ()\ncf.br ^exit\n^exit:\nfunc.return\n}}\n"
        );
        if output.len() + function.len() + suffix.len() > bytes {
            break;
        }
        output.extend_from_slice(function.as_bytes());
        index += 1;
    }
    output.resize(bytes - suffix.len(), b' ');
    output.extend_from_slice(suffix);
    output
}

fn exact_fixture(bytes: usize, prefix: &[u8], line: &[u8], suffix: &[u8]) -> Vec<u8> {
    assert!(bytes >= prefix.len() + suffix.len());
    let mut output = Vec::with_capacity(bytes);
    output.extend_from_slice(prefix);
    while output.len() + line.len() + suffix.len() <= bytes {
        output.extend_from_slice(line);
    }
    output.resize(bytes - suffix.len(), b' ');
    output.extend_from_slice(suffix);
    output
}