Skip to main content

Creating and Working with Nodes in LionWeb

LionWeb provides a flexible and language-agnostic model for working with models (or trees, or ASTs: let's consider these as synonyms in this context).

The main component is the Node.

When working with LionWeb nodes in Python, there are two complementary approaches depending on your needs:

  1. Homogeneous nodes, using generic, universal APIs which work with all form of nodes. When choosing this approach, we may want to consider DynamicNode.
  2. Heterogeneous nodes, using language-specific, statically-typed Python classes, defined for a certain LionWeb language and just that one.

The Core Abstraction: Node​

At the heart of LionWeb is the Node interface. Implementing it guarantees:

  • Serialization and deserialization
  • Compatibility with the LionWeb Repository
  • Introspection through classifiers and features
  • Tool support (e.g., editors, model processors)

By relying on this interface, LionWeb tooling can process, manipulate, and analyze any conforming node in a uniform manner.

Option 1: Homogeneous Nodes​

This approach is ideal for generic tools and runtime interoperability. The key class here is DynamicNode.

When to Use​

  • You receive nodes from external systems or clients
  • You want to handle unknown or dynamic languages
  • You’re building generic tools (e.g., validators, browsers)

How it Works​

DynamicNode implements Node and stores features dynamically. You can query and manipulate the node’s structure by name.

Example​

from lionweb.language import Language, Concept, Property, Containment
from lionweb.model import DynamicNode
from lionweb.language import LionCoreBuiltins
from lionweb.utils.node_tree_validator import NodeTreeValidator

# === Define the Language ===

# Define the 'TaskList' concept
task_list_concept = Concept(
name="TaskList", key="TaskList", id="TaskList-id", abstract=False, partition=True
)

# Define the 'Task' concept
task_concept = Concept(
name="Task", key="Task", id="Task-id", abstract=False, partition=False
)

# Add a 'tasks' containment
tasks_containment = Containment(
name="tasks",
key="TasksList-tasks",
id="TasksList-tasks-id",
type=task_concept,
multiple=True,
optional=False,
)
task_list_concept.add_feature(tasks_containment)

# Add a 'name' property
name_property = Property(
name="name", key="task-name", id="task-name-id", type=LionCoreBuiltins.get_string()
)
task_concept.add_feature(name_property)

# Define the language container
task_language = Language(
name="Task Language",
key="task",
id="task-id",
version="1.0"
)
task_language.add_element(task_list_concept)
task_language.add_element(task_concept)

# === Use DynamicNode to Build a Model ===

# Create root node (a TaskList)
errands = DynamicNode(id="errands", concept=task_list_concept)

# Create first Task node
task1 = DynamicNode(id="task1-id", concept=task_concept)
task1.set_property_value(property=name_property, value="My Task #1")
errands.add_child(tasks_containment, task1)

# Create second Task node
task2 = DynamicNode(id="task2-id", concept=task_concept)
task2.set_property_value(name_property, "My Task #2")
errands.add_child(tasks_containment, task2)

# Validate the model tree
result = NodeTreeValidator().validate(errands)
if result.has_errors():
raise ValueError(f"The tree is invalid: {result}")

# Access the model using direct containment
tasks = errands.get_children(tasks_containment)
print(f"Tasks found: {len(tasks)}")
for task in tasks:
print(f" - {task.get_property_value(name_property)}")

# Access the model using name-based utilities
tasks_again = errands.get_children("tasks")
print(f"Tasks found again: {len(tasks_again)}")
for task in tasks_again:
print(f" - {task.get_property_value('name')}")

Evaluation​

  • No static typing
  • No compile-time safety
  • No code completion or type checking
  • Work out of the box, without the need to write any code for each language

If you misspell "name" or access a non-existent feature, you’ll get a runtime exception.

Option 2: Heterogeneous Nodes​

This approach is recommended when building interpreters, compilers, or other tools for a specific language.

You define a Python class for each concept, typically:

  • Implementing the Node interface
  • Optionally extending DynamicNode for convenience

But how can you define these classes?​

Of course, you can do that in the good old way: writing the code yourself.

Or you can use the built-in generator which, given a language, produce the corresponding classes. See more under Generating Code.

In the rest of this section we will show how to define the classes manually.

When to Use​

  • You are building tooling for a specific DSL or language
  • You want type-safe code with IDE support
  • You require structured, validated access to features

Example​

import uuid
from typing import List, cast

from lionweb.language import Language, Concept, Property, Containment
from lionweb.model import DynamicNode
from lionweb.language import LionCoreBuiltins
from lionweb.utils.node_tree_validator import NodeTreeValidator


# === Define the Language ===

# Global elements
task_list_concept: Concept
task_concept: Concept
name_property: Property
tasks_containment: Containment
task_language: Language


def define_language():
global task_list_concept, task_concept, name_property, tasks_containment, task_language

# Define the 'TaskList' concept
task_list_concept = Concept(
name="TaskList", key="TaskList", id="TaskList-id", abstract=False, partition=True
)

# Define the 'Task' concept
task_concept = Concept(
name="Task", key="Task", id="Task-id", abstract=False, partition=False
)

# Add a 'tasks' containment
tasks_containment = Containment(
name="tasks",
key="TasksList-tasks",
id="TasksList-tasks-id",
type=task_concept,
multiple=True,
optional=False,
)
task_list_concept.add_feature(tasks_containment)

# Add a 'name' property
name_property = Property(
name="name", key="task-name", id="task-name-id", type=LionCoreBuiltins.get_string()
)
task_concept.add_feature(name_property)

# Define the language container
task_language = Language(
name="Task Language",
key="task",
id="task-id",
version="1.0"
)
task_language.add_element(task_list_concept)
task_language.add_element(task_concept)


# === Define specific DynamicNode subclasses ===

class Task(DynamicNode):
def __init__(self, name: str):
super().__init__(str(uuid.uuid4()), task_concept)
self.set_name(name)

def set_name(self, name: str):
self.set_property_value(name_property, name)

def get_name(self) -> str:
return self.get_property_value(name_property)


class TaskList(DynamicNode):
def __init__(self):
super().__init__(str(uuid.uuid4()), task_list_concept)

def add_task(self, task: Task):
self.add_child(tasks_containment, task)

def get_tasks(self) -> List[Task]:
return self.get_children(tasks_containment)


# === Main usage function ===

def use_specific_classes():
define_language()

errands = TaskList()

task1 = Task("My Task #1")
errands.add_task(task1)

task2 = Task("My Task #2")
errands.add_task(task2)

# Validate
result = NodeTreeValidator().validate(errands)
if result.has_errors():
raise ValueError(f"The tree is invalid: {result}")

# Access
tasks = errands.get_tasks()
print(f"Tasks found: {len(tasks)}")
for task in tasks:
print(f" - {task.get_name()}")


# === Entry Point ===

if __name__ == "__main__":
use_specific_classes()

Evaluation​

  • Full IDE support (auto-completion, navigation)
  • Catch errors at compile time
  • Clear API for collaborators
  • Require extra work for defining the classes

Suggested approach​

  • Use DynamicNode in model editors, importers, migrators
  • Use custom classes (like PersonNode) in interpreters, generators, type checkers

Interoperability​

Both approaches can co-exist. For example, you might parse a file into DynamicNode objects and then convert them into typed classes using a factory or builder.