Skip to content

feat(codegen): share the validated-parse surface via runtime types - #331

Merged
tinovyatkin merged 3 commits into
mainfrom
feat/319-shared-validated-surface
Aug 10, 2026
Merged

feat(codegen): share the validated-parse surface via runtime types#331
tinovyatkin merged 3 commits into
mainfrom
feat/319-shared-validated-surface

Conversation

@tinovyatkin

@tinovyatkin tinovyatkin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #319.

Summary

Moves the per-grammar validated-parse surface out of generated code into the runtime, and collapses each repeated-child count check in validate_tree_structure to a single runtime call.

  • New antlr4_runtime::validated module (re-exported at the crate root): ValidatedTree<Grammar>, ValidatedRuleNode<'a, Grammar>, FromValidatedRuleNode, ValidationError, and require_min_count(actual, minimum, context, child).

  • Generated modules now emit only the module-local ValidatedTreeContext marker plus branded aliases and one trait re-export:

    pub type TomlValidatedTree = antlr4_runtime::ValidatedTree<ValidatedTreeContext>;
    pub type ValidatedRuleNode<'a> = antlr4_runtime::ValidatedRuleNode<'a, ValidatedTreeContext>;
    pub use antlr4_runtime::FromValidatedRuleNode;
    pub type TomlValidationError = antlr4_runtime::ValidationError;
  • Each min-count site becomes one antlr4_runtime::require_min_count(...)?; call (site counts match the issue: toml 6, g4 14, rust 27), and — per review — each required-child site becomes context.method()?; via the runtime From<MissingChildError> conversion.

Design decisions

Trees and nodes are branded per grammar; the error type is shared. ValidatedTree/ValidatedRuleNode carry a Grammar type parameter instantiated with the generated module's ValidatedTreeContext marker (which already had to stay module-local so the recovery-oriented and validated accessor impls remain coherent — the #318 interplay). This keeps different grammars' validated trees nominally distinct and makes cross-grammar downcast_ref a compile error, since rule indexes and context kinds are grammar-local numbers (review finding, Codex P1; pinned by a negative-compile test). FromValidatedRuleNode carries the brand as an associated type, so the trait keeps its one-lifetime arity for user-written bounds.

ValidationError is deliberately one unbranded type: grammar identity was never encoded in its behavior — all grammar-specific detail (context/child names) is variant data — so one type means one compiled copy of the Display/Error/From machinery and uniform error handling in multi-parser binaries. Trade-off (documented in docs/migration.md): downstream code with per-grammar impls on the error name (e.g. two thiserror #[from] variants for two grammars) now hits E0119 and must collapse them into a single impl.

Error behavior preserved exactly. ValidationError is thiserror-based but keeps the previous semantics: same variants and Display texts, Error::source returns the inner error for Recognition/MissingChild (#[error("{0}")] + #[from], deliberately not #[error(transparent)], which would forward source() one level too far), and both From conversions. The contract is now pinned by tests in the runtime (Display snapshot per variant, source() expectations, From conversions, require_min_count both ways).

Known limitation (from review, Codex P2 / Claude #2): the doc-hidden __new constructors used by generated code are technically callable from any crate, whereas revision-9 modules kept them module-private. Hand-constructing a validated value over an unvalidated parse makes the infallible accessors panic. This is an inherent cost of hoisting across the crate boundary and cannot be fully sealed with public-API mechanisms; it is stated plainly in the __new docs and docs/migration.md. The accidental-misuse path (cross-grammar confusion) is closed by the branding above.

Compatibility contract

__ANTLR4_RUST_CODEGEN_API is now 10. Revisions 1–9 remain accepted: the __antlr4_rust_context! macro gained an optional validated_downcast: branded, field — revision-10 codegen selects the branded impl against the runtime-owned surface, while invocations without it keep expanding against the module-local validated types older modules declare themselves. Per review (Claude #3), this is no longer just a manual check: a frozen revision-9 generated module (produced by the pre-hoist v0.32.0 generator, declaring its own ValidatedTree/ValidatedRuleNode/FromValidatedRuleNode/ValidationError) is checked in under tests/antlr4_rust_gen_cli/fixtures/revision9/ and compiled against the current runtime in the compatibility test. The revision-literal sweep now covers 1–9 and the diagnostic names revisions 1–10. All checked-in recognizers are regenerated, including the runtime-internal XPath lexer (revision 10; its regenerated output is byte-identical apart from the header).

Existing user code compiles unchanged apart from the handshake: <G>ValidationError::Variant paths, exhaustive matches, validate(), parse_validated, downcast_ref::<Ctx<'_, ValidatedTreeContext>>(), T: FromValidatedRuleNode<'a> bounds, and the validated listener/visitor surfaces all work through the branded aliases.

Measurements

Generated source (checked-in parsers, before → after; branding costs one validated_downcast: branded, line per context vs the first revision of this PR):

module lines bytes
toml_parser.rs 2,851 → 2,693 (−158) 156,688 → 151,933 (−4,755)
antlr_v4_parser.rs 7,323 → 7,128 (−195) 459,008 → 451,066 (−7,942)
rust_parser.rs 30,703 → 30,526 (−177) 2,284,896 → 2,278,259 (−6,637)
total −530 lines −19,334 bytes

Binary size: a stripped release binary linking the TOML and G4 parsers and exercising parse_validated plus error Display went from 4,105,568 to 4,104,352 bytes (−1,216), with byte-identical program output. The delta is modest because the linker already deduplicated much of the identical machinery; the win is mainly source-level and grows per additional linked grammar.

Testing

  • cargo clippy --locked --workspace --all-targets --all-features -- -D warnings — clean
  • cargo test --workspace — 1,451 passed, 0 failed, including:
    • frozen_revision9_generated_source_compiles_against_current_runtime (new)
    • validated_rule_nodes_of_different_grammars_do_not_cross_downcast (new; asserts the E0271 compile failure and a successful same-grammar downcast at runtime)
    • runtime validated::tests error-contract suite (new)
  • ANTLR runtime conformance sweep — 357 passed, 0 failed
  • cargo fmt --check --all, rumdl fmt — clean
  • tools/toml-syntax/update-generated.sh --check, tools/rust-syntax/update-generated.sh --check, tools/grammar-frontend/update-stage0.sh --check — all report current

Note for reviewers: the local hk pre-commit harness (hk 1.54 vs the pinned 1.49 config) fails on any commit touching .rs files because its cargo-fmt builtin passes rust-toolchain.toml as --manifest-path; all hook steps were run manually instead (results above).

…mmar-agnostic types

Every generated parser re-declared an identical (modulo grammar-name prefix)
148-line validated-parse block: `<Grammar>ValidatedTree`, `ValidatedRuleNode`,
`FromValidatedRuleNode`, and `<Grammar>ValidationError` with its
Display/Error/From machinery, plus a ~12-line InvalidChildCount check per
required list child in `validate_tree_structure`. Measured across six
checked-in parsers (three here, three in mehen), the blocks hash identically
after stripping the prefix, so a multi-parser binary compiled the same
machinery once per grammar.

The runtime now owns the surface as grammar-agnostic types in the new
`validated` module: `ValidatedTree`, `ValidatedRuleNode`,
`FromValidatedRuleNode`, `ValidationError`, and a `require_min_count` helper
that collapses each child-count check to a single call. Variant data,
Display texts, `Error::source`, and both `From` conversions are preserved
exactly (thiserror-based; `MissingChild` uses `#[error("{0}")]` + `#[from]`
rather than `transparent` so `source()` still returns the inner error).
Grammar-specific strings stay in generated code as variant data, keeping the
runtime grammar-agnostic.

Generated modules now emit only the module-local `ValidatedTreeContext`
marker plus two plain type aliases (`pub type TomlValidatedTree =
antlr4_runtime::ValidatedTree;`), and re-export the unprefixed names so the
runtime context macros keep resolving unchanged. The marker must remain
module-local: the accessor macro's recovery-oriented and validated inherent
impls stay coherent only while rustc can prove the local marker never
implements the runtime's `__RecoveryContextState`. Aliasing deliberately
makes the validated types of different grammars interchangeable — one
compiled copy, uniform error handling across parsers.

This changes the emitted-source contract, so `__ANTLR4_RUST_CODEGEN_API` is
now 10. Revisions 1 through 9 remain accepted: the runtime only gained API,
and the macros keep unqualified name resolution, so older generated modules
that define the surface locally still compile (verified by building the
workspace with the previous checked-in parsers before regenerating). All
checked-in recognizers are regenerated; the compatibility test now sweeps
revisions 1-9 and pins the new diagnostic.

Generated-source savings: toml 2851->2658 lines, g4 7323->7050, rust
30703->30300 (869 lines, 24,211 bytes across the three parsers); a stripped
release binary linking the TOML and G4 parsers shrinks by 1,320 bytes with
byte-identical output.

Closes #319
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Copy/Paste Detection

Found 10 duplication(s) across 11 changed non-generated Rust file(s) (threshold: 100 tokens).

Show duplications

Found a 20 line (161 tokens) duplication in the following files:

  • Starting at line 342 of crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/fixtures/revision9/codegen_api_parser.rs
  • Starting at line 436 of crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/fixtures/revision9/codegen_api_parser.rs
    fn __walk<E, T: CodegenApiListener<E>>(
        listener: &mut T,
        tree: antlr4_runtime::Node<'_>,
        mut invocation_states: Option<Vec<isize>>,
    ) -> Result<(), E> {
        enum Event<'tree> {
            Enter(antlr4_runtime::Node<'tree>),
            Exit(RuleNodeView<'tree>),
        }

        let mut stack = vec![Event::Enter(tree)];
        while let Some(event) = stack.pop() {
            match event {
                Event::Enter(node) => match node.kind() {
                    antlr4_runtime::NodeKind::Rule => {
                        let context = node.as_rule().expect("rule node kind checked");
                        if let Some(states) = &mut invocation_states {
                            states.insert(0, context.invoking_state());
                        }
                        listener.enter_every_rule(context)?;
```rust

---

Found a 26 line (142 tokens) duplication in the following files:
* Starting at line 4215 of crates/antlr-rust-codegen/src/generator/tests.rs
* Starting at line 4438 of crates/antlr-rust-codegen/src/generator/tests.rs

```rust
    atn.set_end_state(1, 4).expect("block end state");
    atn.add_transition(0, ParserTransitionSpec::Epsilon { target: 1 })
        .expect("transition");
    atn.add_transition(1, ParserTransitionSpec::Epsilon { target: 2 })
        .expect("transition");
    atn.add_transition(1, ParserTransitionSpec::Epsilon { target: 3 })
        .expect("transition");
    atn.add_transition(
        2,
        ParserTransitionSpec::Atom {
            target: 4,
            label: 1,
        },
    )
    .expect("transition");
    atn.add_transition(
        3,
        ParserTransitionSpec::Atom {
            target: 4,
            label: 2,
        },
    )
    .expect("transition");
    atn.add_transition(4, ParserTransitionSpec::Epsilon { target: 5 })
        .expect("transition");
    atn.add_decision_state(1).expect("decision state");

Found a 26 line (125 tokens) duplication in the following files:

  • Starting at line 788 of crates/antlr-rust-runtime/src/generated.rs
  • Starting at line 959 of crates/antlr-rust-runtime/src/generated.rs
            $input: $crate::char_stream::CharStream,
            $hooks: $crate::parser::SemanticHooks,
        {
            pub fn metadata() -> &'static $crate::generated::GrammarMetadata {
                $metadata()
            }

            /// Adds a listener for lexer diagnostics.
            pub fn add_error_listener<T>(&mut self, listener: T)
            where
                T: for<'a> $crate::errors::ErrorListener<dyn $crate::recognizer::Recognizer + 'a>
                    + ::core::marker::Send
                    + 'static,
            {
                $crate::recognizer::Recognizer::add_error_listener(&mut self.$base, listener);
            }

            /// Removes every lexer error listener, including the default console listener.
            pub fn remove_error_listeners(&mut self) {
                $crate::recognizer::Recognizer::remove_error_listeners(&mut self.$base);
            }

            /// Routes every token through ATN interpretation instead of the compiled
            /// lexer DFA, so the learned-DFA trace (`lexer_dfa_string`) observes each
            /// match.
            pub fn set_force_interpreted(&mut self, force_interpreted: bool) {
```rust

---

Found a 25 line (115 tokens) duplication in the following files:
* Starting at line 4186 of crates/antlr-rust-codegen/src/generator/tests.rs
* Starting at line 4396 of crates/antlr-rust-codegen/src/generator/tests.rs

```rust
        atn.add_state(AtnStateKind::BlockStart, Some(0))
            .expect("state")
            .index(),
        1
    );
    assert_eq!(
        atn.add_state(AtnStateKind::Basic, Some(0))
            .expect("state")
            .index(),
        2
    );
    assert_eq!(
        atn.add_state(AtnStateKind::Basic, Some(0))
            .expect("state")
            .index(),
        3
    );
    assert_eq!(
        atn.add_state(AtnStateKind::BlockEnd, Some(0))
            .expect("state")
            .index(),
        4
    );
    assert_eq!(
        atn.add_state(AtnStateKind::RuleStop, Some(0))

Found a 22 line (112 tokens) duplication in the following files:

  • Starting at line 4313 of crates/antlr-rust-codegen/src/generator/tests.rs
  • Starting at line 4387 of crates/antlr-rust-codegen/src/generator/tests.rs
fn plus_loop_atn() -> ParserAtn {
    let mut atn = ParserAtnBuilder::new(2);
    assert_eq!(
        atn.add_state(AtnStateKind::RuleStart, Some(0))
            .expect("state")
            .index(),
        0
    );
    assert_eq!(
        atn.add_state(AtnStateKind::PlusBlockStart, Some(0))
            .expect("state")
            .index(),
        1
    );
    assert_eq!(
        atn.add_state(AtnStateKind::Basic, Some(0))
            .expect("state")
            .index(),
        2
    );
    assert_eq!(
        atn.add_state(AtnStateKind::BlockEnd, Some(0))
```rust

---

Found a 27 line (110 tokens) duplication in the following files:
* Starting at line 3540 of crates/antlr-rust-codegen/src/generator/tests.rs
* Starting at line 3652 of crates/antlr-rust-codegen/src/generator/tests.rs

```rust
            decision: 0,
            alts: (1, 2),
            track_alt_number: false,
            allow_semantic_context: true,
            force_context: false,
            plus_loop: false,
            fast_path: None,
            body: &body,
        },
        0,
        GeneratedStepRenderContext {
            current_rule_index: 0,
            embedded: None,
            portable_locals: None,
            decision_routing: DecisionRoutingRender::default(),
            inline_action_statements: &BTreeMap::new(),
            track_alt_numbers: false,
            track_context_alt_numbers: false,
            direct_generated_rule_calls: &[],
            atn_preferred_rule_calls: &[],
            adaptive_atn_preferred_rule_slots: &[],
            adaptive_atn_probe_rule_slots: &[],
        },
    );

    // The whole rendered star-loop captures the leading-predicate-to-exit-alt filtering.
    insta::assert_snapshot!(

Found a 17 line (105 tokens) duplication in the following files:

  • Starting at line 177 of crates/antlr-rust-runtime/src/generated.rs
  • Starting at line 274 of crates/antlr-rust-runtime/src/generated.rs
            fn __from_node_with_invocation_states(
                node: $crate::RuleNodeView<'a>,
                invocation_states: Option<Vec<isize>>,
            ) -> Self {
                $(
                    let __default = <$attrs>::default();
                    let __attrs = node.generated_attrs::<$attrs>().unwrap_or(&__default);
                )?
                Self {
                    __node: __GeneratedRuleContext::Stored(node),
                    __invocation_states: invocation_states,
                    __state: std::marker::PhantomData,
                    $(
                        $($field: __attrs.$field.clone(),)+
                    )?
                }
            }
```rust

---

Found a 25 line (104 tokens) duplication in the following files:
* Starting at line 3280 of crates/antlr-rust-codegen/src/generator/tests.rs
* Starting at line 3447 of crates/antlr-rust-codegen/src/generator/tests.rs

```rust
            state: 1,
            decision: 0,
            track_alt_number: false,
            allow_semantic_context: false,
            force_context: false,
            fast_path: None,
            alts: &alts,
        },
        0,
        GeneratedStepRenderContext {
            current_rule_index: 0,
            embedded: None,
            portable_locals: None,
            decision_routing: DecisionRoutingRender::default(),
            inline_action_statements: &BTreeMap::new(),
            track_alt_numbers: false,
            track_context_alt_numbers: false,
            direct_generated_rule_calls: &[],
            atn_preferred_rule_calls: &[],
            adaptive_atn_preferred_rule_slots: &[],
            adaptive_atn_probe_rule_slots: &[],
        },
    );

    assert!(rendered.contains("ll1_decision_prediction(atn(), 1)"));

Found a 28 line (102 tokens) duplication in the following files:

  • Starting at line 3333 of crates/antlr-rust-codegen/src/generator/tests.rs
  • Starting at line 3493 of crates/antlr-rust-codegen/src/generator/tests.rs
            state: 1,
            decision: 0,
            track_alt_number: false,
            allow_semantic_context: true,
            force_context: false,
            fast_path: None,
            alts: &alts,
        },
        0,
        GeneratedStepRenderContext {
            current_rule_index: 0,
            embedded: None,
            portable_locals: None,
            decision_routing: DecisionRoutingRender::default(),
            inline_action_statements: &BTreeMap::new(),
            track_alt_numbers: false,
            track_context_alt_numbers: false,
            direct_generated_rule_calls: &[],
            atn_preferred_rule_calls: &[],
            adaptive_atn_preferred_rule_slots: &[],
            adaptive_atn_probe_rule_slots: &[],
        },
    );

    // One decision renders into a fresh String; snapshot the whole emitted control flow (the
    // semantic-context gate, both predicate probes, the alt rewrite, the no-viable fallback)
    // instead of six positive probes plus one negative guard.
    insta::assert_snapshot!(
```rust

---

Found a 16 line (101 tokens) duplication in the following files:
* Starting at line 4286 of crates/antlr-rust-codegen/src/generator/tests.rs
* Starting at line 4438 of crates/antlr-rust-codegen/src/generator/tests.rs

```rust
    atn.set_loop_back_state(3, 4).expect("loop back state");
    atn.add_transition(0, ParserTransitionSpec::Epsilon { target: 1 })
        .expect("transition");
    atn.add_transition(1, ParserTransitionSpec::Epsilon { target: 2 })
        .expect("transition");
    atn.add_transition(1, ParserTransitionSpec::Epsilon { target: 3 })
        .expect("transition");
    atn.add_transition(
        2,
        ParserTransitionSpec::Atom {
            target: 4,
            label: 1,
        },
    )
    .expect("transition");
    atn.add_transition(4, ParserTransitionSpec::Epsilon { target: 1 })

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime now provides shared validated-parse types and validation helpers. Generated parsers use grammar-branded aliases and revision-specific downcast handling. API revision 10 support, revision-9 compatibility tests, generated fixtures, and documentation were updated.

Changes

Validated-parse runtime migration

Layer / File(s) Summary
Runtime validated-parse APIs
crates/antlr-rust-runtime/src/validated.rs, crates/antlr-rust-runtime/src/lib.rs
The runtime adds validated tree and node types, conversion support, shared validation errors, minimum-count validation, tests, and public re-exports.
Branded generated-parser integration
crates/antlr-rust-runtime/src/generated.rs, crates/antlr-rust-codegen/src/parser/surface/*, crates/antlr-rust-codegen/src/generator/tests.rs, crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/typed_tree.rs
Generated parsers emit grammar-branded runtime aliases, construct nodes through ValidatedRuleNode::__new, use require_min_count, and validate same-grammar versus cross-grammar downcasts.
API revision and frozen revision-9 compatibility
crates/antlr-rust-runtime/src/lib.rs, crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/cli.rs, crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/fixtures/revision9/*, README.md, third_party/antlr-v4-grammar/self-hosted.sha256
API revision 10 is accepted and reported. Frozen revision-9 generated modules compile against the current runtime. Compatibility documentation and generated-file checksums are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The reviewable changes satisfy the core issue objectives, but excluded snapshots, generated recognizers, and migration documentation prevent full verification. Review the excluded snapshot, generated recognizer, and documentation files to confirm compatibility updates and required reporting.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The reviewed changes support runtime sharing, compatibility testing, generated output updates, and validation behavior required by issue #319.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes moving the validated-parse surface into shared runtime types, which is the primary change.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/319-shared-validated-surface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @tinovyatkin's task in 18m 13s —— View job


Review of feat/319-shared-validated-surface (through 864ba820)

  • Gather context (diff, CLAUDE.md, prior review threads)
  • Verify 864ba820 — XPath lexer regeneration at revision 10
  • Review runtime validated.rs (branding, error contract, safety docs)
  • Review generated.rs macro arms (legacy vs. branded dispatch)
  • Review codegen changes (accessors / contexts / traversal)
  • Review the revision-9 frozen fixture + compatibility test
  • Check generated-parser regeneration consistency
  • Verify findings & post consolidated review

Unlike my previous pass, cargo was usable in this environment, so most claims below are empirically verified rather than read off the diff.


Verified

Codex's newest P1 (stale XPath lexer) is fully closed — and reproducible. I regenerated it from source and diffed:

cargo run -p antlr-rust-codegen --bin antlr4-rust-gen -- \
    crates/antlr-rust-runtime/src/xpath/XPathLexer.g4 --out-dir /tmp/xpath-regen
diff -u crates/antlr-rust-runtime/src/xpath/generated/x_path_lexer.rs /tmp/xpath-regen/x_path_lexer.rs   # BYTE-IDENTICAL
diff -u crates/antlr-rust-runtime/src/xpath/generated/semantics.json  /tmp/xpath-regen/semantics.json    # IDENTICAL

All seven checked-in recognizers now declare __antlr4_rust_require_codegen_api!(10, "0.32.0"); no in-tree module is left behind. The three regeneration guards all report current (toml-syntax --check, rust-syntax --check, grammar-frontend/update-stage0.sh --check → "the checked-in frontend is the tested self-hosting fixed point"), and third_party/antlr-v4-grammar/self-hosted.sha256 matches the actual sha256sum of both regenerated g4 modules.

Branding genuinely closes the cross-grammar hole. Confirmed by compiling a scratch crate against the real antlr-rust-toml-parser: downcast_ref into another grammar's context is error[E0271] at validated.rs:145. crates/antlr-rust-codegen/tests/antlr4_rust_gen_cli/typed_tree.rs:1528 pins both directions, and it passes.

Error contract preserved byte-for-byte. The new snapshot at crates/antlr-rust-runtime/src/snapshots/antlr4_runtime__validated__tests__validation_error_display_texts.snap reproduces each removed hand-written Display arm exactly (including MissingChild delegating to the inner error), and #[error("{0}")] + #[from] gives the same source() behaviour the old hand-written impl Error had — #[error(transparent)] would indeed have been wrong here.

Builds and tests. cargo check -p antlr-rust-runtime --all-features ✅ · cargo clippy -p antlr-rust-runtime --all-features --all-targets -- -D warnings ✅ · the four load-bearing codegen tests (frozen_revision9_generated_source_compiles_against_current_runtime, generated_modules_enforce_codegen_api_compatibility, validated_rule_nodes_of_different_grammars_do_not_cross_downcast, validated_tree_makes_required_children_infallible_after_full_validation) all pass. I also verified the frozen fixture really does hit the legacy macro arm (one __antlr4_rust_context! invocation, zero validated_downcast fields) and that it isn't accidentally auto-discovered as a cargo test target. No dead validation_error_name plumbing remains in parser/surface/.


Findings

1. T: FromValidatedRuleNode<'a> bounds do break — migration guide (and PR description) say otherwise · medium

The PR description states these bounds "work through the branded aliases". They don't, once downcast_ref is involved. Verified against the real crate:

pub fn generic_downcast<'a, T: FromValidatedRuleNode<'a>>(
    node: toml_parser::ValidatedRuleNode<'a>,
) -> Option<T> {
    node.downcast_ref::<T>()
}
error[E0271]: type mismatch resolving `<T as FromValidatedRuleNode<'_>>::Grammar == ValidatedTreeContext`
  --> required by a bound in `ValidatedRuleNode::<'a, Grammar>::downcast_ref`
      crates/antlr-rust-runtime/src/validated.rs:145

Any user-written generic helper over validated contexts — a plausible shape for anyone who built a dispatch layer on the revision-9 surface — needs Grammar = <module>::ValidatedTreeContext added. It's a legitimate cost of the fix and the revision bump covers it, but it belongs in docs/migration.md alongside the E0119 note, because the fix isn't guessable from the error: rustc's own "consider constraining" suggestion names the internal path antlr_rust_toml_parser::generated::toml_parser::__antlr4_rust_generated::ValidatedTreeContext, which is not a path users should write. Spelling out the public incantation in the guide will save real time.

Fix this →

2. The __new forge can be fully sealed — the brand marker is already an unforgeable witness · medium

You wrote in the Codex thread: "any constructor reachable by generated code in downstream crates is necessarily pub; a sealed-token scheme just moves the problem, because the token type must itself be public" — and invited a construction that closes it. There is one, and the PR already added the missing ingredient.

ValidatedTreeContext is emitted as pub struct ValidatedTreeContext { __private: () } (traversal.rs:15). The type is public, but the value is not constructible outside the generated module. Verified:

antlr_rust_toml_parser::generated::toml_parser::ValidatedTreeContext { __private: () }
// error[E0451]: field `__private` of struct `...::ValidatedTreeContext` is private

So a witness parameter seals __new completely:

pub fn __new(parsed: ParsedFile, _brand: Grammar) -> Self {}

Generated code calls TomlValidatedTree::__new(parsed, ValidatedTreeContext { __private: () }) — legal, because validate(), the walker, and the visitor bridge are emitted into the same module that declares the marker. Downstream crates cannot produce the second argument at all. Today, by contrast, this compiles from an ordinary dependent crate (verified):

let parsed = toml_parser::parse(src, TomlLexer::new, TomlParser::document)?;  // recovery-oriented
TomlValidatedTree::__new(parsed)                                             // forged "validated" tree

Two caveats: __new loses const (a const fn can't drop a generic Grammar) — irrelevant here, nothing calls it in a const context; and if you want to keep const, store the brand as a real field instead of PhantomData and bound the Copy impl on Grammar: Copy. Either way this is a strictly-better outcome than the documented limitation, and it costs nothing on the compat axis since revision 10 is already being minted. If you'd rather not take it in this PR, it's worth a follow-up issue rather than closing the question as unsolvable.

Fix this →

3. No in-tree module exercises a pre-revision-9 arm any more · low

The frozen fixture is a real improvement, but it covers revision 9 only. Before 864ba820, the XPath lexer was the last in-tree consumer of revision 8; now nothing exercises arms 1–8, and generated_modules_enforce_codegen_api_compatibility only rewrites the revision literal inside a current-shape module — precisely the weakness the new fixture was added to address. (__antlr4_rust_context! only arrived at revision 9, so pre-9 modules are self-contained; the risk is the runtime dropping an API they still name, which a literal rewrite cannot detect.) Either freeze one pre-9 fixture the same way, or soften docs/migration.md:44 — "a checked-in frozen revision-9 module is compiled against the runtime in CI to enforce that claim" reads as covering all of 1–9 when it covers one revision.

4. The XPath lexer has no regeneration guard, so this drift will recur · low

toml, rust, and g4 each have a --check script wired into CI; crates/antlr-rust-runtime/src/xpath/generated/ has none, which is exactly why it silently sat at revision 8 / v0.31.0 through a release until Codex noticed. Since I confirmed regeneration is byte-identical, a guard would be stable — either a tools/xpath/update-generated.sh --check mirroring the others, or (cheaper) a unit test asserting every crates/*/src/**/generated/*.rs declares __ANTLR4_RUST_CODEGEN_API, scoped to exclude the frozen revision-9 fixture.

Fix this →

5. ValidationError now lives on the runtime's semver surface · nit

It is a public, non-#[non_exhaustive] enum. Adding a variant is now a breaking change for every downstream exhaustive match and is gated by the runtime's semver rather than by regeneration — a genuine change from the per-grammar generated enum, where a new variant rode along with a regeneration the user was already doing. Not marking it #[non_exhaustive] is the right call (it would break existing matches today), but the constraint deserves a line in the validated module docs so a future variant addition isn't taken lightly.

6. Debug output changed · nit

TomlValidatedTree now renders as ValidatedTree { parsed: … } (was TomlValidatedTree { … }), and ValidatedRuleNode's field prints as node rather than __node. Harmless unless someone snapshots it downstream; one line in the migration guide would cover it.


Verdict

No merge blockers. The revision-10 contract, the branding, the error-behaviour preservation, and the regeneration inventory all hold up under direct verification. Finding #2 is the one I'd genuinely like you to look at before merging — not because the current state is unsafe, but because the PR records "cannot be sealed with public-API mechanisms" as a conclusion, and the marker you added for branding turns out to be exactly the unforgeable witness that closes it. #1 is a small doc addition that will save a real user a confusing rustc suggestion. #3#6 are follow-up material.
· branch feat/319-shared-validated-surface

@codspeed-hq

codspeed-hq Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing feat/319-shared-validated-surface (864ba82) with main (d3b0af8)

Open in CodSpeed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1375b90265

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +22
/// Alias of the grammar-agnostic `antlr4_runtime::ValidatedTree`; the
/// validated-parse types of every generated parser are interchangeable.
pub type {validated_tree} = antlr4_runtime::ValidatedTree;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the grammar brand on validated trees

When a binary links two generated parsers, this alias erases grammar identity from their tree nodes: for example, TomlValidatedTree::tree() now produces the same ValidatedRuleNode accepted by GrammarSpecContext<ValidatedTreeContext>. The runtime macro's FromValidatedRuleNode implementation checks only the grammar-local numeric rule index/context kind (both TOML document and ANTLRv4 grammarSpec use index 0), so downcast_ref returns Some for the wrong grammar and subsequent infallible accessors can wrap unrelated children or panic. Keep a grammar-specific brand on ValidatedTree/ValidatedRuleNode even if the validation error type remains shared.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in baa6c4f — good catch, this was a real regression. ValidatedTree and ValidatedRuleNode are now branded with a Grammar type parameter that each generated module instantiates with its own module-local ValidatedTreeContext marker:

pub type TomlValidatedTree = antlr4_runtime::ValidatedTree<ValidatedTreeContext>;
pub type ValidatedRuleNode<'a> = antlr4_runtime::ValidatedRuleNode<'a, ValidatedTreeContext>;

FromValidatedRuleNode carries the brand as an associated type (type Grammar), and downcast_ref bounds on Grammar = Grammar, so TomlValidatedTree::tree() no longer produces a node that GrammarSpecContext<ValidatedTreeContext> accepts — the cross-grammar downcast is a compile error (E0271) again. Pinned by a new test, validated_rule_nodes_of_different_grammars_do_not_cross_downcast, which generates two grammars into one crate, asserts the cross-grammar downcast fails to compile, and asserts the same-grammar downcast still resolves at runtime. ValidationError remains the one shared type, as you suggested.

Comment on lines +80 to +83
#[doc(hidden)]
#[must_use]
pub const fn __new(node: RuleNodeView<'a>) -> Self {
Self { node }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent safe callers from forging validated rule nodes

#[doc(hidden)] does not restrict access, so any downstream caller can take a RuleNodeView from an ordinary recovery-oriented ParsedFile and invoke this safe public constructor without running syntax or structural validation. The forged node can then be downcast into a generated validated context, whose required-child accessors use unreachable! when invariants are absent, turning recovered or malformed parses into panics; previously the generated node's private field prevented this. Seal this construction path, require a validation proof, or otherwise prevent ordinary safe code from minting ValidatedRuleNode (and likewise ValidatedTree) directly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed in baa6c4f; the remainder is documented as a known limitation rather than sealed, and here is why. Once the types live in the runtime, any constructor reachable by generated code in downstream crates is necessarily pub; a sealed-token scheme just moves the problem, because the token type must itself be public for generated code to name it (Claude's review reached the same conclusion). What the commit does instead:

  1. The accidental-misuse path you demonstrated is closed structurally: minting or downcasting now requires naming a specific grammar's ValidatedTreeContext brand, and a node forged from grammar A can no longer be downcast into grammar B's contexts (compile error, covered by a new negative-compile test).
  2. Deliberate misuse — calling a #[doc(hidden)] dunder constructor with an unvalidated tree of the same grammar — remains possible and panics, exactly as calling any other __-prefixed runtime item out of contract would misbehave. The __new doc comments and docs/migration.md now state this plainly (previously the docs overclaimed "only generated code may construct").

If you see a construction that seals this fully without making the token public to generated code in downstream crates, I'm happy to adopt it.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.28767% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/antlr-rust-runtime/src/validated.rs 78.19% 29 Missing ⚠️
crates/antlr-rust-runtime/src/generated.rs 0.00% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

📊 Source Code Metrics (this PR vs main)

File Cyclomatic Cognitive Functions LLOC MI
crates/antlr-rust-runtime/src/validated.rs 30 🆕 2 🆕 20 🆕 31 🆕 14.54 🆕
crates/antlr-rust-codegen/src/parser/surface/contexts.rs 25 ⚪ 22 ⚪ 4 ⚪ 69 ⚪ 16.63 (main: 16.57) 🟢
crates/antlr-rust-codegen/src/parser/surface/traversal.rs 6 ⚪ 2 ⚪ 3 ⚪ 36 ⚪ 17.77 (main: 15.68) 🟢
crates/antlr-rust-runtime/src/lib.rs 4 ⚪ 3 ⚪ 1 ⚪ 7 ⚪ 28.97 (main: 29.51) 🔴

Generated by mehen v1.8.1 — the code quality watcher.

…den the revision contract

Addresses PR #331 review findings.

Codex P1: the initial hoist made all grammars' validated trees one type, so
`downcast_ref` on a TOML validated node accepted another grammar's context
whenever the grammar-local rule index happened to match, and the resulting
"infallible" accessors could panic. `ValidatedTree<Grammar>` and
`ValidatedRuleNode<'a, Grammar>` are now branded with the generated module's
own `ValidatedTreeContext` marker (which already had to stay module-local for
accessor-impl coherence), and `FromValidatedRuleNode` carries the brand as an
associated type so the trait keeps its `FromValidatedRuleNode<'a>` arity for
user-written bounds. Generated modules emit branded aliases; cross-grammar
downcasts are compile errors again, pinned by a new negative-compile test.
`ValidationError` deliberately stays one shared unbranded type.

The `__antlr4_rust_context!` macro gains an optional
`validated_downcast: branded,` field: revision-10 codegen selects the impl
against the runtime-owned branded surface, while invocations without it keep
expanding against the module-local validated types that revisions 9 and
earlier declare, so all old revision arms remain accepted. That claim is now
enforced by CI (Claude review #3): a frozen revision-9 generated module,
produced by the pre-hoist generator, is checked in as a fixture and compiled
against the current runtime in the compatibility test.

Also from review: the error surface gets its own tests in the runtime
(Display snapshot per variant, source() expectations, From conversions,
require_min_count both ways); required-child validation sites shrink to
`context.method()?;` via the runtime `From<MissingChildError>` conversion,
retiring the dead validation_error_name plumbing in the accessor renderers;
the doc-hidden `__new` constructors and the migration/README pages now state
plainly that the constructors are a contract rather than a sealed boundary
and that per-grammar trait impls on the shared error type collapse (E0119).

Checked-in recognizers regenerated. Updated savings vs main: 530 lines /
19,334 bytes across the three parsers; the two-parser probe binary stays
1,216 bytes smaller with identical output.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@claude Thanks for the deep pass — every finding is addressed in baa6c4f:

  1. E0119 on per-grammar impls (medium) — documented. docs/migration.md now spells out that every grammar's <G>ValidationError is one shared type and that per-grammar From/trait impls (e.g. two thiserror #[from] variants) fail with E0119 and must collapse into a single impl; README carries the caveat with a pointer. Note the scope narrowed: after Codex's P1 finding, ValidatedTree/ValidatedRuleNode are now branded per grammar with the module-local ValidatedTreeContext marker (associated type Grammar on FromValidatedRuleNode), so the E0119 collapse applies to ValidationError only — trees and nodes of different grammars stay distinct types and cross-grammar downcast_ref is a compile error again (pinned by a new negative-compile test).

  2. Forgeable __new (medium) — documented as a stated limitation, per your "at minimum" option. The __new doc comments no longer claim "only generated code may construct"; they and docs/migration.md now say plainly that these are doc-hidden contracts, technically callable from any crate, whose violation panics — and that revision-9 modules kept them module-private. A sealed token can't fully close this (as you noted), and the branding above closes the accidental cross-grammar variant of the forge.

  3. Old source shapes untested (medium) — converted into a CI guard as suggested. A frozen revision-9 module (generated by the pre-hoist v0.32.0 generator from the same CodegenApi.g4, declaring its own module-local ValidatedTree/ValidatedRuleNode/FromValidatedRuleNode/ValidationError) is checked in under tests/antlr4_rust_gen_cli/fixtures/revision9/ and compiled against the current runtime in frozen_revision9_generated_source_compiles_against_current_runtime. This exercises exactly the name-resolution contract the retained arms promise, now that __antlr4_rust_context! dispatches on an optional validated_downcast: branded, field (absent → legacy module-local expansion, present → branded runtime surface).

  4. Error-surface tests (low) — added in crates/antlr-rust-runtime/src/validated.rs: a named external insta snapshot over the Display of every variant, source() expectations (Some for Recognition/MissingChild, None elsewhere), both From conversions, and require_min_count pass/fail, with the required #[allow(clippy::disallowed_methods)] attribute.

  5. README "aliases" wording (low) — fixed; the paragraph now distinguishes the branded type aliases from the FromValidatedRuleNode re-export.

  6. require_min_count placement (nit) — kept at the crate root, deliberately: unlike the __-prefixed generated items, it constructs the public ValidationError and is reasonable for hand-written strict-validation code to call; java_style_list is the precedent. Agreed this is the moment to decide, and the decision is: public API.

  7. Required-accessor simplification (optional) — adopted rather than deferred, since the revision is being bumped anyway: required sites now emit context.method()?; through the runtime From<MissingChildError> conversion, and the now-dead validation_error_name plumbing in the accessor renderers is removed.

Verification on the new commit: clippy -D warnings clean, 1,451 workspace tests pass, conformance sweep 357/357, all three regeneration scripts report current, and the PR description's measurements are updated for the branded shape (−530 lines / −19,334 bytes across the three checked-in parsers; probe binary −1,216 bytes).

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude finished @tinovyatkin's task in 1m 53s —— View job


Verification of baa6c4f fixes

I reviewed baa6c4f4 against the two prior findings (Codex P1 branding, Claude review's 7 points) by inspecting the diff directly (git diff origin/main...HEAD). All seven claims check out:

1. Branding (E0119 scope narrowed to ValidationError only). Confirmed in crates/antlr-rust-runtime/src/validated.rs:38-172ValidatedTree<Grammar>/ValidatedRuleNode<'a, Grammar> carry a PhantomData<Grammar> brand, FromValidatedRuleNode gained type Grammar, and downcast_ref bounds on Grammar = Grammar (line 145). The macro side in crates/antlr-rust-runtime/src/generated.rs now dispatches on an optional validated_downcast: branded, field with two @from_validated arms (legacy module-local vs. branded runtime-owned), so revision-10 impls are per-grammar-brand. This is exercised by the new negative-compile test validated_rule_nodes_of_different_grammars_do_not_cross_downcast in typed_tree.rs, which asserts an E0271 on cross-grammar downcast_ref and a successful same-grammar downcast at runtime.

2. __new limitation. validated.rs:44-59 and :102-118 now state plainly these are "a doc-hidden contract for generated code, not a sealed boundary... technically callable from any crate," with the panic consequence spelled out and the revision-9 contrast noted. docs/migration.md repeats this. No overclaiming remains.

3. Revision-9 compile guard. frozen_revision9_generated_source_compiles_against_current_runtime in cli.rs stages the checked-in tests/antlr4_rust_gen_cli/fixtures/revision9/{codegen_api_lexer,codegen_api_parser}.rs, asserts the fixture declares its own module-local ValidatedRuleNode/etc. and requests revision 9, then compiles it against the current runtime — this is a real regression guard, not just a literal-rewrite check.

4. Error-surface tests. validated.rs:235-332 adds a named insta snapshot (validation_error_display_texts) plus source(), From conversion, and require_min_count pass/fail tests, with the required #[allow(clippy::disallowed_methods)] on the test module.

5. README wording. README.md now distinguishes the branded type aliases (TomlValidatedTree = antlr4_runtime::ValidatedTree<ValidatedTreeContext>, etc.) from the plain pub use antlr4_runtime::FromValidatedRuleNode; re-export, and explains the E0119 collapse for ValidationError.

6. require_min_count placement. Confirmed at the crate root and re-exported in lib.rs, consistent with the stated rationale (it constructs the public ValidationError, parallel to other public validation helpers).

7. Required-accessor simplification. crates/antlr-rust-codegen/src/parser/surface/accessors.rsrender_required_accessor_validation now emits context.{method}()?; (was let _ = context.{method}().map_err({validation_error_name}::MissingChild)?;), and every validation_error_name: &str parameter/field threaded through render_rule_label_accessor, render_token_label_accessor, ContextAccessorsRender, etc. is fully removed — no dead plumbing left behind. render_repeated_accessor_validation now emits a single antlr4_runtime::require_min_count(...)?; call. Both rely on ValidationError's #[from] MissingChildError conversion (validated.rs:187), which is unchanged.

Note on local verification: this sandboxed environment requires interactive approval for cargo invocations, which isn't available here, so I could not directly run cargo test/cargo clippy to re-confirm the described 1,451 passing tests. I verified everything by reading the actual diff (not just trusting the PR description), and the changes are internally consistent — types, macro arms, and tests all line up with the claims. CI's own CI and ANTLR Runtime Testsuite workflow runs are still in_progress at the time of this review; worth confirming they go green before merge, but nothing in the diff suggests they won't.

No further concerns — all seven points from the prior deep-pass review are substantively addressed, not just documented around.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: baa6c4f40d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// Current generated-source/runtime contract revision emitted by the bundled generator.
#[doc(hidden)]
pub const __ANTLR4_RUST_CODEGEN_API: u32 = 9;
pub const __ANTLR4_RUST_CODEGEN_API: u32 = 10;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate the XPath lexer for revision 10

When bumping the current generated-code API to 10, every checked-in recognizer must be regenerated, but a repo-wide search still finds crates/antlr-rust-runtime/src/xpath/generated/x_path_lexer.rs declaring revision 8 from generator 0.31.0. Although the retained revision-8 arm keeps builds passing, this leaves the current runtime package dependent on stale generated output and prevents the revision inventory from accurately representing the checked-in recognizers; regenerate the XPath lexer alongside the other recognizers.

AGENTS.md reference: AGENTS.md:L17-L23

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 864ba82 — the XPath lexer is regenerated with the current generator and now declares __antlr4_rust_require_codegen_api!(10, "0.32.0"). The regenerated output is byte-identical apart from the header (generator version and revision handshake), and its semantics.json is unchanged, so this is purely the inventory correction you asked for. Runtime tests and clippy re-verified after the swap.

(For the record: it had been left at revision 8 following the precedent of the revision-9 bump, but you're right that AGENTS.md says all checked-in recognizers, and there's no reason for the runtime's own recognizer to lag.)

Codex review on PR #331 flagged that the revision-10 bump left
crates/antlr-rust-runtime/src/xpath/generated/x_path_lexer.rs declaring
revision 8 from generator 0.31.0, so the checked-in recognizer inventory no
longer matched the current revision even though the retained arm kept it
compiling. Regenerated with the current generator; the output is
byte-identical apart from the header (generator version and revision
handshake), and semantics.json is unchanged.
@tinovyatkin

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 864ba82082

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@tinovyatkin
tinovyatkin merged commit 0d118b3 into main Aug 10, 2026
16 checks passed
@tinovyatkin
tinovyatkin deleted the feat/319-shared-validated-surface branch August 10, 2026 08:04
@ophiarch ophiarch Bot mentioned this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

codegen: replace per-grammar validated-tree and validation-error types with shared runtime types

1 participant