| name | architecture |
| description | Guidelines for the architecture and design principles of the parser in the `src/Seiton.Core/Parsing/` folder. This includes layer responsibilities, hand-written parsing rationale, and evolution strategies. |
Parser Architecture
Purpose
This document explains the parser architecture and design principles used.
It is intentionally implementation-oriented so contributors can quickly answer:
- What each layer is responsible for.
- Why hand-written parsing is used.
- How to evolve the parser without breaking performance and diagnostics.
To understand the rationale behind design decisions, see the related specs:
.github/docs/architecture_spec_csharp.md
.github/docs/Seiton_Parser_spec.md — パーサー仕様(言語非依存)
.github/docs/Seiton_Parser_csharp_spec.md — C# 実装仕様
.github/docs/architecture_spec_performance.md
Core Design Principles
Parser design follows actionlint-style architecture, adapted to C# and VYaml.
- Parse by structure, not by full object deserialization.
- Build typed model while validating shape (single pass where possible).
- Preserve source positions for diagnostics at every stage.
- Continue parsing after recoverable errors to return multiple diagnostics.
- Keep hot paths allocation-aware (UTF-8 span checks, minimal string materialization).
- Separate syntax validation from semantic/policy validation.
Why This Architecture
GitHub Actions workflows require more than schema validation.
Many constraints are contextual (for example key combinations, event-dependent behavior, expression semantics).
Therefore the parser uses a hybrid model:
- Hand-written parser for syntax shape and recovery behavior.
- Expression parser/analyzer for
${{ }} domain rules.
- Rule-style semantic validation over parsed model.
- Optional external schema and generated metadata as supporting data, not primary truth.
AST Storage Model (Data-Oriented)
The AST is not an object graph. Every composite node is a struct row in a typed NodeTable<T> owned by AstArena:
- Handles are 1-based typed ID record structs (
JobId, StepId, ...); default = absent.
- Child lists are
(first, count) ranges — over shared ID stores (StringIdRange, StepIdRange) when nested parsing makes row tables non-contiguous, or NodeRange directly over contiguous rows (key-embedded maps).
- Maps embed the key
Utf8Slice in the row; lookup is a linear scan within the range. Case sensitivity is fixed per map type (permissions scopes and env vars are case-sensitive; all others case-insensitive).
- Polymorphic nodes are tagged unions: a
Kind enum (None = 0 first) plus a 1-based payload index into a kind-specific payload table (StepExecKind, EventKind, RawYamlKind).
- Consumers (rules, tests) read through readonly-struct Ref facades (
WorkflowRef / JobRef / StepRef / StringRef, ...); default refs chain safely (HasValue == false, never throw).
- Arena reset clears table counters only — no object pools, no per-node
Reset(), no manual buffer registration. A DEBUG-only generation counter turns use-after-dispose into an immediate exception.
Contract details: .github/docs/Seiton_Parser_csharp_spec.md §2. Design conventions, invariants (lifecycle wiring, contiguity rule), and lessons learned: .github/docs/architecture_spec_ast.md.
Layered Architecture
1) Input and YAML Stream Layer
Responsibilities:
- Read UTF-8 YAML input.
- Expose YAML events/tokens with location metadata.
- Enable subtree skip for recovery.
Design notes:
- Parsing logic should compare keys/values via UTF-8 spans on hot path.
- Avoid string conversion unless needed for diagnostics.
2) Workflow Syntax Parsing Layer
Responsibilities:
- Traverse workflow/job/step mappings and sequences.
- Validate shape constraints (required keys, key types, key combinations).
- Record parser diagnostics with text positions.
- Produce parsed workflow document model.
Design notes:
- Unknown keys should emit diagnostics and skip value subtree.
- Missing required keys should be validated after scope parse completes.
- Do not stop at first syntax error when safe recovery is possible.
3) Expression Parsing and Semantics Layer
Responsibilities:
- Extract
${{ }} expressions from relevant YAML fields.
- Parse expression grammar into compact expression nodes.
- Run semantic checks (function/identifier usage, context validity).
Design notes:
- Expression parser should be independent from YAML parser state.
- Keep expression representation compact for frequent evaluations.
4) Diagnostics Layer
Responsibilities:
- Represent severity/message/location consistently.
- Keep locations stable and useful for users.
- Support multiple findings from one parse run.
Design notes:
- Prefer key-span diagnostics for key-level problems.
- Prefer value-span diagnostics for type/value problems.
- For relationship errors, keep one primary location and add related locations when needed.
End-to-End Parse Flow
- Read YAML stream with location-aware reader.
- Parse top-level workflow mapping.
- Parse nested structures (
on, jobs, steps, etc.) with local constraints.
- Extract and parse expressions where applicable.
- Run expression semantic checks.
- Return parsed workflow model + collected diagnostics.
Error Recovery Strategy
Parser behavior is recovery-first, not fail-fast.
- On invalid key/value node, emit diagnostic.
- Skip current subtree safely.
- Resume at next sibling boundary.
- Preserve structural parsing state to avoid cascading false errors.
This strategy maximizes actionable feedback for users in a single run.
Performance and Allocation Principles
For the parser, the following are mandatory:
- Use UTF-8 span comparisons for key checks in hot paths.
- Avoid
GetScalarString() and Encoding.UTF8.GetString(...) on success paths.
- Allow string conversion only for diagnostics/fallbacks.
- Do not introduce
List<T>, Dictionary<TKey, TValue>, LINQ, regex, or per-node allocations in new hot paths unless justified and measured.
- Reuse parsed metadata instead of repeated lookups.
- Prefer offset/length slices (
Utf8Slice) over materialized strings when values must be retained.
Architectural Boundaries
To keep the system maintainable and fast, keep these boundaries strict:
- YAML stream handling code should not own semantic decisions.
- Workflow shape parser should not perform deep expression semantics.
- Expression semantic analyzer should not depend on YAML event internals.
- Diagnostics format should be independent from parser control flow.
What to Change vs. What to Keep Stable
Easily evolvable:
- Supported workflow keys and constraints.
- Event metadata and compatibility tables.
- Expression semantic rules.
Keep stable:
- Layer boundaries.
- Recovery-first parser behavior.
- UTF-8 span-based hot path checks.
- Position-preserving diagnostics contract.
Implementation Checklist for Parser Changes
Before completing parser/AST changes, verify all of the following:
- No new success-path string materialization was added in hot loops.
- New key checks are UTF-8 span based.
- Diagnostics remain location-accurate and human-readable.
- Recovery behavior still allows multi-error reporting.
- Parser-related tests pass.
Non-Goals
This architecture intentionally does not aim for:
- Full behavior definition by JSON Schema alone.
- Immediate termination on first parse error.
- Rich object graph deserialization as primary parse strategy.
- Premature abstraction that hides parse-state control.
Summary
The parser architecture is a performance-aware, recovery-first, hand-written parser design with explicit layer separation:
- YAML stream reading with location fidelity.
- Shape-validating workflow parser.
- Dedicated expression parse/semantic pipeline.
- Consistent diagnostics model.
This enables high-quality diagnostics, predictable extensibility, and controlled allocation behavior while tracking GitHub Actions spec changes over time.