Skip to content

Part III: OSP

Part III: Object-Spatial Programming (OSP)

Section titled “Part III: Object-Spatial Programming (OSP)”

In this part:


Related Sections:

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.

  • 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
ConceptDescriptionKeyword
NodeGraph vertex holding datanode
EdgeConnection between nodesedge
WalkerMobile agent that traverseswalker
RootEntry point to graphroot
HereWalker’s current locationhere
VisitorReference to visiting walkervisitor
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 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.

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 { }

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 Root 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:

FormTriggers When
with entryAny walker enters (no type filter)
with TypeName entryWalker of TypeName enters
with Root entryAt root node entry
with Type1 | Type2 entryWalker of either type enters
with exitAny walker exits
with TypeName exitWalker of TypeName exits
node Entity {
has id: str;
has created_at: str;
}
node User(Entity) {
has username: str;
has email: str;
}

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.

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;
}
}

Walkers can trigger abilities on edges during traversal:

edge Road {
has distance: float;
can on_traverse with Traveler entry {
visitor.total_distance += self.distance;
}
}

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 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.

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 [-->];
}
}

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.

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.

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
}

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 [-->];
}
}
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
}
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__}");
}
}

These keywords have special meaning in specific contexts:

ReferenceValid ContextDescriptionSee Also
selfAny method/abilityCurrent instance (walker, node, object)Part II: Functions
hereWalker abilityCurrent node the walker is visitingWalkers
visitorNode abilityThe walker that triggered this abilityNodes
rootAnywhereRoot node of the current graphGraph Construction
superSubclass methodParent class referencePart II
initObject bodyConstructor method namePart II
postinitObject bodyPost-constructor hookPart I
propsJSX contextComponent props referencePart 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:

Contextselfherevisitorroot
Walker abilityWalker instanceCurrent nodeN/AGraph root
Node abilityNode instanceN/AVisiting walkerGraph root
Object methodObject instanceN/AN/AGraph root
Free codeN/AN/AN/AGraph root

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);
}
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;
}
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;
}
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;
}
FunctionDescription
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);
}

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
}
}
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);
}
}
node Room {
can on_enter with Visitor entry {
print("Entering room");
}
can on_exit with Visitor exit {
print("Exiting room");
}
}

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.

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:->];
}
}
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);
}
}

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
walker Processor {
can process with (Node1, Node2) entry {
# Handle when visiting involves both types
}
}

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}");
}
}
}

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 [-->];
}
}