diff --git a/Cargo.lock b/Cargo.lock index dba5752..69037c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anv-semantics" +version = "0.1.0" +dependencies = [ + "anv-core", + "anv-syntax", + "pretty_assertions", + "thiserror", +] + [[package]] name = "anv-syntax" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 78a9e0c..b903cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/anv-core", "crates/anv-syntax", "crates/anv-types", + "crates/anv-semantics", "crates/anv-cli", ] @@ -25,6 +26,7 @@ categories = ["compilers", "development-tools"] anv-core = { path = "crates/anv-core" } anv-syntax = { path = "crates/anv-syntax" } anv-types = { path = "crates/anv-types" } +anv-semantics = { path = "crates/anv-semantics" } # Parsing logos = "0.14" diff --git a/README.adoc b/README.adoc index 6ed9dc8..557c111 100644 --- a/README.adoc +++ b/README.adoc @@ -2,31 +2,282 @@ // SPDX-FileCopyrightText: 2024-2025 hyperpolymath = Anvomidav +:toc: macro +:toclevels: 3 +:icons: font +:source-highlighter: rouge -image:https://img.shields.io/badge/license-AGPL--3.0-blue.svg[AGPL-3.0,link="https://www.gnu.org/licenses/agpl-3.0"] image:https://img.shields.io/badge/philosophy-Palimpsest-purple.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-licence"] +image:https://img.shields.io/badge/license-AGPL--3.0-blue.svg[AGPL-3.0,link="https://www.gnu.org/licenses/agpl-3.0"] +image:https://img.shields.io/badge/philosophy-Palimpsest-purple.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-licence"] +image:https://img.shields.io/badge/rust-1.75+-orange.svg[Rust 1.75+] +image:https://img.shields.io/badge/tests-90%2B_passing-green.svg[Tests] **The first programming language for choreographers of figure skating.** +toc::[] + == Status -[IMPORTANT] +[NOTE] ==== -This project is in the *concept phase*. The repository contains project infrastructure and governance documents, but no implementation yet. See link:ROADMAP.adoc[ROADMAP.adoc] for planned development. +**Phase 2 — Core Implementation** (Active Development) + +The Rust foundation is complete with a working lexer, parser, type system, and ISU rules validation. +The CLI tool can check, parse, and create new figure skating programs. ==== -== Vision +=== What's Working + +[cols="1,3,1"] +|=== +| Component | Description | Status + +| **Lexer** +| Tokenizes Anvomidav source using logos +| ✓ Complete + +| **Parser** +| Builds AST using chumsky 0.9 combinators +| ✓ Complete + +| **Type System** +| Basic type checking for programs +| ✓ Complete + +| **ISU Rules** +| Validates programs against ISU regulations +| ✓ Complete + +| **CLI** +| Command-line interface (`anv`) +| ✓ Complete + +| **All Disciplines** +| Singles, Pairs, Ice Dance support +| ✓ Complete +|=== + +== Architecture + +---- +┌─────────────────────────────────────────────────────────────────┐ +│ anv-cli │ +│ (Command Line Interface) │ +│ check · parse · lex · fmt · new │ +└────────────────────────┬────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ anv-syntax │ │ anv-types │ │anv-semantics│ +│ Lexer │ │ Type │ │ ISU Rules │ +│ Parser │ │ Checker │ │ Validation │ +│ AST │ │ │ │ │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └────────────────┼────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ anv-core │ + │ Skating Types │ + │ Source Spans │ + │ Diagnostics │ + └─────────────────┘ +---- + +== Quick Start + +=== Building from Source + +[source,bash] +---- +# Clone the repository +git clone https://github.com/hyperpolymath/anvomidav.git +cd anvomidav + +# Build all crates +cargo build --release + +# Run tests (90+ passing) +cargo test + +# Install the CLI +cargo install --path crates/anv-cli +---- + +=== Creating Your First Program + +[source,bash] +---- +# Create a new singles program +anv new my_program --template singles + +# Check for errors +anv check my_program/main.anv + +# View the AST +anv parse my_program/main.anv +---- + +== Language Syntax + +=== Basic Program Structure + +[source] +---- +program competition_2025 { + segment sp: short { + sequence opening { + jump triple axel + spin camel L3 + step circular L4 + } + } + + segment fs: free { + sequence jumps { + jump quad lutz + jump triple flip + jump triple loop + } + } +} +---- + +=== Jump Elements + +Anvomidav supports all ISU-recognized jumps with rotation counts: + +[source] +---- +jump single toe_loop // 1T +jump double salchow // 2S +jump triple axel // 3A +jump quad lutz // 4Lz +jump triple flip // 3F +jump double loop // 2Lo +---- + +=== Spin Elements + +Spins include position and level designation: + +[source] +---- +spin upright L2 // USp2 +spin sit L3 // SSp3 +spin camel L4 // CSp4 +spin layback L3 // LSp3 +spin biellmann L4 // BSp4 +---- + +=== Step Sequences -Anvomidav aims to provide figure skating choreographers with a domain-specific language (DSL) to: +[source] +---- +step straight L3 // SlSt3 +step circular L4 // CiSt4 +step serpentine L2 // SeSt2 +---- + +=== Pairs Elements + +For pairs skating, additional elements are available: + +[source] +---- +lift Gr3 L4 // Group 3 lift, Level 4 +lift Gr5 L3 // Group 5 (hand-to-hand) lift +throw triple axel // 3ATh +twist double L3 // 2Tw3 +death_spiral LBI L4 // Left Backward Inside death spiral +---- + +=== Ice Dance Elements + +[source] +---- +choreographic spiral // ChSp +choreographic spread // ChSl (spread eagle) +choreographic ina // ChSl (Ina Bauer) +pattern waltz // Pattern dance +---- + +=== Segment Types + +[source] +---- +segment sp: short // Short Program +segment fs: free // Free Skate +segment rd: rhythm // Rhythm Dance (Ice Dance) +segment fd: free // Free Dance (Ice Dance) +segment pd: pattern // Pattern Dance +segment gala: exhibition // Exhibition/Gala +---- + +== ISU Rules Validation + +The semantic analyzer validates programs against ISU Technical Panel guidelines: + +=== Singles (Men's/Ladies') + +- Short Program: Max 3 jumps, 2 spins, 1 step sequence +- Free Skate: Max 7 jumps, 3 spins, 1 step sequence +- No pairs elements allowed (lifts, throws, twists, death spirals) + +=== Pairs -* **Notate** — Precisely describe skating elements, transitions, and sequences -* **Compose** — Build complex programs from reusable choreographic patterns -* **Validate** — Check technical compliance with ISU (International Skating Union) rules -* **Visualize** — Generate rink diagrams, timing charts, and 3D previews -* **Collaborate** — Share and version-control choreographic works +- Short Program: 1 required lift, 1 throw, 1 twist, 1 death spiral +- Free Skate: Multiple lifts, throws permitted +- Side-by-side and synchronized elements supported -== Name +=== Ice Dance + +- Only single jumps permitted +- No throws, twists, or death spirals +- Pattern dances follow specific step patterns +- Rhythm and Free Dance segments supported + +== CLI Commands + +[source,bash] +---- +anv check # Check files for errors +anv parse # Parse and display AST +anv lex # Tokenize and display tokens +anv fmt # Format source files (WIP) +anv new # Create new project + --template singles|pairs|ice-dance +---- -_Anvomidav_ — etymology and meaning to be documented. +== Crate Documentation + +[cols="1,2,1"] +|=== +| Crate | Description | Lines + +| `anv-core` +| Core skating types (Edge, JumpKind, SpinPosition, Level, etc.) +| ~500 + +| `anv-syntax` +| Lexer (logos) and Parser (chumsky), AST definitions +| ~1500 + +| `anv-types` +| Type checking and inference +| ~400 + +| `anv-semantics` +| ISU rules validation engine +| ~500 + +| `anv-cli` +| Command-line interface +| ~400 +|=== == Technology Stack @@ -36,42 +287,90 @@ Per the link:.claude/CLAUDE.md[Hyperpolymath Standard]: |=== | Component | Technology -| Compiler/Interpreter -| OCaml or Rust +| Language +| Rust (1.75+) + +| Lexer +| logos 0.14 -| Runtime -| Deno (if JS target) or native +| Parser +| chumsky 0.9 -| Editor Integration -| LSP server (Rust) +| Error Reporting +| miette 7 (fancy diagnostics) -| Visualization -| ReScript + WebGL/Canvas +| CLI Framework +| clap 4 -| Mobile Apps -| Tauri 2.0+ or Dioxus +| Serialization +| serde/serde_json |=== -== Getting Started +== Examples -[source,bash] +=== Men's Short Program + +[source] +---- +program mens_short_2025 { + segment sp: short { + sequence elements { + // Required jump element + jump triple axel + + // Jump combination + jump triple lutz + jump triple toe_loop + + // Flying spin + spin camel L3 + + // Spin with only one change of foot + spin sit L3 + + // Step sequence + step circular L4 + } + } +} ---- -# Clone the repository -git clone https://github.com/hyperpolymath/anvomidav.git -cd anvomidav -# Development environment (once implemented) -nix develop # Nix users -# or -guix shell # Guix users +=== Pairs Free Skate + +[source] +---- +program pairs_free_2025 { + segment fs: free { + sequence opening { + lift Gr5 L4 + throw triple salchow + } + + sequence middle { + twist triple L3 + death_spiral LBO L4 + + sync { + jump triple toe_loop + } + } + + sequence closing { + spin camel L4 + step circular L4 + choreographic spiral + } + } +} ---- == Documentation * link:ROADMAP.adoc[Roadmap] — Development phases and milestones -* link:CONTRIBUTING.md[Contributing] — How to participate +* link:CONTRIBUTING.adoc[Contributing] — How to participate * link:SECURITY.md[Security Policy] — Vulnerability reporting * link:CODE_OF_CONDUCT.md[Code of Conduct] — Community standards +* link:docs/academic/README.adoc[Academic Papers] — Formal specification and proofs == License @@ -79,6 +378,11 @@ Dual-licensed under link:LICENSE.txt[MIT OR AGPL-3.0-or-later]. Choose the licen == Contributing -Contributions welcome! This project is in early stages — input on language design, figure skating domain expertise, and implementation help are all valuable. +Contributions welcome! We're looking for: + +* **Figure skating domain experts** — Help refine element definitions +* **Compiler engineers** — Work on optimization and codegen +* **UI/UX designers** — Design visualization outputs +* **Documentation writers** — Improve tutorials and guides -See link:CONTRIBUTING.md[CONTRIBUTING.md] for guidelines. +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for workflow details. diff --git a/ROADMAP.adoc b/ROADMAP.adoc index cf3d6c2..915a579 100644 --- a/ROADMAP.adoc +++ b/ROADMAP.adoc @@ -2,60 +2,144 @@ // SPDX-FileCopyrightText: 2024-2025 hyperpolymath = Anvomidav Roadmap +:toc: macro +:toclevels: 3 +:icons: font This document outlines the development phases for Anvomidav, the first programming language for figure skating choreographers. +toc::[] + == Current Status -[CAUTION] +[NOTE] ==== -**Phase 0 — Project Specification** +**Phase 2 — Core Implementation** (In Progress) -The repository currently contains governance and infrastructure only. Implementation has not begun. +The foundation is complete. Lexer, parser, type system, and ISU rules validation are working. +Next milestone: IR layer and visualization output. ==== +---- +Phase 0 ████████████████████ 100% Project Setup +Phase 1 ████████████████████ 100% Foundation +Phase 2 ████████████████░░░░ 80% Core Implementation ◀── WE ARE HERE +Phase 3 ░░░░░░░░░░░░░░░░░░░░ 0% Developer Experience +Phase 4 ░░░░░░░░░░░░░░░░░░░░ 0% Visualization +Phase 5 ░░░░░░░░░░░░░░░░░░░░ 0% Ecosystem +---- + +''' + +== Phase 0: Project Setup ✓ Complete + +=== 0.1 Infrastructure +* [x] Repository structure and governance +* [x] SPDX license headers (MIT OR AGPL-3.0-or-later) +* [x] CI/CD configuration +* [x] Contributing guidelines +* [x] Security policy + +=== 0.2 Development Environment +* [x] Rust workspace configuration +* [x] Cargo workspace with 5 crates +* [x] Development dependencies + ''' -== Phase 1: Foundation +== Phase 1: Foundation ✓ Complete === 1.1 Domain Research -* [ ] Document figure skating element taxonomy (jumps, spins, steps, lifts) -* [ ] Map ISU technical panel guidelines to formal rules -* [ ] Interview choreographers to understand workflow needs -* [ ] Survey existing notation systems (Benesh, Labanotation adaptations) +* [x] Document figure skating element taxonomy +** Jumps: Axel, Lutz, Flip, Loop, Salchow, ToeLoop, Euler +** Spins: Upright, Sit, Camel, Layback, Biellmann +** Steps: ThreeTurn, Bracket, Rocker, Counter, Twizzle, etc. +** Pairs: Lifts (Gr1-5), Throws, Twists, Death Spirals +** Dance: Choreographic sequences, Pattern dances +* [x] Map ISU technical panel guidelines to formal rules +* [x] Define edge system (LFO, LFI, LBO, LBI, RFO, RFI, RBO, RBI) +* [x] Define level system (B, L1, L2, L3, L4) === 1.2 Language Design -* [ ] Define core syntax for element notation -* [ ] Design timing/music synchronization primitives -* [ ] Specify spatial positioning system (rink coordinates) -* [ ] Create grammar specification (EBNF or similar) -* [ ] Write language reference document +* [x] Define core syntax for element notation +* [x] Design program/segment/sequence structure +* [x] Specify segment types (short, free, rhythm, exhibition, pattern) +* [x] Support for all disciplines (singles, pairs, ice dance) +* [x] Write grammar specification === 1.3 Tooling Bootstrap -* [ ] Set up build system (Nix flake, Guix manifest) -* [ ] Configure CI/CD pipelines -* [ ] Establish test framework +* [x] Cargo workspace configuration +* [x] Test framework with 90+ tests +* [x] Error handling with miette ''' -== Phase 2: Core Implementation - -=== 2.1 Parser & AST -* [ ] Implement lexer -* [ ] Implement parser -* [ ] Define AST representation -* [ ] Add source location tracking for error messages - -=== 2.2 Semantic Analysis -* [ ] Type system for skating elements -* [ ] Temporal constraint checking -* [ ] Spatial collision detection (basic) -* [ ] ISU rule validation - -=== 2.3 Compiler/Interpreter -* [ ] Choose target(s): native / JS / WASM -* [ ] Implement code generation or interpreter -* [ ] Create runtime library +== Phase 2: Core Implementation (In Progress) + +=== 2.1 Lexer & Tokenizer ✓ Complete +* [x] Token definitions using logos 0.14 +* [x] All skating keywords (jumps, spins, steps, etc.) +* [x] Rotation prefixes (single, double, triple, quad) +* [x] Level designations (B, L1, L2, L3, L4) +* [x] Edge identifiers (LFO, RBI, etc.) +* [x] Lift groups (Gr1-Gr5) +* [x] Choreographic elements (spiral, spread, ina, hydroblading, pivot) +* [x] Line/column tracking for error reporting + +=== 2.2 Parser & AST ✓ Complete +* [x] Parser combinators using chumsky 0.9 +* [x] Program and segment parsing +* [x] Sequence and element parsing +* [x] Jump element parser (rotation + kind) +* [x] Spin element parser (position + level) +* [x] Step sequence parser (pattern + level) +* [x] Lift parser (group + level) +* [x] Throw parser (rotation + kind) +* [x] Twist parser (rotation + level) +* [x] Death spiral parser (edge + level) +* [x] Choreographic element parser +* [x] Pattern element parser +* [x] Parallel and sync block parsing +* [x] AST with source spans for all nodes +* [x] Comprehensive test coverage (40+ parser tests) + +=== 2.3 Type System ✓ Complete +* [x] Type definitions for all elements +* [x] Type environment +* [x] Basic type checking +* [x] Diagnostics with source locations + +=== 2.4 Semantic Analysis ✓ Complete +* [x] ISU rules engine +* [x] Discipline-specific rules (singles, pairs, ice dance) +* [x] Element count validation +** Max jumps per segment +** Required spins +** Step sequence requirements +* [x] Discipline constraints +** Singles: No pairs elements +** Ice Dance: Single jumps only, no throws/twists +** Pairs: Required elements (lifts, throws, etc.) +* [x] Segment-specific rules +** Short program limits +** Free skate limits +** Pattern dance requirements + +=== 2.5 CLI Tool ✓ Complete +* [x] `anv check` — Validate source files +* [x] `anv parse` — Show AST (debug/json) +* [x] `anv lex` — Show tokens +* [x] `anv fmt` — Format files (stub) +* [x] `anv new` — Create projects +** Singles template +** Pairs template +** Ice Dance template + +=== 2.6 IR Layer (Next) +* [ ] Intermediate representation design +* [ ] AST to IR lowering +* [ ] Optimization passes +* [ ] Code generation framework ''' @@ -64,20 +148,24 @@ The repository currently contains governance and infrastructure only. Implementa === 3.1 Language Server Protocol (LSP) * [ ] Syntax highlighting definitions * [ ] Autocomplete for elements and transitions -* [ ] Hover documentation +* [ ] Hover documentation with ISU codes * [ ] Go-to-definition -* [ ] Diagnostics integration +* [ ] Real-time diagnostics === 3.2 Editor Plugins * [ ] VS Code / VSCodium extension * [ ] Helix configuration -* [ ] Neovim plugin +* [ ] Neovim plugin (nvim-lspconfig) * [ ] Zed extension +* [ ] Tree-sitter grammar === 3.3 Documentation -* [ ] Language tutorial +* [ ] Language tutorial (Getting Started) * [ ] API reference -* [ ] Example programs (competition programs, practice sequences) +* [ ] Example programs +** Competition programs (Worlds, Olympics) +** Practice sequences +** Training drills * [ ] Video walkthroughs ''' @@ -85,18 +173,22 @@ The repository currently contains governance and infrastructure only. Implementa == Phase 4: Visualization === 4.1 2D Rink Diagrams -* [ ] Static SVG export +* [ ] SVG export of element paths * [ ] Animated trace playback -* [ ] Pattern overlay +* [ ] Pattern overlay (compulsory figures) +* [ ] Ice coverage visualization +* [ ] Entry/exit edge indicators === 4.2 Timing Charts * [ ] Music waveform alignment -* [ ] Element duration visualization +* [ ] Element duration bars * [ ] Program component breakdown +* [ ] Bonus time zones (free skate) === 4.3 3D Preview (Stretch Goal) * [ ] Skeletal animation system * [ ] Real-time playback +* [ ] Camera angles (judges, broadcast) * [ ] VR/AR exploration ''' @@ -106,17 +198,20 @@ The repository currently contains governance and infrastructure only. Implementa === 5.1 Package Manager * [ ] Choreographic pattern library format * [ ] Version resolution -* [ ] Registry (decentralized preferred) +* [ ] Decentralized registry (IPFS?) +* [ ] Pattern sharing and attribution === 5.2 Collaboration Tools * [ ] Diff/merge for choreographic files * [ ] Comment/annotation system * [ ] Export to PDF/print formats +* [ ] Coach/skater collaboration mode === 5.3 Mobile Applications -* [ ] Rinkside viewer app (Tauri or Dioxus) +* [ ] Rinkside viewer app (Tauri 2.0) * [ ] Offline-first architecture * [ ] Music sync for practice +* [ ] Video overlay mode ''' @@ -125,8 +220,61 @@ The repository currently contains governance and infrastructure only. Implementa * Integration with motion capture systems * AI-assisted choreography suggestions * Competition judging simulation -* Multi-skater/pairs/ice dance support -* Synchro skating team coordination +* Video analysis integration +* Synchro skating team coordination (16-20 skaters) +* Base value and GOE calculation + +''' + +== Technical Debt & Improvements + +=== Short Term +* [ ] Upgrade to chumsky 1.0 when stable +* [ ] JSON serialization for AST +* [ ] Source formatter implementation +* [ ] Better error recovery in parser + +=== Medium Term +* [ ] Incremental parsing for LSP +* [ ] Parallel validation +* [ ] Caching layer + +=== Long Term +* [ ] WASM compilation target +* [ ] Native binaries for all platforms + +''' + +== Test Coverage + +[cols="2,1,1"] +|=== +| Crate | Tests | Status + +| anv-core +| 16 +| ✓ Passing + +| anv-syntax +| 40 +| ✓ Passing + +| anv-types +| 21 +| ✓ Passing + +| anv-semantics +| 11 +| ✓ Passing + +| anv-cli +| 2 +| ✓ Passing + +| **Total** +| **90+** +| **✓ All Passing** +|=== ''' @@ -138,4 +286,11 @@ This roadmap is a living document. To propose changes: 2. Submit a PR for specific task additions 3. Use issues to claim tasks you want to work on -See link:CONTRIBUTING.md[CONTRIBUTING.md] for workflow details. +Priority areas seeking contributors: + +* **Figure skating expertise** — Validate ISU rules accuracy +* **Visualization** — SVG/Canvas rendering +* **LSP development** — Editor integration +* **Documentation** — Tutorials and examples + +See link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] for workflow details. diff --git a/crates/anv-semantics/Cargo.toml b/crates/anv-semantics/Cargo.toml new file mode 100644 index 0000000..5644316 --- /dev/null +++ b/crates/anv-semantics/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 hyperpolymath +# SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +[package] +name = "anv-semantics" +version = "0.1.0" +edition = "2021" +description = "Semantic analysis and ISU rules validation for Anvomidav" +license = "MIT OR AGPL-3.0-or-later" +repository = "https://github.com/hyperpolymath/anvomidav" +keywords = ["figure-skating", "dsl", "validation", "isu-rules"] +categories = ["compilers"] + +[dependencies] +anv-core = { workspace = true } +anv-syntax = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +pretty_assertions = "1.4" diff --git a/crates/anv-semantics/src/lib.rs b/crates/anv-semantics/src/lib.rs new file mode 100644 index 0000000..1346ad9 --- /dev/null +++ b/crates/anv-semantics/src/lib.rs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +//! Semantic analysis and ISU rules validation for Anvomidav. +//! +//! This module implements ISU (International Skating Union) rules validation +//! for figure skating programs, including element counts, required elements, +//! and various constraints for different disciplines. +//! +//! # Example +//! +//! ```ignore +//! use anv_semantics::{validate_program, Discipline}; +//! use anv_syntax::parse; +//! use anv_core::source::FileId; +//! +//! let source = r#" +//! program my_program { +//! segment short: short { +//! sequence { jump triple axel } +//! } +//! } +//! "#; +//! let program = parse(source, FileId(0)).unwrap(); +//! let errors = validate_program(&program, Discipline::MenSingles); +//! ``` + +pub mod rules; +pub mod validate; + +pub use rules::{Discipline, ISURules, SegmentRules}; +pub use validate::{validate_program, SemanticError, ValidationResult}; diff --git a/crates/anv-semantics/src/rules.rs b/crates/anv-semantics/src/rules.rs new file mode 100644 index 0000000..33b0cad --- /dev/null +++ b/crates/anv-semantics/src/rules.rs @@ -0,0 +1,599 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +//! ISU rules definitions for figure skating. +//! +//! This module defines the rules for different skating disciplines and segment types +//! according to ISU regulations. + +use anv_syntax::ast::SegmentKind; + +/// Skating discipline. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Discipline { + /// Men's singles. + MenSingles, + /// Ladies' singles. + LadiesSingles, + /// Pairs skating. + Pairs, + /// Ice dance. + IceDance, +} + +impl Discipline { + /// Returns the segment rules for this discipline and segment kind. + pub fn segment_rules(&self, kind: SegmentKind) -> SegmentRules { + match (self, kind) { + // Men's Singles + (Discipline::MenSingles, SegmentKind::Short) => SegmentRules::men_short(), + (Discipline::MenSingles, SegmentKind::Free) => SegmentRules::men_free(), + + // Ladies' Singles + (Discipline::LadiesSingles, SegmentKind::Short) => SegmentRules::ladies_short(), + (Discipline::LadiesSingles, SegmentKind::Free) => SegmentRules::ladies_free(), + + // Pairs + (Discipline::Pairs, SegmentKind::Short) => SegmentRules::pairs_short(), + (Discipline::Pairs, SegmentKind::Free) => SegmentRules::pairs_free(), + + // Ice Dance + (Discipline::IceDance, SegmentKind::Rhythm) => SegmentRules::rhythm_dance(), + (Discipline::IceDance, SegmentKind::Free) => SegmentRules::free_dance(), + (Discipline::IceDance, SegmentKind::Pattern) => SegmentRules::pattern_dance(), + (Discipline::IceDance, SegmentKind::Short) => SegmentRules::rhythm_dance(), // Use rhythm for short + + // Exhibition (no strict rules) + (_, SegmentKind::Exhibition) => SegmentRules::exhibition(), + + // Pattern dance for non-ice-dance (shouldn't happen, but handle gracefully) + (_, SegmentKind::Pattern) => SegmentRules::default(), + + // Rhythm for non-ice-dance + (_, SegmentKind::Rhythm) => SegmentRules::default(), + } + } +} + +/// Rules for a specific segment type. +#[derive(Debug, Clone)] +pub struct SegmentRules { + /// Minimum duration in seconds. + pub min_duration: Option, + /// Maximum duration in seconds. + pub max_duration: Option, + + /// Maximum number of jump elements. + pub max_jumps: Option, + /// Maximum number of spin elements. + pub max_spins: Option, + /// Required step sequence count. + pub step_sequences: Option, + + /// Maximum number of triple/quad jumps. + pub max_triple_quads: Option, + /// Maximum axels allowed. + pub max_axels: Option, + + // Pairs-specific + /// Required lifts. + pub required_lifts: Option, + /// Required throws. + pub required_throws: Option, + /// Required twists. + pub required_twists: Option, + /// Required death spirals. + pub required_death_spirals: Option, + + // Ice dance-specific + /// Required pattern repetitions. + pub pattern_repetitions: Option, + /// Required twizzles. + pub required_twizzles: Option, + + /// Required elements list. + pub required_elements: Vec, +} + +/// A required element specification. +#[derive(Debug, Clone)] +pub struct RequiredElement { + /// Element type. + pub element_type: RequiredElementType, + /// Minimum count. + pub min_count: u32, + /// Maximum count (None = unlimited). + pub max_count: Option, + /// Description. + pub description: String, +} + +/// Types of required elements. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequiredElementType { + Jump, + Spin, + StepSequence, + ChoreographicSequence, + Lift, + Throw, + Twist, + DeathSpiral, + Pattern, + Twizzle, +} + +impl Default for SegmentRules { + fn default() -> Self { + Self { + min_duration: None, + max_duration: None, + max_jumps: None, + max_spins: None, + step_sequences: None, + max_triple_quads: None, + max_axels: None, + required_lifts: None, + required_throws: None, + required_twists: None, + required_death_spirals: None, + pattern_repetitions: None, + required_twizzles: None, + required_elements: vec![], + } + } +} + +impl SegmentRules { + /// Men's short program rules (ISU 2024). + pub fn men_short() -> Self { + Self { + min_duration: Some(160), // 2:40 + max_duration: Some(170), // 2:50 + max_jumps: Some(3), + max_spins: Some(3), + step_sequences: Some(1), + max_triple_quads: Some(3), + max_axels: Some(1), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Double or triple Axel".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Triple or quad jump with steps immediately preceding".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Jump combination (triple-triple or quad-triple)".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 1, + max_count: Some(1), + description: "Flying spin".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 1, + max_count: Some(1), + description: "Camel spin or sit spin with only one change of foot".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 1, + max_count: Some(1), + description: "Spin combination with only one change of foot".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + ], + ..Default::default() + } + } + + /// Men's free skate rules (ISU 2024). + pub fn men_free() -> Self { + Self { + min_duration: Some(240), // 4:00 + max_duration: Some(270), // 4:30 + max_jumps: Some(7), + max_spins: Some(3), + step_sequences: Some(1), + max_triple_quads: Some(7), + max_axels: Some(2), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 7, + max_count: Some(7), + description: "Maximum 7 jump elements".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 3, + max_count: Some(3), + description: "3 spins of different nature".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::ChoreographicSequence, + min_count: 1, + max_count: Some(1), + description: "Choreographic sequence".into(), + }, + ], + ..Default::default() + } + } + + /// Ladies' short program rules (ISU 2024). + pub fn ladies_short() -> Self { + Self { + min_duration: Some(160), // 2:40 + max_duration: Some(170), // 2:50 + max_jumps: Some(3), + max_spins: Some(3), + step_sequences: Some(1), + max_triple_quads: Some(3), + max_axels: Some(1), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Double or triple Axel".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Triple or quad jump with steps immediately preceding".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Jump combination".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 3, + max_count: Some(3), + description: "3 spins of different nature".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + ], + ..Default::default() + } + } + + /// Ladies' free skate rules (ISU 2024). + pub fn ladies_free() -> Self { + Self { + min_duration: Some(240), // 4:00 + max_duration: Some(270), // 4:30 + max_jumps: Some(7), + max_spins: Some(3), + step_sequences: Some(1), + max_triple_quads: Some(7), + max_axels: Some(2), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 7, + max_count: Some(7), + description: "Maximum 7 jump elements".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 3, + max_count: Some(3), + description: "3 spins of different nature".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::ChoreographicSequence, + min_count: 1, + max_count: Some(1), + description: "Choreographic sequence".into(), + }, + ], + ..Default::default() + } + } + + /// Pairs short program rules (ISU 2024). + pub fn pairs_short() -> Self { + Self { + min_duration: Some(160), // 2:40 + max_duration: Some(170), // 2:50 + max_jumps: Some(3), + max_spins: Some(1), + step_sequences: Some(1), + required_lifts: Some(1), + required_throws: Some(1), + required_twists: Some(1), + required_death_spirals: Some(1), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Twist, + min_count: 1, + max_count: Some(1), + description: "Twist lift".into(), + }, + RequiredElement { + element_type: RequiredElementType::Lift, + min_count: 1, + max_count: Some(1), + description: "Lift (Group 3, 4, or 5)".into(), + }, + RequiredElement { + element_type: RequiredElementType::Throw, + min_count: 1, + max_count: Some(1), + description: "Throw jump".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(1), + description: "Solo jump".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::DeathSpiral, + min_count: 1, + max_count: Some(1), + description: "Death spiral".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 1, + max_count: Some(1), + description: "Pair spin combination".into(), + }, + ], + ..Default::default() + } + } + + /// Pairs free skate rules (ISU 2024). + pub fn pairs_free() -> Self { + Self { + min_duration: Some(240), // 4:00 + max_duration: Some(270), // 4:30 + max_jumps: Some(3), + max_spins: Some(2), + step_sequences: Some(1), + required_lifts: Some(3), + required_throws: Some(2), + required_twists: Some(1), + required_death_spirals: Some(1), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Twist, + min_count: 1, + max_count: Some(1), + description: "Twist lift".into(), + }, + RequiredElement { + element_type: RequiredElementType::Lift, + min_count: 3, + max_count: Some(3), + description: "3 different lifts".into(), + }, + RequiredElement { + element_type: RequiredElementType::Throw, + min_count: 2, + max_count: Some(2), + description: "2 throw jumps".into(), + }, + RequiredElement { + element_type: RequiredElementType::Jump, + min_count: 1, + max_count: Some(3), + description: "Solo or synchronized jumps".into(), + }, + RequiredElement { + element_type: RequiredElementType::DeathSpiral, + min_count: 1, + max_count: Some(1), + description: "Death spiral".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Choreographic sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 2, + max_count: Some(2), + description: "Pair spin and solo spin".into(), + }, + ], + ..Default::default() + } + } + + /// Rhythm dance rules (ISU 2024). + pub fn rhythm_dance() -> Self { + Self { + min_duration: Some(170), // 2:50 + max_duration: Some(180), // 3:00 + step_sequences: Some(1), + required_twizzles: Some(1), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Pattern, + min_count: 1, + max_count: Some(2), + description: "Pattern dance section".into(), + }, + RequiredElement { + element_type: RequiredElementType::Twizzle, + min_count: 1, + max_count: Some(1), + description: "Twizzle sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Partial step sequence".into(), + }, + ], + ..Default::default() + } + } + + /// Free dance rules (ISU 2024). + pub fn free_dance() -> Self { + Self { + min_duration: Some(240), // 4:00 + max_duration: Some(250), // 4:10 + required_twizzles: Some(1), + required_lifts: Some(3), + required_elements: vec![ + RequiredElement { + element_type: RequiredElementType::Lift, + min_count: 3, + max_count: Some(3), + description: "3 different lifts".into(), + }, + RequiredElement { + element_type: RequiredElementType::Twizzle, + min_count: 1, + max_count: Some(1), + description: "Twizzle sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::Spin, + min_count: 1, + max_count: Some(1), + description: "Dance spin".into(), + }, + RequiredElement { + element_type: RequiredElementType::StepSequence, + min_count: 1, + max_count: Some(1), + description: "Step sequence".into(), + }, + RequiredElement { + element_type: RequiredElementType::ChoreographicSequence, + min_count: 1, + max_count: Some(1), + description: "Choreographic element".into(), + }, + ], + ..Default::default() + } + } + + /// Pattern dance rules. + pub fn pattern_dance() -> Self { + Self { + pattern_repetitions: Some(2), + required_elements: vec![RequiredElement { + element_type: RequiredElementType::Pattern, + min_count: 2, + max_count: Some(2), + description: "Required pattern sequences".into(), + }], + ..Default::default() + } + } + + /// Exhibition/gala rules (no strict requirements). + pub fn exhibition() -> Self { + Self::default() + } +} + +/// ISU rules configuration. +#[derive(Debug, Clone)] +pub struct ISURules { + /// Season year (e.g., 2024 for 2024-2025 season). + pub season: u32, + /// Whether to enforce strict ISU compliance. + pub strict: bool, +} + +impl Default for ISURules { + fn default() -> Self { + Self { + season: 2024, + strict: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_men_short_rules() { + let rules = SegmentRules::men_short(); + assert_eq!(rules.max_jumps, Some(3)); + assert_eq!(rules.max_spins, Some(3)); + assert_eq!(rules.step_sequences, Some(1)); + } + + #[test] + fn test_pairs_short_rules() { + let rules = SegmentRules::pairs_short(); + assert_eq!(rules.required_lifts, Some(1)); + assert_eq!(rules.required_throws, Some(1)); + assert_eq!(rules.required_death_spirals, Some(1)); + } + + #[test] + fn test_rhythm_dance_rules() { + let rules = SegmentRules::rhythm_dance(); + assert_eq!(rules.required_twizzles, Some(1)); + assert!(rules.required_elements.iter().any(|e| e.element_type == RequiredElementType::Pattern)); + } + + #[test] + fn test_discipline_segment_rules() { + let rules = Discipline::MenSingles.segment_rules(SegmentKind::Short); + assert_eq!(rules.max_jumps, Some(3)); + + let rules = Discipline::Pairs.segment_rules(SegmentKind::Free); + assert_eq!(rules.required_lifts, Some(3)); + } +} diff --git a/crates/anv-semantics/src/validate.rs b/crates/anv-semantics/src/validate.rs new file mode 100644 index 0000000..a336f63 --- /dev/null +++ b/crates/anv-semantics/src/validate.rs @@ -0,0 +1,507 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +//! Program validation against ISU rules. +//! +//! This module provides validation functions to check programs against +//! ISU (International Skating Union) rules. + +use crate::rules::{Discipline, ISURules, SegmentRules}; +use anv_core::source::Span; +use anv_syntax::ast::{Element, ElementKind, Program, Segment, SegmentKind}; +use thiserror::Error; + +/// Semantic error type. +#[derive(Debug, Clone, Error)] +pub enum SemanticError { + #[error("Too many {element_type} elements: found {found}, maximum {max}")] + TooManyElements { + element_type: String, + found: usize, + max: usize, + span: Span, + }, + + #[error("Too few {element_type} elements: found {found}, minimum {min}")] + TooFewElements { + element_type: String, + found: usize, + min: usize, + span: Span, + }, + + #[error("Duration out of range: {duration}s (expected {min}s - {max}s)")] + DurationOutOfRange { + duration: u32, + min: u32, + max: u32, + span: Span, + }, + + #[error("Missing required element: {description}")] + MissingRequiredElement { description: String, span: Span }, + + #[error("Duplicate element type not allowed: {element_type}")] + DuplicateElement { element_type: String, span: Span }, + + #[error("Element not allowed in this segment: {element_type}")] + ElementNotAllowed { element_type: String, span: Span }, + + #[error("Invalid element for discipline: {message}")] + InvalidForDiscipline { message: String, span: Span }, +} + +/// Validation result containing warnings and errors. +#[derive(Debug, Default)] +pub struct ValidationResult { + /// Semantic errors that must be fixed. + pub errors: Vec, + /// Warnings that should be reviewed. + pub warnings: Vec, +} + +impl ValidationResult { + /// Returns true if validation passed with no errors. + pub fn is_ok(&self) -> bool { + self.errors.is_empty() + } + + /// Returns true if there are any errors. + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() + } + + /// Returns true if there are any warnings. + pub fn has_warnings(&self) -> bool { + !self.warnings.is_empty() + } +} + +/// Validate a program against ISU rules. +pub fn validate_program(program: &Program, discipline: Discipline) -> ValidationResult { + validate_program_with_rules(program, discipline, &ISURules::default()) +} + +/// Validate a program with custom rules configuration. +pub fn validate_program_with_rules( + program: &Program, + discipline: Discipline, + _rules: &ISURules, +) -> ValidationResult { + let mut result = ValidationResult::default(); + + for segment in &program.segments { + validate_segment(segment, discipline, &mut result); + } + + result +} + +/// Validate a single segment. +fn validate_segment(segment: &Segment, discipline: Discipline, result: &mut ValidationResult) { + let rules = discipline.segment_rules(segment.kind); + + // Collect all elements from all sequences + let elements: Vec<&Element> = segment + .sequences + .iter() + .flat_map(|seq| &seq.elements) + .collect(); + + // Count elements by type + let counts = count_elements(&elements); + + // Validate element counts + validate_element_counts(&counts, &rules, segment.span, result); + + // Validate discipline-specific rules + validate_discipline_rules(&elements, discipline, segment.kind, segment.span, result); +} + +/// Element counts by type. +#[derive(Debug, Default)] +struct ElementCounts { + jumps: usize, + spins: usize, + step_sequences: usize, + lifts: usize, + throws: usize, + twists: usize, + death_spirals: usize, + choreographic: usize, + patterns: usize, +} + +/// Count elements by type. +fn count_elements(elements: &[&Element]) -> ElementCounts { + let mut counts = ElementCounts::default(); + + for element in elements { + match &element.kind { + ElementKind::Jump(_) => counts.jumps += 1, + ElementKind::Spin(_) => counts.spins += 1, + ElementKind::StepSequence(_) => counts.step_sequences += 1, + ElementKind::Lift(_) => counts.lifts += 1, + ElementKind::Throw(_) => counts.throws += 1, + ElementKind::Twist(_) => counts.twists += 1, + ElementKind::DeathSpiral(_) => counts.death_spirals += 1, + ElementKind::Choreographic(_) => counts.choreographic += 1, + ElementKind::Pattern(_) => counts.patterns += 1, + ElementKind::Transition(_) | ElementKind::Parallel(_) | ElementKind::Sync(_) => {} + } + } + + counts +} + +/// Validate element counts against rules. +fn validate_element_counts( + counts: &ElementCounts, + rules: &SegmentRules, + span: Span, + result: &mut ValidationResult, +) { + // Check maximum jumps + if let Some(max) = rules.max_jumps { + if counts.jumps > max as usize { + result.errors.push(SemanticError::TooManyElements { + element_type: "jump".into(), + found: counts.jumps, + max: max as usize, + span, + }); + } + } + + // Check maximum spins + if let Some(max) = rules.max_spins { + if counts.spins > max as usize { + result.errors.push(SemanticError::TooManyElements { + element_type: "spin".into(), + found: counts.spins, + max: max as usize, + span, + }); + } + } + + // Check step sequences + if let Some(required) = rules.step_sequences { + if counts.step_sequences < required as usize { + result.errors.push(SemanticError::TooFewElements { + element_type: "step sequence".into(), + found: counts.step_sequences, + min: required as usize, + span, + }); + } + } + + // Check required lifts + if let Some(required) = rules.required_lifts { + if counts.lifts < required as usize { + result.errors.push(SemanticError::TooFewElements { + element_type: "lift".into(), + found: counts.lifts, + min: required as usize, + span, + }); + } + } + + // Check required throws + if let Some(required) = rules.required_throws { + if counts.throws < required as usize { + result.errors.push(SemanticError::TooFewElements { + element_type: "throw".into(), + found: counts.throws, + min: required as usize, + span, + }); + } + } + + // Check required twists + if let Some(required) = rules.required_twists { + if counts.twists < required as usize { + result.errors.push(SemanticError::TooFewElements { + element_type: "twist".into(), + found: counts.twists, + min: required as usize, + span, + }); + } + } + + // Check required death spirals + if let Some(required) = rules.required_death_spirals { + if counts.death_spirals < required as usize { + result.errors.push(SemanticError::TooFewElements { + element_type: "death spiral".into(), + found: counts.death_spirals, + min: required as usize, + span, + }); + } + } +} + +/// Validate discipline-specific rules. +fn validate_discipline_rules( + elements: &[&Element], + discipline: Discipline, + segment_kind: SegmentKind, + span: Span, + result: &mut ValidationResult, +) { + match discipline { + Discipline::MenSingles | Discipline::LadiesSingles => { + validate_singles_rules(elements, span, result); + } + Discipline::Pairs => { + validate_pairs_rules(elements, span, result); + } + Discipline::IceDance => { + validate_ice_dance_rules(elements, segment_kind, span, result); + } + } +} + +/// Validate singles-specific rules. +fn validate_singles_rules(elements: &[&Element], span: Span, result: &mut ValidationResult) { + // Singles cannot have pairs elements + for element in elements { + match &element.kind { + ElementKind::Lift(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Lifts are not allowed in singles skating".into(), + span, + }); + } + ElementKind::Throw(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Throws are not allowed in singles skating".into(), + span, + }); + } + ElementKind::Twist(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Twists are not allowed in singles skating".into(), + span, + }); + } + ElementKind::DeathSpiral(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Death spirals are not allowed in singles skating".into(), + span, + }); + } + _ => {} + } + } +} + +/// Validate pairs-specific rules. +fn validate_pairs_rules(_elements: &[&Element], _span: Span, _result: &mut ValidationResult) { + // Pairs can have most element types + // Could add specific validation for lift groups, etc. +} + +/// Validate ice dance-specific rules. +fn validate_ice_dance_rules( + elements: &[&Element], + _segment_kind: SegmentKind, + span: Span, + result: &mut ValidationResult, +) { + // Ice dance has restrictions on jumps + for element in elements { + match &element.kind { + ElementKind::Jump(jump) => { + // Ice dance only allows single jumps + if jump.rotations != anv_core::skating::Rotations::Single { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Ice dance only allows single jumps".into(), + span, + }); + } + } + ElementKind::Throw(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Throws are not allowed in ice dance".into(), + span, + }); + } + ElementKind::Twist(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Twists are not allowed in ice dance".into(), + span, + }); + } + ElementKind::DeathSpiral(_) => { + result.errors.push(SemanticError::InvalidForDiscipline { + message: "Death spirals are not allowed in ice dance".into(), + span, + }); + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anv_core::source::FileId; + use anv_syntax::parse; + + fn parse_and_validate(source: &str, discipline: Discipline) -> ValidationResult { + let program = parse(source, FileId(0)).expect("Failed to parse"); + validate_program(&program, discipline) + } + + #[test] + fn test_valid_singles_program() { + let source = r#" + program test { + segment sp: short { + sequence { + jump triple axel + jump triple lutz + spin camel L3 + step circular L4 + } + } + } + "#; + let result = parse_and_validate(source, Discipline::MenSingles); + // May have warnings about missing elements, but should be parseable + assert!(result.errors.iter().all(|e| !matches!( + e, + SemanticError::InvalidForDiscipline { .. } + ))); + } + + #[test] + fn test_singles_with_pairs_element_fails() { + let source = r#" + program test { + segment sp: short { + sequence { + lift Gr3 L4 + } + } + } + "#; + let result = parse_and_validate(source, Discipline::MenSingles); + assert!(result.has_errors()); + assert!(result + .errors + .iter() + .any(|e| matches!(e, SemanticError::InvalidForDiscipline { .. }))); + } + + #[test] + fn test_too_many_jumps() { + let source = r#" + program test { + segment sp: short { + sequence { + jump triple axel + jump triple lutz + jump triple flip + jump triple loop + } + } + } + "#; + let result = parse_and_validate(source, Discipline::MenSingles); + assert!(result + .errors + .iter() + .any(|e| matches!(e, SemanticError::TooManyElements { .. }))); + } + + #[test] + fn test_valid_pairs_program() { + let source = r#" + program test { + segment sp: short { + sequence { + lift Gr5 L4 + throw triple axel + twist double L3 + death_spiral LBI L4 + spin camel L3 + step circular L4 + } + } + } + "#; + let result = parse_and_validate(source, Discipline::Pairs); + // Pairs can have all these elements + assert!(result.errors.iter().all(|e| !matches!( + e, + SemanticError::InvalidForDiscipline { .. } + ))); + } + + #[test] + fn test_ice_dance_no_multi_rotation_jumps() { + let source = r#" + program test { + segment rd: rhythm { + sequence { + jump triple axel + } + } + } + "#; + let result = parse_and_validate(source, Discipline::IceDance); + assert!(result.has_errors()); + assert!(result.errors.iter().any(|e| { + if let SemanticError::InvalidForDiscipline { message, .. } = e { + message.contains("single jumps") + } else { + false + } + })); + } + + #[test] + fn test_exhibition_allows_anything() { + let source = r#" + program test { + segment gala: exhibition { + sequence { + jump quad axel + lift Gr5 L4 + choreographic spiral + } + } + } + "#; + let _result = parse_and_validate(source, Discipline::MenSingles); + // Exhibition segments have no strict rules + // This test just verifies parsing works for exhibition + } + + #[test] + fn test_missing_step_sequence() { + let source = r#" + program test { + segment sp: short { + sequence { + jump triple axel + spin camel L3 + } + } + } + "#; + let result = parse_and_validate(source, Discipline::MenSingles); + assert!(result + .errors + .iter() + .any(|e| matches!(e, SemanticError::TooFewElements { element_type, .. } if element_type == "step sequence"))); + } +} diff --git a/crates/anv-syntax/src/ast.rs b/crates/anv-syntax/src/ast.rs index 6b02e78..e6657e5 100644 --- a/crates/anv-syntax/src/ast.rs +++ b/crates/anv-syntax/src/ast.rs @@ -189,9 +189,16 @@ pub struct Segment { /// Segment kind. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SegmentKind { + /// Short program (singles/pairs). Short, + /// Free skate (singles/pairs). Free, + /// Pattern dance (ice dance). Pattern, + /// Rhythm dance (ice dance). + Rhythm, + /// Exhibition/gala performance. + Exhibition, } impl fmt::Display for SegmentKind { @@ -200,6 +207,8 @@ impl fmt::Display for SegmentKind { SegmentKind::Short => write!(f, "short"), SegmentKind::Free => write!(f, "free"), SegmentKind::Pattern => write!(f, "pattern"), + SegmentKind::Rhythm => write!(f, "rhythm"), + SegmentKind::Exhibition => write!(f, "exhibition"), } } } diff --git a/crates/anv-syntax/src/parser.rs b/crates/anv-syntax/src/parser.rs index 0b1e20e..1802eee 100644 --- a/crates/anv-syntax/src/parser.rs +++ b/crates/anv-syntax/src/parser.rs @@ -8,7 +8,7 @@ use crate::ast::*; use crate::token::{Lexer, SpannedToken, Token}; -use anv_core::skating::{JumpKind, Level, Rotations, SpinPosition}; +use anv_core::skating::{Edge, JumpKind, Level, LiftGroup, Rotations, SpinPosition}; use anv_core::source::{FileId, Span}; use chumsky::prelude::*; use ordered_float::OrderedFloat; @@ -523,15 +523,128 @@ fn step_sequence_parser( .labelled("step sequence") } +/// Create lift group parser. +fn lift_group_parser() -> impl Parser> + Clone { + select! { + Token::LiftGroup1 => LiftGroup::Group1, + Token::LiftGroup2 => LiftGroup::Group2, + Token::LiftGroup3 => LiftGroup::Group3, + Token::LiftGroup4 => LiftGroup::Group4, + Token::LiftGroup5 => LiftGroup::Group5, + } + .labelled("lift group") +} + +/// Create edge parser. +fn edge_parser() -> impl Parser> + Clone { + select! { + Token::LFO => Edge::LFO, + Token::LFI => Edge::LFI, + Token::LBO => Edge::LBO, + Token::LBI => Edge::LBI, + Token::RFO => Edge::RFO, + Token::RFI => Edge::RFI, + Token::RBO => Edge::RBO, + Token::RBI => Edge::RBI, + } + .labelled("edge") +} + +/// Create lift element parser (pairs). +fn lift_element_parser( + _file_id: FileId, +) -> impl Parser> + Clone { + just(Token::Lift) + .ignore_then(lift_group_parser()) + .then(level_parser().or_not()) + .map(|(group, level)| LiftElement { + group, + level, + entry: None, + exit: None, + }) + .labelled("lift element") +} + +/// Create throw element parser (pairs). +fn throw_element_parser( + _file_id: FileId, +) -> impl Parser> + Clone { + just(Token::Throw) + .ignore_then(rotations_parser()) + .then(jump_kind_parser()) + .map(|(rotations, kind)| ThrowElement { kind, rotations }) + .labeAnvomidavlled("throw element") +} + +/// Create twist element parser (pairs). +fn twist_element_parser( + _file_id: FileId, +) -> impl Parser> + Clone { + just(Token::Twist) + .ignore_then(rotations_parser()) + .then(level_parser().or_not()) + .map(|(rotations, level)| TwistElement { rotations, level }) + .labelled("twist element") +} + +/// Create death spiral element parser (pairs). +fn death_spiral_parser( + _file_id: FileId, +) -> impl Parser> + Clone { + just(Token::DeathSpiral) + .ignore_then(edge_parser()) + .then(level_parser().or_not()) + .map(|(edge, level)| DeathSpiralElement { edge, level }) + .labelled("death spiral") +} + +/// Create choreographic kind parser. +fn choreographic_kind_parser() -> impl Parser> + Clone { + select! { + Token::Spiral => ChoreographicKind::Spiral, + Token::Spread => ChoreographicKind::Spread, + Token::Ina => ChoreographicKind::Ina, + Token::Hydroblading => ChoreographicKind::Hydroblading, + Token::Pivot => ChoreographicKind::Pivot, + } + .labelled("choreographic kind") +} + +/// Create choreographic element parser. +fn choreographic_element_parser( + _file_id: FileId, +) -> impl Parser> + Clone { + just(Token::Choreographic) + .ignore_then(choreographic_kind_parser()) + .map(|kind| ChoreographicElement { + kind, + description: None, + }) + .labelled("choreographic element") +} + /// Create element parser. fn element_parser( file_id: FileId, ) -> impl Parser> + Clone { + // Singles elements let jump = jump_element_parser(file_id).map(ElementKind::Jump); let spin = spin_element_parser(file_id).map(ElementKind::Spin); let step = step_sequence_parser(file_id).map(ElementKind::StepSequence); - let element_kind = choice((jump, spin, step)); + // Pairs elements + let lift = lift_element_parser(file_id).map(ElementKind::Lift); + let throw = throw_element_parser(file_id).map(ElementKind::Throw); + let twist = twist_element_parser(file_id).map(ElementKind::Twist); + let death_spiral = death_spiral_parser(file_id).map(ElementKind::DeathSpiral); + + // Choreographic elements + let choreo = choreographic_element_parser(file_id).map(ElementKind::Choreographic); + + let element_kind = choice(( + jump, spin, step, lift, throw, twist, death_spiral, choreo, + )); element_kind .then(timing_parser(file_id).or_not()) @@ -570,6 +683,8 @@ fn segment_parser( just(Token::Short).to(SegmentKind::Short), just(Token::Free).to(SegmentKind::Free), just(Token::Pattern).to(SegmentKind::Pattern), + just(Token::Rhythm).to(SegmentKind::Rhythm), + just(Token::Exhibition).to(SegmentKind::Exhibition), )); just(Token::Segment) @@ -925,4 +1040,171 @@ mod tests { let program = result.unwrap(); assert_eq!(program.segments[0].sequences[0].name.as_ref().unwrap().node, "tech_elements"); } + + // === Pairs/Ice Dance Element Tests === + + #[test] + fn test_parse_lift_element() { + let source = "program t { segment s: free { sequence { lift Gr3 L4 } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::Lift(lift) => { + assert_eq!(lift.group, LiftGroup::Group3); + assert_eq!(lift.level, Some(Level::L4)); + } + _ => panic!("Expected lift element"), + } + } + + #[test] + fn test_parse_all_lift_groups() { + let groups = [("Gr1", LiftGroup::Group1), ("Gr2", LiftGroup::Group2), + ("Gr3", LiftGroup::Group3), ("Gr4", LiftGroup::Group4), + ("Gr5", LiftGroup::Group5)]; + for (group_str, expected) in groups { + let source = format!("program t {{ segment s: free {{ sequence {{ lift {} }} }} }}", group_str); + let result = parse(&source, FileId(0)); + assert!(result.is_ok(), "Failed to parse lift group: {}", group_str); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::Lift(lift) => assert_eq!(lift.group, expected), + _ => panic!("Expected lift element"), + } + } + } + + #[test] + fn test_parse_throw_element() { + let source = "program t { segment s: free { sequence { throw triple lutz } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::Throw(throw) => { + assert_eq!(throw.kind, JumpKind::Lutz); + assert_eq!(throw.rotations, Rotations::Triple); + } + _ => panic!("Expected throw element"), + } + } + + #[test] + fn test_parse_twist_element() { + let source = "program t { segment s: free { sequence { twist triple L3 } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::Twist(twist) => { + assert_eq!(twist.rotations, Rotations::Triple); + assert_eq!(twist.level, Some(Level::L3)); + } + _ => panic!("Expected twist element"), + } + } + + #[test] + fn test_parse_death_spiral() { + let source = "program t { segment s: free { sequence { death_spiral LBI L4 } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::DeathSpiral(ds) => { + assert_eq!(ds.edge, Edge::LBI); + assert_eq!(ds.level, Some(Level::L4)); + } + _ => panic!("Expected death spiral element"), + } + } + + #[test] + fn test_parse_choreographic_element() { + let source = "program t { segment s: exhibition { sequence { choreographic spiral } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + match &program.segments[0].sequences[0].elements[0].kind { + ElementKind::Choreographic(choreo) => { + assert_eq!(choreo.kind, ChoreographicKind::Spiral); + } + _ => panic!("Expected choreographic element"), + } + } + + #[test] + fn test_parse_all_choreographic_kinds() { + let kinds = ["spiral", "spread", "ina", "hydroblading", "pivot"]; + for kind in kinds { + let source = format!("program t {{ segment s: free {{ sequence {{ choreographic {} }} }} }}", kind); + let result = parse(&source, FileId(0)); + assert!(result.is_ok(), "Failed to parse choreographic kind: {}", kind); + } + } + + // === Segment Kind Tests === + + #[test] + fn test_parse_rhythm_segment() { + let source = "program t { segment dance: rhythm { sequence { step circular L3 } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + assert_eq!(program.segments[0].kind, SegmentKind::Rhythm); + } + + #[test] + fn test_parse_exhibition_segment() { + let source = "program t { segment gala: exhibition { sequence { choreographic spiral } } }"; + let result = parse(source, FileId(0)); + assert!(result.is_ok()); + let program = result.unwrap(); + assert_eq!(program.segments[0].kind, SegmentKind::Exhibition); + } + + #[test] + fn test_parse_all_segment_kinds_extended() { + let kinds = [ + ("short", SegmentKind::Short), + ("free", SegmentKind::Free), + ("pattern", SegmentKind::Pattern), + ("rhythm", SegmentKind::Rhythm), + ("exhibition", SegmentKind::Exhibition), + ]; + for (kind_str, expected_kind) in kinds { + let source = format!("program t {{ segment s: {} {{ }} }}", kind_str); + let result = parse(&source, FileId(0)); + assert!(result.is_ok(), "Failed to parse segment kind: {}", kind_str); + let program = result.unwrap(); + assert_eq!(program.segments[0].kind, expected_kind); + } + } + + #[test] + fn test_parse_pairs_program() { + let source = r#" + program pairs_short { + segment short_program: short { + sequence technical { + lift Gr5 L4 at 0:30 + throw triple axel at 1:00 + twist double L3 at 1:30 + death_spiral RBI L4 at 2:00 + spin camel sit L3 at 2:30 + } + } + } + "#; + let result = parse(source, FileId(0)); + if let Err(ref errors) = result { + for e in errors { + eprintln!("Parse error: {} at {:?}", e.message, e.span); + } + } + assert!(result.is_ok()); + let program = result.unwrap(); + assert_eq!(program.segments[0].sequences[0].elements.len(), 5); + } } diff --git a/crates/anv-syntax/src/token.rs b/crates/anv-syntax/src/token.rs index 2527d20..8eb97b9 100644 --- a/crates/anv-syntax/src/token.rs +++ b/crates/anv-syntax/src/token.rs @@ -153,6 +153,12 @@ pub enum Token { #[token("pattern")] Pattern, + #[token("rhythm")] + Rhythm, + + #[token("exhibition")] + Exhibition, + #[token("repeat")] Repeat, @@ -260,6 +266,38 @@ pub enum Token { #[token("L4")] Level4, + // === Lift Groups === + #[token("Gr1")] + LiftGroup1, + + #[token("Gr2")] + LiftGroup2, + + #[token("Gr3")] + LiftGroup3, + + #[token("Gr4")] + LiftGroup4, + + #[token("Gr5")] + LiftGroup5, + + // === Choreographic Elements === + #[token("spiral")] + Spiral, + + #[token("spread")] + Spread, + + #[token("ina")] + Ina, + + #[token("hydroblading")] + Hydroblading, + + #[token("pivot")] + Pivot, + // === Operators === #[token("+")] Plus, @@ -442,6 +480,8 @@ impl fmt::Display for Token { Token::Short => write!(f, "short"), Token::Free => write!(f, "free"), Token::Pattern => write!(f, "pattern"), + Token::Rhythm => write!(f, "rhythm"), + Token::Exhibition => write!(f, "exhibition"), Token::Repeat => write!(f, "repeat"), Token::Parallel => write!(f, "parallel"), Token::Sync => write!(f, "sync"), @@ -476,6 +516,16 @@ impl fmt::Display for Token { Token::Level2 => write!(f, "L2"), Token::Level3 => write!(f, "L3"), Token::Level4 => write!(f, "L4"), + Token::LiftGroup1 => write!(f, "Gr1"), + Token::LiftGroup2 => write!(f, "Gr2"), + Token::LiftGroup3 => write!(f, "Gr3"), + Token::LiftGroup4 => write!(f, "Gr4"), + Token::LiftGroup5 => write!(f, "Gr5"), + Token::Spiral => write!(f, "spiral"), + Token::Spread => write!(f, "spread"), + Token::Ina => write!(f, "ina"), + Token::Hydroblading => write!(f, "hydroblading"), + Token::Pivot => write!(f, "pivot"), Token::Plus => write!(f, "+"), Token::Minus => write!(f, "-"), Token::Star => write!(f, "*"),