Skip to content

Typed context blocks (OSP)#

Code Example

Runnable Example in Jac and JacLib

"""Typed context blocks: Conditional code execution based on node/walker types."""

# Inheritance chain for products
node Product {
    has price: float;
}

node Media(Product) {
    has title: str;
}

node Book(Media) {
    has author: str;
}

node Magazine(Media) {
    has issue: int;
}

node Electronics(Product) {
    has name: str;
    has warranty_years: int;
}

# Inheritance chain for customers
walker Customer {
    has name: str;
    has total_spent: float = 0.0;
}

walker RegularCustomer(Customer) {
    has loyalty_points: int = 0;
}

walker VIPCustomer(Customer) {
    has vip_discount: float = 0.15;
}

# Walker that processes different item types
walker ShoppingCart {
    has items_count: int = 0;
    has total: float = 0.0;

    can start with `root entry {
        # Create diverse inventory
        root ++> Book(title="Jac Programming", author="John Doe", price=29.99);
        root ++> Magazine(title="Tech Today", issue=42, price=5.99);
        root ++> Electronics(name="Laptop", price=999.99, warranty_years=2);

        visit [-->];
    }

    # Example 1: Base type in signature + typed context blocks for derived types
    # Ability accepts Product (base type), so ALL subtypes trigger this ability
    can process_item with Product entry {
        print(f"[Inheritance] Processing {type(here).__name__}...");

        # Typed context block for derived type Book
        -> Book {
            print(f"  -> Book block: '{here.title}' by {here.author}");
        }

        # Typed context block for derived type Magazine
        -> Magazine {
            print(f"  -> Magazine block: '{here.title}' Issue #{here.issue}");
        }

        # Typed context block for derived type Electronics
        -> Electronics {
            print(f"  -> Electronics block: {here.name}, {here.warranty_years}yr warranty");
        }

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

    # Example 2: Multiple specific types in tuple (not using inheritance)
    # Ability ONLY triggers for the explicitly listed types
    can apply_discount with (Book, Magazine) entry {
        print(f"[Tuple] Applying media discount to {type(here).__name__}");

        # Typed context block differentiates between the tuple members
        -> Book {
            discount = here.price * 0.10;
            print(f"  -> Book: 10% off = ${discount}");
        }

        -> Magazine {
            discount = here.price * 0.20;
            print(f"  -> Magazine: 20% off = ${discount}");
        }

        visit [-->];
    }
}

# Node that responds differently to different walker types
node Checkout {
    has transactions: int = 0;

    # Ability accepts base type Customer (all subtypes trigger this)
    # Typed context blocks select specific types in the inheritance chain
    can process_payment with Customer entry {
        self.transactions += 1;

        # Specific type: RegularCustomer (subtype of Customer)
        -> RegularCustomer {
            print(f"\nRegular customer: {visitor.name}");
            print(f"Total: ${visitor.total_spent}, Points: {visitor.loyalty_points}");
        }

        # Specific type: VIPCustomer (subtype of Customer)
        -> VIPCustomer {
            discount = visitor.total_spent * visitor.vip_discount;
            final_total = visitor.total_spent - discount;
            print(f"\nVIP customer: {visitor.name}");
            print(f"Subtotal: ${visitor.total_spent}, Discount: -${discount}, Final: ${final_total}");
        }

        # Base type: Customer (matches all Customers including subtypes)
        -> Customer {
            print(f"Processing payment for {visitor.name}");
        }
    }
}

with entry {
    # Demo 1: ShoppingCart walker uses typed context blocks with inheritance
    print("=== Demo 1: Product Inheritance Chain ===");
    print("Hierarchy: Product -> Media -> Book/Magazine");
    print("            Product -> Electronics\n");
    cart = ShoppingCart();
    root spawn cart;
    print(f"\nCart: {cart.items_count} items, Total: ${cart.total}\n");

    # Demo 2: Checkout node uses typed context blocks with customer inheritance
    print("=== Demo 2: Customer Inheritance Chain ===");
    print("Hierarchy: Customer -> RegularCustomer/VIPCustomer\n");

    checkout = root ++> Checkout();

    # Base type Customer
    base_customer = Customer(name="Charlie", total_spent=50.00);
    checkout spawn base_customer;

    # Subtype RegularCustomer
    regular = RegularCustomer(name="Alice", total_spent=89.97, loyalty_points=150);
    checkout spawn regular;

    # Subtype VIPCustomer
    vip = VIPCustomer(name="Bob", total_spent=89.97, vip_discount=0.15);
    checkout spawn vip;
}
"""Typed context blocks: Conditional code execution based on node/walker types."""

# Inheritance chain for products
node Product {
    has price: float;
}

node Media(Product) {
    has title: str;
}

node Book(Media) {
    has author: str;
}

node Magazine(Media) {
    has issue: int;
}

node Electronics(Product) {
    has name: str;
    has warranty_years: int;
}

# Inheritance chain for customers
walker Customer {
    has name: str;
    has total_spent: float = 0.0;
}

walker RegularCustomer(Customer) {
    has loyalty_points: int = 0;
}

walker VIPCustomer(Customer) {
    has vip_discount: float = 0.15;
}

# Walker that processes different item types
walker ShoppingCart {
    has items_count: int = 0;
    has total: float = 0.0;

    can start with `root entry {
        # Create diverse inventory
        root ++> Book(title="Jac Programming", author="John Doe", price=29.99);
        root ++> Magazine(title="Tech Today", issue=42, price=5.99);
        root ++> Electronics(name="Laptop", price=999.99, warranty_years=2);

        visit [-->];
    }

    # Example 1: Base type in signature + typed context blocks for derived types
    # Ability accepts Product (base type), so ALL subtypes trigger this ability
    can process_item with Product entry {
        print(f"[Inheritance] Processing {type(here).__name__}...");

        # Typed context block for derived type Book
        -> Book {
            print(f"  -> Book block: '{here.title}' by {here.author}");
        }

        # Typed context block for derived type Magazine
        -> Magazine {
            print(f"  -> Magazine block: '{here.title}' Issue #{here.issue}");
        }

        # Typed context block for derived type Electronics
        -> Electronics {
            print(f"  -> Electronics block: {here.name}, {here.warranty_years}yr warranty");
        }

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

    # Example 2: Multiple specific types in tuple (not using inheritance)
    # Ability ONLY triggers for the explicitly listed types
    can apply_discount with (Book, Magazine) entry {
        print(f"[Tuple] Applying media discount to {type(here).__name__}");

        # Typed context block differentiates between the tuple members
        -> Book {
            discount = here.price * 0.10;
            print(f"  -> Book: 10% off = ${discount}");
        }

        -> Magazine {
            discount = here.price * 0.20;
            print(f"  -> Magazine: 20% off = ${discount}");
        }

        visit [-->];
    }
}

# Node that responds differently to different walker types
node Checkout {
    has transactions: int = 0;

    # Ability accepts base type Customer (all subtypes trigger this)
    # Typed context blocks select specific types in the inheritance chain
    can process_payment with Customer entry {
        self.transactions += 1;

        # Specific type: RegularCustomer (subtype of Customer)
        -> RegularCustomer {
            print(f"\nRegular customer: {visitor.name}");
            print(f"Total: ${visitor.total_spent}, Points: {visitor.loyalty_points}");
        }

        # Specific type: VIPCustomer (subtype of Customer)
        -> VIPCustomer {
            discount = visitor.total_spent * visitor.vip_discount;
            final_total = visitor.total_spent - discount;
            print(f"\nVIP customer: {visitor.name}");
            print(f"Subtotal: ${visitor.total_spent}, Discount: -${discount}, Final: ${final_total}");
        }

        # Base type: Customer (matches all Customers including subtypes)
        -> Customer {
            print(f"Processing payment for {visitor.name}");
        }
    }
}

with entry {
    # Demo 1: ShoppingCart walker uses typed context blocks with inheritance
    print("=== Demo 1: Product Inheritance Chain ===");
    print("Hierarchy: Product -> Media -> Book/Magazine");
    print("            Product -> Electronics\n");
    cart = ShoppingCart();
    root spawn cart;
    print(f"\nCart: {cart.items_count} items, Total: ${cart.total}\n");

    # Demo 2: Checkout node uses typed context blocks with customer inheritance
    print("=== Demo 2: Customer Inheritance Chain ===");
    print("Hierarchy: Customer -> RegularCustomer/VIPCustomer\n");

    checkout = root ++> Checkout();

    # Base type Customer
    base_customer = Customer(name="Charlie", total_spent=50.00);
    checkout spawn base_customer;

    # Subtype RegularCustomer
    regular = RegularCustomer(name="Alice", total_spent=89.97, loyalty_points=150);
    checkout spawn regular;

    # Subtype VIPCustomer
    vip = VIPCustomer(name="Bob", total_spent=89.97, vip_discount=0.15);
    checkout spawn vip;
}
"""Typed context blocks: Conditional code execution based on node/walker types."""
from __future__ import annotations
from jaclang.runtimelib.builtin import *
from jaclang import JacMachineInterface as _jl

class Product(_jl.Node):
    price: float

class Media(Product, _jl.Node):
    title: str

class Book(Media, _jl.Node):
    author: str

class Magazine(Media, _jl.Node):
    issue: int

class Electronics(Product, _jl.Node):
    name: str
    warranty_years: int

class Customer(_jl.Walker):
    name: str
    total_spent: float = 0.0

class RegularCustomer(Customer, _jl.Walker):
    loyalty_points: int = 0

class VIPCustomer(Customer, _jl.Walker):
    vip_discount: float = 0.15

class ShoppingCart(_jl.Walker):
    items_count: int = 0
    total: float = 0.0

    @_jl.entry
    def start(self, here: _jl.Root) -> None:
        _jl.connect(left=_jl.root(), right=Book(title='Jac Programming', author='John Doe', price=29.99))
        _jl.connect(left=_jl.root(), right=Magazine(title='Tech Today', issue=42, price=5.99))
        _jl.connect(left=_jl.root(), right=Electronics(name='Laptop', price=999.99, warranty_years=2))
        _jl.visit(self, _jl.refs(_jl.Path(here)._out().visit()))

    @_jl.entry
    def process_item(self, here: Product) -> None:
        print(f'[Inheritance] Processing {type(here).__name__}...')
        if isinstance(here, Book):
            print(f"  -> Book block: '{here.title}' by {here.author}")
        if isinstance(here, Magazine):
            print(f"  -> Magazine block: '{here.title}' Issue #{here.issue}")
        if isinstance(here, Electronics):
            print(f'  -> Electronics block: {here.name}, {here.warranty_years}yr warranty')
        self.items_count += 1
        self.total += here.price
        _jl.visit(self, _jl.refs(_jl.Path(here)._out().visit()))

    @_jl.entry
    def apply_discount(self, here: (Book, Magazine)) -> None:
        print(f'[Tuple] Applying media discount to {type(here).__name__}')
        if isinstance(here, Book):
            discount = here.price * 0.1
            print(f'  -> Book: 10% off = ${discount}')
        if isinstance(here, Magazine):
            discount = here.price * 0.2
            print(f'  -> Magazine: 20% off = ${discount}')
        _jl.visit(self, _jl.refs(_jl.Path(here)._out().visit()))

class Checkout(_jl.Node):
    transactions: int = 0

    @_jl.entry
    def process_payment(self, visitor: Customer) -> None:
        self.transactions += 1
        if isinstance(visitor, RegularCustomer):
            print(f'\\nRegular customer: {visitor.name}')
            print(f'Total: ${visitor.total_spent}, Points: {visitor.loyalty_points}')
        if isinstance(visitor, VIPCustomer):
            discount = visitor.total_spent * visitor.vip_discount
            final_total = visitor.total_spent - discount
            print(f'\\nVIP customer: {visitor.name}')
            print(f'Subtotal: ${visitor.total_spent}, Discount: -${discount}, Final: ${final_total}')
        if isinstance(visitor, Customer):
            print(f'Processing payment for {visitor.name}')
print('=== Demo 1: Product Inheritance Chain ===')
print('Hierarchy: Product -> Media -> Book/Magazine')
print('            Product -> Electronics\n')
cart = ShoppingCart()
_jl.spawn(_jl.root(), cart)
print(f'\\nCart: {cart.items_count} items, Total: ${cart.total}\\n')
print('=== Demo 2: Customer Inheritance Chain ===')
print('Hierarchy: Customer -> RegularCustomer/VIPCustomer\n')
checkout = _jl.connect(left=_jl.root(), right=Checkout())
base_customer = Customer(name='Charlie', total_spent=50.0)
_jl.spawn(checkout, base_customer)
regular = RegularCustomer(name='Alice', total_spent=89.97, loyalty_points=150)
_jl.spawn(checkout, regular)
vip = VIPCustomer(name='Bob', total_spent=89.97, vip_discount=0.15)
_jl.spawn(checkout, vip)
Jac Grammar Snippet
typed_ctx_block: RETURN_HINT expression code_block

Description

Typed Context Blocks (OSP)

Typed context blocks are a powerful feature in Jac's Object-Spatial Programming (OSP) model that allow conditional code execution based on the runtime type of nodes or walkers. They enable polymorphic behavior where a single ability can handle multiple types differently.

Syntax

The syntax uses the -> operator followed by a type name and a code block:

-> TypeName { #* code *# }

The code inside executes only if the runtime type matches.

Two Use Cases

Typed context blocks work in two contexts:

  1. In Walker Abilities (lines 55-95): Check the type of here (the current node)
  2. In Node Abilities (lines 96-117): Check the type of visitor (the current walker)

Two Approaches: Inheritance vs. Tuple

Lines 4-23 define an inheritance hierarchy:

Product (base)
├── Media (intermediate)
│   ├── Book (leaf)
│   └── Magazine (leaf)
└── Electronics (leaf)

Approach 1: Base Type with Inheritance (Lines 55-76)

Use a base type in the ability signature to accept ALL subtypes. See line 55: the ability signature is with Product entry, which means it triggers for Product AND all its subtypes (Book, Magazine, Electronics).

Lines 59-71 show typed context blocks that differentiate between specific subtypes: - Line 59: -> Book executes ONLY if here is a Book - Line 64: -> Magazine executes ONLY if here is a Magazine - Line 69: -> Electronics executes ONLY if here is Electronics

Approach 2: Tuple of Specific Types (Lines 80-95)

Use parentheses to list only the specific types that should trigger the ability. See line 80: the ability signature is with (Book, Magazine) entry, which means it triggers ONLY for Book and Magazine (NOT Electronics).

Lines 84-92 show typed context blocks differentiating the tuple members: - Line 84: -> Book provides Book-specific discount logic - Line 89: -> Magazine provides Magazine-specific discount logic

Key Difference: - with Product entry: Electronics WILL trigger (inherits from Product) - with (Book, Magazine) entry: Electronics will NOT trigger (not in tuple)

Node Abilities with Inheritance Chains

Lines 25-37 define a customer inheritance hierarchy:

Customer (base)
├── RegularCustomer (subtype)
└── VIPCustomer (subtype)

Lines 96-117 show the same pattern for visitor types. The ability signature at line 98 is with Customer entry, accepting the base type Customer, so ALL subtypes trigger this ability.

Lines 101-115 show typed context blocks for specific customer types: - Lines 101-103: -> RegularCustomer handles regular customers - Lines 106-111: -> VIPCustomer handles VIP customers with discount calculation - Lines 114-116: -> Customer handles all Customers (including subtypes)

The Key Difference: - In walker abilities: Typed context blocks check here (the node type) - In node abilities: Typed context blocks check visitor (the walker type) - Both use the same inheritance matching behavior

Execution Flow

When the walker visits a node:

graph TD
    A[Walker visits Node] --> B{Ability matches?}
    B -->|Yes| C[Execute ability]
    B -->|No| D[Skip]
    C --> E{Type context matches?}
    E -->|Yes| F[Execute typed block]
    E -->|No| G[Skip block]
    F --> H[Continue]
    G --> H

Demo 1: Shopping Cart (Lines 44-96)

The ShoppingCart walker demonstrates BOTH approaches:

  1. Lines 44-51: Creates inventory (Book, Magazine, Electronics - all inherit from Product)

  2. Lines 55-76: process_item ability - Inheritance approach

  3. Signature: with Product entry
  4. Triggers for ALL Product subtypes (Book, Magazine, Electronics)
  5. Typed context blocks differentiate: -> Book, -> Magazine, -> Electronics

  6. Lines 80-95: apply_discount ability - Tuple approach

  7. Signature: with (Book, Magazine) entry
  8. Triggers ONLY for Book and Magazine (NOT Electronics)
  9. Typed context blocks differentiate: -> Book, -> Magazine

Output shows: - [Inheritance] appears for all 3 products (base type accepts all) - [Tuple] appears only for Book and Magazine (explicit types only)

Demo 2: Checkout (Lines 98-117)

The Checkout node demonstrates typed context blocks for handling different customer types:

  1. Lines 98-117: Single ability with three typed context blocks
  2. Lines 101-103: -> RegularCustomer block handles regular customers
  3. Lines 106-111: -> VIPCustomer block applies VIP discount
  4. Lines 114-116: -> Customer block handles base customer type

When different walkers visit the checkout node: - Customer walker → triggers Customer block - RegularCustomer walker → triggers RegularCustomer AND Customer blocks - VIPCustomer walker → triggers VIPCustomer AND Customer blocks

Why Use Typed Context Blocks?

1. Polymorphic Handling

Handle multiple types in a single ability without explicit if-statements. Compare line 175-183 (without typed context blocks) versus line 186-189 (with typed context blocks) - the latter is cleaner and more declarative.

2. Type Safety

The compiler knows the exact type inside the block, providing better type checking and autocomplete.

3. Extensibility

Add new typed context blocks without modifying existing code. See lines 199-204 for an example of adding a new Furniture type.

4. Separation of Concerns

Each type's logic is isolated in its own block, improving readability.

Relationship to Abilities

Typed context blocks complement the ability system:

Feature Purpose Example
Ability signature Declares when ability triggers with Book entry
Typed context block Provides type-specific logic -> Book { }

You can combine them as shown in lines 55-76 (general ability with typed blocks) or use more specific ability signatures as shown in line 80.

Multiple Typed Context Blocks

You can have multiple typed context blocks in a single ability (see lines 98-117 for example). Code outside the blocks runs for all types, while each typed context block runs only when its type matches.

Inheritance and Type Hierarchy

Typed context blocks work seamlessly with inheritance. Lines 4-23 define the Product hierarchy.

Two Ways to Handle Subtypes:

1. Base Type in Ability Signature

Line 55 shows with Product entry - this triggers for Product AND all subtypes (Book, Magazine, Electronics).

2. Tuple of Specific Types

Line 80 shows with (Book, Magazine) entry - this triggers ONLY for the listed types.

Typed Context Block Matching:

A typed context block -> Product matches Product and all its subtypes (Book, Electronics, etc.), while -> Book only matches Book specifically. This is why in the output you see multiple blocks executing for a single node visit.

Best Practice: Check most specific types first if you have multiple blocks, as shown in lines 59-71 where specific types (Book, Magazine, Electronics) are checked before any generic Product handling.

When to Use Typed Context Blocks

Use when: - A single ability needs to handle multiple types differently - You want type-specific logic within a general ability - Adding new types should be easy (extensibility) - You want cleaner code than nested if-statements

Don't use when: - Only handling one type (just use normal ability signature) - Logic is the same for all types (no need for type checking) - Types are unrelated and should have completely separate abilities

Key Insights

  1. Dual context: Check here in walker abilities, visitor in node abilities
  2. Runtime dispatch: Type checking happens at runtime based on actual instance types
  3. Optional execution: Blocks only execute when types match, silently skip otherwise
  4. Complements abilities: Work together with ability signatures for powerful dispatch
  5. Type-safe: Compiler understands the type inside the block
  6. Inheritance aware: Blocks match base types and all derived types

Comparison to Traditional OOP

In traditional OOP, you might use: - Method overloading (compile-time): Different methods for different parameter types - Visitor pattern (runtime): Double dispatch for type-specific behavior - Type checking (runtime): isinstance() checks with if-statements

Typed context blocks provide: - Spatial dispatch: Combines where (node/walker) with what (type) - Declarative syntax: Clear, readable type-specific blocks - Bidirectional: Both nodes and walkers can type-check each other

This is unique to Jac's Object-Spatial Programming model, where computation (walkers) moves through data (nodes/edges), and both can respond based on each other's types.