Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions crates/html/src/conformance/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,167 @@ pub enum ObservationRequest {
},
}

#[cfg(test)]
mod execution_identity_tests {
use super::*;

#[test]
fn every_closed_parser_observation_error_identity_is_preserved_without_text_classification() {
for (error, identity) in [
(
ParserObservationExecutionError::ParserInvariant,
ParserObservationExecutionIdentity::ParserInvariant,
),
(
ParserObservationExecutionError::TokenCanonicalizationInvariant,
ParserObservationExecutionIdentity::TokenCanonicalizationInvariant,
),
(
ParserObservationExecutionError::TreeTransitionTokenCanonicalizationInvariant,
ParserObservationExecutionIdentity::TreeTransitionTokenCanonicalizationInvariant,
),
(
ParserObservationExecutionError::ObservationRecorderMissing,
ParserObservationExecutionIdentity::ObservationRecorderMissing,
),
(
ParserObservationExecutionError::PatchHistoryCaptureMissing,
ParserObservationExecutionIdentity::PatchHistoryCaptureMissing,
),
] {
assert_eq!(error.identity(), identity);
}

assert_eq!(
ParserObservationExecutionError::ParserFatal(crate::ParserFatalError::EngineInvariant)
.identity(),
ParserObservationExecutionIdentity::ParserFatal(ParserFatalIdentity::EngineInvariant),
);
for (site, identity) in [
(
crate::ParserReservationSite::KnownTagAtomStorage,
ParserReservationSiteIdentity::KnownTagAtomStorage,
),
(
crate::ParserReservationSite::KnownTagLookupStorage,
ParserReservationSiteIdentity::KnownTagLookupStorage,
),
(
crate::ParserReservationSite::TemplateChildStorage,
ParserReservationSiteIdentity::TemplateChildStorage,
),
(
crate::ParserReservationSite::PatchHistoryObservationStorage,
ParserReservationSiteIdentity::PatchHistoryObservationStorage,
),
] {
let error = crate::ParserResourceExhaustion::at(site);
assert_eq!(
ParserObservationExecutionError::ParserFatal(error.into()).identity(),
ParserObservationExecutionIdentity::ParserFatal(
ParserFatalIdentity::ResourceExhaustion(identity)
)
);
}

for code in [
ParserTokenizerInvariantError::SelfClosingFlagMissingSolidusPosition,
ParserTokenizerInvariantError::SolidusPositionWithoutPendingTag,
ParserTokenizerInvariantError::SolidusPositionOutsideCurrentPendingTag,
ParserTokenizerInvariantError::SolidusPositionDoesNotReferenceConsumedSlash,
ParserTokenizerInvariantError::DoctypeNameStartMissingForNameState,
ParserTokenizerInvariantError::DoctypeNameStartMissingForTailScan,
ParserTokenizerInvariantError::DoctypeNameStartMissingForResourceObservation,
ParserTokenizerInvariantError::DoctypeNameStartAfterCursor,
ParserTokenizerInvariantError::DoctypeNameRangeInvalid,
ParserTokenizerInvariantError::DoctypeTailRangeInvalid,
ParserTokenizerInvariantError::AsciiPrefixCandidateRangeInvalid,
ParserTokenizerInvariantError::CommentStateMissingPendingStart,
ParserTokenizerInvariantError::CommentPendingRangeInvalid,
ParserTokenizerInvariantError::CommentPendingDelimiterOutsideCurrentRange,
ParserTokenizerInvariantError::CommentPendingDelimiterDoesNotMatchState,
ParserTokenizerInvariantError::TextModeEndTagCandidateRangeInvalid,
ParserTokenizerInvariantError::TextModeEndTagAttributePositionInvalid,
ParserTokenizerInvariantError::TextModeEndTagSolidusPositionInvalid,
ParserTokenizerInvariantError::PendingTextRangeInvalid,
ParserTokenizerInvariantError::CdataStateMissingPendingTextStart,
ParserTokenizerInvariantError::CdataEndDelimiterOutsidePendingTextRange,
ParserTokenizerInvariantError::CdataEndDelimiterDoesNotMatchState,
ParserTokenizerInvariantError::ProcessingInstructionStateMissingPendingMetadata,
ParserTokenizerInvariantError::ProcessingInstructionMetadataOutsideState,
ParserTokenizerInvariantError::ProcessingInstructionTargetRangeInvalid,
ParserTokenizerInvariantError::ProcessingInstructionDataRangeInvalid,
ParserTokenizerInvariantError::ProcessingInstructionTargetStartAfterCursor,
ParserTokenizerInvariantError::ProcessingInstructionDataStartAfterCursor,
] {
assert_eq!(
ParserObservationExecutionError::TokenizerInvariant(code).identity(),
ParserObservationExecutionIdentity::TokenizerInvariant(code)
);
}

for code in [
UnsupportedFeatureObservationInvariantError::TokenAttributeNameUnavailable,
UnsupportedFeatureObservationInvariantError::ExistingHtmlElementSemanticsUnavailable,
UnsupportedFeatureObservationInvariantError::ExistingBodyElementSemanticsUnavailable,
UnsupportedFeatureObservationInvariantError::ExistingElementIdentityContradiction,
] {
assert_eq!(
ParserObservationExecutionError::UnsupportedFeatureObservationInvariant(code)
.identity(),
ParserObservationExecutionIdentity::UnsupportedFeatureObservationInvariant(code)
);
}

for code in [
ParserObservationInvariantError::ParseErrorOccurrenceOverflow,
ParserObservationInvariantError::ImplementationDiagnosticOccurrenceOverflow,
ParserObservationInvariantError::TreeTransitionOccurrenceOverflow,
ParserObservationInvariantError::UnsupportedFeatureOccurrenceOverflow,
ParserObservationInvariantError::TokenDroppedCountOverflow,
ParserObservationInvariantError::ParseErrorDroppedCountOverflow,
ParserObservationInvariantError::ImplementationDiagnosticDroppedCountOverflow,
ParserObservationInvariantError::TreeTransitionDroppedCountOverflow,
ParserObservationInvariantError::UnsupportedFeatureDroppedCountOverflow,
ParserObservationInvariantError::NormalizedPositionOverflow,
ParserObservationInvariantError::NormalizedPositionIndexDiscontinuity,
ParserObservationInvariantError::NormalizedPositionIndexMissing,
ParserObservationInvariantError::InvalidNormalizedPositionOffset,
ParserObservationInvariantError::PatchDroppedCountOverflow,
ParserObservationInvariantError::CanonicalTreeUnitCountOverflow,
ParserObservationInvariantError::CanonicalTreeRootNotDocument,
ParserObservationInvariantError::UnexpectedLegacyDocumentDoctypeMetadata,
ParserObservationInvariantError::MissingHtmlTemplateContents,
ParserObservationInvariantError::InvalidTemplateContentsKind,
ParserObservationInvariantError::CanonicalTreeTraversalContradiction,
ParserObservationInvariantError::CanonicalTreePreflightProjectionMismatch,
ParserObservationInvariantError::InvalidPatchKey,
ParserObservationInvariantError::DuplicatePatchCreation,
ParserObservationInvariantError::MissingPatchCreationHistory,
ParserObservationInvariantError::SnapshotLabelSequenceOverflow,
] {
assert_eq!(
ParserObservationExecutionError::ObservationInvariant(code).identity(),
ParserObservationExecutionIdentity::ObservationInvariant(code)
);
}

for site in [
ObservationReservationSite::CanonicalTreeProjection,
ObservationReservationSite::CanonicalPatchProjection,
ObservationReservationSite::SnapshotLabelStorage,
] {
assert_eq!(
ParserObservationExecutionError::ResourceExhaustion(
ObservationResourceExhaustion::at(site)
)
.identity(),
ParserObservationExecutionIdentity::ResourceExhaustion(site)
);
}
}
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScalarObservationRequest {
#[default]
Expand Down Expand Up @@ -82,6 +243,104 @@ pub enum ParserObservationExecutionError {
ResourceExhaustion(ObservationResourceExhaustion),
}

/// Closed, message-independent identity for fixture disposition matching.
///
/// `ParserFatalError` and its reservation site are deliberately non-exhaustive
/// at the ordinary parser API boundary. Canonical test support therefore asks
/// the owning HTML subsystem for this feature-gated identity instead of
/// classifying `Display` or `Debug` text.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParserObservationExecutionIdentity {
ParserFatal(ParserFatalIdentity),
ParserInvariant,
TokenizerInvariant(ParserTokenizerInvariantError),
TokenCanonicalizationInvariant,
TreeTransitionTokenCanonicalizationInvariant,
UnsupportedFeatureObservationInvariant(UnsupportedFeatureObservationInvariantError),
ObservationRecorderMissing,
PatchHistoryCaptureMissing,
ObservationInvariant(ParserObservationInvariantError),
ResourceExhaustion(ObservationReservationSite),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParserFatalIdentity {
EngineInvariant,
ResourceExhaustion(ParserReservationSiteIdentity),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParserReservationSiteIdentity {
KnownTagAtomStorage,
KnownTagLookupStorage,
TemplateChildStorage,
PatchHistoryObservationStorage,
}

impl ParserObservationExecutionError {
#[must_use]
pub const fn identity(self) -> ParserObservationExecutionIdentity {
match self {
Self::ParserFatal(error) => {
ParserObservationExecutionIdentity::ParserFatal(parser_fatal_identity(error))
}
Self::ParserInvariant => ParserObservationExecutionIdentity::ParserInvariant,
Self::TokenizerInvariant(error) => {
ParserObservationExecutionIdentity::TokenizerInvariant(error)
}
Self::TokenCanonicalizationInvariant => {
ParserObservationExecutionIdentity::TokenCanonicalizationInvariant
}
Self::TreeTransitionTokenCanonicalizationInvariant => {
ParserObservationExecutionIdentity::TreeTransitionTokenCanonicalizationInvariant
}
Self::UnsupportedFeatureObservationInvariant(error) => {
ParserObservationExecutionIdentity::UnsupportedFeatureObservationInvariant(error)
}
Self::ObservationRecorderMissing => {
ParserObservationExecutionIdentity::ObservationRecorderMissing
}
Self::PatchHistoryCaptureMissing => {
ParserObservationExecutionIdentity::PatchHistoryCaptureMissing
}
Self::ObservationInvariant(error) => {
ParserObservationExecutionIdentity::ObservationInvariant(error)
}
Self::ResourceExhaustion(error) => {
ParserObservationExecutionIdentity::ResourceExhaustion(error.site())
}
}
}
}

const fn parser_fatal_identity(error: crate::ParserFatalError) -> ParserFatalIdentity {
match error {
crate::ParserFatalError::EngineInvariant => ParserFatalIdentity::EngineInvariant,
crate::ParserFatalError::ResourceExhaustion(error) => {
ParserFatalIdentity::ResourceExhaustion(parser_reservation_site_identity(error.site()))
}
}
}

const fn parser_reservation_site_identity(
site: crate::ParserReservationSite,
) -> ParserReservationSiteIdentity {
match site {
crate::ParserReservationSite::KnownTagAtomStorage => {
ParserReservationSiteIdentity::KnownTagAtomStorage
}
crate::ParserReservationSite::KnownTagLookupStorage => {
ParserReservationSiteIdentity::KnownTagLookupStorage
}
crate::ParserReservationSite::TemplateChildStorage => {
ParserReservationSiteIdentity::TemplateChildStorage
}
crate::ParserReservationSite::PatchHistoryObservationStorage => {
ParserReservationSiteIdentity::PatchHistoryObservationStorage
}
}
}

/// Fallible allocation boundary owned by post-parse canonical observation.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ObservationReservationSite {
Expand Down
5 changes: 3 additions & 2 deletions crates/html/src/conformance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ pub use crate::html5::shared::{
};
pub use execution::{
ObservationRequest, ObservationReservationSite, ObservationResourceExhaustion,
ParserObservationExecutionError, ParserObservationInput, ParserObservationInvariantError,
ParserObservationRequest, ParserObservationTarget, ParserTokenizerInvariantError,
ParserFatalIdentity, ParserObservationExecutionError, ParserObservationExecutionIdentity,
ParserObservationInput, ParserObservationInvariantError, ParserObservationRequest,
ParserObservationTarget, ParserReservationSiteIdentity, ParserTokenizerInvariantError,
ScalarObservationRequest, UnsupportedFeatureObservationInvariantError,
execute_parser_observation,
};
Expand Down
20 changes: 14 additions & 6 deletions crates/html/tests/fixtures/html5/conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Canonical HTML Parser Conformance Fixtures

This is the native fixture root for `borrowser-html-parser-fixture-v1`. Discovery
This is the native fixture root for `borrowser-html-parser-fixture-v2`. Discovery
is recursive and sorted by normalized repository-relative bundle path. Add a
directory containing `fixture.toml`, exact input, and declared snapshots; no Rust
registration is required.
Expand All @@ -12,7 +12,7 @@ directory containing `fixture.toml` is a leaf; nested bundles are rejected.
Native fixtures in this directory must be `source = native` and
`disposition.status = active`. Xfail, skip, and expected-unsupported entries
belong only to later external/adapted inputs or a separately identified
quarantine source. Fixture-v1 permits skips only for an exact unsupported
quarantine source. Fixture-v2 permits skips only for an exact unsupported
capability; broad external-source and environment skips are rejected.

Use `input.html` only for valid UTF-8 input whose intended checkout form has LF
Expand All @@ -21,11 +21,19 @@ byte delivery, and any byte-sensitive case. `input.html` containing a carriage
return is rejected. Always update the mandatory SHA-256 from the exact stored
bytes; the loader never trims input.

AE13a executes only whole-input standalone-tokenizer fixtures with a declared
`tokens.txt` in `html5-token-v1`. Other fixture-v1 surfaces are declarable but
fail explicitly as unsupported expectations until their owning AE13 slice lands.
AE13b5 executes supported whole-input standalone-tokenizer and document
fixtures from typed canonical observations. Ordinary surfaces are unioned on
the reference delivery; transition expectations may name another declared
whole delivery. Each planned delivery executes once. Unused declared whole
deliveries are capability-checked but do not execute.

See `docs/html5/parser-fixture-format-v1.md` for the complete schema and
Canonical sidecars use the exact AE13b5 formats, including `html5-token-v2`,
`html5-dom-v3`, and `html5-dompatch-v3`. Header-only diagnostic, transition,
unsupported-feature, tree, and patch snapshots represent requested empty
collections. Fixture-v1 remains an isolated compatibility format.

See `docs/html5/parser-fixture-format-v2.md` and
`docs/html5/ae13b5-parser-snapshot-formats.md` for the schema/codecs, and
`docs/html5/ae13-parser-conformance-regression-harness.md` for ownership and
slice boundaries.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
format = "borrowser-html-parser-fixture-v2"
id = "document-recovery-diagnostics"

[source]
kind = "native"

[input]
path = "input.html"
kind = "utf8-text"
sha256 = "f6a8bd0ddf91049b25303c3e04f39f422817603e9a9b4ed051a8d0832294bb8a"

[execution]
reference_delivery = "whole"

[execution.target]
kind = "document"
scripting = "disabled"

[[execution.deliveries]]
name = "whole"
unit = "unicode-scalars"
strategy = "whole"

[expectations]
parse_errors = "parse-errors.txt"
implementation_diagnostics = "implementation-diagnostics.txt"

[disposition]
status = "active"

[metadata]
description = "Duplicate-attribute and non-void self-closing recovery use typed canonical diagnostics."
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# format: html5-implementation-diagnostics-v1
IMPLEMENTATION_DIAGNOSTIC occurrence=1 stage=tree-construction code=tree-construction:non-void-html-self-closing-flag-altered-stack-disposition payload=none position=unavailable:parser-did-not-provide-position context=present context-token=start-tag context-mode=in-body context-namespace=html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><div a='first' a='second'/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# format: html5-parse-errors-v1
PARSE_ERROR occurrence=1 stage=tokenizer code=standard:duplicate-attribute recovery=drop-duplicate-attribute position=normalized-utf8:30:1:31:source-unavailable:no-input-provenance-map context=absent context-token=null context-mode=null context-namespace=null
PARSE_ERROR occurrence=2 stage=tree-construction code=tree-construction:unacknowledged-self-closing-flag recovery=null position=unavailable:parser-did-not-provide-position context=present context-token=start-tag context-mode=in-body context-namespace=html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# format: html5-document-mode-v1
MODE value=no-quirks
Loading
Loading