Path System

1. Purpose & Motivation

Problem Solved

The Path System provides type-safe navigation through Viper's nested Value hierarchies. It solves:

  • Deep value access complexity: Accessing values buried in nested structures (Optional<Map<Key, Vector<Structure>>>) requires verbose, error-prone code
  • Type safety gaps: String-based paths ("user.settings[0].theme") offer no compile-time validation and fail silently at runtime
  • Structure mutation risks: Mixing "navigate to value" operations with "edit structure" operations causes production bugs (e.g., accidentally adding a map key when trying to update a value)
  • Validation timing: Runtime crashes ("field 'x' doesn't exist") instead of early detection at path construction time

Without Path System, applications must manually traverse Value trees, cast types at each step, and handle errors inconsistently.

Use Cases

Developers use Path System when they need to:

  1. Form field binding: Bind UI inputs to nested data structures
  2. Example: user.settings['theme'].color → Path validates schema, accesses value safely

  3. Change tracking: Record mutation locations in commit systems

  4. Example: Commit stores Path to mutation → replay operation later

  5. API field resolution: Implement GraphQL/REST field selectors

  6. Example: Query user { settings { theme } } → Path validates against schema, extracts values

  7. Error reporting with context: Generate debugging breadcrumbs

  8. Example: Validation error at a.b[0].c → Path provides ancestors [a, a.b, a.b[0], a.b[0].c] for full context

  9. Serializable navigation: Persist paths to database or send over network

  10. Example: Save user's "last edited field" as encoded Path → restore on reload

Position in Architecture

Utility Domain - Optional for applications, not used by Viper runtime.

┌─────────────────────────────────────────────────────────────┐
│                    Application Layer                         │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Path System (Optional Utility)                      │    │
│  │  - Type-safe Value navigation                        │    │
│  │  - Schema validation                                 │    │
│  │  - Serialization (Blob/Stream)                       │    │
│  └──────────────┬──────────────────────────────────────┘    │
└─────────────────┼──────────────────────────────────────────┘
                  │ 100% depends on
                  ▼
┌─────────────────────────────────────────────────────────────┐
│              Foundation Layer 0                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Type and Value System (Critical)                    │   │
│  │  - Type metadata (Structure, Map, Optional, etc.)    │   │
│  │  - Value instantiation and access                    │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌────────────────┐  ┌────────────────┐  ┌──────────────┐  │
│  │ Stream/Codec   │  │ Blob Storage   │  │ UUId         │  │
│  └────────────────┘  └────────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────────┘

Key characteristics: - NOT a Viper runtime dependency: Zero C++ files in Viper core include Viper_Path.hpp - Application utility: Opt-in for applications needing structured value access - Foundation dependency: Every Path operation relies on Type metadata for validation


2. Domain Overview

Scope

Path System provides capabilities for:

  • Type-safe value navigation: Traverse nested Value hierarchies with compile-time and runtime validation
  • Path construction: Fluent API using Builder pattern for readable, chainable path creation
  • Dual validation: Static schema validation (checkType) and runtime value validation (isApplicable)
  • Safety distinctions: Prevent structure mutation bugs via Regular (serializable) vs Irregular (transient) path classification
  • Serialization: Persist paths to Blob or Stream for storage/network transmission
  • Hierarchy inspection: Navigate path ancestry for debugging and error reporting

Key Concepts

1. PathComponent - Atomic Navigation Step

A PathComponent represents a single step in a path traversal. Each component has: - Type (enum PathComponentType): Semantic meaning (Field, Index, Key, Unwrap, Position, Entry, Element) - Value (std::shared_ptr<Value const>): Step-specific data (field name string, index integer, map key value, etc.)

7 PathComponentTypes:

Regular (5) - Serializable, cacheable:
  • Field      - Access Structure field by name
  • Index      - Access Vector/Tuple/Set by integer position
  • Key        - Access Map value by key
  • Position   - Access XArray by UUId position
  • Unwrap     - Unwrap Optional value

Irregular (2) - Transient operations, NOT serializable:
  • Entry      - Edit Map structure (add/remove entries)
  • Element    - Edit Set structure (add/remove elements)

Why the split? Regular components represent locations (serializable). Irregular components represent operations (transient, context-dependent).

2. Builder Pattern - Fluent Path Construction

Path uses method chaining for readable construction:

// C++
auto path = Path::make()
    ->field("settings")
    ->unwrap()
    ->key("theme")
    ->index(0)
    ->field("color");

// Python
path = Path().field('settings').unwrap().key('theme').index(0).field('color').const()

Each method: - Appends a PathComponent to internal vector - Returns shared_from_this() for chaining - Enables fluent API: path.a().b().c()

Trade-off: shared_ptr overhead vs code clarity. Rejected alternative: String paths ("settings[0].theme.color") - type-unsafe, no validation.

3. Regular vs Irregular - Safety Classification

Regular paths (Field, Index, Key, Position, Unwrap): - Navigate to locations in value hierarchy - Serializable: Can be encoded to Blob, sent over network - Cacheable: Can be used as map keys, cached for performance - Idempotent: Same path always means same location

Irregular paths (Entry, Element): - Represent operations on structure (add/remove entries/elements) - NOT serializable: entry("newKey") meaningless without context - Transient: Used during editing, converted to regular after mutation completes - Operation semantics: element(3) means "remove element at index 3", not "access element 3"

Critical rule: Call regularized() to convert irregular → regular after structure edits.

4. Dual Validation - Static vs Runtime

Static validation - checkType(type): - Validates path against Type schema at construction time - Catches permanent bugs: "Field 'x' doesn't exist in Structure S" - Use case: Code generation, compile-time checks, API schema validation

Runtime validation - isApplicable(value): - Checks if path works on specific Value instance at runtime - Handles transient states: "Optional field is nil right now" - Use case: Conditional UI (show button only if path applicable)

Why separate? Different failure modes: - Schema errors → Fix code (permanent bug) - Runtime nil → Handle gracefully (transient state)

5. Entry/Element vs Key/Index - Structure Mutation Safety

Critical distinction to prevent bugs:

For Maps: - Key path (Path().field("map").key("existing")) - Navigate to value (read/write safe) - Entry path (Path().field("map").entry("newKey")) - Edit structure (add/remove entries)

For Sets: - Index path (Path().field("set").index(0)) - Access element at position (iteration) - Element path (Path().field("set").element(0)) - Edit structure (remove element)

Bug prevented: "I wanted to update the value at key 'foo', but I accidentally created a new entry 'foo' instead."

Pattern: Use Entry/Element during mutation → call regularized() → use Key/Index for access.

External Dependencies

Uses (Foundation Layer):

  • Type and Value System (100% dependency) - Every path operation validates against Type metadata and navigates Value hierarchies
  • Pattern: checkType() walks Type tree in parallel with path components
  • Usage: at(value) and set(value, newValue) rely on Type-driven navigation

  • Stream System - PathReader/PathWriter for stream-based serialization

  • Pattern: Namespace functions PathReader::read(), PathWriter::write()

  • Blob Storage - PathEncoder/PathDecoder for Blob-based persistence

  • Pattern: Namespace functions PathEncoder::encode(), PathDecoder::decode()

  • UUId - Position component uses UUId for XArray navigation

  • Usage: Path::makePosition(uuid) for CRDT position-based indexing

Used By (Application Layer):

  • Applications (client code) - Optional utility for structured value access
  • NOT used by Viper runtime - Zero includes from Viper core C++ files (grep analysis confirms)

Interpretation: Path System is an opt-in utility. Applications can navigate Values manually without Paths if preferred.


3. Functional Decomposition

3.1 Sub-domains

1. Core Navigation

Central API for path construction and value access.

Components: - Path - Main class, provides Builder pattern API via method chaining - Factory methods: make(), makeField(), makeIndex(), makeKey(), etc. - Builder methods: field(), index(), key(), unwrap(), position(), entry(), element() - Navigation: at(value) - read, set(value, newValue) - write

  • PathComponent - Atomic step wrapper
  • Stores (PathComponentType type, std::shared_ptr<Value const> value) pair
  • Immutable after construction (const members)

Pattern: Factory + Builder - Factory: Static make*() methods hide constructor complexity - Builder: Instance methods return shared_from_this() for chaining


2. Component Types

7 PathComponentTypes define navigation semantics.

Regular Components (5) - Serializable, cacheable:

  1. Field - Access Structure field by name
  2. Value: ValueString (field name)
  3. Example: path.field("username")
  4. Type constraint: Only valid on TypeCode::Structure

  5. Index - Access by integer position

  6. Value: ValueUInt64 (index)
  7. Example: path.index(0)
  8. Valid on: Vector, Tuple, Vec, Mat, Set

  9. Key - Access Map value by key

  10. Value: Any Value type (map key type must match)
  11. Example: path.key("theme") or path.key((1, "Two")) for Tuple keys
  12. Valid on: TypeCode::Map

  13. Position - Access XArray by UUId position

  14. Value: ValueUUId (CRDT position identifier)
  15. Example: path.position(uuid)
  16. Valid on: TypeCode::XArray (CRDT semantics)

  17. Unwrap - Extract value from Optional

  18. Value: ValueVoid::Instance() (no data needed)
  19. Example: path.unwrap()
  20. Valid on: TypeCode::Optional

Irregular Components (2) - Transient operations, NOT serializable:

  1. Entry - Edit Map structure (add/remove entries)
  2. Value: Map key value (same as Key component)
  3. Example: path.entry("newKey")
  4. Semantics: "Add or remove entry with this key"
  5. Must call regularized() to convert Entry → Key after mutation

  6. Element - Edit Set structure (add/remove elements)

  7. Value: ValueUInt64 (element index)
  8. Example: path.element(3)
  9. Semantics: "Remove element at index 3"
  10. Must call regularized() to convert Element → Index after mutation

Why irregular paths exist: - Prevent bugs: Distinguish "navigate to value" vs "mutate structure" - Safety: isRegular() check before serialization prevents data loss - Workflow: Edit operation → regularized() → navigation path

Source of truth: Viper_PathComponentType.hpp:8-18 (enum PathComponentType)


3. Hierarchy

Navigate path ancestry for debugging and optimization.

Components: - parent() - Returns path with last component removed - Throws PathErrors::unexpectedRootPath if called on root - Use case: Error messages "Value at 'a.b.c' is invalid" → check parent() for context

  • ancestors() - Returns vector of all parent paths up to root
  • Result: [self, parent, grandparent, ..., root]
  • Use case: Breadcrumb navigation, cumulative validation along path

  • hasPrefix(other) - Checks if this path starts with other

  • Use case: Path matching (e.g., "Does mutation affect this subtree?")

  • isRoot() - Checks if path has zero components

  • Use case: Recursion termination

Pattern: Immutable navigation - Each method returns new Path (copy with modifications) - Original path unchanged


4. Map/Set Editing ⚠️ CRITICAL SAFETY

Distinguish navigation (read/write values) vs mutation (add/remove entries/elements).

Map Editing:

Entry vs Key: - Key path - Navigate to existing key's value - path.field("map").key("existing") → Read/write value safely - Regular: Can serialize, cache, transmit

  • Entry path - Edit map structure
  • path.field("map").entry("newKey") → Add/remove entry operation
  • Irregular: Cannot serialize (operation, not location)

EntryKeyInfo - Decompose Entry path:

struct EntryKeyInfo {
    std::shared_ptr<Path const> mapPath;    // Path to map itself
    std::shared_ptr<Path const> keyPath;    // Path to key definition (if nested)
    std::shared_ptr<Value const> key;       // Key value
};
  • Returned by: entryKeyInfo() when isEntryKeyPath() is true
  • Use case: Undo/redo needs to reconstruct "added entry 'foo' to map at path 'a.b'"

Set Editing:

Element vs Index: - Index path - Access element at position (for reading, iteration) - path.field("set").index(0) → Read element at position 0 - Regular: Serializable

  • Element path - Edit set structure
  • path.field("set").element(0) → Remove element at index 0
  • Irregular: Operation, not location

ElementInfo - Decompose Element path:

struct ElementInfo {
    std::shared_ptr<Path const> setPath;      // Path to set itself
    std::shared_ptr<Path const> elementPath;  // Path within element (if nested)
    std::size_t index;                         // Element index
};
  • Returned by: elementInfo() when isElementPath() is true
  • Use case: Change tracking "removed element at index 3 from set 'tags'"

Safety workflow: 1. During mutation: Use Entry/Element paths 2. After mutation completes: Call regularized() to convert Entry→Key, Element→Index 3. For access: Use Key/Index paths (regular, safe)

Why this matters: Real production bug prevented: - Wrong: path.entry("newKey").set(value, 42) - Adds key, doesn't update existing - Right: path.key("newKey").set(value, 42) - Updates value at existing key


5. Serialization

Persist paths to Blob or Stream for storage/network transmission.

Blob-based (PathEncoder/Decoder): - PathEncoder::encode(path, codecInstancing)Blob - Encodes path as binary blob - Use case: Store path in database, compact representation

  • PathDecoder::decode(blob, codecInstancing, definitions)Path
  • Decodes blob back to path
  • Requires Definitions for Type reconstruction

Stream-based (PathReader/Writer): - PathWriter::write(path, streamWriting) - Write path to stream - Use case: Network transmission, file serialization

  • PathReader::read(streamReading, definitions) - Read path from stream
  • Requires Definitions for Type reconstruction

Critical constraint: Only regular paths can be serialized reliably - Regular (Field, Index, Key, Position, Unwrap) → Serialize correctly - Irregular (Entry, Element) → May lose semantics on deserialization - Always check isRegular() before encoding

Pattern: Namespace functions (not class methods) - Rationale: Encoder/Decoder are stateless utilities, not path operations - Similar to BlobEncoder/BlobDecoder pattern in Blob Storage domain


6. Validation & Safety

Type-safe validation at construction and runtime.

Static validation - checkType(ctx, type): - Validates path against Type schema - Walks Type tree in parallel with PathComponents - Returns result Type if valid, throws exception if invalid - Example: ```cpp // Valid: Structure has field 'x' of type Float auto resultType = path.checkType("ctx", structureType); // Returns Type::FLOAT

// Invalid: Structure missing field 'y' path.checkType("ctx", structureType); // Throws PathErrors::unsupportedType ```

Runtime validation - isApplicable(value): - Checks if path works on specific Value instance - Handles transient states (e.g., Optional is nil) - Returns bool (no exception) - Example: ```cpp auto path = Path::make()->field("opt")->unwrap()->field("x");

path.isApplicable(valueWithNilOptional); // false - unwrap() will fail path.isApplicable(valueWithSomeOptional); // true - can navigate ```

Regularity checking: - isRegular() - Returns true if all components are regular (no Entry/Element) - regularized() - Converts irregular path to regular (Entry→Key, Element→Index) - Use case: Validate before serialization

PathErrors namespace: - unexpectedRootPath - Called parent() on root path - unsupportedType - Type doesn't support navigation (e.g., Field on Int64) - unexpectedComponentType - Component type doesn't match Type expectation (e.g., Index on Structure expecting Field) - indexOutOfRange - Vector/Tuple/Vec/Mat bounds exceeded - isNotRegular / isRegular - Regular/irregular validation failures

Pattern: Type-driven navigation - checkType() uses switch on TypeCode to validate each component - Each Type enforces specific component types (Structure→Field, Vector→Index, Optional→Unwrap) - Fail fast: Catch errors at path construction, not during value access


3.2 Key Components (Entry Points)

Component Purpose Entry Point File
Path Main navigation API, Builder pattern src/Viper/Viper_Path.hpp
PathComponent Atomic step wrapper (type, value) src/Viper/Viper_PathComponent.hpp
PathComponentType 7-variant enum (Field, Index, Key, etc.) src/Viper/Viper_PathComponentType.hpp
PathEncoder Blob-based serialization src/Viper/Viper_PathEncoder.hpp
PathDecoder Blob-based deserialization src/Viper/Viper_PathDecoder.hpp
PathReader Stream-based deserialization src/Viper/Viper_PathReader.hpp
PathWriter Stream-based serialization src/Viper/Viper_PathWriter.hpp
PathErrors Type-safe exception handling src/Viper/Viper_PathErrors.hpp

Python Bindings (5 files): - P_Viper_Path.cpp - Main Path class binding - P_Viper_PathComponent.cpp - PathComponent binding - P_Viper_PathConst.cpp - Const path operations - P_Viper_PathElementInfo.cpp - ElementInfo struct binding - P_Viper_PathEntryKeyInfo.cpp - EntryKeyInfo struct binding


3.3 Component Map (Visual)

┌──────────────────────────────────────────────────────────────────────┐
│                           Path (Builder)                              │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ vector<PathComponent>                                           │  │
│  │  [Field("settings"), Unwrap, Key("theme"), Index(0)]            │  │
│  └────────────────────────────────────────────────────────────────┘  │
│                                                                       │
│  Factory:  make(), makeField(), makeIndex(), makeKey(), ...          │
│  Builder:  field(), index(), key(), unwrap(), position(), ...        │
│  Navigate: at(value), set(value, newValue)                           │
│  Validate: checkType(type), isApplicable(value)                      │
│  Safety:   isRegular(), regularized()                                │
│  Hierarchy: parent(), ancestors(), hasPrefix()                       │
└───────────────────────────────┬───────────────────────────────────────┘
                                │
                                │ Stores vector of
                                ▼
        ┌───────────────────────────────────────┐
        │     PathComponent (Immutable)          │
        │  ┌─────────────────────────────────┐  │
        │  │ PathComponentType type (enum)   │  │
        │  │ shared_ptr<Value const> value   │  │
        │  └─────────────────────────────────┘  │
        └───────────────┬───────────────────────┘
                        │
                        │ Type is one of 7:
                        ▼
    ┌──────────────────────────────────────────────────────┐
    │       PathComponentType (enum)                        │
    │                                                       │
    │  REGULAR (Serializable):                             │
    │    • Field      - Structure field access             │
    │    • Index      - Vector/Tuple/Set position          │
    │    • Key        - Map value lookup                   │
    │    • Position   - XArray UUId position               │
    │    • Unwrap     - Optional extraction                │
    │                                                       │
    │  IRREGULAR (Transient operations):                   │
    │    • Entry      - Map structure mutation ⚠️          │
    │    • Element    - Set structure mutation ⚠️          │
    └──────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    Serialization                             │
│  ┌────────────────────┐      ┌──────────────────────┐      │
│  │  PathEncoder       │      │  PathReader          │      │
│  │  encode() → Blob   │      │  read() → Path       │      │
│  └────────────────────┘      └──────────────────────┘      │
│                                                              │
│  ┌────────────────────┐      ┌──────────────────────┐      │
│  │  PathDecoder       │      │  PathWriter          │      │
│  │  decode() → Path   │      │  write(Path)         │      │
│  └────────────────────┘      └──────────────────────┘      │
│                                                              │
│  ⚠️ Constraint: Only REGULAR paths can be serialized        │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                Type-Driven Navigation                        │
│                                                              │
│  Path: field("map").key("k").index(0).field("x")            │
│    ↓ checkType() validates in parallel                      │
│  Type: Structure → Map → Vector → Structure                 │
│         ↓Field     ↓Key   ↓Index    ↓Field                  │
│       (valid)   (valid)  (valid)   (valid) → Returns Float  │
│                                                              │
│  Validation switches on TypeCode at each step:              │
│    • Structure → expects Field component                    │
│    • Map       → expects Key or Entry component             │
│    • Vector    → expects Index or Element component         │
│    • Optional  → expects Unwrap component                   │
└─────────────────────────────────────────────────────────────┘

4. Developer Usage Patterns (Practical)

4.1 Core Scenarios

Each scenario extracted from real test code.

Scenario 1: Basic Field Navigation

When to use: Simple struct field access Test source: test_path.py:119-123TestPathBasicConstruction::test_field

from dsviper import Path

# Structure S has field 'f_field' (Type.FLOAT)
path = Path().field('f_field').const()

# Validate path against schema (static check)
path.check_type(structure_type)  # Returns Type.FLOAT if valid, raises if not

# Access value at path location
value = path.at(my_structure)  # Returns 42 (float value)

# Update value at path location
path.set(my_structure, 43.0)

Key APIs: field(name), check_type(type), at(value), set(value, newValue)

Pattern: Field access is the simplest navigation - one component, one step.


Scenario 2: Optional Unwrapping

When to use: Navigate through Optional values Test source: test_path.py:124-134TestPathBasicConstruction::test_optional

# Path: structure.f_opt_s (Optional<Structure>) → unwrap → f_x (float)
path = Path().field('f_opt_s').unwrap().field('f_x').const()
path.check_type(structure_type)  # Validates: Field(Optional) → Unwrap → Field(float)

# Create value with non-nil Optional
value = my_structure.copy()
value.f_opt_s = Value.create(inner_structure_type, {'f_x': 42})

# Read value (succeeds because Optional is non-nil)
result = path.at(value)  # Returns 42

# Write value
path.set(value, 43)
assert path.at(value) == 43

Key APIs: unwrap(), runtime nil handling

Safety note: unwrap() on nil Optional raises exception at runtime. Use isApplicable() to check before access:

if path.is_applicable(value):
    result = path.at(value)  # Safe: Optional is non-nil
else:
    # Handle nil case gracefully
    result = default_value

Scenario 3: Complex Chained Navigation

When to use: Deep nested structures (real-world complexity) Test source: test_path.py:172-181TestPathBasicConstruction::test_complicated

# Path: f_c (Optional<Map>) → unwrap → key(tuple) → index(0) → f_y (float)
# Type signature: Optional<Map<Tuple<uint8, string>, Vector<Structure>>>

# Create complex key (Tuple type)
key_value = (1, 'Two')

# Build path using method chaining
path = Path().field('f_c').unwrap().key(key_value).index(0).field('f_y').const()

# Validate entire path against schema
path.check_type(structure_type)

# Access deeply nested value
value = my_structure.copy()
result = path.at(value)  # Returns 2 (f_y value in nested structure)

# Update deeply nested value
path.set(value, 3)
assert path.at(value) == 3

# Runtime check: Does path work on this value?
assert path.is_applicable(value)  # True: All intermediate values exist

Key APIs: key() with complex Value, index() for Vector, method chaining

Pattern: Builder pattern shines with complex paths - each method adds one component, returns self for chaining.

Validation flow: 1. field('f_c') → Validates Structure has field 'f_c' of type Optional 2. unwrap() → Validates Optional, extracts Map type 3. key(tuple) → Validates Map key type matches Tuple, extracts Vector type 4. index(0) → Validates Vector, extracts Structure element type 5. field('f_y') → Validates Structure has field 'f_y', returns Float type


Scenario 4: Entry vs Key - Map Editing Safety ⚠️ CRITICAL

When to use: Distinguish "edit map value" vs "add map entry" Test source: test_path.py:396-416TestMapSetEditing::test_entry_vs_key_semantics_for_maps

# Map structure: Map<string, float>

# Key path - Navigate to EXISTING key's VALUE (safe read/write)
key_path = Path().field('f_map').key('existing').const()

# Check component type
assert key_path.components()[-1].type() == 'Key'

# Access existing value
value = my_structure.copy()
value.f_map['existing'] = 42.0
result = key_path.at(value)  # Returns 42.0

# Update existing value
key_path.set(value, 43.0)
assert key_path.at(value) == 43.0

# ---

# Entry path - Edit map STRUCTURE (add/remove entries)
entry_path = Path().field('f_map').entry('new_key').const()

# Check component type
assert entry_path.components()[-1].type() == 'Entry'

# Entry paths are IRREGULAR - cannot serialize
assert not entry_path.is_regular()

# After adding entry to map structure (via mutation operation):
# Convert Entry path to Key path for future access
regular_path = entry_path.regularized()
assert regular_path.const().components()[-1].type() == 'Key'
assert regular_path.const().is_regular()

# Now safe to use regular_path for reading/writing the new key's value

Key distinction: - Key path: Navigate to value (regular, serializable, cacheable) - Entry path: Structure edit operation (irregular, transient, NOT serializable)

Real bug prevented:

# WRONG: Trying to use Entry path for value access
entry_path.set(value, 99)  # May create new entry instead of updating existing!

# RIGHT: Use Key path for value access
key_path.set(value, 99)    # Updates existing value safely

Workflow: 1. During map mutation: Use Entry path to target structure edit 2. After mutation completes: Call regularized() to get Key path 3. For value access: Use Key path (regular, safe)


Scenario 5: Regular Path Serialization ⚠️ CRITICAL

When to use: Persist paths to database, send over network Test source: test_path.py:573-591TestPathRegularization::test_why_regularization_matters

# Regular paths (navigation only) are serializable
regular_path = Path().field('f_map').key('mykey').const()

# Check if path is regular (no Entry/Element components)
assert regular_path.is_regular()  # True - safe to serialize

# Serialize to Blob
encoded_blob = regular_path.encode()

# Deserialize from Blob (requires Definitions for Type reconstruction)
decoded_path = Path.decode(encoded_blob, definitions)

# Round-trip preserves path exactly
assert regular_path.representation() == decoded_path.const().representation()

# ---

# Irregular paths (Entry/Element) CANNOT be serialized reliably
irregular = Path().field('f_map').entry('k').const()
assert not irregular.is_regular()  # False - DO NOT serialize

# Why irregular paths fail serialization:
# - Entry('k') means "add entry with key 'k'" (operation, not location)
# - After deserialization, context is lost (is 'k' existing or new?)
# - May cause bugs: serialize Entry → deserialize → treated as Key → wrong semantics

# Convert to regular before serializing
regular = irregular.regularized()  # Entry → Key conversion
assert regular.const().is_regular()  # True - now safe to serialize

encoded = regular.const().encode()
decoded = Path.decode(encoded, definitions)
# Success: Key path round-trips correctly

Key APIs: is_regular(), encode(), decode(), regularized()

Safety pattern:

def save_path(path, db):
    """Save path to database with safety check."""
    if not path.is_regular():
        # Option 1: Convert to regular
        path = path.regularized()

        # Option 2: Reject irregular paths
        # raise ValueError("Cannot serialize irregular path")

    blob = path.encode()
    db.store(blob)

def load_path(db, definitions):
    """Load path from database."""
    blob = db.fetch()
    path = Path.decode(blob, definitions)

    # Verify deserialization produced regular path
    assert path.const().is_regular()

    return path

Use cases: - Database storage: Save "user's last edited field" as encoded Path - Network transmission: Send path to remote server for distributed editing - Caching: Use path as cache key (only regular paths are stable keys)


Scenario 6: Type Validation

When to use: Validate path before using in production Test source: test_path.py:608-627TestPathValidationAndSafety::test_check_type_validates_against_schema

# Validate path against type schema (static check - catches bugs early)
path = Path().field('f_field').const()

# check_type() returns result type if valid, raises exception if invalid
result_type = path.check_type(structure_type)

# Result type is the type at path location
assert result_type.representation() == 'float'  # f_field is Type.FLOAT

# ---

# Invalid path raises exception (fail fast)
invalid_path = Path().field('nonexistent').const()

try:
    invalid_path.check_type(structure_type)
    # Should not reach here
    assert False, "Expected exception"
except Exception as e:
    # Error message: "Field 'nonexistent' not found in Structure"
    # Caught at construction time, not runtime access
    pass

# ---

# Type validation walks path in parallel with Type tree:
path = Path().field('f_map').key('k').index(0).const()

# Validation steps:
# 1. field('f_map') → Structure has f_map? Yes → Type is Map<string, Vector<float>>
# 2. key('k')      → Map accepts Key? Yes → Type is Vector<float>
# 3. index(0)      → Vector accepts Index? Yes → Type is float
# Result: check_type() returns float Type

result_type = path.check_type(structure_type)
assert result_type.representation() == 'float'

Key APIs: check_type(type) for static validation

Validation logic (from C++ implementation):

// Viper_Path.cpp:344-449
std::shared_ptr<Type> Path::checkType(...) const {
    auto w_type = type;  // Start with root type

    for (each component in path) {
        switch (w_type->typeCode) {
            case TypeCode::Structure:
                // Expects Field component
                if (component->type != PathComponentType::Field)
                    throw unexpectedComponentType(...);
                // Update type to field's type
                w_type = structureType->fieldType(fieldName);
                break;

            case TypeCode::Map:
                // Expects Key or Entry component
                if (component->type != PathComponentType::Key &&
                    component->type != PathComponentType::Entry)
                    throw unexpectedComponentType(...);
                // Update type to map's value type
                w_type = mapType->elementType;
                break;

            case TypeCode::Optional:
                // Expects Unwrap component
                if (component->type != PathComponentType::Unwrap)
                    throw unexpectedComponentType(...);
                // Update type to Optional's inner type
                w_type = optionalType->elementType;
                break;

            // ... (15+ type codes handled)
        }
    }

    return w_type;  // Returns type at path location
}

Use cases: - Code generation: Generate correct accessor code (e.g., GraphQL resolvers) - API validation: Reject invalid field selectors at API boundary - Form validation: Check form field path before binding to UI


4.2 Integration Patterns

Commit System integration (mutation tracking):

# Record mutation location as Path
commit_state.recordMutation(
    path=Path().field('settings').unwrap().key('theme').const(),
    old_value=old_theme,
    new_value=new_theme
)

# Replay mutation later
path = commit_command.mutationPath()
path.set(current_state, commit_command.newValue())

GraphQL field resolution:

# Query: user { settings { theme } }
# Translate to Path for type-safe access

path = Path().field('settings').field('theme').const()

# Validate against schema
path.check_type(user_type)  # Ensures fields exist

# Execute query
result = path.at(user_value)

UI form binding:

class FormField:
    def __init__(self, path, initial_value):
        self.path = path
        # Validate path against schema
        self.result_type = path.check_type(form_schema)
        self.value = initial_value

    def update(self, form_data):
        # Check if path is applicable to current data
        if self.path.is_applicable(form_data):
            self.path.set(form_data, self.value)
        else:
            # Handle nil Optional, missing key, etc.
            self.show_error("Field not available")

4.3 Test Suite Reference

Full test coverage: python/tests/unit/test_path.py (853 lines, 44 tests, 9 test classes)

Test organization: - TestPathBase - Base class with complex Structure setup (Lines 9-56) - TestPathBasicConstruction - Static constructors, basic operations (15 tests, Lines 63-198) - TestPathHierarchy - parent(), ancestors(), hasPrefix() (5 tests, Lines 205-300) - TestPathComponentManipulation - Component access, fromComponent(), toComponent() (5 tests, Lines 302-383) - TestMapSetEditing - Entry vs Key, Element vs Index semantics (6 tests, Lines 386-509) ⚠️ CRITICAL - TestPathRegularization - is_regular(), regularized() (3 tests, Lines 517-591) ⚠️ CRITICAL - TestPathValidationAndSafety - check_type(), is_applicable(), error cases (6 tests, Lines 597-730) - TestPathEqualityAndRepresentation - equals(), representation() (3 tests, Lines 733-800) - TestPathOperatorOverloading - Python operator support (1 test, Lines 803-853)

Coverage by PathComponentType: | Type | Test Methods | Example Test | |------|--------------|--------------| | Field | 15+ tests | test_field, test_complicated | | Index | 10+ tests | test_index_vector, test_index_tuple | | Key | 8+ tests | test_key, test_entry_vs_key | | Unwrap | 6+ tests | test_optional, test_complicated | | Position | 3 tests | test_position, test_create_position | | Entry | 6 tests | test_entry_vs_key, test_entry_key_info ⚠️ | | Element | 4 tests | test_element_vs_index, test_element_info ⚠️ |

Serialization coverage: - test_stream_codec (Lines 192-197) - Blob encode/decode - test_why_regularization_matters (Lines 573-591) - Serialization safety

Safety emphasis: 10/44 tests (23%) focus on Regular/Irregular distinction and Entry/Element safety.


5. Technical Constraints

Performance Considerations

  1. Path construction: O(n) where n = component count
  2. Each builder method appends PathComponent to vector
  3. Allocation: std::vector::push_back amortized O(1)
  4. Typical paths: 3-5 components → negligible overhead

  5. Type checking: O(n) type tree walk

  6. checkType() validates each component against Type
  7. Switch on TypeCode per component (15+ cases)
  8. Typical paths: 3-5 components → <1ms validation time

  9. Value navigation: O(n) resolution

  10. at() and set() traverse value hierarchy step-by-step
  11. Each step: Type check + value cast + container access
  12. No caching (paths are stateless)

  13. Serialization: Linear in path length

  14. Encode/decode each component to/from binary
  15. Blob size: ~10-50 bytes per component (depends on Value size)
  16. Example: 5-component path → ~100 bytes encoded

  17. Hierarchy operations: O(n) for ancestors()

  18. Copies path n times (each ancestor is new Path instance)
  19. Allocation: n shared_ptr<Path> objects
  20. Use case: Debugging only (not performance-critical)

Optimization opportunities: - Path caching: Applications can cache Path instances for repeated access - Type validation skip: If type is known stable, cache checkType() result - Shared components: PathComponents are immutable, can be shared across paths (not currently implemented)


Thread Safety

Immutable after construction: - Path with .const(): Thread-safe (read-only) - Once const() is called, Path is frozen (C++ convention) - Safe to share across threads for read operations (at(), checkType(), isApplicable())

  • PathComponent: Always immutable (const members)
  • PathComponentType const type - Cannot change after construction
  • std::shared_ptr<Value const> const value - Cannot change pointer or value
  • Safe to share across threads

Mutable during construction: - Path builder: NOT thread-safe (expected behavior) - Builder methods (field(), index(), etc.) modify internal _pathComponents vector - Do not share Path instances across threads during construction phase - Pattern: Construct on one thread → call const() → share immutable result

Value access thread safety: - Path operations (at(), set()) delegate to Value operations - Thread safety depends on Value mutability: - Reading immutable Values: Thread-safe - Writing Values: NOT thread-safe (standard Value semantics)

Recommendation: Treat Path as immutable after construction (call const() immediately).


Error Handling

Exception types from PathErrors namespace:

  1. Type validation errors (construction-time):
  2. unsupportedType - Type doesn't support navigation (e.g., Field on Int64)

    • Example: Path().field("x").checkType(Type::INT64) → throws (Int64 has no fields)
  3. unexpectedComponentType - Component type doesn't match Type expectation

    • Example: Path().index(0).checkType(structureType) → throws (Structure expects Field, not Index)
  4. Range errors (runtime):

  5. indexOutOfRange - Vector/Tuple/Vec/Mat bounds exceeded

    • Example: Path().index(10).at(3-element-vector) → throws (index 10 > size 3)
  6. Hierarchy errors:

  7. unexpectedRootPath - Called parent() on root path (0 components)

    • Example: Path().parent() → throws (no parent of root)
  8. Regular/Irregular validation errors:

  9. isNotRegular - Operation requires regular path, but Entry/Element present

    • Example: entryKeyInfo() called on regular path → throws (only works on Entry paths)
  10. isRegular - Operation requires irregular path, but none present

    • Example: Attempting to serialize irregular path → throws (must regularize first)
  11. Path index errors:

  12. invalidPathIndex - fromComponent()/toComponent() index out of range
    • Example: path.toComponent(99) on 5-component path → throws

Error handling patterns:

# Pattern 1: Validate at construction time (fail fast)
try:
    path = Path().field('x').const()
    path.check_type(my_type)  # Throws if 'x' not in type
except Exception as e:
    # Handle schema error (permanent bug - fix code)
    log_error(f"Invalid path: {e}")

# Pattern 2: Check applicability before runtime access
path = Path().field('opt').unwrap().field('x').const()
if path.is_applicable(my_value):
    result = path.at(my_value)  # Safe: Optional is non-nil
else:
    # Handle transient nil (graceful fallback)
    result = default_value

# Pattern 3: Serialize only regular paths
def save_path(path):
    if not path.is_regular():
        raise ValueError("Cannot serialize irregular path")
    return path.encode()

Design principle: Fail fast with clear error messages. All PathErrors include: - Component name: Which C++ component threw (e.g., "Viper.Path") - Context string: Which method failed (e.g., "FUNCTION") - Path representation: What path caused the error (e.g., "a.b[0].c")


Memory Model

Reference semantics (Viper standard): - Path: std::shared_ptr<Path> - Reference counting, no manual delete - PathComponent: std::shared_ptr<PathComponent> - Shared ownership - Component values: std::shared_ptr<Value const> - Immutable values

Builder pattern memory:

auto path = Path::make();         // Allocate Path (shared_ptr)
path->field("a");                 // Allocate PathComponent, append to vector
path->index(0);                   // Allocate PathComponent, append to vector
return path;                      // Return shared_ptr (ref count = 1)

// When path goes out of scope:
// 1. shared_ptr<Path> ref count → 0 → delete Path
// 2. Path destructor deletes vector<shared_ptr<PathComponent>>
// 3. Each PathComponent ref count → 0 → delete PathComponent
// 4. Each Value ref count decrements (may delete if last reference)

Builder chaining:

path->field("a")->index(0)->field("b");
// Each method returns shared_from_this() (same shared_ptr)
// No copies, no temporary allocations
// Single Path object modified in-place

Copy semantics: - copy() - Shallow copy (shares PathComponent instances) - Copies vector, but PathComponents are shared (immutable anyway) - parent() - New Path with copied vector (last component removed) - ancestors() - Allocates n new Path objects (n = component count)

Lifetime management: - Path instances typically short-lived (construction → use → discard) - Applications may cache frequently-used paths (e.g., "user.settings.theme") - PathComponents are immutable → safe to share across Paths

Critical constraint: ⚠️ Must call const() after construction (Python binding) - Builder returns mutable Path (Python convention) - .const() returns immutable wrapper (safe for concurrent reads) - Pattern: Path().field("a").index(0).const() - always end with const()


6. Cross-References

  • doc/domains/Type_And_Value_System.md - Foundation for path navigation (100% dependency)
  • Path validates against Type metadata
  • Path navigates Value hierarchies
  • Understanding Type/Value is prerequisite for Path usage

  • doc/domains/Stream_Codec.md - PathReader/PathWriter serialization

  • Stream-based path persistence
  • Codec selection (Binary, TokenBinary, Raw)

  • doc/domains/Blob_Storage.md - PathEncoder/PathDecoder persistence

  • Blob-based path encoding (~100 bytes per path)
  • Content-addressable storage for paths

  • doc/Getting_Started_With_Viper.md - Value navigation examples

  • May include path-based value access patterns
  • Entry point for learning Viper basics

  • doc/DSM.md - DSM language specification

  • Structure/Map/Vector type definitions
  • How Type schema constrains path construction

Dependencies

This domain USES:

  • Type and Value System (Foundation Layer 0) - CRITICAL, 100% dependency
  • Why: Every Path operation validates against Type metadata and navigates Value trees
  • How: checkType() walks Type tree, at()/set() navigate Value tree
  • Pattern: Type-driven navigation (switch on TypeCode to validate components)
  • Coupling: Cannot use Path without understanding Type and Value

  • Stream System (Foundation Layer 0) - Serialization dependency

  • Why: PathReader/PathWriter use StreamReading/StreamWriting for serialization
  • How: Namespace functions delegate to Stream API
  • Pattern: Adapter pattern (Path → Stream format)

  • Blob Storage (Foundation Layer 0) - Persistence dependency

  • Why: PathEncoder/PathDecoder convert Path ↔ Blob for storage
  • How: Namespace functions encode components to binary, decode back
  • Pattern: Codec pattern (Path → Blob → Path round-trip)

  • UUId (Foundation Layer 0) - Position component dependency

  • Why: PathComponentType::Position uses UUId for XArray CRDT navigation
  • How: position(uuid) stores UUId value in PathComponent
  • Pattern: Value wrapper (UUId wrapped in ValueUUId)

This domain is USED BY:

  • Applications (client code) - Optional utility for structured value access
  • NOT used by Viper runtime: Grep analysis shows 0 includes of Viper_Path.hpp from Viper core C++ files
  • Interpretation: Path is an opt-in utility. Applications can navigate Values manually without Paths if preferred.

Coupling strength: - Path → Type & Value: Strong (100% dependency, every operation) - Path → Stream, Blob: Weak (serialization only, optional feature) - Path → UUId: Very weak (1 component type only) - Viper Core → Path: Zero (completely optional)


Key Type References

C++ Headers: - src/Viper/Viper_Path.hpp - Main API (22 public methods, Builder + Factory pattern) - Entry points: make(), makeField(), makeIndex(), etc. - Builder: field(), index(), key(), unwrap(), etc. - Navigation: at(), set(), checkType(), isApplicable() - Hierarchy: parent(), ancestors(), hasPrefix() - Safety: isRegular(), regularized()

  • src/Viper/Viper_PathComponent.hpp - Atomic step (type, value pair)
  • Factory: make(type, value)
  • Members: PathComponentType const type, shared_ptr<Value const> const value

  • src/Viper/Viper_PathComponentType.hpp - 7-variant enum

  • Regular: Field, Index, Key, Position, Unwrap
  • Irregular: Entry, Element

  • src/Viper/Viper_PathEncoder.hpp / Viper_PathDecoder.hpp - Blob serialization

  • Functions: encode(path, codec), decode(blob, codec, definitions)

  • src/Viper/Viper_PathReader.hpp / Viper_PathWriter.hpp - Stream serialization

  • Functions: read(stream, definitions), write(path, stream)

  • src/Viper/Viper_PathErrors.hpp - Exception types and helpers

  • Errors: 10+ error types (unexpectedRootPath, unsupportedType, etc.)
  • Helpers: checkIsRegular(), checkIsNotRegular() inline functions

Python Bindings: - src/P_Viper/P_Viper_Path.cpp - Main Path class binding (~300 lines) - src/P_Viper/P_Viper_PathComponent.cpp - PathComponent binding (~100 lines) - src/P_Viper/P_Viper_PathConst.cpp - Immutable path operations (~150 lines) - src/P_Viper/P_Viper_PathElementInfo.cpp - ElementInfo struct binding (~50 lines) - src/P_Viper/P_Viper_PathEntryKeyInfo.cpp - EntryKeyInfo struct binding (~50 lines)

Python Type Hints: - dsviper_wheel/__init__.pyi - Python type stubs for Path API - Type annotations for all public methods - IDE autocomplete and static analysis support

Test Files: - python/tests/unit/test_path.py (853 lines, 44 tests) - Test classes: TestPathBasicConstruction, TestPathHierarchy, TestMapSetEditing, TestPathRegularization, etc. - Coverage: All 7 PathComponentTypes, serialization, validation, safety


Document Metadata

Methodology Version: v1.3.1 Generated Date: 2025-11-14 Last Updated: 2025-11-14 Review Status: ✅ Complete Test Files Analyzed: 1 file (test_path.py) Test Coverage: 853 lines, 44 tests, 9 test classes Golden Examples: 6 scenarios extracted C++ Files: 15 files (8 headers + 7 implementations) Python Bindings: 5 files

Changelog: - v1.0 (2025-11-14): Initial documentation following /document-domain v1.3.1 - Phase 0.5 audit: 7 PathComponentTypes verified via Enumeration Matrix, 6 sub-domains identified - Phase 0.75 C++ analysis: 4 design patterns (Builder, Factory, Composite, Type-Driven Navigation) - Phase 1 golden scenarios: 6 scenarios extracted from test_path.py (Lines: 119-123, 124-134, 172-181, 396-416, 573-591, 608-627) - Phase 5 implementation: 6 sections completed (Purpose, Overview, Decomposition, Usage, Technical, References) - Emphasis: Entry/Element vs Key/Index safety distinction (23% of tests focus on this) - Emphasis: Regular vs Irregular path classification (serialization constraint)

Regeneration Trigger: - When /document-domain reaches v2.0 (methodology changes) - When Path System C++ API changes significantly (major version bump) - When test coverage patterns change (e.g., new safety patterns discovered)


Appendix: Domain Statistics

C++ Files: 15 (8 headers + 7 implementations) Python Bindings: 5 files Test Files: 1 file (test_path.py) Test Methods: 44 tests across 9 test classes Test Lines: 853 lines Sub-domains: 6 (Core Navigation, Component Types, Hierarchy, Map/Set Editing, Serialization, Validation) PathComponentTypes: 7 (5 regular + 2 irregular) Design Patterns: 4 (Builder, Factory, Composite, Type-Driven Navigation) Safety-focused tests: 10/44 (23%) - Entry/Element, Regular/Irregular validation

Dependencies: - USES: Type and Value System (100%), Stream/Codec, Blob Storage, UUId - USED BY: Applications (optional utility, 0 Viper core includes)

Key characteristics: - Utility layer: Optional for applications, not Viper runtime dependency - Type-safe navigation: 100% reliance on Type metadata for validation - Safety emphasis: Regular/Irregular distinction prevents structure mutation bugs - Fluent API: Builder pattern with method chaining for readable path construction