Skip to content

Part III: Object-Spatial Programming (OSP)#

In this part:


Related Sections:

Introduction to OSP#

1 What is OSP?#

Object-Spatial Programming models data as graphs and computation as mobile agents (walkers) that traverse the graph. Instead of calling functions on objects, walkers visit nodes and perform operations based on location.

2 Why OSP?#

  • Natural graph modeling: Social networks, knowledge graphs, state machines
  • AI agent architecture: Walkers are natural representations of AI agents
  • Separation of concerns: Data (nodes/edges) separate from behavior (walkers)
  • Spatial context: here, visitor provide natural context

3 Core Concepts#

Concept Description Keyword
Node Graph vertex holding data node
Edge Connection between nodes edge
Walker Mobile agent that traverses walker
Root Entry point to graph root
Here Walker's current location here
Visitor Reference to visiting walker visitor

4 Complete Example#

node Person {
    has name: str;
    has age: int;
}

edge Knows {
    has since: int;
}

walker Greeter {
    can greet with `root entry {
        visit [-->];
    }

    can say_hello with Person entry {
        print(f"Hello, {here.name}!");
        visit [-->];
    }
}

with entry {
    # Build graph
    alice = Person(name="Alice", age=30);
    bob = Person(name="Bob", age=25);

    root ++> alice;
    alice +>: Knows(since=2020) :+> bob;

    # Spawn walker
    root spawn Greeter();
}

Nodes#

Nodes are the vertices of your graph -- they hold data and can have abilities that execute when walkers visit them. Think of nodes as "smart objects" that know when they're being visited and can react accordingly. Unlike regular objects, nodes can be connected via edges and participate in graph traversals.

1 Node Declaration#

node Person {
    has name: str;
    has age: int = 0;

    can greet with Visitor entry {
        print(f"Hello from {self.name}");
    }
}

# Node with no data
node Waypoint { }

2 Node Entry/Exit Abilities#

Abilities triggered when walkers enter or exit. The event clause syntax is:

can ability_name with [TypeExpression] (entry | exit) { ... }

Where TypeExpression is optional - if omitted, the ability triggers for ALL walkers.

node SecureRoom {
    has clearance_required: int;

    # Generic entry - triggers for ANY walker (no type filter)
    can on_enter with entry {
        print("Someone entered");
    }

    # Typed entry - triggers only for Inspector walkers
    can check_clearance with Inspector entry {
        if visitor.clearance < self.clearance_required {
            print("Access denied");
            disengage;
        }
    }

    # Type reference entry - using backtick for root
    can at_root with `root entry {
        print("At root node");
    }

    # Walker exiting
    can on_exit with Inspector exit {
        print("Inspector leaving");
    }

    # Multiple walker types (union)
    can process with Walker1 | Walker2 entry {
        print("Processing for Walker1 or Walker2");
    }
}

Event Clause Forms:

Form Triggers When
with entry Any walker enters (no type filter)
with TypeName entry Walker of TypeName enters
with `root entry At root node entry
with Type1 \| Type2 entry Walker of either type enters
with exit Any walker exits
with TypeName exit Walker of TypeName exits

3 Node Inheritance#

node Entity {
    has id: str;
    has created_at: str;
}

node User(Entity) {
    has username: str;
    has email: str;
}

Edges#

Edges are first-class connections between nodes. Unlike simple object references, edges can carry their own data (like relationship strength or timestamps) and have their own types. This lets you model rich relationships -- "Alice knows Bob since 2020" becomes natural to express. Use typed edges when the relationship itself has meaningful attributes.

1 Edge Declaration#

edge Friend {
    has since: int;
    has strength: float = 1.0;
}

edge Follows { }  # Edge with no data

edge Weighted {
    has weight: float;

    def get_normalized(max_weight: float) -> float {
        return self.weight / max_weight;
    }
}

2 Edge Entry/Exit#

Walkers can trigger abilities on edges during traversal:

edge Road {
    has distance: float;

    can on_traverse with Traveler entry {
        visitor.total_distance += self.distance;
    }
}

3 Directed vs Undirected#

Edge direction is determined by connection operators:

node Item {}

with entry {
    a = Item();
    b = Item();

    a ++> b;          # Directed: a → b
    a <++> b;         # Undirected: a ↔ b (creates edges both ways)
}

Walkers#

Walkers are mobile agents that traverse the graph, executing abilities at each node they visit. Unlike functions that you call, walkers go to data. They maintain state throughout their journey, making them ideal for tasks like collecting information across a graph, implementing AI agents that navigate knowledge structures, or processing pipelines where context accumulates. Spawn a walker with root spawn MyWalker() to begin traversal.

1 Walker Declaration#

walker Collector {
    has items: list = [];
    has max_items: int = 10;

    can start with `root entry {
        print("Starting collection");
        visit [-->];
    }

    can collect with DataNode entry {
        if len(self.items) < self.max_items {
            self.items.append(here.value);
        }
        visit [-->];
    }
}

2 Walker State#

Walkers maintain state throughout their traversal:

node DataNode {
    has value: int;
}

walker Counter {
    has count: int = 0;

    can start with `root entry {
        self.count += 1;
        visit [-->];
    }

    can count_nodes with DataNode entry {
        self.count += 1;
        visit [-->];
    }
}

with entry {
    root ++> DataNode(value=1) ++> DataNode(value=2);
    walker_instance = Counter();
    root spawn walker_instance;
    print(f"Counted {walker_instance.count} nodes");  # Output: 3
}

Note: Walker abilities must specify which node types they handle. Use `root for the root node and specific node types for others. A generic with entry only triggers at the spawn location.

3 The visit Statement#

The visit statement tells the walker where to go next. It doesn't immediately move -- it queues nodes for the next step of traversal. This queue-based approach lets you control breadth-first vs depth-first traversal and handle cases where there's nowhere to go (using the else clause).

Basic Syntax:

node Item {}

walker Visitor {
    can go with Item entry {
        visit [-->];                    # Visit all outgoing nodes
        visit [<--];                    # Visit all incoming nodes
        visit [<-->];                   # Visit both directions
    }
}

With Type Filters:

node Person {}
edge Friend { has since: int = 2020; }

walker Visitor {
    can filter with Person entry {
        visit [-->(`?Person)];          # Visit Person nodes only
        visit [->:Friend:->];           # Visit via Friend edges only
        visit [->:Friend:since>2020:->]; # Via Friend edges with condition
    }
}

With Else Clause:

node Item {}

walker Visitor {
    can traverse with Item entry {
        visit [-->] else {              # Fallback if no nodes to visit
            print("No outgoing edges");
        }
    }
}

Direct Node Visit:

node Item {}

walker Visitor {
    has target: Item | None = None;

    can direct with Item entry {
        visit here;                     # Visit current node
        visit self.target;              # Visit node stored in walker field
    }
}

Indexed Visit:

node Item {}

walker Visitor {
    can indexed with Item entry {
        visit : 0 : [-->];              # Visit first outgoing node only
        visit : -1 : [-->];             # Visit last outgoing node only
        visit : 2 : [-->];              # Visit third node (0-indexed)
    }
}

Out-of-bounds indices result in no visit.

4 The report Statement#

Send data back without stopping:

node DataNode {
    has value: int = 0;
}

walker DataCollector {
    can collect with DataNode entry {
        report here.value;  # Continues execution
        visit [-->];
    }
}

with entry {
    root ++> DataNode(value=1);
    result = root spawn DataCollector();
    all_values = result.reports;  # List of reported values
}

5 The disengage Statement#

The disengage statement immediately terminates a walker's traversal. Use it when the walker has found what it was looking for (like a search hitting its target) or when a condition means further traversal would be pointless. It's the walker equivalent of return from a recursive function.

walker Searcher {
    has target: str;

    can search with Person entry {
        if here.name == self.target {
            report here;
            disengage;  # Stop traversal
        }
        visit [-->];
    }
}

6 Spawning Walkers#

node Item { has value: int = 0; }

walker MyWalker {
    has param: int = 0;

    can visit with `root entry {
        visit [-->];
    }
    can collect with Item entry {
        report here.value;
    }
}

with entry {
    node1 = Item(value=1);
    node2 = Item(value=2);
    node3 = Item(value=3);
    root ++> node1 ++> node2 ++> node3;

    # Basic spawn
    result = root spawn MyWalker();

    # Spawn with parameters
    result = root spawn MyWalker(param=10);

    # Access results
    print(result.returns);  # Return value
    print(result.reports);  # All reported values
}

7 Walker Inheritance#

walker BaseVisitor {
    can log with entry {
        print(f"Visiting: {here}");
    }
}

walker DetailedVisitor(BaseVisitor) {
    override can log with entry {
        print(f"Detailed visit to: {type(here).__name__}");
    }
}

8 Special References#

These keywords have special meaning in specific contexts:

Reference Valid Context Description See Also
self Any method/ability Current instance (walker, node, object) Part II: Functions
here Walker ability Current node the walker is visiting Walkers
visitor Node ability The walker that triggered this ability Nodes
root Anywhere Root node of the current graph Graph Construction
super Subclass method Parent class reference Part II
init Object body Constructor method name Part II
postinit Object body Post-constructor hook Part I
props JSX context Component props reference Part IV: Full-Stack

Usage examples:

node SecureRoom {
    has required_level: int;

    # 'visitor' refers to the walker visiting this node
    # 'self' refers to this node instance
    can check with Inspector entry {
        if visitor.clearance >= self.required_level {
            print("Access granted to " + visitor.name);
        }
    }
}

walker Inspector {
    has clearance: int;
    has name: str;

    # 'here' refers to the current node being visited
    # 'self' refers to this walker instance
    can inspect with SecureRoom entry {
        print(f"{self.name} inspecting room at {here}");
        print(f"Room requires level {here.required_level}");
    }

    can start with `root entry {
        # 'root' is always the graph root
        print(f"Starting from root: {root}");
        visit [-->];
    }
}

When each reference is valid:

Context self here visitor root
Walker ability Walker instance Current node N/A Graph root
Node ability Node instance N/A Visiting walker Graph root
Object method Object instance N/A N/A Graph root
Free code N/A N/A N/A Graph root

Graph Construction#

1 Creating Nodes#

node Person {
    has name: str;
    has age: int;
}

with entry {
    # Create and assign
    alice = Person(name="Alice", age=30);
    bob = Person(name="Bob", age=25);

    # Inline creation in connection
    root ++> Person(name="Charlie", age=35);
}

2 Creating Edges#

node Person { has name: str; }
edge Friend { has since: int = 2020; }
edge Colleague { has department: str = ""; }

with entry {
    alice = Person(name="Alice");
    bob = Person(name="Bob");

    # Untyped (generic edge)
    alice ++> bob;

    # Typed edge
    alice +>: Friend(since=2020) :+> bob;

    # Bidirectional typed
    alice <+: Colleague(department="Engineering") :+> bob;
}

3 Chained Construction#

node Item {}
edge Start {}
edge Next {}
edge End {}

with entry {
    a = Item();
    b = Item();
    c = Item();
    d = Item();

    # Build chains in one expression
    root ++> a ++> b ++> c ++> d;

    # With typed edges
    root +>: Start :+> a +>: Next :+> b +>: Next :+> c +>: End :+> d;
}

4 Deleting Nodes and Edges#

node Person { has name: str; }
edge Friend {}

with entry {
    alice = Person(name="Alice");
    bob = Person(name="Bob");
    alice +>: Friend :+> bob;

    # Delete specific edge
    alice del --> bob;

    # Delete node
    del bob;
}

5 Built-in Graph Functions#

Function Description
jid(node) Get unique Jac ID of object
jobj(node) Get Jac object wrapper
grant(node, user) Grant access permission
revoke(node, user) Revoke access permission
allroots() Get all root references
save(node) Persist node to storage
commit() Commit pending changes
printgraph(root) Print graph for debugging
node Person { has name: str; }

with entry {
    alice = Person(name="Alice");
    bob = Person(name="Bob");
    secret_node = Person(name="Secret");

    id = jid(alice);
    save(alice);
    printgraph(root);
}

Graph Traversal#

1 Basic Traversal#

Walker traversal is queue-based (BFS-like by default):

walker BFSWalker {
    can start with `root entry {
        print(f"Starting at: {here}");
        visit [-->];
    }

    can traverse with Person entry {
        print(f"Visiting: {here.name}");
        visit [-->];  # Queue all outgoing for later visits
    }
}

2 Filtered Traversal#

node Person { has age: int = 0; }
edge Friend { has since: int = 2020; }

walker FilteredWalker {
    can start with `root entry {
        visit [-->];  # Start traversal from root
    }

    can traverse with Person entry {
        # By node type
        visit [-->(`?Person)];

        # By edge type
        visit [->:Friend:->];

        # Combined: Friend edges to Person nodes since 2020
        visit [->:Friend:since > 2020:->(`?Person)];
    }
}

3 Entry and Exit Events#

node Room {
    can on_enter with Visitor entry {
        print("Entering room");
    }

    can on_exit with Visitor exit {
        print("Exiting room");
    }
}

Data Spatial Queries#

1 Edge Reference Syntax#

node Person {}
edge EdgeType {}
edge Edge { has attr: int = 0; has a: int = 0; has b: int = 0; }
edge Friend {}

walker Traverser {
    can query with Person entry {
        # Basic forms
        outgoing = [-->];                     # All outgoing nodes
        incoming = [<--];                     # All incoming nodes
        both = [<-->];                        # Both directions

        # Typed forms
        via_type = [->:EdgeType:->];          # Outgoing via EdgeType

        # With conditions
        filtered = [->:Edge:attr > 0:->];     # Filter by edge attribute

        # Node type filter
        people = [-->(`?Person)];             # Filter result nodes by type

        # Get edges vs nodes
        edges = [edge -->];                   # Get edge objects
        friends = [edge ->:Friend:->];        # Typed edge objects
    }
}

Use [edge -->] when you need to access edge attributes or visit edges directly.

2 Attribute Filtering#

node User {
    has age: int = 0;
    has status: str = "";
    has verified: bool = False;
}
edge Friend { has since: int = 2020; }
edge Link { has weight: float = 0.0; }

walker Filter {
    can query with User entry {
        # Filter by node attributes (after traversal)
        adults = [-->](?age >= 18);
        active = [-->](?status == "active");

        # Filter by edge attributes (during traversal)
        recent_friends = [->:Friend:since > 2020:->];
        strong_connections = [->:Link:weight > 0.8:->];
    }
}

3 Complex Queries#

node Person { has age: int = 0; }
edge Friend { has since: int = 2020; }
edge Colleague {}

walker Querier {
    can complex with Person entry {
        # Chained traversal (multi-hop)
        friends_of_friends = [here ->:Friend:-> ->:Friend:->];

        # Mixed edge types
        path = [here ->:Friend:-> ->:Colleague:->];

        # Combined with filters
        target = [->:Friend:since < 2020:->(`?Person)](?age > 30);
    }
}

Typed Context Blocks#

1 What are Typed Context Blocks?#

Handle different types with specialized code paths. The syntax uses ->Type{code} with no space between the arrow and type name:

walker AnimalVisitor {
    can visit with Animal entry {
        # Typed context block for Dog (subtype of Animal)
        ->Dog{print(f"{here.name} is a {here.breed} dog");}

        # Typed context block for Cat (subtype of Animal)
        ->Cat{print(f"{here.name} says meow");}

        # Default case (any other Animal type)
        ->_{print(f"{here.name} is some animal");}
    }
}

Syntax Notes:

  • No space between -> and the type name: ->Dog{ not -> Dog {
  • Opening brace immediately follows the type
  • Code typically on same line with closing brace
  • Use ->_ for default/catch-all case

2 Tuple-Based Dispatch#

walker Processor {
    can process with (Node1, Node2) entry {
        # Handle when visiting involves both types
    }
}

3 Context Blocks in Nodes#

Nodes reacting to different walker types:

node DataNode {
    has value: int;

    can handle with Walker entry {
        ->Reader{print(f"Read value: {self.value}");}

        ->Writer{
            self.value = visitor.new_value;
            print(f"Updated to: {self.value}");
        }
    }
}

4 Complex Typed Context Example#

From the reference examples, showing inheritance-based dispatch:

walker ShoppingCart {
    can process_item with Product entry {
        print(f"Processing {type(here).__name__}...");

        # Each subtype gets its own block
        ->Book{print(f"  -> Book: '{here.title}' by {here.author}");}
        ->Magazine{print(f"  -> Magazine: '{here.title}' Issue #{here.issue}");}
        ->Electronics{print(f"  -> Electronics: {here.name}, warranty {here.warranty_years}yr");}

        self.total += here.price;
        visit [-->];
    }
}

See Also#