Importing from JSON

In this example, we will use the same schema defined in the previous example to build an importer that re-constructs a register model from a JSON file.

We will use Python’s built-in JSON library to read the input file, as well as the RDLImporter helper class to help construct RDL components.

The full example code can be found in the systemrdl-compiler repository at: examples/import_json.py

Walkthrough

Start by creating a JSON importer class extended from RDLImporter.

from systemrdl.importer import RDLImporter

class JSONImporter(RDLImporter):

Just like the JSON exporter example, we will define a function that converts each component type. This time, Python’s JSON library will read the input and present each component as a dictionary of keys:value pairs that represents its description.

Per-component conversion functions

When developing your importer, it is important to always validate inputs, and create human-friendly error messages or warnings whenever appropriate. Prior to decoding any component, we start by checking the validity of the JSON dict object. Starting with the function to decode fields:

    def decode_field(self, json_obj: dict) -> comp.Field:
        # validate that this json object contains all the required fields
        if 'inst_name' not in json_obj:
            self.msg.fatal("JSON object is missing 'inst_name'", self.default_src_ref)
        if 'lsb' not in json_obj:
            self.msg.fatal("JSON object is missing 'lsb'", self.default_src_ref)
        if 'msb' not in json_obj:
            self.msg.fatal("JSON object is missing 'msb'", self.default_src_ref)
        if 'reset' not in json_obj:
            self.msg.fatal("JSON object is missing 'reset'", self.default_src_ref)
        if 'sw_access' not in json_obj:
            self.msg.fatal("JSON object is missing 'sw_access'", self.default_src_ref)

Next, we use some methods provided by the RDLImporter to:

  • Create an anonymous definition of a field component

  • Assign some properties

  • Instantiate it

Note that the field instance is not fully instantiated yet. It still needs to be attached to a parent component. This will be done later outside of this function.

        # Create an RDL field definition
        comp_def = self.create_field_definition()

        # Apply reset property if it was set
        if json_obj['reset'] is not None:
            self.assign_property(comp_def, 'reset', json_obj['reset'])

        # decode and apply the sw access property.
        # Since it uses the native enumeration name, we can do this directly
        sw = AccessType[json_obj['sw_access']]
        self.assign_property(comp_def, 'sw', sw)

        # Instantiate the component definition
        inst = self.instantiate_field(
            comp_def,
            json_obj['inst_name'],
            json_obj['lsb'],
            json_obj['msb'] - json_obj['lsb'] + 1
        )
        return inst

Next is the function that constructs the enclosing register component. The reg component is constructed in nearly the same way as before, except that here we iterate over all the child fields and attach them to the parent reg definition.

    def decode_reg(self, json_obj: dict) -> comp.Reg:
        # validate that this json object contains all the required fields
        if 'inst_name' not in json_obj:
            self.msg.fatal("JSON object is missing 'inst_name'", self.default_src_ref)
        if 'addr_offset' not in json_obj:
            self.msg.fatal("JSON object '%s' is missing 'addr_offset'" % json_obj['inst_name'], self.default_src_ref)
        if 'children' not in json_obj:
            self.msg.fatal("JSON object is missing 'children'", self.default_src_ref)

        comp_def = self.create_reg_definition()

        # Collect children
        for child_json in json_obj['children']:
            # Check that the child is the correct type. Reg can only contain fields
            t = child_json.get('type', None)
            if t != "field":
                self.msg.fatal(
                    "Invalid child type '%s'" % t,
                    self.default_src_ref
                )

            # Convert each child component and add it to our reg definition
            child_inst = self.decode_field(child_json)
            self.add_child(comp_def, child_inst)

        # Convert the definition into an instance
        inst = self.instantiate_reg(
            comp_def,
            json_obj['inst_name'],
            json_obj['addr_offset']
        )
        return inst

Decoding a regfile is nearly the same. Here, children can be multiple different types, so we call the appropriate decode function based on the ‘type’ key:

    def decode_regfile(self, json_obj: dict) -> comp.Regfile:
        if 'inst_name' not in json_obj:
            self.msg.fatal("JSON object is missing 'inst_name'", self.default_src_ref)
        if 'addr_offset' not in json_obj:
            self.msg.fatal("JSON object '%s' is missing 'addr_offset'" % json_obj['inst_name'], self.default_src_ref)
        if 'children' not in json_obj:
            self.msg.fatal("JSON object is missing 'children'", self.default_src_ref)

        comp_def = self.create_regfile_definition()

        for child_json in json_obj['children']:
            t = child_json.get('type', None)
            if t == "regfile":
                child_inst = self.decode_regfile(child_json)
            elif t == "reg":
                child_inst = self.decode_reg(child_json)
            else:
                self.msg.fatal(
                    "Invalid child type '%s'" % t,
                    self.default_src_ref
                )
            self.add_child(comp_def, child_inst)

        inst = self.instantiate_regfile(
            comp_def,
            json_obj['inst_name'],
            json_obj['addr_offset']
        )
        return inst

The addrmap decode function is nearly identical, however we need the option to produce a named definition instead of an anonymous component instantiation. This is because we want to be able to register the top-level addrmap in the root type namespace. The type namespace can only contain component definitions, not instances.

    def decode_addrmap(self, json_obj: dict, is_top: bool=False) -> comp.Addrmap:
        # validate that this json object contains all the required fields
        if 'inst_name' not in json_obj:
            self.msg.fatal("JSON object is missing 'inst_name'", self.default_src_ref)
        if 'addr_offset' not in json_obj:
            self.msg.fatal("JSON object '%s' is missing 'addr_offset'" % json_obj['inst_name'], self.default_src_ref)
        if 'children' not in json_obj:
            self.msg.fatal("JSON object is missing 'children'", self.default_src_ref)

        if is_top:
            # if this is the top node, then instantiation is skipped, and the
            # definition inherits the inst name as its type name
            comp_def = self.create_addrmap_definition(json_obj['inst_name'])
        else:
            # otherwise, create an anonymous definition
            comp_def = self.create_addrmap_definition()

        # Collect children
        for child_json in json_obj['children']:
            # Lookup the child type and call the appropriate conversion function
            t = child_json.get('type', None)
            if t == "addrmap":
                child_inst = self.decode_addrmap(child_json)
            elif t == "regfile":
                child_inst = self.decode_regfile(child_json)
            elif t == "reg":
                child_inst = self.decode_reg(child_json)
            else:
                self.msg.fatal(
                    "Invalid child type '%s'" % t,
                    self.default_src_ref
                )

            # Add the child component to this
            self.add_child(comp_def, child_inst)

        if is_top:
            # keep top-level addrmap as a definition. Skip instantiation
            return comp_def

        # For everything else, convert the definition into an instance
        inst = self.instantiate_addrmap(
            comp_def,
            json_obj['inst_name'],
            json_obj['addr_offset']
        )
        return inst

Loading JSON and decoding

Now that we have our decoding utility functions completed, we are ready to implement the importer’s import_file() entry function.

First, call the superclass. This ensures that per-component error message context works properly later.

    def import_file(self, path: str) -> None:
        super().import_file(path)

Next, load the JSON file. For this importer, we’ll be assuming that the top-level component is always an addrmap. Any time an assumption about the input is made, you should validate it to ensure it is true:

        # Load the JSON from a file and convert it to primitive Python objects
        with open(path, 'r', encoding='utf-8') as f:
            json_obj = json.load(f)

        # Make sure top level object is an addrmap type
        if json_obj.get('type', None) != "addrmap":
            self.msg.fatal(
                "Top JSON object must be an addrmap type",
                self.default_src_ref
            )

Finally, call our decode function and register the resulting top-level component definition with the root namespace:

        # Decode the JSON object
        # Set is_top=True so that decode returns a definition rather than an instance
        top_addrmap_def = self.decode_addrmap(json_obj, is_top=True)

        # Register the top definition in the root namespace
        self.register_root_component(top_addrmap_def)

Using our new importer

Importing data into the SystemRDL compiler is similar to compiling a file normally. You can even intermingle compiling RDL files with importing custom formats.

  • Create a RDLCompiler instance

  • Bind any custom importers

  • Compile RDL inputs and/or import custom file formats

  • Run elaboration

Here we detect the file type based on its extension:

# Create a compiler session, and an importer attached to it
rdlc = RDLCompiler()
json_importer = JSONImporter(rdlc)

# import each JSON file provided from the command line
input_files = sys.argv[1:]
try:
    for input_file in input_files:
        # compile or import based on the file extension
        ext = os.path.splitext(input_file)[1]
        if ext == ".rdl":
            rdlc.compile_file(input_file)
        elif ext == ".json":
            json_importer.import_file(input_file)
        else:
            rdlc.msg.fatal(
                "Unknown file extension: %s" % ext,
                FileSourceRef(input_file)
            )
    # Elaborate when done
    root = rdlc.elaborate()
except RDLCompileError:
    sys.exit(1)

Equivalence check

Let’s see if our importer is working properly. We can use the model printing listener from the first example to dump the register model:

from print_hierarchy import MyModelPrintingListener
walker = RDLWalker()
listener = MyModelPrintingListener()
walker.walk(root, listener)

Running the example code on tiny.rdl and its JSON counterpart we exported in the previous example, tiny.json, we should see identical output:

$ ./import_json.py tiny.json
tiny
        r1
                [7:0] f1 sw=rw
                [15:8] f2 sw=r
$ ./import_json.py tiny.rdl
tiny
        r1
                [7:0] f1 sw=rw
                [15:8] f2 sw=r