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:
- Form field binding: Bind UI inputs to nested data structures
-
Example:
user.settings['theme'].color→ Path validates schema, accesses value safely -
Change tracking: Record mutation locations in commit systems
-
Example: Commit stores Path to mutation → replay operation later
-
API field resolution: Implement GraphQL/REST field selectors
-
Example: Query
user { settings { theme } }→ Path validates against schema, extracts values -
Error reporting with context: Generate debugging breadcrumbs
-
Example: Validation error at
a.b[0].c→ Path provides ancestors[a, a.b, a.b[0], a.b[0].c]for full context -
Serializable navigation: Persist paths to database or send over network
- 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)andset(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:
- Field - Access Structure field by name
- Value:
ValueString(field name) - Example:
path.field("username") -
Type constraint: Only valid on TypeCode::Structure
-
Index - Access by integer position
- Value:
ValueUInt64(index) - Example:
path.index(0) -
Valid on: Vector, Tuple, Vec, Mat, Set
-
Key - Access Map value by key
- Value: Any Value type (map key type must match)
- Example:
path.key("theme")orpath.key((1, "Two"))for Tuple keys -
Valid on: TypeCode::Map
-
Position - Access XArray by UUId position
- Value:
ValueUUId(CRDT position identifier) - Example:
path.position(uuid) -
Valid on: TypeCode::XArray (CRDT semantics)
-
Unwrap - Extract value from Optional
- Value:
ValueVoid::Instance()(no data needed) - Example:
path.unwrap() - Valid on: TypeCode::Optional
Irregular Components (2) - Transient operations, NOT serializable:
- Entry - Edit Map structure (add/remove entries)
- Value: Map key value (same as Key component)
- Example:
path.entry("newKey") - Semantics: "Add or remove entry with this key"
-
Must call
regularized()to convert Entry → Key after mutation -
Element - Edit Set structure (add/remove elements)
- Value:
ValueUInt64(element index) - Example:
path.element(3) - Semantics: "Remove element at index 3"
- 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 withother -
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()whenisEntryKeyPath()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()whenisElementPath()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-123 → TestPathBasicConstruction::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-134 → TestPathBasicConstruction::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-181 → TestPathBasicConstruction::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 Tupleindex(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-416 → TestMapSetEditing::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-591 → TestPathRegularization::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-627 → TestPathValidationAndSafety::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¶
- Path construction: O(n) where n = component count
- Each builder method appends PathComponent to vector
- Allocation:
std::vector::push_backamortized O(1) -
Typical paths: 3-5 components → negligible overhead
-
Type checking: O(n) type tree walk
checkType()validates each component against Type- Switch on TypeCode per component (15+ cases)
-
Typical paths: 3-5 components → <1ms validation time
-
Value navigation: O(n) resolution
at()andset()traverse value hierarchy step-by-step- Each step: Type check + value cast + container access
-
No caching (paths are stateless)
-
Serialization: Linear in path length
- Encode/decode each component to/from binary
- Blob size: ~10-50 bytes per component (depends on Value size)
-
Example: 5-component path → ~100 bytes encoded
-
Hierarchy operations: O(n) for ancestors()
- Copies path n times (each ancestor is new Path instance)
- Allocation: n
shared_ptr<Path>objects - 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 constructionstd::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:
- Type validation errors (construction-time):
-
unsupportedType- Type doesn't support navigation (e.g., Field on Int64)- Example:
Path().field("x").checkType(Type::INT64)→ throws (Int64 has no fields)
- Example:
-
unexpectedComponentType- Component type doesn't match Type expectation- Example:
Path().index(0).checkType(structureType)→ throws (Structure expects Field, not Index)
- Example:
-
Range errors (runtime):
-
indexOutOfRange- Vector/Tuple/Vec/Mat bounds exceeded- Example:
Path().index(10).at(3-element-vector)→ throws (index 10 > size 3)
- Example:
-
Hierarchy errors:
-
unexpectedRootPath- Called parent() on root path (0 components)- Example:
Path().parent()→ throws (no parent of root)
- Example:
-
Regular/Irregular validation errors:
-
isNotRegular- Operation requires regular path, but Entry/Element present- Example:
entryKeyInfo()called on regular path → throws (only works on Entry paths)
- Example:
-
isRegular- Operation requires irregular path, but none present- Example: Attempting to serialize irregular path → throws (must regularize first)
-
Path index errors:
invalidPathIndex- fromComponent()/toComponent() index out of range- Example:
path.toComponent(99)on 5-component path → throws
- Example:
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¶
Related Documentation¶
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.hppfrom 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