///|
fn validate_index_document_ids(
documents : Array[RomanDocument],
) -> Result[Unit, RomanIndexConstructionError] {
let seen : @hashmap.HashMap[String, Int] = @hashmap.HashMap([])
for index = 0; index < documents.length(); index = index + 1 {
if is_blank_batch_id(documents[index].id) {
return Err(EmptyIndexDocumentId(index))
}
if seen.contains(documents[index].id) {
return Err(DuplicateIndexDocumentId(documents[index].id))
}
seen.set(documents[index].id, index)
}
Ok(())
}
///|
fn accumulate_index_statistic(total : Int64, increment : Int) -> Int64 {
total + increment.to_int64()
}
///|
fn validate_index_report_span(
document_id : String,
source_chars : Array[Char],
kind : RomanIndexEvidenceKind,
item_index : Int,
span : SourceSpan,
retained_source : String,
) -> Result[Unit, RomanIndexConstructionError] {
if span.start < 0 ||
span.end <= span.start ||
span.end > source_chars.length() {
return Err(
IndexReportInvalidSpan(
document_id,
kind,
item_index,
span,
source_chars.length(),
),
)
}
let actual_source = scan_substring(source_chars, span.start, span.end)
if actual_source != retained_source {
return Err(
IndexReportSourceMismatch(
document_id, kind, item_index, actual_source, retained_source,
),
)
}
Ok(())
}
///|
fn validate_index_match_policy(
matched : RomanScanMatch,
config : ScanConfig,
) -> Bool {
let source_length = matched.span.end - matched.span.start
source_length <= config.max_candidate_length &&
(
config.include_single_symbol ||
matched.report.normalized.to_array().length() != 1
)
}
///|
fn validate_index_report_matches(
document_id : String,
source_chars : Array[Char],
config : ScanConfig,
matches : Array[RomanScanMatch],
) -> Result[Unit, RomanIndexConstructionError] {
for index = 0; index < matches.length(); index = index + 1 {
let matched = matches[index]
match
validate_index_report_span(
document_id,
source_chars,
IndexMatchEvidence,
index,
matched.span,
matched.source_text,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index > 0 {
if matched.span.start < matches[index - 1].span.start {
return Err(
IndexReportOutOfOrder(document_id, IndexMatchEvidence, index),
)
}
if matched.span.start < matches[index - 1].span.end {
return Err(
IndexReportOverlappingEvidence(
document_id,
IndexMatchEvidence,
index - 1,
IndexMatchEvidence,
index,
),
)
}
}
let reparsed = match
parse_with_config(matched.source_text, config.parse_config) {
Err(error) =>
return Err(IndexReportMatchParseFailed(document_id, index, error))
Ok(report) => report
}
if reparsed != matched.report {
return Err(IndexReportMatchEvidenceMismatch(document_id, index))
}
if !validate_index_match_policy(matched, config) {
return Err(IndexReportMatchPolicyMismatch(document_id, index))
}
}
Ok(())
}
///|
fn index_rejection_reason_matches(
rejected : RomanScanRejection,
config : ScanConfig,
) -> Bool {
let source_length = rejected.span.end - rejected.span.start
match rejected.reason {
CandidateTooLong(retained_length) =>
source_length > config.max_candidate_length &&
retained_length == source_length
SingleSymbolExcluded =>
if source_length > config.max_candidate_length ||
config.include_single_symbol {
false
} else {
match parse_with_config(rejected.source_text, config.parse_config) {
Ok(report) => report.normalized.to_array().length() == 1
Err(_) => false
}
}
CandidateParseFailed(retained_error) =>
if source_length > config.max_candidate_length {
false
} else {
match parse_with_config(rejected.source_text, config.parse_config) {
Err(actual_error) => actual_error == retained_error
Ok(_) => false
}
}
}
}
///|
fn validate_index_report_rejections(
document_id : String,
source_chars : Array[Char],
config : ScanConfig,
rejections : Array[RomanScanRejection],
) -> Result[Unit, RomanIndexConstructionError] {
if !config.retain_rejected && !rejections.is_empty() {
return Err(IndexReportUnexpectedRejectionDetails(document_id))
}
for index = 0; index < rejections.length(); index = index + 1 {
let rejected = rejections[index]
match
validate_index_report_span(
document_id,
source_chars,
IndexRejectionEvidence,
index,
rejected.span,
rejected.source_text,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index > 0 {
if rejected.span.start < rejections[index - 1].span.start {
return Err(
IndexReportOutOfOrder(document_id, IndexRejectionEvidence, index),
)
}
if rejected.span.start < rejections[index - 1].span.end {
return Err(
IndexReportOverlappingEvidence(
document_id,
IndexRejectionEvidence,
index - 1,
IndexRejectionEvidence,
index,
),
)
}
}
if !index_rejection_reason_matches(rejected, config) {
return Err(IndexReportRejectionReasonMismatch(document_id, index))
}
}
Ok(())
}
///|
struct IndexCrossOverlapScan {
overlap : (Int, Int)?
steps : Int
} derive(Eq, Debug)
///|
fn scan_index_cross_overlaps(
match_spans : Array[SourceSpan],
rejection_spans : Array[SourceSpan],
) -> IndexCrossOverlapScan {
let mut match_index = 0
let mut rejection_index = 0
let mut steps = 0
while match_index < match_spans.length() &&
rejection_index < rejection_spans.length() {
steps = steps + 1
let matched = match_spans[match_index]
let rejected = rejection_spans[rejection_index]
if matched.start < rejected.end && rejected.start < matched.end {
return { overlap: Some((match_index, rejection_index)), steps }
}
if matched.end <= rejected.start {
match_index = match_index + 1
} else {
rejection_index = rejection_index + 1
}
}
{ overlap: None, steps }
}
///|
fn validate_index_report_cross_overlaps(
document_id : String,
matches : Array[RomanScanMatch],
rejections : Array[RomanScanRejection],
) -> Result[Unit, RomanIndexConstructionError] {
let match_spans : Array[SourceSpan] = []
let rejection_spans : Array[SourceSpan] = []
for matched in matches {
match_spans.push(matched.span)
}
for rejected in rejections {
rejection_spans.push(rejected.span)
}
match scan_index_cross_overlaps(match_spans, rejection_spans).overlap {
Some((match_index, rejection_index)) =>
Err(
IndexReportOverlappingEvidence(
document_id,
IndexMatchEvidence,
match_index,
IndexRejectionEvidence,
rejection_index,
),
)
None => Ok(())
}
}
///|
fn validate_index_scan_report_against_expected(
document_id : String,
source : String,
config : ScanConfig,
expected : RomanScanReport,
report : RomanScanReport,
) -> Result[Unit, RomanIndexConstructionError] {
match validate_scan_config(config) {
Err(error) => return Err(IndexDocumentScanFailed(document_id, error))
Ok(_) => ()
}
let retained_count = report.matches.length() + report.rejections.length()
if report.candidates_examined < retained_count {
return Err(
IndexReportCandidateCountTooSmall(
document_id,
report.candidates_examined,
retained_count,
),
)
}
let source_chars = source.to_array()
match
validate_index_report_matches(
document_id,
source_chars,
config,
report.matches,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
match
validate_index_report_rejections(
document_id,
source_chars,
config,
report.rejections,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
match
validate_index_report_cross_overlaps(
document_id,
report.matches,
report.rejections,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
if expected != report {
return Err(IndexScanReportMismatch(document_id))
}
Ok(())
}
///|
fn validate_index_scan_report(
document_id : String,
source : String,
config : ScanConfig,
report : RomanScanReport,
) -> Result[Unit, RomanIndexConstructionError] {
let expected = match scan_roman_text(source, config) {
Err(error) => return Err(IndexDocumentScanFailed(document_id, error))
Ok(expected) => expected
}
validate_index_scan_report_against_expected(
document_id, source, config, expected, report,
)
}
///|
fn index_entry_from_match(
document_id : String,
mode : RomanMode,
matched : RomanScanMatch,
ordinal : Int,
) -> RomanIndexEntry {
let parsed = matched.report
{
document_id,
source_span: matched.span,
source_text: matched.source_text,
value: parsed.value,
canonical: parsed.canonical,
normalized_text: parsed.normalized,
mode,
used_unicode_compatibility: parsed.used_unicode_compatibility,
normalized_input: parsed.original != parsed.normalized,
trimmed_outer_whitespace: parsed.trimmed_outer_whitespace,
token_count: parsed.tokens.length(),
ordinal,
}
}
///|
fn rejection_summary_from_scan(
document_id : String,
rejected : RomanScanRejection,
) -> RomanIndexRejectionSummary {
{
document_id,
source_span: rejected.span,
source_text: rejected.source_text,
reason: rejected.reason,
}
}
///|
fn materialize_verified_scan_index(
document_id : String,
source : String,
config : ScanConfig,
report : RomanScanReport,
first_global_ordinal : Int,
) -> RomanDocumentIndex {
let entries : Array[RomanIndexEntry] = []
let rejections : Array[RomanIndexRejectionSummary] = []
let mut used_unicode_compatibility = 0L
let mut normalized_inputs = 0L
for matched in report.matches {
let entry = index_entry_from_match(
document_id,
config.parse_config.mode,
matched,
first_global_ordinal + entries.length(),
)
if entry.used_unicode_compatibility {
used_unicode_compatibility = accumulate_index_statistic(
used_unicode_compatibility, 1,
)
}
if entry.normalized_input {
normalized_inputs = accumulate_index_statistic(normalized_inputs, 1)
}
entries.push(entry)
}
for rejected in report.rejections {
rejections.push(rejection_summary_from_scan(document_id, rejected))
}
let candidates_examined = accumulate_index_statistic(
0L,
report.candidates_examined,
)
let accepted_entries = accumulate_index_statistic(0L, report.matches.length())
let rejected_count = report.candidates_examined - report.matches.length()
let rejected_candidates = accumulate_index_statistic(0L, rejected_count)
let first_ordinal = if entries.is_empty() {
None
} else {
Some(first_global_ordinal)
}
{
entries,
documents: [
{
id: document_id,
source_length: source.to_array().length(),
parse_mode: config.parse_config.mode,
candidates_examined: report.candidates_examined,
accepted_count: report.matches.length(),
rejected_count,
first_ordinal,
},
],
rejections,
statistics: {
total_documents: 1L,
candidates_examined,
accepted_entries,
rejected_candidates,
used_unicode_compatibility,
normalized_inputs,
},
}
}
///|
fn build_index_from_validated_scan(
document_id : String,
source : String,
config : ScanConfig,
report : RomanScanReport,
first_global_ordinal : Int,
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
match validate_index_scan_report(document_id, source, config, report) {
Err(error) => return Err(error)
Ok(_) => ()
}
Ok(
materialize_verified_scan_index(
document_id, source, config, report, first_global_ordinal,
),
)
}
///|
fn append_document_index(
entries : Array[RomanIndexEntry],
documents : Array[RomanIndexDocumentMetadata],
rejections : Array[RomanIndexRejectionSummary],
document_index : RomanDocumentIndex,
) -> Unit {
for entry in document_index.entries {
entries.push(entry)
}
documents.push(document_index.documents[0])
for rejected in document_index.rejections {
rejections.push(rejected)
}
}
///|
/// Build from an existing report after rescanning to reject mutable evidence.
/// This correctness check is not a performance optimization.
pub fn build_roman_index_from_scan_report(
document_id : String,
source : String,
config : ScanConfig,
report : RomanScanReport,
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
let documents : Array[RomanDocument] = [
{ id: document_id, text: source, config },
]
match validate_index_document_ids(documents) {
Err(error) => return Err(error)
Ok(_) => ()
}
build_index_from_validated_scan(document_id, source, config, report, 0)
}
///|
/// Build an index for one document by rescanning caller-owned source/config.
pub fn build_roman_index_from_scan(
document_id : String,
source : String,
config : ScanConfig,
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
let documents : Array[RomanDocument] = [
{ id: document_id, text: source, config },
]
match validate_index_document_ids(documents) {
Err(error) => return Err(error)
Ok(_) => ()
}
match scan_roman_text(source, config) {
Err(error) => Err(IndexDocumentScanFailed(document_id, error))
Ok(report) =>
Ok(
materialize_verified_scan_index(document_id, source, config, report, 0),
)
}
}
///|
fn validate_corpus_report_identity(
inputs : Array[RomanDocument],
report : RomanCorpusReport,
) -> Result[Unit, RomanIndexConstructionError] {
if report.documents.length() != inputs.length() {
return Err(
IndexReportDocumentCountMismatch(
inputs.length(),
report.documents.length(),
),
)
}
for index = 0; index < inputs.length(); index = index + 1 {
if report.documents[index].id != inputs[index].id {
return Err(
IndexReportDocumentIdMismatch(
index,
inputs[index].id,
report.documents[index].id,
),
)
}
if report.documents[index].text != inputs[index].text {
return Err(
IndexReportDocumentTextMismatch(
index,
inputs[index].text,
report.documents[index].text,
),
)
}
}
Ok(())
}
///|
fn validated_corpus_scan(
input : RomanDocument,
expected_result : RomanDocumentResult,
result : RomanDocumentResult,
) -> Result[RomanScanReport, RomanIndexConstructionError] {
match (expected_result.outcome, result.outcome) {
(CorpusScanned(expected), CorpusScanned(scan)) =>
match
validate_index_scan_report_against_expected(
input.id,
input.text,
input.config,
expected,
scan,
) {
Err(error) => Err(error)
Ok(_) => Ok(scan)
}
(CorpusScanned(_), CorpusScanFailed(error)) =>
Err(IndexReportUnexpectedScanFailure(input.id, error))
(CorpusScanFailed(expected), CorpusScanned(_)) =>
Err(IndexReportUnexpectedScanSuccess(input.id, expected))
(CorpusScanFailed(expected), CorpusScanFailed(actual)) =>
if expected == actual {
Err(IndexDocumentScanFailed(input.id, actual))
} else {
Err(IndexReportScanFailureMismatch(input.id, expected, actual))
}
}
}
///|
fn build_index_from_trusted_corpus(
inputs : Array[RomanDocument],
report : RomanCorpusReport,
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
let entries : Array[RomanIndexEntry] = []
let documents : Array[RomanIndexDocumentMetadata] = []
let rejections : Array[RomanIndexRejectionSummary] = []
let mut candidates_examined = 0L
let mut rejected_candidates = 0L
let mut used_unicode_compatibility = 0L
let mut normalized_inputs = 0L
for index = 0; index < inputs.length(); index = index + 1 {
let scan = match report.documents[index].outcome {
CorpusScanFailed(error) =>
return Err(IndexDocumentScanFailed(inputs[index].id, error))
CorpusScanned(scan) => scan
}
let document_index = materialize_verified_scan_index(
inputs[index].id,
inputs[index].text,
inputs[index].config,
scan,
entries.length(),
)
candidates_examined = candidates_examined +
document_index.statistics.candidates_examined
rejected_candidates = rejected_candidates +
document_index.statistics.rejected_candidates
used_unicode_compatibility = used_unicode_compatibility +
document_index.statistics.used_unicode_compatibility
normalized_inputs = normalized_inputs +
document_index.statistics.normalized_inputs
append_document_index(entries, documents, rejections, document_index)
}
Ok({
entries,
documents,
rejections,
statistics: {
total_documents: inputs.length().to_int64(),
candidates_examined,
accepted_entries: entries.length().to_int64(),
rejected_candidates,
used_unicode_compatibility,
normalized_inputs,
},
})
}
///|
/// Build after rescanning every result and comparing complete corpus statistics.
/// This correctness check is not a performance optimization.
pub fn build_roman_index_from_corpus(
inputs : Array[RomanDocument],
report : RomanCorpusReport,
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
match validate_index_document_ids(inputs) {
Err(error) => return Err(error)
Ok(_) => ()
}
match validate_corpus_report_identity(inputs, report) {
Err(error) => return Err(error)
Ok(_) => ()
}
let expected_corpus = match process_roman_corpus(inputs) {
Err(EmptyDocumentId(index)) => return Err(EmptyIndexDocumentId(index))
Err(DuplicateDocumentId(id)) => return Err(DuplicateIndexDocumentId(id))
Ok(expected) => expected
}
for index = 0; index < inputs.length(); index = index + 1 {
match
validated_corpus_scan(
inputs[index],
expected_corpus.documents[index],
report.documents[index],
) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
if expected_corpus.statistics != report.statistics {
return Err(
IndexCorpusStatisticsMismatch(
expected_corpus.statistics,
report.statistics,
),
)
}
build_index_from_trusted_corpus(inputs, report)
}
///|
/// Build one stable index by scanning explicit ID-addressed source/config inputs.
pub fn build_roman_document_index(
inputs : Array[RomanDocument],
) -> Result[RomanDocumentIndex, RomanIndexConstructionError] {
match process_roman_corpus(inputs) {
Err(EmptyDocumentId(index)) => Err(EmptyIndexDocumentId(index))
Err(DuplicateDocumentId(id)) => Err(DuplicateIndexDocumentId(id))
Ok(report) => build_index_from_trusted_corpus(inputs, report)
}
}