TextX: Model Modularization

Similar to xtext_modularization.md, we show how to achive model modularization across files in TextX.

Modularization within the same Meta Model

Ready to use scope providers exists to handle multiple files. See the (TextX) documentation for details.

Referencing Model Elements form other Meta Models

If you wish to reference model elements from other metamodels, this can be achieved using the multi meta model support of TextX (see (TextX)).

The following example is self contained and shows how to deploy two python packages for two DSLs, one referencing the other.

  • mydsl: a simple "Hello World" language
  • mydsl1: a language referencing Greetings from the mydsl language.

mydsl - a simple model of "Greetings"

File layout

├── mydsl
│   ├── __init__.py
│   ├── metamodel.py
│   └── MyDsl.tx
└── setup.py

Grammar MyDsl.tx

The grammar defines the structure...

Model: greetings+=Greeting;
Greeting: 'Hello' name=ID'!';

metamodel.py

The metamodel is created based on the grammar...

from textx import metamodel_from_file
from os.path import dirname, abspath, join

def get_metamodel():
    this_folder = dirname(abspath(__file__))
    meta_model = metamodel_from_file(join(this_folder,"MyDsl.tx"))
    return meta_model

__init__.py

The entry point for the DSL "compiler" (it just outputs some model data)...

import argparse
from mydsl.metamodel import get_metamodel

def mydslc():
    parser = argparse.ArgumentParser(description='generate code for the model.')
    parser.add_argument('model_files', metavar='model_files', type=str, nargs='+',
                        help='model filenames')

    args = parser.parse_args()
    mm = get_metamodel()

    for model_file in args.model_files:
        model = mm.model_from_file(model_file)
        for greeting in model.greetings:
            print(" - hello for '{}'".format(greeting.name))

setup.py

The installer configuration (see textx_project_setupx.md for more details).

...
setup(name='mydsl',
...
      entry_points={
          'console_scripts': [
              'mydslc=mydsl:mydslc',
          ]
      },
...

Create installer

Create an installer to help pip find its dependencies:

python setup.py  sdist

mydsl1 - a model referencing the "Greetings" from mydsl

File layout

├── mydsl1
│   ├── __init__.py
│   ├── metamodel.py
│   └── MyDsl1.tx
└── setup.py

Grammar MyDsl1.tx

Note: we use the grammar rule "Greeting" from "mydsl". See metamodel.py how this is resolved ("refrenced_metamodels").

Model: imports+=Import greetings+=RefGreeting;
RefGreeting: 'Hello' '-->' ref=[Greeting];
Import: 'import' importURI=STRING;

metamodel.py

The metamodel is created based on the grammar, the metamodel to be referenced and some scope providers (to allow to "import" other model files)...

from textx import metamodel_from_file
import textx.scoping as scoping
import textx.scoping.providers as scoping_providers
from os.path import dirname, abspath, join
import mydsl

def get_metamodel():
    this_folder = dirname(abspath(__file__))

    # get the "mydsl" meta model
    other_meta_model = mydsl.get_metamodel()

    # create the meta model and reference "mydsl"
    meta_model = metamodel_from_file(
        join(this_folder,"MyDsl1.tx"), 
        referenced_metamodels=[other_meta_model])

    # register scope provider (allow import models into mydsl1 models)
    meta_model.register_scope_providers(
        {"*.*": scoping_providers.PlainNameImportURI()})

    # register file endings
    scoping.MetaModelProvider.add_metamodel("*.mydsl", other_meta_model)
    scoping.MetaModelProvider.add_metamodel("*.mydsl1", meta_model)

    return meta_model

__init__.py

The entry point for the DSL "compiler" (it just outputs some model data)...

import argparse
from mydsl1.metamodel import get_metamodel

def mydsl1c():
    parser = argparse.ArgumentParser(description='generate code for the model.')
    parser.add_argument('model_files', metavar='model_files', type=str, nargs='+',
                        help='model filenames')

    args = parser.parse_args()
    mm = get_metamodel()

    for model_file in args.model_files:
        model = mm.model_from_file(model_file)
        for greeting in model.greetings:
            print(" - hello for referenced '{}'".format(greeting.ref.name))

setup.py

The installer configuration (see textx_project_setupx.md for more details). The language "mydsl1" depends on "mydsl".

...
setup(name='mydsl1',
...
      install_requires=["textx","arpeggio","mydsl"],
...
      entry_points={
          'console_scripts': [
              'mydsl1c=mydsl1:mydsl1c',
          ]
      },
...

Usage

Install both DSLs and compilers

The option "find-links" is used to point to the local version of mydsl (created above; setup.py of "mydsl1" includes this dependency):

pip3 install . --find-links=file:///$(pwd)/../mydsl/dist

Model file data.mydsl

Hello Pi!
Hello Tim!

Model file data.mydsl1

import "data.mydsl"
Hello --> Pi
Hello --> Tim

Model file error.mydsl1

import "data.mydsl"
Hello --> NoName

Example using mydslc and mydsl1c

$ mydslc model/data.mydsl
 - hello for 'Pi'
 - hello for 'Tim'
$ mydsl1c model/data.mydsl1
 - hello for referenced 'Pi'
 - hello for referenced 'Tim'
$ mydsl1c model/error.mydsl1
...
textx.exceptions.TextXSemanticError: model/error.mydsl1:2:11: 
error: Unknown object "NoName" of class "Greeting"